fedipod 1.50.2 → 1.55.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +2 -1
  2. package/gateway.md +25 -0
  3. package/gui.md +11 -3
  4. package/lib/client/masto/timelines.mjs +5 -0
  5. package/lib/connections/acctfeed.mjs +4 -1
  6. package/lib/connections/tagfeed.mjs +4 -1
  7. package/lib/core/intake/index.mjs +35 -2
  8. package/lib/core/intake/verify.mjs +3 -1
  9. package/lib/core/pod-only.mjs +4 -2
  10. package/lib/core/publisher/index.mjs +19 -5
  11. package/lib/gateway/account-agent.mjs +11 -5
  12. package/lib/gateway/check-letter.mjs +123 -0
  13. package/lib/gateway/connected-feeds.mjs +116 -0
  14. package/lib/gateway/copy.mjs +19 -0
  15. package/lib/gateway/front-core.mjs +5 -3
  16. package/lib/gateway/held-mail.mjs +25 -1
  17. package/lib/gateway/keeper.mjs +2 -1
  18. package/lib/gateway/masto-gateway.mjs +47 -7
  19. package/lib/surface/routes/owner.mjs +7 -4
  20. package/package.json +3 -3
  21. package/scripts/stage-site.mjs +2 -3
  22. package/vendor/sanitize-html.cjs +358 -171
  23. package/web/admin/bar.js +45 -0
  24. package/web/admin/group.js +17 -0
  25. package/web/admin/index.html +12 -8
  26. package/web/admin/record.js +19 -41
  27. package/web/admin/upkeep.js +3 -0
  28. package/web/app/README.md +3 -3
  29. package/web/app/admin-facade.mjs +19 -16
  30. package/web/app/agent.mjs +65 -2
  31. package/web/app/atproto-browser.mjs +28 -22
  32. package/web/app/deliver-relay.mjs +2 -1
  33. package/web/app/dist/boot.js +1 -0
  34. package/web/app/dist/boot.js.map +2 -2
  35. package/web/app/dist/sw.js +567 -265
  36. package/web/app/dist/sw.js.map +4 -4
  37. package/web/app/fediacct-browser.mjs +32 -30
  38. package/web/app/idb-kv.mjs +4 -4
  39. package/web/app/shims/prelude.js +1 -0
  40. package/web/app/sw-src.mjs +44 -3
@@ -54,6 +54,7 @@ globalThis.Buffer ??= (() => {
54
54
  concat: (list) => { const n = list.reduce((s, x) => s + x.length, 0); const o = new Buf(n); let i = 0; for (const x of list) { o.set(x, i); i += x.length; } return o; },
55
55
  alloc: (n) => new Buf(n),
56
56
  isBuffer: (x) => x instanceof Uint8Array,
57
+ byteLength: (d, enc) => (typeof d === 'string' ? fromString(d, enc).length : d.byteLength ?? d.length),
57
58
  };
58
59
  })();
59
60
 
@@ -3636,7 +3637,6 @@ var require_escape_string_regexp = __commonJS({
3636
3637
  var require_is_plain_object = __commonJS({
3637
3638
  "node_modules/is-plain-object/dist/is-plain-object.js"(exports) {
3638
3639
  "use strict";
3639
- Object.defineProperty(exports, "__esModule", { value: true });
3640
3640
  function isObject(o) {
3641
3641
  return Object.prototype.toString.call(o) === "[object Object]";
3642
3642
  }
@@ -3656,109 +3656,6 @@ var require_is_plain_object = __commonJS({
3656
3656
  }
3657
3657
  });
3658
3658
 
3659
- // node_modules/deepmerge/dist/cjs.js
3660
- var require_cjs = __commonJS({
3661
- "node_modules/deepmerge/dist/cjs.js"(exports, module2) {
3662
- "use strict";
3663
- var isMergeableObject = function isMergeableObject2(value) {
3664
- return isNonNullObject(value) && !isSpecial(value);
3665
- };
3666
- function isNonNullObject(value) {
3667
- return !!value && typeof value === "object";
3668
- }
3669
- function isSpecial(value) {
3670
- var stringValue = Object.prototype.toString.call(value);
3671
- return stringValue === "[object RegExp]" || stringValue === "[object Date]" || isReactElement(value);
3672
- }
3673
- var canUseSymbol = typeof Symbol === "function" && Symbol.for;
3674
- var REACT_ELEMENT_TYPE = canUseSymbol ? /* @__PURE__ */ Symbol.for("react.element") : 60103;
3675
- function isReactElement(value) {
3676
- return value.$$typeof === REACT_ELEMENT_TYPE;
3677
- }
3678
- function emptyTarget(val) {
3679
- return Array.isArray(val) ? [] : {};
3680
- }
3681
- function cloneUnlessOtherwiseSpecified(value, options) {
3682
- return options.clone !== false && options.isMergeableObject(value) ? deepmerge(emptyTarget(value), value, options) : value;
3683
- }
3684
- function defaultArrayMerge(target, source, options) {
3685
- return target.concat(source).map(function(element) {
3686
- return cloneUnlessOtherwiseSpecified(element, options);
3687
- });
3688
- }
3689
- function getMergeFunction(key, options) {
3690
- if (!options.customMerge) {
3691
- return deepmerge;
3692
- }
3693
- var customMerge = options.customMerge(key);
3694
- return typeof customMerge === "function" ? customMerge : deepmerge;
3695
- }
3696
- function getEnumerableOwnPropertySymbols(target) {
3697
- return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(target).filter(function(symbol) {
3698
- return Object.propertyIsEnumerable.call(target, symbol);
3699
- }) : [];
3700
- }
3701
- function getKeys(target) {
3702
- return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target));
3703
- }
3704
- function propertyIsOnObject(object, property) {
3705
- try {
3706
- return property in object;
3707
- } catch (_) {
3708
- return false;
3709
- }
3710
- }
3711
- function propertyIsUnsafe(target, key) {
3712
- return propertyIsOnObject(target, key) && !(Object.hasOwnProperty.call(target, key) && Object.propertyIsEnumerable.call(target, key));
3713
- }
3714
- function mergeObject(target, source, options) {
3715
- var destination = {};
3716
- if (options.isMergeableObject(target)) {
3717
- getKeys(target).forEach(function(key) {
3718
- destination[key] = cloneUnlessOtherwiseSpecified(target[key], options);
3719
- });
3720
- }
3721
- getKeys(source).forEach(function(key) {
3722
- if (propertyIsUnsafe(target, key)) {
3723
- return;
3724
- }
3725
- if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {
3726
- destination[key] = getMergeFunction(key, options)(target[key], source[key], options);
3727
- } else {
3728
- destination[key] = cloneUnlessOtherwiseSpecified(source[key], options);
3729
- }
3730
- });
3731
- return destination;
3732
- }
3733
- function deepmerge(target, source, options) {
3734
- options = options || {};
3735
- options.arrayMerge = options.arrayMerge || defaultArrayMerge;
3736
- options.isMergeableObject = options.isMergeableObject || isMergeableObject;
3737
- options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified;
3738
- var sourceIsArray = Array.isArray(source);
3739
- var targetIsArray = Array.isArray(target);
3740
- var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;
3741
- if (!sourceAndTargetTypesMatch) {
3742
- return cloneUnlessOtherwiseSpecified(source, options);
3743
- } else if (sourceIsArray) {
3744
- return options.arrayMerge(target, source, options);
3745
- } else {
3746
- return mergeObject(target, source, options);
3747
- }
3748
- }
3749
- deepmerge.all = function deepmergeAll(array, options) {
3750
- if (!Array.isArray(array)) {
3751
- throw new Error("first argument should be an array");
3752
- }
3753
- return array.reduce(function(prev, next) {
3754
- return deepmerge(prev, next, options);
3755
- }, {});
3756
- };
3757
- var deepmerge_1 = deepmerge;
3758
- module2.exports = deepmerge_1;
3759
- }
3760
- });
3761
-
3762
3659
  // node_modules/parse-srcset/src/parse-srcset.js
3763
3660
  var require_parse_srcset = __commonJS({
3764
3661
  "node_modules/parse-srcset/src/parse-srcset.js"(exports, module2) {
@@ -4037,14 +3934,7 @@ var require_css_syntax_error = __commonJS({
4037
3934
  var require_stringifier = __commonJS({
4038
3935
  "node_modules/postcss/lib/stringifier.js"(exports, module2) {
4039
3936
  "use strict";
4040
- var STYLE_TAG = /(<)(\/?style\b)/gi;
4041
- var COMMENT_OPEN = /(<)(!--)/g;
4042
3937
  var AT_NAME_END = /[\t\n\f\r "#'()/;[\\\]{}]/;
4043
- function escapeHTMLInCSS(str) {
4044
- if (typeof str !== "string") return str;
4045
- if (!str.includes("<")) return str;
4046
- return str.replace(STYLE_TAG, "\\3c $2").replace(COMMENT_OPEN, "\\3c $2");
4047
- }
4048
3938
  var DEFAULT_RAW = {
4049
3939
  after: "\n",
4050
3940
  beforeClose: "\n",
@@ -4073,6 +3963,11 @@ var require_stringifier = __commonJS({
4073
3963
  }
4074
3964
  return name + afterName + params;
4075
3965
  }
3966
+ function isCustomProperty(node) {
3967
+ if (!node.prop.startsWith("--")) return false;
3968
+ let before = node.raws.before;
3969
+ return typeof before === "undefined" || !/[^\s;]$/.test(before);
3970
+ }
4076
3971
  function pushBody(str, stack, node) {
4077
3972
  let nodes = node.nodes;
4078
3973
  let last = nodes.length - 1;
@@ -4085,7 +3980,7 @@ var require_stringifier = __commonJS({
4085
3980
  for (let i = nodes.length - 1; i >= 0; i--) {
4086
3981
  let child = nodes[i];
4087
3982
  let childSemicolon = last !== i || semicolon;
4088
- if (!childSemicolon && i < nodes.length - 1 && (child.type === "atrule" && !child.nodes || child.type === "decl" && child.prop.startsWith("--"))) {
3983
+ if (!childSemicolon && i < nodes.length - 1 && (child.type === "atrule" && !child.nodes || child.type === "decl" && isCustomProperty(child))) {
4089
3984
  childSemicolon = true;
4090
3985
  }
4091
3986
  stack.push({
@@ -4097,14 +3992,14 @@ var require_stringifier = __commonJS({
4097
3992
  }
4098
3993
  function pushBlock(str, stack, node, start) {
4099
3994
  let between = str.raw(node, "between", "beforeOpen");
4100
- str.builder(escapeHTMLInCSS(start + between) + "{", node, "start");
3995
+ str.builder(start + between + "{", node, "start");
4101
3996
  let hasNodes = node.nodes && node.nodes.length;
4102
3997
  let close = () => {
4103
3998
  let after = hasNodes ? str.raw(node, "after") : str.raw(node, "after", "emptyBody");
4104
- if (after) str.builder(escapeHTMLInCSS(after));
3999
+ if (after) str.builder(after);
4105
4000
  str.builder("}", node, "end");
4106
4001
  if (node.type === "rule" && node.raws.ownSemicolon) {
4107
- str.builder(escapeHTMLInCSS(node.raws.ownSemicolon), node, "end");
4002
+ str.builder(node.raws.ownSemicolon, node, "end");
4108
4003
  }
4109
4004
  };
4110
4005
  if (hasNodes) {
@@ -4124,7 +4019,7 @@ var require_stringifier = __commonJS({
4124
4019
  this.block(node, start);
4125
4020
  } else {
4126
4021
  let end = (node.raws.between || "") + (semicolon ? ";" : "");
4127
- this.builder(escapeHTMLInCSS(start + end), node);
4022
+ this.builder(start + end, node);
4128
4023
  }
4129
4024
  }
4130
4025
  beforeAfter(node, detect) {
@@ -4154,7 +4049,7 @@ var require_stringifier = __commonJS({
4154
4049
  }
4155
4050
  block(node, start) {
4156
4051
  let between = this.raw(node, "between", "beforeOpen");
4157
- this.builder(escapeHTMLInCSS(start + between) + "{", node, "start");
4052
+ this.builder(start + between + "{", node, "start");
4158
4053
  let after;
4159
4054
  if (node.nodes && node.nodes.length) {
4160
4055
  this.body(node);
@@ -4162,7 +4057,7 @@ var require_stringifier = __commonJS({
4162
4057
  } else {
4163
4058
  after = this.raw(node, "after", "emptyBody");
4164
4059
  }
4165
- if (after) this.builder(escapeHTMLInCSS(after));
4060
+ if (after) this.builder(after);
4166
4061
  this.builder("}", node, "end");
4167
4062
  }
4168
4063
  body(node) {
@@ -4181,7 +4076,7 @@ var require_stringifier = __commonJS({
4181
4076
  let child = entry.node;
4182
4077
  let before = this.raw(child, "before");
4183
4078
  if (before) {
4184
- this.builder(entry.document ? before : escapeHTMLInCSS(before));
4079
+ this.builder(before);
4185
4080
  }
4186
4081
  if (expandable && child.type === "rule") {
4187
4082
  pushBlock(this, stack, child, this.rawValue(child, "selector"));
@@ -4195,7 +4090,7 @@ var require_stringifier = __commonJS({
4195
4090
  comment(node) {
4196
4091
  let left = this.raw(node, "left", "commentLeft");
4197
4092
  let right = this.raw(node, "right", "commentRight");
4198
- this.builder(escapeHTMLInCSS("/*" + left + node.text + right + "*/"), node);
4093
+ this.builder("/*" + left + node.text + right + "*/", node);
4199
4094
  }
4200
4095
  decl(node, semicolon) {
4201
4096
  let raws = node.raws;
@@ -4205,7 +4100,7 @@ var require_stringifier = __commonJS({
4205
4100
  string += raws.important || " !important";
4206
4101
  }
4207
4102
  if (semicolon) string += ";";
4208
- this.builder(escapeHTMLInCSS(string), node);
4103
+ this.builder(string, node);
4209
4104
  }
4210
4105
  document(node) {
4211
4106
  this.body(node);
@@ -4382,20 +4277,19 @@ var require_stringifier = __commonJS({
4382
4277
  return value;
4383
4278
  }
4384
4279
  root(node) {
4385
- if (node.source && node.source.input.hasBOM) {
4280
+ let inDocument = node.parent && node.parent.type === "document";
4281
+ if (!inDocument && node.source && node.source.input.hasBOM) {
4386
4282
  this.builder("\uFEFF", node, "start");
4387
4283
  }
4388
4284
  this.body(node);
4389
4285
  if (node.raws.after) {
4390
- let after = node.raws.after;
4391
- let isDocument2 = node.parent && node.parent.type === "document";
4392
- this.builder(isDocument2 ? after : escapeHTMLInCSS(after));
4286
+ this.builder(node.raws.after);
4393
4287
  }
4394
4288
  }
4395
4289
  rule(node) {
4396
4290
  this.block(node, this.rawValue(node, "selector"));
4397
4291
  if (node.raws.ownSemicolon) {
4398
- this.builder(escapeHTMLInCSS(node.raws.ownSemicolon), node, "end");
4292
+ this.builder(node.raws.ownSemicolon, node, "end");
4399
4293
  }
4400
4294
  }
4401
4295
  stringify(node, semicolon) {
@@ -4417,9 +4311,78 @@ var require_stringify = __commonJS({
4417
4311
  "node_modules/postcss/lib/stringify.js"(exports, module2) {
4418
4312
  "use strict";
4419
4313
  var Stringifier = require_stringifier();
4314
+ var STYLE_TAG = /(<)(\/?style\b)/gi;
4315
+ var COMMENT_OPEN = /(<)(!--)/g;
4316
+ function escapeHTMLInCSS(str) {
4317
+ if (!str.includes("<")) return str;
4318
+ return str.replace(STYLE_TAG, "\\3c $2").replace(COMMENT_OPEN, "\\3c $2");
4319
+ }
4320
+ function cutEscaped(chunks, css, escaped) {
4321
+ let parts = [];
4322
+ let at = 0;
4323
+ let into = 0;
4324
+ for (let i = 0; i < chunks.length; i += 3) {
4325
+ let from = into;
4326
+ let end = at + chunks[i].length;
4327
+ while (at < end) {
4328
+ if (css[at] === escaped[into]) {
4329
+ at += 1;
4330
+ into += 1;
4331
+ } else {
4332
+ at += 1;
4333
+ into += 4;
4334
+ }
4335
+ }
4336
+ parts.push(escaped.slice(from, into));
4337
+ }
4338
+ return parts;
4339
+ }
4340
+ var SafeStringifier = class extends Stringifier {
4341
+ escapeHTML(print) {
4342
+ let builder111 = this.builder;
4343
+ let chunks, css;
4344
+ this.builder = (str, child, type) => {
4345
+ if (chunks) {
4346
+ css += str;
4347
+ chunks.push(str, child, type);
4348
+ } else if (str.includes("<")) {
4349
+ css = str;
4350
+ chunks = [str, child, type];
4351
+ } else {
4352
+ builder111(str, child, type);
4353
+ }
4354
+ };
4355
+ print();
4356
+ this.builder = builder111;
4357
+ if (!chunks) return;
4358
+ let escaped = escapeHTMLInCSS(css);
4359
+ if (escaped === css) {
4360
+ for (let i = 0; i < chunks.length; i += 3) {
4361
+ builder111(chunks[i], chunks[i + 1], chunks[i + 2]);
4362
+ }
4363
+ } else {
4364
+ let parts = cutEscaped(chunks, css, escaped);
4365
+ for (let i = 0; i < chunks.length; i += 3) {
4366
+ builder111(parts[i / 3], chunks[i + 1], chunks[i + 2]);
4367
+ }
4368
+ }
4369
+ }
4370
+ root(node) {
4371
+ if (node.parent && node.parent.type === "document") {
4372
+ this.escapeHTML(() => this.body(node));
4373
+ if (node.raws.after) this.builder(node.raws.after);
4374
+ } else {
4375
+ this.escapeHTML(() => super.root(node));
4376
+ }
4377
+ }
4378
+ };
4420
4379
  function stringify(node, builder111) {
4421
- let str = new Stringifier(builder111);
4422
- str.stringify(node);
4380
+ let str = new SafeStringifier(builder111);
4381
+ if (node.type === "root" || node.type === "document") {
4382
+ str.stringify(node);
4383
+ } else {
4384
+ str.escapeHTML(() => str.stringify(node));
4385
+ }
4423
4386
  }
4424
4387
  module2.exports = stringify;
4425
4388
  stringify.default = stringify;
@@ -5419,9 +5382,16 @@ var require_fs = __commonJS({
5419
5382
  var require_previous_map = __commonJS({
5420
5383
  "node_modules/postcss/lib/previous-map.js"(exports, module2) {
5421
5384
  "use strict";
5422
- var { existsSync: existsSync2, readFileSync: readFileSync2 } = require_fs();
5385
+ var { existsSync: existsSync2, readFileSync: readFileSync2, realpathSync } = require_fs();
5423
5386
  var { dirname: dirname2, isAbsolute, join: join3, relative, sep } = require_path();
5424
5387
  var { SourceMapConsumer, SourceMapGenerator } = require_source_map();
5388
+ function realPath(path) {
5389
+ try {
5390
+ return realpathSync(path);
5391
+ } catch {
5392
+ return path;
5393
+ }
5394
+ }
5425
5395
  function fromBase64(str) {
5426
5396
  if (Buffer) {
5427
5397
  return Buffer.from(str, "base64").toString();
@@ -5486,7 +5456,7 @@ var require_previous_map = __commonJS({
5486
5456
  if (!trusted && !this.unsafeMap) {
5487
5457
  if (!/\.map$/i.test(path)) return void 0;
5488
5458
  if (!cssFile) return void 0;
5489
- let rel = relative(dirname2(cssFile), path);
5459
+ let rel = relative(realPath(dirname2(cssFile)), realPath(path));
5490
5460
  if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
5491
5461
  return void 0;
5492
5462
  }
@@ -5862,6 +5832,7 @@ var require_list = __commonJS({
5862
5832
  return list3.split(string, spaces);
5863
5833
  },
5864
5834
  split(string, separators, last) {
5835
+ if (typeof string !== "string") return [];
5865
5836
  let array = [];
5866
5837
  let current = "";
5867
5838
  let split = false;
@@ -5869,7 +5840,18 @@ var require_list = __commonJS({
5869
5840
  let inQuote = false;
5870
5841
  let prevQuote = "";
5871
5842
  let escape2 = false;
5872
- for (let letter of string) {
5843
+ let inComment = false;
5844
+ for (let i = 0; i < string.length; i++) {
5845
+ let letter = string[i];
5846
+ if (inComment) {
5847
+ current += letter;
5848
+ if (letter === "*" && string[i + 1] === "/") {
5849
+ current += "/";
5850
+ i += 1;
5851
+ inComment = false;
5852
+ }
5853
+ continue;
5854
+ }
5873
5855
  if (escape2) {
5874
5856
  escape2 = false;
5875
5857
  } else if (letter === "\\") {
@@ -5881,6 +5863,11 @@ var require_list = __commonJS({
5881
5863
  } else if (letter === '"' || letter === "'") {
5882
5864
  inQuote = true;
5883
5865
  prevQuote = letter;
5866
+ } else if (letter === "/" && string[i + 1] === "*") {
5867
+ current += "/*";
5868
+ i += 1;
5869
+ inComment = true;
5870
+ continue;
5884
5871
  } else if (letter === "(") {
5885
5872
  func += 1;
5886
5873
  } else if (letter === ")") {
@@ -5889,14 +5876,16 @@ var require_list = __commonJS({
5889
5876
  if (separators.includes(letter)) split = true;
5890
5877
  }
5891
5878
  if (split) {
5892
- if (current !== "") array.push(current.trim());
5879
+ let value2 = current.trim();
5880
+ if (last || value2 !== "") array.push(value2);
5893
5881
  current = "";
5894
5882
  split = false;
5895
5883
  } else {
5896
5884
  current += letter;
5897
5885
  }
5898
5886
  }
5899
- if (last || current !== "") array.push(current.trim());
5887
+ let value = current.trim();
5888
+ if (last || value !== "") array.push(value);
5900
5889
  return array;
5901
5890
  }
5902
5891
  };
@@ -6092,15 +6081,20 @@ var require_map_generator = __commonJS({
6092
6081
  }
6093
6082
  }
6094
6083
  } else if (this.css) {
6084
+ let annotation = "/*# sourceMappingURL=";
6085
+ let css = "";
6086
+ let cursor = 0;
6095
6087
  let startIndex;
6096
- while ((startIndex = this.css.lastIndexOf("/*#")) !== -1) {
6097
- let endIndex = this.css.indexOf("*/", startIndex + 3);
6088
+ while ((startIndex = this.css.indexOf(annotation, cursor)) !== -1) {
6089
+ let endIndex = this.css.indexOf("*/", startIndex + annotation.length);
6098
6090
  if (endIndex === -1) break;
6099
- while (startIndex > 0 && this.css[startIndex - 1] === "\n") {
6091
+ while (startIndex > cursor && this.css[startIndex - 1] === "\n") {
6100
6092
  startIndex--;
6101
6093
  }
6102
- this.css = this.css.slice(0, startIndex) + this.css.slice(endIndex + 2);
6094
+ css += this.css.slice(cursor, startIndex);
6095
+ cursor = endIndex + 2;
6103
6096
  }
6097
+ if (cursor > 0) this.css = css + this.css.slice(cursor);
6104
6098
  }
6105
6099
  }
6106
6100
  generate() {
@@ -6881,7 +6875,7 @@ var require_parser = __commonJS({
6881
6875
  prev.raws.ownSemicolon = this.spaces;
6882
6876
  this.spaces = "";
6883
6877
  prev.source.end = this.getPosition(token[2]);
6884
- prev.source.end.offset += prev.raws.ownSemicolon.length;
6878
+ prev.source.end.offset++;
6885
6879
  }
6886
6880
  }
6887
6881
  }
@@ -6997,6 +6991,7 @@ var require_parser = __commonJS({
6997
6991
  let token, type;
6998
6992
  let length = tokens2.length;
6999
6993
  let value = "";
6994
+ let last = "";
7000
6995
  let clean = true;
7001
6996
  let next, prev;
7002
6997
  for (let i = 0; i < length; i += 1) {
@@ -7008,16 +7003,18 @@ var require_parser = __commonJS({
7008
7003
  prev = tokens2[i - 1] ? tokens2[i - 1][0] : "empty";
7009
7004
  next = tokens2[i + 1] ? tokens2[i + 1][0] : "empty";
7010
7005
  if (!SAFE_COMMENT_NEIGHBOR[prev] && !SAFE_COMMENT_NEIGHBOR[next]) {
7011
- if (value.slice(-1) === ",") {
7006
+ if (last === ",") {
7012
7007
  clean = false;
7013
7008
  } else {
7014
7009
  value += token[1];
7010
+ last = token[1].slice(-1);
7015
7011
  }
7016
7012
  } else {
7017
7013
  clean = false;
7018
7014
  }
7019
7015
  } else {
7020
7016
  value += token[1];
7017
+ last = token[1].slice(-1);
7021
7018
  }
7022
7019
  }
7023
7020
  if (!clean) {
@@ -7667,14 +7664,19 @@ var require_lazy_result = __commonJS({
7667
7664
  }
7668
7665
  if (visit.iterator !== 0) {
7669
7666
  let iterator = visit.iterator;
7667
+ if (visit.descending) {
7668
+ visit.descending = false;
7669
+ node.indexes[iterator] += 1;
7670
+ }
7670
7671
  let child;
7671
7672
  while (child = node.nodes[node.indexes[iterator]]) {
7672
- node.indexes[iterator] += 1;
7673
7673
  if (!child[isClean]) {
7674
7674
  child[isClean] = true;
7675
+ visit.descending = true;
7675
7676
  stack.push(toStack(child));
7676
7677
  return;
7677
7678
  }
7679
+ node.indexes[iterator] += 1;
7678
7680
  }
7679
7681
  visit.iterator = 0;
7680
7682
  delete node.indexes[iterator];
@@ -7704,12 +7706,16 @@ var require_lazy_result = __commonJS({
7704
7706
  let visitNode = visit.node;
7705
7707
  if (visit.iterator !== 0) {
7706
7708
  let iterator = visit.iterator;
7709
+ if (visit.descending) {
7710
+ visit.descending = false;
7711
+ visitNode.indexes[iterator] += 1;
7712
+ }
7707
7713
  let child;
7708
7714
  let descended = false;
7709
7715
  while (child = visitNode.nodes[visitNode.indexes[iterator]]) {
7710
- visitNode.indexes[iterator] += 1;
7711
7716
  if (!child[isClean]) {
7712
7717
  child[isClean] = true;
7718
+ visit.descending = true;
7713
7719
  stack.push({
7714
7720
  eventIndex: 0,
7715
7721
  events: getEvents(child),
@@ -7719,6 +7725,7 @@ var require_lazy_result = __commonJS({
7719
7725
  descended = true;
7720
7726
  break;
7721
7727
  }
7728
+ visitNode.indexes[iterator] += 1;
7722
7729
  }
7723
7730
  if (descended) continue;
7724
7731
  visit.iterator = 0;
@@ -7881,7 +7888,7 @@ var require_processor = __commonJS({
7881
7888
  var Root2 = require_root();
7882
7889
  var Processor = class {
7883
7890
  constructor(plugins = []) {
7884
- this.version = "8.5.24";
7891
+ this.version = "8.5.31";
7885
7892
  this.plugins = this.normalize(plugins);
7886
7893
  }
7887
7894
  normalize(plugins) {
@@ -7965,7 +7972,7 @@ var require_postcss = __commonJS({
7965
7972
  console.warn(
7966
7973
  name + ": postcss.plugin was deprecated. Migration guide:\nhttps://evilmartians.com/chronicles/postcss-8-plugin-migration"
7967
7974
  );
7968
- if (process.env.LANG && process.env.LANG.startsWith("cn")) {
7975
+ if (process.env.LANG && process.env.LANG.startsWith("zh")) {
7969
7976
  console.warn(
7970
7977
  name + ": \u91CC\u9762 postcss.plugin \u88AB\u5F03\u7528. \u8FC1\u79FB\u6307\u5357:\nhttps://www.w3ctech.com/topic/2226"
7971
7978
  );
@@ -8672,7 +8679,6 @@ var require_sanitize_html = __commonJS({
8672
8679
  var htmlparser = (init_dist6(), __toCommonJS(dist_exports3));
8673
8680
  var escapeStringRegexp = require_escape_string_regexp();
8674
8681
  var { isPlainObject } = require_is_plain_object();
8675
- var deepmerge = require_cjs();
8676
8682
  var parseSrcset = require_parse_srcset();
8677
8683
  var { parse: postcssParse } = require_postcss();
8678
8684
  var { naughtyHref: launderNaughtyHref } = require_launder();
@@ -8696,6 +8702,15 @@ var require_sanitize_html = __commonJS({
8696
8702
  "set"
8697
8703
  ];
8698
8704
  var alwaysUrlAttributes = ["href"];
8705
+ var severities = ["debug", "info", "warn", "error"];
8706
+ function loggerFor(options) {
8707
+ const source = options && options.logger || console;
8708
+ const logger = {};
8709
+ for (const severity of severities) {
8710
+ logger[severity] = typeof source[severity] === "function" ? (...args) => source[severity](...args) : (...args) => console[severity](...args);
8711
+ }
8712
+ return logger;
8713
+ }
8699
8714
  function each(obj, cb) {
8700
8715
  if (obj) {
8701
8716
  Object.keys(obj).forEach(function(key) {
@@ -8706,6 +8721,21 @@ var require_sanitize_html = __commonJS({
8706
8721
  function has(obj, key) {
8707
8722
  return {}.hasOwnProperty.call(obj, key);
8708
8723
  }
8724
+ function mergeClasses(specific, wildcard) {
8725
+ if (!Array.isArray(specific) || !Array.isArray(wildcard)) {
8726
+ return wildcard;
8727
+ }
8728
+ return specific.concat(wildcard);
8729
+ }
8730
+ function mergeStyles(specific, wildcard) {
8731
+ const merged = /* @__PURE__ */ Object.create(null);
8732
+ for (const rules of [specific, wildcard]) {
8733
+ for (const prop of Object.keys(rules)) {
8734
+ merged[prop] = has(merged, prop) ? merged[prop].concat(rules[prop]) : rules[prop];
8735
+ }
8736
+ }
8737
+ return merged;
8738
+ }
8709
8739
  function filter2(a, cb) {
8710
8740
  const n = [];
8711
8741
  each(a, function(v) {
@@ -8733,10 +8763,13 @@ var require_sanitize_html = __commonJS({
8733
8763
  }
8734
8764
  module2.exports = sanitizeHtml2;
8735
8765
  var VALID_HTML_ATTRIBUTE_NAME = /^[^\0\t\n\f\r /<=>]+$/;
8736
- function sanitizeHtml2(html, options, _recursing) {
8766
+ var MAX_IFRAME_FALLBACK_DEPTH = 8;
8767
+ function sanitizeHtml2(html, options, _fallback) {
8737
8768
  if (html == null) {
8738
8769
  return "";
8739
8770
  }
8771
+ const baseDepth = _fallback && _fallback.depth || 0;
8772
+ const iframeFallbackDepth = _fallback && _fallback.iframeFallbackDepth || 0;
8740
8773
  if (typeof html === "number") {
8741
8774
  html = html.toString();
8742
8775
  }
@@ -8765,19 +8798,15 @@ var require_sanitize_html = __commonJS({
8765
8798
  }
8766
8799
  options = Object.assign({}, sanitizeHtml2.defaults, options);
8767
8800
  options.parser = Object.assign({}, htmlParserDefaults, options.parser);
8801
+ const logger = loggerFor(options);
8768
8802
  const tagAllowed = function(name) {
8769
8803
  return options.allowedTags === false || (options.allowedTags || []).indexOf(name) > -1;
8770
8804
  };
8771
8805
  vulnerableTags.forEach(function(tag) {
8772
8806
  if (tagAllowed(tag) && !options.allowVulnerableTags) {
8773
- console.warn(`
8774
-
8775
- \u26A0\uFE0F Your \`allowedTags\` option includes, \`${tag}\`, which is inherently
8776
- vulnerable to XSS attacks. Please remove it from \`allowedTags\`.
8777
- Or, to disable this warning, add the \`allowVulnerableTags\` option
8778
- and ensure you are accounting for this risk.
8779
-
8780
- `);
8807
+ logger.warn(
8808
+ `Your \`allowedTags\` option includes \`${tag}\`, which is inherently vulnerable to XSS attacks. Please remove it from \`allowedTags\`, or, to disable this warning, add the \`allowVulnerableTags\` option and ensure you are accounting for this risk.`
8809
+ );
8781
8810
  }
8782
8811
  });
8783
8812
  const nonTextTagsArray = options.nonTextTags || [
@@ -8857,6 +8886,8 @@ and ensure you are accounting for this risk.
8857
8886
  let transformMap;
8858
8887
  let skipText;
8859
8888
  let skipTextDepth;
8889
+ let rawTextEnd;
8890
+ let skipRawText;
8860
8891
  let addedText = false;
8861
8892
  initializeState();
8862
8893
  const parser = new htmlparser.Parser({
@@ -8864,9 +8895,13 @@ and ensure you are accounting for this risk.
8864
8895
  if (options.onOpenTag) {
8865
8896
  options.onOpenTag(name, attribs);
8866
8897
  }
8898
+ updateRawTextRegion();
8867
8899
  if (options.enforceHtmlBoundary && name === "html") {
8868
8900
  initializeState();
8869
8901
  }
8902
+ if (skipRawText) {
8903
+ return;
8904
+ }
8870
8905
  if (skipText) {
8871
8906
  skipTextDepth++;
8872
8907
  return;
@@ -8902,6 +8937,9 @@ and ensure you are accounting for this risk.
8902
8937
  if (nonTextTagsArray.indexOf(name) !== -1) {
8903
8938
  skipText = true;
8904
8939
  skipTextDepth = 1;
8940
+ if (frame.tag.toLowerCase() === "noscript") {
8941
+ rawTextEnd = findRawTextEnd("noscript", parser.endIndex + 1);
8942
+ }
8905
8943
  }
8906
8944
  }
8907
8945
  }
@@ -8977,6 +9015,10 @@ and ensure you are accounting for this risk.
8977
9015
  return;
8978
9016
  }
8979
9017
  }
9018
+ if (name === "meta" && a.toLowerCase() === "content" && isRefresh(attribs) && naughtyRefresh(value)) {
9019
+ delete frame.attribs[a];
9020
+ return;
9021
+ }
8980
9022
  if (name === "script" && a === "src") {
8981
9023
  let allowed = true;
8982
9024
  try {
@@ -9025,7 +9067,7 @@ and ensure you are accounting for this risk.
9025
9067
  try {
9026
9068
  let parsed = parseSrcset(value);
9027
9069
  parsed.forEach(function(value2) {
9028
- if (naughtyHref(a, value2.url)) {
9070
+ if (naughtyHref(name, value2.url)) {
9029
9071
  value2.evil = true;
9030
9072
  }
9031
9073
  });
@@ -9062,7 +9104,7 @@ and ensure you are accounting for this risk.
9062
9104
  if (allowedSpecificClasses && allowedWildcardClasses) {
9063
9105
  value = filterClasses(
9064
9106
  value,
9065
- deepmerge(allowedSpecificClasses, allowedWildcardClasses),
9107
+ mergeClasses(allowedSpecificClasses, allowedWildcardClasses),
9066
9108
  allowedClassesGlobs
9067
9109
  );
9068
9110
  } else {
@@ -9092,7 +9134,9 @@ and ensure you are accounting for this risk.
9092
9134
  }
9093
9135
  } catch (e) {
9094
9136
  if (typeof window !== "undefined") {
9095
- console.warn('Failed to parse "' + name + " {" + value + `}", If you're running this in a browser, we recommend to disable style parsing: options.parseStyleAttributes: false, since this only works in a node environment due to a postcss dependency, More info: https://github.com/apostrophecms/sanitize-html/issues/547`);
9137
+ logger.warn(
9138
+ `Failed to parse "${name} {${value}}". If you are running this in a browser, we recommend disabling style parsing with the parseStyleAttributes option, since it only works in a node environment due to a postcss dependency. More info: https://github.com/apostrophecms/sanitize-html/issues/547`
9139
+ );
9096
9140
  }
9097
9141
  delete frame.attribs[a];
9098
9142
  return;
@@ -9133,7 +9177,8 @@ and ensure you are accounting for this risk.
9133
9177
  frame.openingTagLength = result.length - frame.tagPosition;
9134
9178
  },
9135
9179
  ontext: function(text) {
9136
- if (skipText) {
9180
+ updateRawTextRegion();
9181
+ if (skipText || skipRawText) {
9137
9182
  return;
9138
9183
  }
9139
9184
  const lastFrame = stack[stack.length - 1];
@@ -9152,6 +9197,19 @@ and ensure you are accounting for this risk.
9152
9197
  } else {
9153
9198
  result += escapeHtml2(text, false);
9154
9199
  }
9200
+ } else if (
9201
+ // htmlparser2 treats <iframe> as a raw-text element, so markup inside
9202
+ // (including after an unclosed <iframe>) arrives as a single text node.
9203
+ // When the iframe is discarded, re-sanitize that fallback markup as HTML
9204
+ // instead of escaping it as plain text (issue #5550).
9205
+ // The nested call continues from the current depth so that
9206
+ // `nestingLimit` applies to the whole output.
9207
+ tag === "iframe" && !tagAllowed(tag) && options.disallowedTagsMode === "discard" && iframeFallbackDepth < MAX_IFRAME_FALLBACK_DEPTH
9208
+ ) {
9209
+ result += sanitizeHtml2(text, options, {
9210
+ depth,
9211
+ iframeFallbackDepth: iframeFallbackDepth + 1
9212
+ });
9155
9213
  } else if (!addedText) {
9156
9214
  const escaped = escapeHtml2(text, false);
9157
9215
  if (options.textFilter) {
@@ -9169,10 +9227,19 @@ and ensure you are accounting for this risk.
9169
9227
  if (options.onCloseTag) {
9170
9228
  options.onCloseTag(name, isImplied);
9171
9229
  }
9172
- if (skipText) {
9230
+ updateRawTextRegion();
9231
+ if (skipRawText) {
9232
+ const lastFrame = stack[stack.length - 1];
9233
+ if (!lastFrame || lastFrame.tag !== name) {
9234
+ return;
9235
+ }
9236
+ } else if (skipText) {
9173
9237
  skipTextDepth--;
9174
9238
  if (!skipTextDepth) {
9175
9239
  skipText = false;
9240
+ if (rawTextEnd !== null) {
9241
+ skipRawText = true;
9242
+ }
9176
9243
  } else {
9177
9244
  return;
9178
9245
  }
@@ -9250,12 +9317,26 @@ and ensure you are accounting for this risk.
9250
9317
  return result;
9251
9318
  function initializeState() {
9252
9319
  result = "";
9253
- depth = 0;
9320
+ depth = baseDepth;
9254
9321
  stack = [];
9255
9322
  skipMap = {};
9256
9323
  transformMap = {};
9257
9324
  skipText = false;
9258
9325
  skipTextDepth = 0;
9326
+ rawTextEnd = null;
9327
+ skipRawText = false;
9328
+ }
9329
+ function updateRawTextRegion() {
9330
+ if (rawTextEnd !== null && parser.startIndex >= rawTextEnd) {
9331
+ rawTextEnd = null;
9332
+ skipRawText = false;
9333
+ }
9334
+ }
9335
+ function findRawTextEnd(tagName, from) {
9336
+ const re = new RegExp("</" + tagName + "[\\t\\n\\f\\r />]", "ig");
9337
+ re.lastIndex = from;
9338
+ const match = re.exec(html);
9339
+ return match ? match.index : Infinity;
9259
9340
  }
9260
9341
  function escapeHtml2(s, quote) {
9261
9342
  if (typeof s !== "string") {
@@ -9280,13 +9361,83 @@ and ensure you are accounting for this risk.
9280
9361
  allowProtocolRelative: options.allowProtocolRelative
9281
9362
  });
9282
9363
  }
9364
+ function isRefresh(attribs) {
9365
+ return Object.keys(attribs).some(function(a) {
9366
+ return a.toLowerCase() === "http-equiv" && String(attribs[a]).trim().toLowerCase() === "refresh";
9367
+ });
9368
+ }
9369
+ function naughtyRefresh(content) {
9370
+ const input = String(content);
9371
+ const isWhitespace2 = function(c) {
9372
+ return c === " " || c === " " || c === "\n" || c === "\f" || c === "\r";
9373
+ };
9374
+ let position = 0;
9375
+ const skipWhitespace = function() {
9376
+ while (position < input.length && isWhitespace2(input[position])) {
9377
+ position++;
9378
+ }
9379
+ };
9380
+ const lowerAt = function(i) {
9381
+ return (input[i] || "").toLowerCase();
9382
+ };
9383
+ skipWhitespace();
9384
+ const timeStart = position;
9385
+ while (position < input.length && /[0-9.]/.test(input[position])) {
9386
+ position++;
9387
+ }
9388
+ if (position === timeStart) {
9389
+ return true;
9390
+ }
9391
+ if (position < input.length) {
9392
+ const c = input[position];
9393
+ if (c !== ";" && c !== "," && !isWhitespace2(c)) {
9394
+ return true;
9395
+ }
9396
+ skipWhitespace();
9397
+ if (input[position] === ";" || input[position] === ",") {
9398
+ position++;
9399
+ }
9400
+ skipWhitespace();
9401
+ }
9402
+ if (position >= input.length) {
9403
+ return false;
9404
+ }
9405
+ let url = input.slice(position);
9406
+ let quoted = true;
9407
+ if (lowerAt(position) === "u") {
9408
+ quoted = false;
9409
+ if (lowerAt(position + 1) === "r" && lowerAt(position + 2) === "l") {
9410
+ position += 3;
9411
+ skipWhitespace();
9412
+ if (input[position] === "=") {
9413
+ position++;
9414
+ skipWhitespace();
9415
+ quoted = true;
9416
+ }
9417
+ }
9418
+ }
9419
+ if (quoted) {
9420
+ const quote = input[position];
9421
+ if (quote === '"' || quote === "'") {
9422
+ position++;
9423
+ }
9424
+ url = input.slice(position);
9425
+ if (quote === '"' || quote === "'") {
9426
+ const end = url.indexOf(quote);
9427
+ if (end !== -1) {
9428
+ url = url.slice(0, end);
9429
+ }
9430
+ }
9431
+ }
9432
+ return naughtyHref("meta", url);
9433
+ }
9283
9434
  function animatesUrlAttribute(name, attribs) {
9284
- if (svgAnimationTags.indexOf(name.toLowerCase()) === -1) {
9435
+ if (svgAnimationTags.indexOf(localPart(name)) === -1) {
9285
9436
  return false;
9286
9437
  }
9287
9438
  const schemeCheckedAttributes = options.allowedSchemesAppliedToAttributes || [];
9288
9439
  return Object.keys(attribs || {}).some(function(attributeName) {
9289
- if (attributeName.toLowerCase() !== "attributename") {
9440
+ if (localPart(attributeName) !== "attributename") {
9290
9441
  return false;
9291
9442
  }
9292
9443
  const target = (attribs[attributeName] || "").trim().toLowerCase();
@@ -9294,6 +9445,10 @@ and ensure you are accounting for this risk.
9294
9445
  return alwaysUrlAttributes.indexOf(localName2) !== -1 || schemeCheckedAttributes.indexOf(target) !== -1 || schemeCheckedAttributes.indexOf(localName2) !== -1;
9295
9446
  });
9296
9447
  }
9448
+ function localPart(name) {
9449
+ const lower = name.toLowerCase();
9450
+ return lower.slice(lower.lastIndexOf(":") + 1);
9451
+ }
9297
9452
  function parseUrl(value) {
9298
9453
  value = value.replace(/^(\w+:)?\s*[\\/]\s*[\\/]/, "$1//");
9299
9454
  if (value.startsWith("relative:")) {
@@ -9317,7 +9472,7 @@ and ensure you are accounting for this risk.
9317
9472
  const astRules = abstractSyntaxTree.nodes[0];
9318
9473
  let selectedRule;
9319
9474
  if (allowedStyles[astRules.selector] && allowedStyles["*"]) {
9320
- selectedRule = deepmerge(
9475
+ selectedRule = mergeStyles(
9321
9476
  allowedStyles[astRules.selector],
9322
9477
  allowedStyles["*"]
9323
9478
  );
@@ -58587,6 +58742,21 @@ var Publisher = class {
58587
58742
  // `clientOrigin` is overridden only by the agent serving a client its own
58588
58743
  // local actor: standalone, the published document cannot name a loopback
58589
58744
  // address, but a client already talking to that address can be told it.
58745
+ // Whether follows wait for the owner, from the setting as saved now: this
58746
+ // config is a copy taken when the agent started, and the saved one may have
58747
+ // changed since.
58748
+ approvesByHand() {
58749
+ const saved = this.store?.getConfig?.() || {};
58750
+ return followsNeedApproval({
58751
+ ...this.config,
58752
+ ..."autoAcceptFollows" in saved ? { autoAcceptFollows: saved.autoAcceptFollows } : {},
58753
+ ..."approveJoins" in saved ? { approveJoins: saved.approveJoins } : {}
58754
+ });
58755
+ }
58756
+ // Whether the profile last published says what the setting says.
58757
+ profileAgrees() {
58758
+ return this.store.read("published.json", {}).approvesByHand === this.approvesByHand();
58759
+ }
58590
58760
  actorDocFor({
58591
58761
  priv = false,
58592
58762
  moderators = null,
@@ -58602,7 +58772,7 @@ var Publisher = class {
58602
58772
  publicKeyPem: this.publicKeyPem,
58603
58773
  movedTo,
58604
58774
  kind: this.config.kind,
58605
- approveJoins: followsNeedApproval(this.config),
58775
+ approveJoins: this.approvesByHand(),
58606
58776
  assertionKey: this.assertionKey,
58607
58777
  summary: this.config.summary || null,
58608
58778
  icon: this.config.icon || null,
@@ -58738,12 +58908,11 @@ var Publisher = class {
58738
58908
  const updated = await this.announceProfileChange(actor, { force });
58739
58909
  await this.ensurePrivateAcls();
58740
58910
  const unreachable = await this.verifyPublicSurface();
58741
- if (!unreachable.length) {
58742
- this.store.write(
58743
- "published.json",
58744
- { ...this.store.read("published.json", {}), surfaceDigest: surface }
58745
- );
58746
- }
58911
+ this.store.write("published.json", {
58912
+ ...this.store.read("published.json", {}),
58913
+ ...!unreachable.length ? { surfaceDigest: surface } : {},
58914
+ approvesByHand: !!actorDoc2.manuallyApprovesFollowers
58915
+ });
58747
58916
  const pubName = publicHandle(this.config);
58748
58917
  const pubHost = this.config.gateway?.frontActor ? new URL(this.config.gateway.frontActor).host : host;
58749
58918
  this.log(this.config.gateway?.frontActor || webfingerHost(urls.base) ? `profile published: @${pubName}@${pubHost} \u2192 ${urls.actor}` : `profile published \u2192 ${urls.actor} \u2014 NOT discoverable as @${pubName}@${pubHost}: this pod is suffixed, and WebFinger is only answered for a subdomained pod`);
@@ -59446,8 +59615,8 @@ async function isGone(intake, url) {
59446
59615
  let res;
59447
59616
  try {
59448
59617
  res = await intake.deliverer.signedFetch(url, { headers: { accept: ACCEPT_AP2 } });
59449
- } catch {
59450
- return null;
59618
+ } catch (e) {
59619
+ return e?.answered && (e.status === 404 || e.status === 410) ? true : null;
59451
59620
  }
59452
59621
  if (res.status === 404 || res.status === 410) return true;
59453
59622
  if (res.status < 400) {
@@ -65492,8 +65661,17 @@ var Intake = class {
65492
65661
  rec.last = String(message || "").slice(0, 200);
65493
65662
  all[url] = rec;
65494
65663
  this.store.write(ATTEMPTS_DOC, all);
65664
+ if (rec.n === 2) this._trouble("failing", url, message);
65495
65665
  return rec.n;
65496
65666
  }
65667
+ // Mail that could not be read, as against mail refused: a second failed try,
65668
+ // or a piece set aside after its last. `onTrouble` tells the owner.
65669
+ _trouble(kind, url, reason) {
65670
+ try {
65671
+ this.onTrouble?.({ kind, url, reason: String(reason || "").slice(0, 200), at: (/* @__PURE__ */ new Date()).toISOString() });
65672
+ } catch {
65673
+ }
65674
+ }
65497
65675
  _clearAttempt(url) {
65498
65676
  const all = this.store.read(ATTEMPTS_DOC, {});
65499
65677
  if (!all[url]) return;
@@ -65537,12 +65715,24 @@ var Intake = class {
65537
65715
  this._draining = null;
65538
65716
  if (this._drainAgain) {
65539
65717
  this._drainAgain = false;
65540
- const t = setTimeout(() => this.drain().catch((e) => this.log(`drain: ${e.message}`)), CHAIN_GAP_MS);
65541
- t.unref?.();
65718
+ this._chainTimer = setTimeout(() => {
65719
+ this._chainTimer = null;
65720
+ this.drain().catch((e) => this.log(`drain: ${e.message}`));
65721
+ }, CHAIN_GAP_MS);
65722
+ this._chainTimer.unref?.();
65542
65723
  }
65543
65724
  });
65544
65725
  return this._draining;
65545
65726
  }
65727
+ // Resolves once no sweep is running and none is waiting to follow on, so a
65728
+ // browser worker can stay up until the round it started is done.
65729
+ async settled() {
65730
+ while (this._draining || this._chainTimer) {
65731
+ if (this._draining) await this._draining.catch(() => {
65732
+ });
65733
+ else await new Promise((r) => setTimeout(r, 500));
65734
+ }
65735
+ }
65546
65736
  // Discard the content waiting in the inbox from before `before`, on the
65547
65737
  // owner's say-so — the admin page asks, this does it. NOT a blind sweep:
65548
65738
  // every item is read, because the type is the only thing that decides its
@@ -65745,6 +65935,7 @@ var Intake = class {
65745
65935
  this.log(`inbox batch ${url} attempt ${n}/${MAX_ITEM_ATTEMPTS}: ${e.message}`);
65746
65936
  if (n >= MAX_ITEM_ATTEMPTS) {
65747
65937
  this.store.addDeadLetter({ inboxUrl: url, reason: `failed ${n}x: ${e.message}`, activity: null });
65938
+ this._trouble("set-aside", url, e.message);
65748
65939
  pending.push(url);
65749
65940
  }
65750
65941
  }
@@ -65768,6 +65959,7 @@ var Intake = class {
65768
65959
  this.log(`inbox item ${url} attempt ${n}/${MAX_ITEM_ATTEMPTS}: ${e.message}`);
65769
65960
  if (n >= MAX_ITEM_ATTEMPTS) {
65770
65961
  this.store.addDeadLetter({ inboxUrl: url, reason: `failed ${n}x: ${e.message}`, activity: trimActivity(at.activity) });
65962
+ this._trouble("set-aside", url, e.message);
65771
65963
  pending.push(url);
65772
65964
  }
65773
65965
  }
@@ -65788,6 +65980,11 @@ var Intake = class {
65788
65980
  const read2 = raw ? await readLenient(raw) : null;
65789
65981
  if (read2?.degraded) this.log(`inbox item ${url} grounded to read: ${read2.degraded}`);
65790
65982
  const activity = at.activity = read2?.view ?? read2?.doc ?? null;
65983
+ const from = typeof activity?.actor === "string" ? activity.actor : activity?.actor?.id;
65984
+ if (from && this.checkLetters?.actor && from === this.checkLetters.actor) {
65985
+ await this.checkLetters.taken(String(activity.id || ""));
65986
+ return;
65987
+ }
65791
65988
  if (read2?.graph) {
65792
65989
  const failure = await checkShapes(read2.graph);
65793
65990
  if (failure) {
@@ -65842,6 +66039,7 @@ var Intake = class {
65842
66039
  const batch = await this._batchEntries(url);
65843
66040
  if (!batch) {
65844
66041
  this.store.addDeadLetter({ inboxUrl: url, reason: "unreadable batch", activity: null });
66042
+ this._trouble("set-aside", url, "unreadable batch");
65845
66043
  return { entries: 1, complete: true };
65846
66044
  }
65847
66045
  let complete = true;
@@ -65861,6 +66059,7 @@ var Intake = class {
65861
66059
  this.log(`inbox batch entry ${key} attempt ${n}/${MAX_ITEM_ATTEMPTS}: ${err.message}`);
65862
66060
  if (n >= MAX_ITEM_ATTEMPTS) {
65863
66061
  this.store.addDeadLetter({ inboxUrl: key, reason: `failed ${n}x: ${err.message}`, activity: trimActivity(at.activity) });
66062
+ this._trouble("set-aside", key, err.message);
65864
66063
  } else {
65865
66064
  complete = false;
65866
66065
  }
@@ -65899,6 +66098,7 @@ var Intake = class {
65899
66098
  this.log(`held delivery ${key} attempt ${n}/${MAX_ITEM_ATTEMPTS}: ${err.message}`);
65900
66099
  if (n >= MAX_ITEM_ATTEMPTS) {
65901
66100
  this.store.addDeadLetter({ inboxUrl: key, reason: `failed ${n}x: ${err.message}`, activity: trimActivity(at.activity) });
66101
+ this._trouble("set-aside", key, err.message);
65902
66102
  done.push(e.name);
65903
66103
  }
65904
66104
  }
@@ -69181,9 +69381,10 @@ async function handle4(api, ctx) {
69181
69381
  const { req, res, pathname, url, send } = ctx;
69182
69382
  if (pathname === "/api/v1/timelines/home" || pathname === "/api/v1/timelines/public" || pathname === "/api/v1/trends/statuses") {
69183
69383
  const localOnly = pathname === "/api/v1/timelines/public" && url.searchParams.get("local") === "true";
69384
+ const hideTags = pathname === "/api/v1/timelines/home" && api.store.getConfig?.()?.tagsInHome === false;
69184
69385
  const all = api.store.getStatuses();
69185
69386
  const muted = new Set(api.store.getMuted().actors);
69186
- let items = all.filter((s) => s.kind !== "remote" && s.kind !== "mention").filter((s) => !s.direct && s.visibility !== "direct").filter((s) => !muted.has(s.actor) && !muted.has(s.via)).filter((s) => !localOnly || s.kind === "post" && s.visibility !== "private").sort((a, b) => String(b.published || "").localeCompare(String(a.published || "")));
69387
+ let items = all.filter((s) => s.kind !== "remote" && s.kind !== "mention").filter((s) => !s.direct && s.visibility !== "direct").filter((s) => !muted.has(s.actor) && !muted.has(s.via)).filter((s) => !hideTags || s.kind !== "tag" || s.sourceAccts?.length).filter((s) => !localOnly || s.kind === "post" && s.visibility !== "private").sort((a, b) => String(b.published || "").localeCompare(String(a.published || "")));
69187
69388
  const { items: page2, headers } = api.page(items, url);
69188
69389
  return send(200, page2.map((s) => api.statusOrBoost(s, { all })), headers);
69189
69390
  }
@@ -70193,8 +70394,9 @@ var TagFeed = class {
70193
70394
  if (Array.isArray(patch.tags)) clean.tags = patch.tags.map((t) => String(t).replace(/^#/, "").trim()).filter(Boolean);
70194
70395
  if (patch.intervalMin) clean.intervalMin = Math.max(5, Number(patch.intervalMin) || DEFAULTS2.intervalMin);
70195
70396
  this.store.write("tagfeed.json", { ...this.config(), ...clean });
70397
+ const sweeping = this.stopped === false;
70196
70398
  this.stop();
70197
- this.start();
70399
+ if (sweeping) this.start();
70198
70400
  return this.config();
70199
70401
  }
70200
70402
  // `lastSweptAt` and `onSwept` are for a browser worker the browser stops and
@@ -71034,6 +71236,7 @@ var RelayDeliverer = class extends Deliverer {
71034
71236
  if (status2 >= 400) {
71035
71237
  const err = new Error(`${method} ${url} \u2192 ${status2}`);
71036
71238
  err.status = status2;
71239
+ err.answered = true;
71037
71240
  if (r0.retryAfter) {
71038
71241
  const secs = Number(r0.retryAfter);
71039
71242
  const ms = Number.isFinite(secs) ? Math.max(secs, 1) * 1e3 : Math.max(Date.parse(r0.retryAfter) - Date.now(), 1e3);
@@ -71418,7 +71621,7 @@ var ImportWorker = class {
71418
71621
 
71419
71622
  // web/app/admin-facade.mjs
71420
71623
  var PERMANENT_CONFIG = ["handle", "remotePod", "issuer", "root", "kind"];
71421
- var WIRE_CONFIG = ["name", "summary", "icon", "image", "fields", "aliases"];
71624
+ var WIRE_CONFIG = ["name", "summary", "icon", "image", "fields", "aliases", "autoAcceptFollows"];
71422
71625
  var ADMIN_PATHS = /* @__PURE__ */ new Set([
71423
71626
  "/status",
71424
71627
  "/config",
@@ -71427,6 +71630,7 @@ var ADMIN_PATHS = /* @__PURE__ */ new Set([
71427
71630
  "/rotate-key",
71428
71631
  "/import",
71429
71632
  "/deadletter",
71633
+ "/trouble",
71430
71634
  "/blocks",
71431
71635
  "/profiles",
71432
71636
  "/modqueue",
@@ -71528,6 +71732,7 @@ var AdminFacade = class {
71528
71732
  fields: cfg.fields || [],
71529
71733
  aliases: cfg.aliases || [],
71530
71734
  autoAcceptFollows: cfg.autoAcceptFollows !== false,
71735
+ tagsInHome: cfg.tagsInHome !== false,
71531
71736
  quiescedAt: cfg.quiescedAt || null,
71532
71737
  movedTo: cfg.movedTo || null,
71533
71738
  mode: a.status().mode,
@@ -71586,8 +71791,7 @@ var AdminFacade = class {
71586
71791
  return json2(200, []);
71587
71792
  // person: no moderation queue
71588
71793
  case "/log":
71589
- return json2(200, { lines: [] });
71590
- // no local log file in the browser
71794
+ return json2(200, { lines: await a.recentLog?.() || [] });
71591
71795
  case "/fediacct":
71592
71796
  return json2(200, { accounts: a.fediaccts?.status() || [] });
71593
71797
  case "/import":
@@ -71624,6 +71828,13 @@ var AdminFacade = class {
71624
71828
  return json2(400, { error: "expected JSON body" });
71625
71829
  }
71626
71830
  switch (p) {
71831
+ // The owner has opened the dead letters: the bar stops saying so.
71832
+ case "/trouble": {
71833
+ if (body.seen !== true) return json2(400, { error: "seen: true is the only thing to say here" });
71834
+ a.troubleSeen?.();
71835
+ await a.store.flush();
71836
+ return json2(200, { ok: true, trouble: a.status().trouble });
71837
+ }
71627
71838
  case "/config": {
71628
71839
  const fixed = PERMANENT_CONFIG.filter((k) => k in body);
71629
71840
  if (fixed.length) return json2(400, { error: `${fixed.join(", ")} cannot be changed \u2014 that is the identity itself` });
@@ -71643,6 +71854,7 @@ var AdminFacade = class {
71643
71854
  cfg.fields = (Array.isArray(body.fields) ? body.fields : []).filter((f) => f?.name?.trim()).map((f) => ({ name: String(f.name).trim(), value: String(f.value ?? "").trim() }));
71644
71855
  }
71645
71856
  if ("autoAcceptFollows" in body) cfg.autoAcceptFollows = !!body.autoAcceptFollows;
71857
+ if ("tagsInHome" in body) cfg.tagsInHome = !!body.tagsInHome;
71646
71858
  const republish = WIRE_CONFIG.some((k) => k in body);
71647
71859
  a.store.setConfig(cfg);
71648
71860
  if ("autoAcceptFollows" in body && a.publisher) a.publisher.config.autoAcceptFollows = cfg.autoAcceptFollows;
@@ -71952,23 +72164,20 @@ var AdminFacade = class {
71952
72164
  await a.store.flush();
71953
72165
  return json2(200, { ok: true });
71954
72166
  }
71955
- // Bluesky settings: pause/resume the mirror, and move the credential
71956
- // between this browser and the pod. Cross-post is default-on, not toggled.
72167
+ // Bluesky settings: pause/resume the mirror. Cross-post is default-on, not
72168
+ // toggled. The credential is kept on the pod, with nowhere else to move.
71957
72169
  case "/atproto": {
71958
72170
  if (!a.atproto?.connected()) return json2(400, { error: "no bluesky account connected" });
72171
+ if ("storage" in body) return json2(400, { error: "a connected account is kept on your pod" });
71959
72172
  if ("feedPaused" in body) {
71960
72173
  a.atproto.setFeedPaused(!!body.feedPaused);
71961
72174
  a.restartBsky?.();
71962
72175
  }
71963
- if ("storage" in body) {
71964
- if (!["pod", "browser"].includes(body.storage)) return json2(400, { error: "storage must be pod or browser" });
71965
- a.atproto.setStorage(body.storage);
71966
- }
71967
72176
  return json2(200, { ok: true, ...a.atproto.status() });
71968
72177
  }
71969
72178
  // Connect a fediverse account on another server: start its OAuth at that
71970
72179
  // server. The browser is then sent to `authorize`; the code comes back to
71971
- // GET /fediacct/callback (above). The token never touches the pod.
72180
+ // GET /fediacct/callback (above). The token is kept on the pod.
71972
72181
  case "/fediacct/connect": {
71973
72182
  if (!body.host) return json2(400, { error: "the address of the server is required" });
71974
72183
  try {
@@ -71985,14 +72194,11 @@ var AdminFacade = class {
71985
72194
  a.restartAccts?.();
71986
72195
  return json2(200, { ok: true });
71987
72196
  }
71988
- // Pause/resume the mirror, or move the credential between browser and pod.
72197
+ // Pause/resume the mirror. The credential is kept on the pod.
71989
72198
  case "/fediacct": {
71990
72199
  if (!body.id) return json2(400, { error: "id required" });
71991
- let row;
71992
- if ("storage" in body) {
71993
- if (!["pod", "browser"].includes(body.storage)) return json2(400, { error: "storage must be pod or browser" });
71994
- row = a.fediaccts.setStorage(body.id, body.storage);
71995
- } else row = a.fediaccts.setEnabled(body.id, !!body.enabled);
72200
+ if ("storage" in body) return json2(400, { error: "a connected account is kept on your pod" });
72201
+ const row = a.fediaccts.setEnabled(body.id, !!body.enabled);
71996
72202
  if (!row) return json2(404, { error: "no such account" });
71997
72203
  a.restartAccts?.();
71998
72204
  return json2(200, { ok: true, account: row });
@@ -72458,8 +72664,10 @@ var BrowserAtproto = class extends Atproto {
72458
72664
  }
72459
72665
  // The IndexedDB copy into memory, so read() stays synchronous as the base
72460
72666
  // class expects. The pod copy is already in the loaded store.
72461
- async load() {
72667
+ // `fromPod`: the pod's record read as well (agent.mjs says when).
72668
+ async load({ fromPod = false } = {}) {
72462
72669
  this._idbRec = await kvGet(IDB_KEY).catch(() => null);
72670
+ if (fromPod) await this.store.refresh?.([POD_DOC], { ifChanged: true }).catch((e) => this.log(`Bluesky account: ${e.message}`));
72463
72671
  return this;
72464
72672
  }
72465
72673
  // Whichever backend holds it, tagged with where it lives. The stamped-record
@@ -72467,15 +72675,13 @@ var BrowserAtproto = class extends Atproto {
72467
72675
  read() {
72468
72676
  let rec = null;
72469
72677
  let where = null;
72470
- if (this._idbRec) {
72678
+ const p = this.store.read(POD_DOC, null);
72679
+ if (p) {
72680
+ rec = p;
72681
+ where = "pod";
72682
+ } else if (this._idbRec) {
72471
72683
  rec = this._idbRec;
72472
72684
  where = "browser";
72473
- } else {
72474
- const p = this.store.read(POD_DOC, null);
72475
- if (p) {
72476
- rec = p;
72477
- where = "pod";
72478
- }
72479
72685
  }
72480
72686
  if (!rec) return null;
72481
72687
  if (rec.mintedFor && this.actorId && rec.mintedFor !== this.actorId) {
@@ -72484,25 +72690,25 @@ var BrowserAtproto = class extends Atproto {
72484
72690
  }
72485
72691
  return { ...rec, storage: where };
72486
72692
  }
72487
- // Write to the record's chosen backend (default browser), and clear the other
72488
- // so a moved credential leaves no copy behind.
72489
72693
  write(rec) {
72490
- const where = rec.storage || "browser";
72491
- const clean = { ...rec, storage: where };
72492
- if (where === "pod") {
72493
- this.store.write(POD_DOC, clean);
72494
- this.store.flush?.().catch(() => {
72495
- });
72496
- this._idbRec = null;
72497
- kvDel(IDB_KEY).catch(() => {
72498
- });
72499
- } else {
72500
- this._idbRec = clean;
72501
- kvPut(IDB_KEY, clean).catch((e) => this.log(`atproto persist: ${e.message}`));
72502
- this.store.write(POD_DOC, null);
72503
- this.store.flush?.().catch(() => {
72504
- });
72694
+ this.store.write(POD_DOC, { ...rec, storage: "pod" });
72695
+ this.store.flush?.().catch(() => {
72696
+ });
72697
+ }
72698
+ // A record still in this browser, onto the pod, and out of the browser once
72699
+ // the pod has it. One the pod already holds is newer there.
72700
+ async moveToPod() {
72701
+ const rec = this._idbRec;
72702
+ if (!rec || rec.mintedFor && this.actorId && rec.mintedFor !== this.actorId) return false;
72703
+ if (!this.store.read(POD_DOC, null)) {
72704
+ this.store.write(POD_DOC, { ...rec, storage: "pod" });
72705
+ if (!await this.store.commit()) return false;
72505
72706
  }
72707
+ this._idbRec = null;
72708
+ await kvDel(IDB_KEY).catch(() => {
72709
+ });
72710
+ this.log("the Bluesky account moved from this browser to the pod");
72711
+ return true;
72506
72712
  }
72507
72713
  status() {
72508
72714
  const rec = this.read();
@@ -72525,11 +72731,6 @@ var BrowserAtproto = class extends Atproto {
72525
72731
  const rec = this.read();
72526
72732
  if (rec) this.write({ ...rec, feedPaused: !!on });
72527
72733
  }
72528
- // Move the credential between backends (the Options menu's Storage control).
72529
- setStorage(where) {
72530
- const rec = this.read();
72531
- if (rec && (where === "pod" || where === "browser")) this.write({ ...rec, storage: where });
72532
- }
72533
72734
  async disconnect() {
72534
72735
  const rec = this.read();
72535
72736
  if (rec?.refreshJwt) {
@@ -72788,7 +72989,9 @@ var BrowserFediAccounts = class extends FediAccounts {
72788
72989
  this._recs = /* @__PURE__ */ new Map();
72789
72990
  this._apps = /* @__PURE__ */ new Map();
72790
72991
  }
72791
- async load() {
72992
+ // `fromPod`: the pod's records read as well (agent.mjs says when).
72993
+ async load({ fromPod = false } = {}) {
72994
+ if (fromPod) await this.store.refresh?.([POD_DOC2], { ifChanged: true }).catch((e) => this.log(`connected accounts: ${e.message}`));
72792
72995
  const all = await kvAll().catch(() => ({}));
72793
72996
  for (const [k, v] of Object.entries(all)) {
72794
72997
  if (k.startsWith("rec:")) this._recs.set(k.slice(4), v);
@@ -72813,15 +73016,13 @@ var BrowserFediAccounts = class extends FediAccounts {
72813
73016
  read(id) {
72814
73017
  let rec = null;
72815
73018
  let where = null;
72816
- if (this._recs.has(id)) {
73019
+ const p = this._podMap()[id];
73020
+ if (p) {
73021
+ rec = p;
73022
+ where = "pod";
73023
+ } else if (this._recs.has(id)) {
72817
73024
  rec = this._recs.get(id);
72818
73025
  where = "browser";
72819
- } else {
72820
- const p = this._podMap()[id];
72821
- if (p) {
72822
- rec = p;
72823
- where = "pod";
72824
- }
72825
73026
  }
72826
73027
  if (!rec) return null;
72827
73028
  if (rec.mintedFor && this.actorId && rec.mintedFor !== this.actorId) {
@@ -72831,27 +73032,28 @@ var BrowserFediAccounts = class extends FediAccounts {
72831
73032
  return { ...rec, storage: where };
72832
73033
  }
72833
73034
  write(rec) {
72834
- const where = rec.storage || "browser";
72835
- const clean = { ...rec, storage: where };
72836
- if (where === "pod") {
72837
- const map = { ...this._podMap(), [clean.id]: clean };
72838
- this.store.write(POD_DOC2, map);
72839
- this.store.flush?.().catch(() => {
72840
- });
72841
- this._recs.delete(clean.id);
72842
- kvDel("rec:" + clean.id).catch(() => {
73035
+ const clean = { ...rec, storage: "pod" };
73036
+ const map = { ...this._podMap(), [clean.id]: clean };
73037
+ this.store.write(POD_DOC2, map);
73038
+ this.store.flush?.().catch(() => {
73039
+ });
73040
+ }
73041
+ // The records still in this browser, onto the pod, and out of the browser
73042
+ // once the pod has them. One the pod already holds is newer there.
73043
+ async moveToPod() {
73044
+ const moving = [...this._recs.values()].filter((r) => !r.mintedFor || !this.actorId || r.mintedFor === this.actorId);
73045
+ if (!moving.length) return 0;
73046
+ const map = { ...this._podMap() };
73047
+ for (const r of moving) if (!(r.id in map)) map[r.id] = { ...r, storage: "pod" };
73048
+ this.store.write(POD_DOC2, map);
73049
+ if (!await this.store.commit()) return 0;
73050
+ for (const r of moving) {
73051
+ this._recs.delete(r.id);
73052
+ await kvDel("rec:" + r.id).catch(() => {
72843
73053
  });
72844
- } else {
72845
- this._recs.set(clean.id, clean);
72846
- kvPut("rec:" + clean.id, clean).catch((e) => this.log(`fediacct persist: ${e.message}`));
72847
- const map = this._podMap();
72848
- if (clean.id in map) {
72849
- delete map[clean.id];
72850
- this.store.write(POD_DOC2, map);
72851
- this.store.flush?.().catch(() => {
72852
- });
72853
- }
72854
73054
  }
73055
+ this.log(`${moving.length} connected account(s) moved from this browser to the pod`);
73056
+ return moving.length;
72855
73057
  }
72856
73058
  // Best-effort: tell the server to revoke our token (RFC 7009), so a
72857
73059
  // disconnect actually invalidates it there — not just locally. Mirrors
@@ -72904,15 +73106,6 @@ var BrowserFediAccounts = class extends FediAccounts {
72904
73106
  cooldownFor: Math.max(0, Math.round(((this.pausedUntil.get(r.host) || 0) - Date.now()) / 1e3))
72905
73107
  }));
72906
73108
  }
72907
- // Move a connection between backends (the Options menu's Storage control).
72908
- setStorage(id, where) {
72909
- const rec = this.read(id);
72910
- if (rec && (where === "pod" || where === "browser")) {
72911
- this.write({ ...rec, storage: where });
72912
- return this.roster().find((r) => r.id === id) || null;
72913
- }
72914
- return null;
72915
- }
72916
73109
  // One app registration per host, remembered — over IndexedDB.
72917
73110
  async appFor(host, redirectUri) {
72918
73111
  const cached = this._apps.get(host);
@@ -73088,8 +73281,11 @@ var AcctFeed = class {
73088
73281
  }
73089
73282
  return map;
73090
73283
  }
73284
+ // A server that does not say what is left (not every Mastodon-API server
73285
+ // does) is not taken to have nothing left.
73091
73286
  _rateGuard(id, handle7, res) {
73092
- const left = Number(res.headers.get("x-ratelimit-remaining"));
73287
+ const said = res.headers.get("x-ratelimit-remaining");
73288
+ const left = said == null || said.trim() === "" ? NaN : Number(said);
73093
73289
  if (!Number.isFinite(left) || left > RATE_FLOOR) return false;
73094
73290
  const reset = Date.parse(res.headers.get("x-ratelimit-reset") || "");
73095
73291
  const wait = Number.isFinite(reset) ? Math.max(0, reset - Date.now()) : BACKOFF_MIN_MS3;
@@ -73199,7 +73395,8 @@ var fail2 = () => {
73199
73395
  var promises_default = new Proxy({}, { get: () => fail2 });
73200
73396
 
73201
73397
  // lib/core/pod-only.mjs
73202
- var podOnly = (name) => name === "keys.json" || name === "lease.json" || name.startsWith("conn-");
73398
+ var CONNECTED = /* @__PURE__ */ new Set(["fediaccts.json", "atproto.json"]);
73399
+ var podOnly = (name) => name === "keys.json" || name === "lease.json" || name.startsWith("conn-") || CONNECTED.has(name);
73203
73400
 
73204
73401
  // lib/core/storage.mjs
73205
73402
  var LDP2 = Namespace("http://www.w3.org/ns/ldp#");
@@ -73761,6 +73958,41 @@ var BrowserAgent = class _BrowserAgent {
73761
73958
  // (lib/gateway/held-mail.mjs: /api/push). Named a moment later, once written,
73762
73959
  // so a burst is one call. One from another network is not pushed.
73763
73960
  static PUSH_AFTER_MS = 2e3;
73961
+ // Mail FediPod could not read: counted until its owner opens the dead
73962
+ // letters, shown in the bar meanwhile, and told to their phones at most
73963
+ // once an hour (lib/gateway/held-mail.mjs: /api/alert).
73964
+ static TROUBLE_ALERT_MS = 60 * 6e4;
73965
+ noteTrouble(t, { tell = true } = {}) {
73966
+ const had = this.store.read("trouble.json", null) || { count: 0 };
73967
+ const now = Date.now();
73968
+ const due = tell && now - (had.alertedAt ? Date.parse(had.alertedAt) : 0) >= _BrowserAgent.TROUBLE_ALERT_MS;
73969
+ const next = {
73970
+ ...had,
73971
+ count: (had.count || 0) + 1,
73972
+ firstAt: had.firstAt || t.at,
73973
+ lastAt: t.at,
73974
+ last: { kind: t.kind, reason: t.reason },
73975
+ ...due ? { alertedAt: new Date(now).toISOString() } : {}
73976
+ };
73977
+ this.store.write("trouble.json", next);
73978
+ this.log(`could not read mail (${t.kind}): ${t.reason}`);
73979
+ if (due) {
73980
+ const many = next.count === 1 ? "a piece of your mail" : `${next.count} pieces of your mail`;
73981
+ this.tellGateway("alert", { handle: this.doorKey, text: `Could not read ${many}. Show dead letters on your manage page lists what it gave up on.` }, { standing: false }).catch(() => {
73982
+ });
73983
+ }
73984
+ }
73985
+ // The owner has looked: the count starts again, and the hourly limit stands.
73986
+ troubleSeen() {
73987
+ const had = this.store.read("trouble.json", null);
73988
+ if (had?.count) {
73989
+ this.store.write("trouble.json", {
73990
+ count: 0,
73991
+ ...had.alertedAt ? { alertedAt: had.alertedAt } : {},
73992
+ ...had.letterMissing ? { letterMissing: had.letterMissing } : {}
73993
+ });
73994
+ }
73995
+ }
73764
73996
  notePush(n) {
73765
73997
  if (!this.gatewayApi || n?.bsky || n?.via) return;
73766
73998
  if (!Object.keys(this.store.read("webpush.json", { subs: {} }).subs || {}).length) return;
@@ -73821,6 +74053,10 @@ var BrowserAgent = class _BrowserAgent {
73821
74053
  const said = await this.tellGateway("here", { handle: this.doorKey, nextAt: this.store ? nextDue(this.store) : null });
73822
74054
  if (said?.status === 200) saveMeta(this.webId, { hereAt: Date.now() }).catch(() => {
73823
74055
  });
74056
+ if (said?.status === 200 && said.missingLetter && this.store?.read("trouble.json", null)?.letterMissing !== said.missingLetter) {
74057
+ this.noteTrouble({ kind: "test-letter", reason: "the gateway's test letter has not arrived", at: (/* @__PURE__ */ new Date()).toISOString() }, { tell: false });
74058
+ this.store.write("trouble.json", { ...this.store.read("trouble.json", {}), letterMissing: said.missingLetter });
74059
+ }
73824
74060
  if (said?.status === 200 && typeof said.hold === "boolean") {
73825
74061
  await followHold(this, this.frontOrigin, said.hold).catch((e) => this.log(`the gateway's hold: ${e.message}`));
73826
74062
  }
@@ -73892,6 +74128,11 @@ var BrowserAgent = class _BrowserAgent {
73892
74128
  this.store.onEvent = (type, n) => {
73893
74129
  if (type === "notification") this.notePush(n);
73894
74130
  };
74131
+ const tried = this.store.read("published.json", {}).healTriedAt;
74132
+ if (!this.publisher.profileAgrees() && !(tried && Date.now() - Date.parse(tried) < 60 * 6e4)) {
74133
+ this.store.write("published.json", { ...this.store.read("published.json", {}), healTriedAt: (/* @__PURE__ */ new Date()).toISOString() });
74134
+ await this.publisher.publishProfile({ force: true }).catch((e) => this.log(`profile: ${e.message}`));
74135
+ }
73895
74136
  const now = Date.now();
73896
74137
  const hereDue = !warm || now - (warm.hereAt || 0) >= _BrowserAgent.HERE_EVERY_MS;
73897
74138
  const here = hereDue ? await this.hereAtGateway() : null;
@@ -73910,6 +74151,8 @@ var BrowserAgent = class _BrowserAgent {
73910
74151
  }, 3e4);
73911
74152
  const drainNow = !warm || now - (warm.drainedAt || 0) >= DRAIN_EVERY_MS || here?.flushed > 0;
73912
74153
  await this.intake.start({ drainNow, subscribe: !warm || !!warm.resubscribe });
74154
+ await this.atproto.moveToPod?.().catch((e) => this.log(`Bluesky account to the pod: ${e.message}`));
74155
+ await this.fediaccts.moveToPod?.().catch((e) => this.log(`connected accounts to the pod: ${e.message}`));
73913
74156
  this.startBsky();
73914
74157
  this.startAccts();
73915
74158
  this.tagfeed?.start(this.mirrorStart("tags"));
@@ -74123,6 +74366,16 @@ var BrowserAgent = class _BrowserAgent {
74123
74366
  lease: this.lease,
74124
74367
  ownerPost: (a, o) => this.c2s.dispatch(a, o)
74125
74368
  });
74369
+ this.intake.onTrouble = (t) => this.noteTrouble(t);
74370
+ if (this.gatewayApi) {
74371
+ this.intake.checkLetters = {
74372
+ actor: `${this.frontOrigin.replace(/\/$/u, "")}/check/actor`,
74373
+ taken: async (letter) => {
74374
+ const said = await this.tellGateway("check", { handle: this.doorKey, letter }, { standing: false });
74375
+ if (said?.status !== 200) throw new Error(`the gateway did not take the answer to its test letter (${said?.status || "no answer"})`);
74376
+ }
74377
+ };
74378
+ }
74126
74379
  this.masto = new MastoApi({
74127
74380
  agent: this,
74128
74381
  log: this.log,
@@ -74138,8 +74391,11 @@ var BrowserAgent = class _BrowserAgent {
74138
74391
  this.tagfeed = new TagFeed({ store: this.store, intake: this.intake, log: this.log, fetcher: relayGet });
74139
74392
  this.provisioning = (async () => {
74140
74393
  if (!this._warm) await provisionPrivate(this.remote, this.urls);
74141
- await this.atproto.load();
74142
- await this.fediaccts.load();
74394
+ const readConn = !this._warm || !this._warm.connRead;
74395
+ await this.atproto.load({ fromPod: readConn });
74396
+ await this.fediaccts.load({ fromPod: readConn });
74397
+ if (readConn) saveMeta(this.webId, { connRead: true }).catch(() => {
74398
+ });
74143
74399
  this.viewer = !(this._warm && !this._warm.renewDue ? true : await this.lease.acquire());
74144
74400
  if (this.viewer) {
74145
74401
  this._watched = true;
@@ -74183,7 +74439,8 @@ var BrowserAgent = class _BrowserAgent {
74183
74439
  podRequests: this.remote?.stats?.() || null,
74184
74440
  update: null,
74185
74441
  inboxCooldownFor: 0,
74186
- stateSkipped: this.store?.lastSkipped || []
74442
+ stateSkipped: this.store?.lastSkipped || [],
74443
+ trouble: this.store?.read("trouble.json", null) || null
74187
74444
  };
74188
74445
  }
74189
74446
  // Rotate the signing key (POST /rotate-key). Mint a fresh keypair, replace the
@@ -74645,6 +74902,28 @@ async function bridge(request, url, { sameOrigin: sameOrigin2 = false } = {}) {
74645
74902
  // web/app/sw-src.mjs
74646
74903
  var agent = null;
74647
74904
  var booting = null;
74905
+ var LOG_KEEP = 300;
74906
+ var logLines = [];
74907
+ var logTimer = null;
74908
+ var logRead = kvGet("log").then((kept) => {
74909
+ logLines = [...Array.isArray(kept) ? kept : [], ...logLines].slice(-LOG_KEEP);
74910
+ }).catch(() => {
74911
+ });
74912
+ function agentLog(m) {
74913
+ console.log("[sw-agent]", m);
74914
+ const at = (/* @__PURE__ */ new Date()).toLocaleString(void 0, { month: "2-digit", day: "2-digit", hour: "numeric", minute: "2-digit", second: "2-digit" });
74915
+ logLines.push(`${at} ${m}`);
74916
+ if (logLines.length > LOG_KEEP) logLines.splice(0, logLines.length - LOG_KEEP);
74917
+ clearTimeout(logTimer);
74918
+ logTimer = setTimeout(() => {
74919
+ logRead.then(() => kvPut("log", logLines)).catch(() => {
74920
+ });
74921
+ }, 1500);
74922
+ }
74923
+ var recentLog = async () => {
74924
+ await logRead;
74925
+ return logLines;
74926
+ };
74648
74927
  var FACADE = /^\/(api\/v[12]\/|oauth\/|nodeinfo\/)/;
74649
74928
  var FACADE_EXACT = /* @__PURE__ */ new Set(["/.well-known/nodeinfo", "/.well-known/oauth-authorization-server", "/nodeinfo/2.0"]);
74650
74929
  var isFacade = (p) => FACADE.test(p) || FACADE_EXACT.has(p);
@@ -74683,7 +74962,8 @@ self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));
74683
74962
  async function bootFromSession(frontOrigin, restart) {
74684
74963
  const oidc = await getSession();
74685
74964
  if (!oidc) throw new Error("no session \u2014 sign in first");
74686
- const a = new BrowserAgent({ log: (m) => console.log("[sw-agent]", m) });
74965
+ const a = new BrowserAgent({ log: agentLog });
74966
+ a.recentLog = recentLog;
74687
74967
  await a.boot({ oidc, frontOrigin: frontOrigin || self.location.origin, restart });
74688
74968
  agent = a;
74689
74969
  return a;
@@ -74705,7 +74985,9 @@ self.addEventListener("message", (e) => {
74705
74985
  return;
74706
74986
  }
74707
74987
  if (e.data?.type !== "boot") return;
74708
- ensureBooting(e.data.frontOrigin).then(() => e.source?.postMessage({ type: "booted" })).catch((err) => e.source?.postMessage({
74988
+ const booted = ensureBooting(e.data.frontOrigin);
74989
+ e.waitUntil(stillWorking(booted));
74990
+ booted.then(() => e.source?.postMessage({ type: "booted" })).catch((err) => e.source?.postMessage({
74709
74991
  type: "boot-error",
74710
74992
  error: err.message,
74711
74993
  code: err.code || null,
@@ -74716,8 +74998,28 @@ self.addEventListener("fetch", (e) => {
74716
74998
  const url = new URL(e.request.url);
74717
74999
  if (url.origin !== self.location.origin || !(isFacade(url.pathname) || isAdmin(url.pathname))) return;
74718
75000
  if (url.pathname.startsWith("/oauth/") && !sameOriginReferrer(e.request)) return;
74719
- e.respondWith(serve(e.request, url));
75001
+ const served = serve(e.request, url);
75002
+ e.respondWith(served);
75003
+ e.waitUntil(stillWorking(served));
74720
75004
  });
75005
+ var STILL_WORKING_MS = 4 * 6e4;
75006
+ function stillWorking(first) {
75007
+ const work = (async () => {
75008
+ await Promise.resolve(first).catch(() => {
75009
+ });
75010
+ const a = agent;
75011
+ if (!a) return;
75012
+ await a.provisioning?.catch(() => {
75013
+ });
75014
+ await a.intake?.settled?.();
75015
+ })().catch(() => {
75016
+ });
75017
+ let timer;
75018
+ const cap = new Promise((r) => {
75019
+ timer = setTimeout(r, STILL_WORKING_MS);
75020
+ });
75021
+ return Promise.race([work, cap]).finally(() => clearTimeout(timer));
75022
+ }
74721
75023
  async function serve(request, url) {
74722
75024
  const refuse = notAllowed(request, url);
74723
75025
  if (refuse) {