fedipod 1.50.0 → 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 (44) 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/render.mjs +11 -4
  5. package/lib/client/masto/timelines.mjs +5 -0
  6. package/lib/connections/acctfeed.mjs +4 -1
  7. package/lib/connections/tagfeed.mjs +4 -1
  8. package/lib/core/graphview.mjs +9 -1
  9. package/lib/core/intake/index.mjs +35 -2
  10. package/lib/core/intake/verify.mjs +3 -1
  11. package/lib/core/pod-only.mjs +4 -2
  12. package/lib/core/publisher/index.mjs +19 -5
  13. package/lib/core/publisher/restore.mjs +1 -0
  14. package/lib/core/store.mjs +27 -6
  15. package/lib/gateway/account-agent.mjs +11 -5
  16. package/lib/gateway/check-letter.mjs +123 -0
  17. package/lib/gateway/connected-feeds.mjs +116 -0
  18. package/lib/gateway/copy.mjs +19 -0
  19. package/lib/gateway/front-core.mjs +5 -3
  20. package/lib/gateway/held-mail.mjs +25 -1
  21. package/lib/gateway/keeper.mjs +2 -1
  22. package/lib/gateway/masto-gateway.mjs +47 -7
  23. package/lib/surface/routes/owner.mjs +7 -4
  24. package/package.json +3 -3
  25. package/scripts/stage-site.mjs +2 -3
  26. package/vendor/sanitize-html.cjs +358 -171
  27. package/web/admin/bar.js +45 -0
  28. package/web/admin/group.js +17 -0
  29. package/web/admin/index.html +12 -8
  30. package/web/admin/record.js +19 -41
  31. package/web/admin/upkeep.js +3 -0
  32. package/web/app/README.md +3 -3
  33. package/web/app/admin-facade.mjs +19 -16
  34. package/web/app/agent.mjs +68 -3
  35. package/web/app/atproto-browser.mjs +28 -22
  36. package/web/app/deliver-relay.mjs +2 -1
  37. package/web/app/dist/boot.js +1 -0
  38. package/web/app/dist/boot.js.map +2 -2
  39. package/web/app/dist/sw.js +601 -273
  40. package/web/app/dist/sw.js.map +4 -4
  41. package/web/app/fediacct-browser.mjs +32 -30
  42. package/web/app/idb-kv.mjs +4 -4
  43. package/web/app/shims/prelude.js +1 -0
  44. 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
  );
@@ -45791,6 +45946,7 @@ var PodStore = class {
45791
45946
  this._held = 0;
45792
45947
  this.chain = Promise.resolve();
45793
45948
  this.verdicts = /* @__PURE__ */ new Map();
45949
+ this.unread = /* @__PURE__ */ new Set();
45794
45950
  this.generation = 0;
45795
45951
  this.onLoaded = null;
45796
45952
  this.onWriting = null;
@@ -45804,6 +45960,7 @@ var PodStore = class {
45804
45960
  this.cache.clear();
45805
45961
  this.etags.clear();
45806
45962
  this.lastText.clear();
45963
+ this.unread.clear();
45807
45964
  }
45808
45965
  this.storage = storage;
45809
45966
  }
@@ -45826,8 +45983,8 @@ var PodStore = class {
45826
45983
  if (listing.missing && this.storage.kind === "pod") {
45827
45984
  throw new Error(`state container missing (HTTP 404) at ${this.base} \u2014 pod unreachable or state gone`);
45828
45985
  }
45829
- this.etags.set("", listing.etag);
45830
45986
  const names = listing.names.filter((n) => n.endsWith(".json"));
45987
+ for (const name of this.unread) if (!names.includes(name)) this.unread.delete(name);
45831
45988
  let fetched = 0;
45832
45989
  const skipped = [];
45833
45990
  for (const name of names) {
@@ -45838,10 +45995,12 @@ var PodStore = class {
45838
45995
  if (name === "config.json" && !this.cache.has(name)) {
45839
45996
  throw new Error(`state doc ${name} unreadable (HTTP ${r.status})`);
45840
45997
  }
45998
+ if (!this.lastText.has(name)) this.unread.add(name);
45841
45999
  skipped.push(`${name} (HTTP ${r.status})`);
45842
46000
  continue;
45843
46001
  }
45844
46002
  fetched++;
46003
+ this.unread.delete(name);
45845
46004
  this.etags.set(name, r.etag);
45846
46005
  try {
45847
46006
  this.cache.set(name, JSON.parse(r.body));
@@ -45851,7 +46010,9 @@ var PodStore = class {
45851
46010
  }
45852
46011
  }
45853
46012
  this.lastSkipped = skipped;
45854
- if (skipped.length) this.log(`state load skipped ${skipped.length}: ${skipped.join(", ")}`);
46013
+ if (skipped.length) this.etags.delete("");
46014
+ else this.etags.set("", listing.etag);
46015
+ if (skipped.length) this.log(`state load skipped ${skipped.length}: ${skipped.join(", ")}` + (this.unread.size ? ` \u2014 not written until read: ${[...this.unread].join(", ")}` : ""));
45855
46016
  this.log(`state loaded: ${this.cache.size} doc(s) from ${this.base} (${fetched} re-fetched)`);
45856
46017
  this.onLoaded?.();
45857
46018
  }
@@ -45866,6 +46027,7 @@ var PodStore = class {
45866
46027
  continue;
45867
46028
  }
45868
46029
  this.lastText.set(name, text);
46030
+ this.unread.delete(name);
45869
46031
  if (etag) this.etags.set(name, etag);
45870
46032
  }
45871
46033
  if (listingEtag) this.etags.set("", listingEtag);
@@ -45884,12 +46046,14 @@ var PodStore = class {
45884
46046
  this.cache.delete(name);
45885
46047
  this.lastText.delete(name);
45886
46048
  this.etags.delete(name);
46049
+ this.unread.delete(name);
45887
46050
  } else if (!r.ok) {
45888
46051
  throw new Error(`state doc ${name} unreadable (HTTP ${r.status})`);
45889
46052
  } else {
45890
46053
  this.cache.set(name, JSON.parse(r.body));
45891
46054
  this.lastText.set(name, r.body);
45892
46055
  this.etags.set(name, r.etag);
46056
+ this.unread.delete(name);
45893
46057
  }
45894
46058
  this.onSettled?.(name);
45895
46059
  }
@@ -45908,6 +46072,10 @@ var PodStore = class {
45908
46072
  write(name, obj) {
45909
46073
  this.cache.set(name, structuredClone(obj));
45910
46074
  if (!this.storage) return;
46075
+ if (this.unread.has(name)) {
46076
+ this.verdicts.set(name, false);
46077
+ return;
46078
+ }
45911
46079
  if (this.lastText.get(name) === serialise(obj)) {
45912
46080
  clearTimeout(this.timers.get(name));
45913
46081
  this.timers.delete(name);
@@ -45957,6 +46125,7 @@ var PodStore = class {
45957
46125
  // the returned promise so one failure cannot poison the queue.
45958
46126
  _put(name) {
45959
46127
  const done = this.chain.then(async () => {
46128
+ if (this.unread.has(name)) return false;
45960
46129
  const body = serialise(this.cache.get(name));
45961
46130
  await Promise.resolve(this.onWriting?.(name, body)).catch(() => {
45962
46131
  });
@@ -46011,6 +46180,8 @@ var PodStore = class {
46011
46180
  // `since`: the generation the caller's work began in. Anything dropped since
46012
46181
  // makes the answer false.
46013
46182
  async commit({ since } = {}) {
46183
+ if (this.unread.size && this.storage) await this.refresh([...this.unread]).catch(() => {
46184
+ });
46014
46185
  const pending = [];
46015
46186
  for (const name of /* @__PURE__ */ new Set([...this.timers.keys(), ...this.dirty])) {
46016
46187
  const t = this.timers.get(name);
@@ -57169,6 +57340,7 @@ function fromLiteral(term3) {
57169
57340
  }
57170
57341
  return term3.value;
57171
57342
  }
57343
+ var isTagged = (term3) => term3.termType === "Literal" && !!term3.language;
57172
57344
  function toValue(term3, ctx, depth, path) {
57173
57345
  if (term3.termType === "Literal") return fromLiteral(term3);
57174
57346
  if (isListHead(term3, ctx)) return readList(term3.value, ctx, depth, path);
@@ -57187,8 +57359,12 @@ function readProperty(subject, name, ctx, depth = 0, path = /* @__PURE__ */ new
57187
57359
  }
57188
57360
  const term3 = TERMS.get(name);
57189
57361
  if (!term3) return void 0;
57190
- const values = preds.get(term3.iri);
57362
+ let values = preds.get(term3.iri);
57191
57363
  if (!values || !values.length) return void 0;
57364
+ if (values.some(isTagged)) {
57365
+ const plain = values.filter((v) => !isTagged(v));
57366
+ values = plain.length ? plain : [values.find(isTagged)];
57367
+ }
57192
57368
  const listed = values.filter((v) => isListHead(v, ctx));
57193
57369
  if (term3.list) {
57194
57370
  if (!listed.length) return void 0;
@@ -58104,6 +58280,7 @@ async function rebuildStatuses(publisher, { fromNotes = false } = {}) {
58104
58280
  }
58105
58281
  async function healStatuses(publisher) {
58106
58282
  const { urls, store } = publisher;
58283
+ if (store.unread?.has("statuses.json")) return { missing: 0, recovered: 0 };
58107
58284
  const own = store.read("outbox.json", []).map((i) => typeof i === "string" ? i : null).filter((id) => id && id.startsWith(urls.notes));
58108
58285
  if (!own.length) return { missing: 0, recovered: 0 };
58109
58286
  const have = new Set(store.getStatuses().map((s) => s.noteId));
@@ -58565,6 +58742,21 @@ var Publisher = class {
58565
58742
  // `clientOrigin` is overridden only by the agent serving a client its own
58566
58743
  // local actor: standalone, the published document cannot name a loopback
58567
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
+ }
58568
58760
  actorDocFor({
58569
58761
  priv = false,
58570
58762
  moderators = null,
@@ -58580,7 +58772,7 @@ var Publisher = class {
58580
58772
  publicKeyPem: this.publicKeyPem,
58581
58773
  movedTo,
58582
58774
  kind: this.config.kind,
58583
- approveJoins: followsNeedApproval(this.config),
58775
+ approveJoins: this.approvesByHand(),
58584
58776
  assertionKey: this.assertionKey,
58585
58777
  summary: this.config.summary || null,
58586
58778
  icon: this.config.icon || null,
@@ -58716,12 +58908,11 @@ var Publisher = class {
58716
58908
  const updated = await this.announceProfileChange(actor, { force });
58717
58909
  await this.ensurePrivateAcls();
58718
58910
  const unreachable = await this.verifyPublicSurface();
58719
- if (!unreachable.length) {
58720
- this.store.write(
58721
- "published.json",
58722
- { ...this.store.read("published.json", {}), surfaceDigest: surface }
58723
- );
58724
- }
58911
+ this.store.write("published.json", {
58912
+ ...this.store.read("published.json", {}),
58913
+ ...!unreachable.length ? { surfaceDigest: surface } : {},
58914
+ approvesByHand: !!actorDoc2.manuallyApprovesFollowers
58915
+ });
58725
58916
  const pubName = publicHandle(this.config);
58726
58917
  const pubHost = this.config.gateway?.frontActor ? new URL(this.config.gateway.frontActor).host : host;
58727
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`);
@@ -59424,8 +59615,8 @@ async function isGone(intake, url) {
59424
59615
  let res;
59425
59616
  try {
59426
59617
  res = await intake.deliverer.signedFetch(url, { headers: { accept: ACCEPT_AP2 } });
59427
- } catch {
59428
- return null;
59618
+ } catch (e) {
59619
+ return e?.answered && (e.status === 404 || e.status === 410) ? true : null;
59429
59620
  }
59430
59621
  if (res.status === 404 || res.status === 410) return true;
59431
59622
  if (res.status < 400) {
@@ -65470,8 +65661,17 @@ var Intake = class {
65470
65661
  rec.last = String(message || "").slice(0, 200);
65471
65662
  all[url] = rec;
65472
65663
  this.store.write(ATTEMPTS_DOC, all);
65664
+ if (rec.n === 2) this._trouble("failing", url, message);
65473
65665
  return rec.n;
65474
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
+ }
65475
65675
  _clearAttempt(url) {
65476
65676
  const all = this.store.read(ATTEMPTS_DOC, {});
65477
65677
  if (!all[url]) return;
@@ -65515,12 +65715,24 @@ var Intake = class {
65515
65715
  this._draining = null;
65516
65716
  if (this._drainAgain) {
65517
65717
  this._drainAgain = false;
65518
- const t = setTimeout(() => this.drain().catch((e) => this.log(`drain: ${e.message}`)), CHAIN_GAP_MS);
65519
- 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?.();
65520
65723
  }
65521
65724
  });
65522
65725
  return this._draining;
65523
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
+ }
65524
65736
  // Discard the content waiting in the inbox from before `before`, on the
65525
65737
  // owner's say-so — the admin page asks, this does it. NOT a blind sweep:
65526
65738
  // every item is read, because the type is the only thing that decides its
@@ -65723,6 +65935,7 @@ var Intake = class {
65723
65935
  this.log(`inbox batch ${url} attempt ${n}/${MAX_ITEM_ATTEMPTS}: ${e.message}`);
65724
65936
  if (n >= MAX_ITEM_ATTEMPTS) {
65725
65937
  this.store.addDeadLetter({ inboxUrl: url, reason: `failed ${n}x: ${e.message}`, activity: null });
65938
+ this._trouble("set-aside", url, e.message);
65726
65939
  pending.push(url);
65727
65940
  }
65728
65941
  }
@@ -65746,6 +65959,7 @@ var Intake = class {
65746
65959
  this.log(`inbox item ${url} attempt ${n}/${MAX_ITEM_ATTEMPTS}: ${e.message}`);
65747
65960
  if (n >= MAX_ITEM_ATTEMPTS) {
65748
65961
  this.store.addDeadLetter({ inboxUrl: url, reason: `failed ${n}x: ${e.message}`, activity: trimActivity(at.activity) });
65962
+ this._trouble("set-aside", url, e.message);
65749
65963
  pending.push(url);
65750
65964
  }
65751
65965
  }
@@ -65766,6 +65980,11 @@ var Intake = class {
65766
65980
  const read2 = raw ? await readLenient(raw) : null;
65767
65981
  if (read2?.degraded) this.log(`inbox item ${url} grounded to read: ${read2.degraded}`);
65768
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
+ }
65769
65988
  if (read2?.graph) {
65770
65989
  const failure = await checkShapes(read2.graph);
65771
65990
  if (failure) {
@@ -65820,6 +66039,7 @@ var Intake = class {
65820
66039
  const batch = await this._batchEntries(url);
65821
66040
  if (!batch) {
65822
66041
  this.store.addDeadLetter({ inboxUrl: url, reason: "unreadable batch", activity: null });
66042
+ this._trouble("set-aside", url, "unreadable batch");
65823
66043
  return { entries: 1, complete: true };
65824
66044
  }
65825
66045
  let complete = true;
@@ -65839,6 +66059,7 @@ var Intake = class {
65839
66059
  this.log(`inbox batch entry ${key} attempt ${n}/${MAX_ITEM_ATTEMPTS}: ${err.message}`);
65840
66060
  if (n >= MAX_ITEM_ATTEMPTS) {
65841
66061
  this.store.addDeadLetter({ inboxUrl: key, reason: `failed ${n}x: ${err.message}`, activity: trimActivity(at.activity) });
66062
+ this._trouble("set-aside", key, err.message);
65842
66063
  } else {
65843
66064
  complete = false;
65844
66065
  }
@@ -65877,6 +66098,7 @@ var Intake = class {
65877
66098
  this.log(`held delivery ${key} attempt ${n}/${MAX_ITEM_ATTEMPTS}: ${err.message}`);
65878
66099
  if (n >= MAX_ITEM_ATTEMPTS) {
65879
66100
  this.store.addDeadLetter({ inboxUrl: key, reason: `failed ${n}x: ${err.message}`, activity: trimActivity(at.activity) });
66101
+ this._trouble("set-aside", key, err.message);
65880
66102
  done.push(e.name);
65881
66103
  }
65882
66104
  }
@@ -68288,6 +68510,10 @@ var BskyFeed = class {
68288
68510
  };
68289
68511
 
68290
68512
  // lib/client/masto/render.mjs
68513
+ var once = (html) => {
68514
+ const m = (html.length - 1) / 2;
68515
+ return Number.isInteger(m) && html[m] === "," && html.slice(0, m) === html.slice(m + 1) ? html.slice(0, m) : html;
68516
+ };
68291
68517
  var TRANSPARENT_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
68292
68518
  var DEFAULT_AVATAR = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAABd0lEQVR42u3ZoW4CQRDG8Xk6NBLbVCJ5gBpMTQ2iBlFR29RXNsFhz1JHXV9hydfkEkoobW5n2F34i0kIHMfN73ZvdwYbTabpmsMAAAAAAAAAAAAAAAAAAACiY3wzS/ePz+n1bZW6zTZ9fH59h17rPX2mYy4OQEktnl5+JP1b6Bgdey6IcIDb2Ty9r7s/Ez8MfUffbRpACfznrp8aDdEIFjnsh9z5YyMhcjqEAWge5ybfh87VFIDuWM7QPzYVokZBCICWM6/k+9A5mwHQmu4NoHM2A+A5/PenQTMA3sn3AQBTgIcgyyAbIbbCFEOUwzREaInRFKUtDgAAFQLcPSxDCqH9Z4V+o0oAXVhU4ofhiWCtJe+NYKV3e6V3idkAHvv9nDqhKIBnxVeqUrRaav5SvQKrqetTols0GGDdbaoB0LWcFUBP31qS72PoimCtD//caWC1tL1Ltc2ttbXfe08wCKC25HP+OgMAAAAAAAAAAAAAAAAArhNgB1PE/x/JOiuNAAAAAElFTkSuQmCC";
68293
68519
  var selfIcon = (api, self2, cached) => (self2 ? api.store.getConfig()?.icon : null) || cached.icon;
@@ -68511,7 +68737,7 @@ function status(api, s, { all, depth = 0 } = {}) {
68511
68737
  in_reply_to_id: s.inReplyTo ? api.store.idFor(s.inReplyTo) : null,
68512
68738
  in_reply_to_account_id: null,
68513
68739
  sensitive: !!(s.spoiler || s.sensitive),
68514
- spoiler_text: s.spoiler || "",
68740
+ spoiler_text: once(s.spoiler || ""),
68515
68741
  visibility: s.visibility || "public",
68516
68742
  language: null,
68517
68743
  edited_at: s.editedAt || null,
@@ -68528,7 +68754,7 @@ function status(api, s, { all, depth = 0 } = {}) {
68528
68754
  muted: false,
68529
68755
  bookmarked: !!s.bookmarked,
68530
68756
  pinned: !!s.pinned,
68531
- content: s.content || "",
68757
+ content: once(s.content || ""),
68532
68758
  reblog: null,
68533
68759
  application: null,
68534
68760
  account: api.account(s.actor),
@@ -68570,7 +68796,7 @@ function status(api, s, { all, depth = 0 } = {}) {
68570
68796
  }
68571
68797
  function filtersFor(api, s) {
68572
68798
  const now = Date.now();
68573
- const hay = `${s.content || ""} ${s.spoiler || ""}`.replace(/<[^>]*>/gu, " ").toLowerCase();
68799
+ const hay = `${once(s.content || "")} ${once(s.spoiler || "")}`.replace(/<[^>]*>/gu, " ").toLowerCase();
68574
68800
  if (!hay.trim()) return [];
68575
68801
  const out = [];
68576
68802
  for (const f of api.store.getFilters?.() || []) {
@@ -68657,7 +68883,7 @@ function pushNotify(api, n) {
68657
68883
  notification_type: n.type,
68658
68884
  preferred_locale: "en",
68659
68885
  title: `${acct.display_name || acct.acct} ${verbs[n.type] || n.type}`,
68660
- body: s ? htmlToText(s.content || "").slice(0, 140) : "",
68886
+ body: s ? htmlToText(once(s.content || "")).slice(0, 140) : "",
68661
68887
  icon: acct.avatar || ""
68662
68888
  });
68663
68889
  }
@@ -69155,9 +69381,10 @@ async function handle4(api, ctx) {
69155
69381
  const { req, res, pathname, url, send } = ctx;
69156
69382
  if (pathname === "/api/v1/timelines/home" || pathname === "/api/v1/timelines/public" || pathname === "/api/v1/trends/statuses") {
69157
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;
69158
69385
  const all = api.store.getStatuses();
69159
69386
  const muted = new Set(api.store.getMuted().actors);
69160
- 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 || "")));
69161
69388
  const { items: page2, headers } = api.page(items, url);
69162
69389
  return send(200, page2.map((s) => api.statusOrBoost(s, { all })), headers);
69163
69390
  }
@@ -70167,8 +70394,9 @@ var TagFeed = class {
70167
70394
  if (Array.isArray(patch.tags)) clean.tags = patch.tags.map((t) => String(t).replace(/^#/, "").trim()).filter(Boolean);
70168
70395
  if (patch.intervalMin) clean.intervalMin = Math.max(5, Number(patch.intervalMin) || DEFAULTS2.intervalMin);
70169
70396
  this.store.write("tagfeed.json", { ...this.config(), ...clean });
70397
+ const sweeping = this.stopped === false;
70170
70398
  this.stop();
70171
- this.start();
70399
+ if (sweeping) this.start();
70172
70400
  return this.config();
70173
70401
  }
70174
70402
  // `lastSweptAt` and `onSwept` are for a browser worker the browser stops and
@@ -71008,6 +71236,7 @@ var RelayDeliverer = class extends Deliverer {
71008
71236
  if (status2 >= 400) {
71009
71237
  const err = new Error(`${method} ${url} \u2192 ${status2}`);
71010
71238
  err.status = status2;
71239
+ err.answered = true;
71011
71240
  if (r0.retryAfter) {
71012
71241
  const secs = Number(r0.retryAfter);
71013
71242
  const ms = Number.isFinite(secs) ? Math.max(secs, 1) * 1e3 : Math.max(Date.parse(r0.retryAfter) - Date.now(), 1e3);
@@ -71392,7 +71621,7 @@ var ImportWorker = class {
71392
71621
 
71393
71622
  // web/app/admin-facade.mjs
71394
71623
  var PERMANENT_CONFIG = ["handle", "remotePod", "issuer", "root", "kind"];
71395
- var WIRE_CONFIG = ["name", "summary", "icon", "image", "fields", "aliases"];
71624
+ var WIRE_CONFIG = ["name", "summary", "icon", "image", "fields", "aliases", "autoAcceptFollows"];
71396
71625
  var ADMIN_PATHS = /* @__PURE__ */ new Set([
71397
71626
  "/status",
71398
71627
  "/config",
@@ -71401,6 +71630,7 @@ var ADMIN_PATHS = /* @__PURE__ */ new Set([
71401
71630
  "/rotate-key",
71402
71631
  "/import",
71403
71632
  "/deadletter",
71633
+ "/trouble",
71404
71634
  "/blocks",
71405
71635
  "/profiles",
71406
71636
  "/modqueue",
@@ -71502,6 +71732,7 @@ var AdminFacade = class {
71502
71732
  fields: cfg.fields || [],
71503
71733
  aliases: cfg.aliases || [],
71504
71734
  autoAcceptFollows: cfg.autoAcceptFollows !== false,
71735
+ tagsInHome: cfg.tagsInHome !== false,
71505
71736
  quiescedAt: cfg.quiescedAt || null,
71506
71737
  movedTo: cfg.movedTo || null,
71507
71738
  mode: a.status().mode,
@@ -71560,8 +71791,7 @@ var AdminFacade = class {
71560
71791
  return json2(200, []);
71561
71792
  // person: no moderation queue
71562
71793
  case "/log":
71563
- return json2(200, { lines: [] });
71564
- // no local log file in the browser
71794
+ return json2(200, { lines: await a.recentLog?.() || [] });
71565
71795
  case "/fediacct":
71566
71796
  return json2(200, { accounts: a.fediaccts?.status() || [] });
71567
71797
  case "/import":
@@ -71598,6 +71828,13 @@ var AdminFacade = class {
71598
71828
  return json2(400, { error: "expected JSON body" });
71599
71829
  }
71600
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
+ }
71601
71838
  case "/config": {
71602
71839
  const fixed = PERMANENT_CONFIG.filter((k) => k in body);
71603
71840
  if (fixed.length) return json2(400, { error: `${fixed.join(", ")} cannot be changed \u2014 that is the identity itself` });
@@ -71617,6 +71854,7 @@ var AdminFacade = class {
71617
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() }));
71618
71855
  }
71619
71856
  if ("autoAcceptFollows" in body) cfg.autoAcceptFollows = !!body.autoAcceptFollows;
71857
+ if ("tagsInHome" in body) cfg.tagsInHome = !!body.tagsInHome;
71620
71858
  const republish = WIRE_CONFIG.some((k) => k in body);
71621
71859
  a.store.setConfig(cfg);
71622
71860
  if ("autoAcceptFollows" in body && a.publisher) a.publisher.config.autoAcceptFollows = cfg.autoAcceptFollows;
@@ -71926,23 +72164,20 @@ var AdminFacade = class {
71926
72164
  await a.store.flush();
71927
72165
  return json2(200, { ok: true });
71928
72166
  }
71929
- // Bluesky settings: pause/resume the mirror, and move the credential
71930
- // 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.
71931
72169
  case "/atproto": {
71932
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" });
71933
72172
  if ("feedPaused" in body) {
71934
72173
  a.atproto.setFeedPaused(!!body.feedPaused);
71935
72174
  a.restartBsky?.();
71936
72175
  }
71937
- if ("storage" in body) {
71938
- if (!["pod", "browser"].includes(body.storage)) return json2(400, { error: "storage must be pod or browser" });
71939
- a.atproto.setStorage(body.storage);
71940
- }
71941
72176
  return json2(200, { ok: true, ...a.atproto.status() });
71942
72177
  }
71943
72178
  // Connect a fediverse account on another server: start its OAuth at that
71944
72179
  // server. The browser is then sent to `authorize`; the code comes back to
71945
- // GET /fediacct/callback (above). The token never touches the pod.
72180
+ // GET /fediacct/callback (above). The token is kept on the pod.
71946
72181
  case "/fediacct/connect": {
71947
72182
  if (!body.host) return json2(400, { error: "the address of the server is required" });
71948
72183
  try {
@@ -71959,14 +72194,11 @@ var AdminFacade = class {
71959
72194
  a.restartAccts?.();
71960
72195
  return json2(200, { ok: true });
71961
72196
  }
71962
- // Pause/resume the mirror, or move the credential between browser and pod.
72197
+ // Pause/resume the mirror. The credential is kept on the pod.
71963
72198
  case "/fediacct": {
71964
72199
  if (!body.id) return json2(400, { error: "id required" });
71965
- let row;
71966
- if ("storage" in body) {
71967
- if (!["pod", "browser"].includes(body.storage)) return json2(400, { error: "storage must be pod or browser" });
71968
- row = a.fediaccts.setStorage(body.id, body.storage);
71969
- } 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);
71970
72202
  if (!row) return json2(404, { error: "no such account" });
71971
72203
  a.restartAccts?.();
71972
72204
  return json2(200, { ok: true, account: row });
@@ -72432,8 +72664,10 @@ var BrowserAtproto = class extends Atproto {
72432
72664
  }
72433
72665
  // The IndexedDB copy into memory, so read() stays synchronous as the base
72434
72666
  // class expects. The pod copy is already in the loaded store.
72435
- async load() {
72667
+ // `fromPod`: the pod's record read as well (agent.mjs says when).
72668
+ async load({ fromPod = false } = {}) {
72436
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}`));
72437
72671
  return this;
72438
72672
  }
72439
72673
  // Whichever backend holds it, tagged with where it lives. The stamped-record
@@ -72441,15 +72675,13 @@ var BrowserAtproto = class extends Atproto {
72441
72675
  read() {
72442
72676
  let rec = null;
72443
72677
  let where = null;
72444
- 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) {
72445
72683
  rec = this._idbRec;
72446
72684
  where = "browser";
72447
- } else {
72448
- const p = this.store.read(POD_DOC, null);
72449
- if (p) {
72450
- rec = p;
72451
- where = "pod";
72452
- }
72453
72685
  }
72454
72686
  if (!rec) return null;
72455
72687
  if (rec.mintedFor && this.actorId && rec.mintedFor !== this.actorId) {
@@ -72458,25 +72690,25 @@ var BrowserAtproto = class extends Atproto {
72458
72690
  }
72459
72691
  return { ...rec, storage: where };
72460
72692
  }
72461
- // Write to the record's chosen backend (default browser), and clear the other
72462
- // so a moved credential leaves no copy behind.
72463
72693
  write(rec) {
72464
- const where = rec.storage || "browser";
72465
- const clean = { ...rec, storage: where };
72466
- if (where === "pod") {
72467
- this.store.write(POD_DOC, clean);
72468
- this.store.flush?.().catch(() => {
72469
- });
72470
- this._idbRec = null;
72471
- kvDel(IDB_KEY).catch(() => {
72472
- });
72473
- } else {
72474
- this._idbRec = clean;
72475
- kvPut(IDB_KEY, clean).catch((e) => this.log(`atproto persist: ${e.message}`));
72476
- this.store.write(POD_DOC, null);
72477
- this.store.flush?.().catch(() => {
72478
- });
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;
72479
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;
72480
72712
  }
72481
72713
  status() {
72482
72714
  const rec = this.read();
@@ -72499,11 +72731,6 @@ var BrowserAtproto = class extends Atproto {
72499
72731
  const rec = this.read();
72500
72732
  if (rec) this.write({ ...rec, feedPaused: !!on });
72501
72733
  }
72502
- // Move the credential between backends (the Options menu's Storage control).
72503
- setStorage(where) {
72504
- const rec = this.read();
72505
- if (rec && (where === "pod" || where === "browser")) this.write({ ...rec, storage: where });
72506
- }
72507
72734
  async disconnect() {
72508
72735
  const rec = this.read();
72509
72736
  if (rec?.refreshJwt) {
@@ -72762,7 +72989,9 @@ var BrowserFediAccounts = class extends FediAccounts {
72762
72989
  this._recs = /* @__PURE__ */ new Map();
72763
72990
  this._apps = /* @__PURE__ */ new Map();
72764
72991
  }
72765
- 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}`));
72766
72995
  const all = await kvAll().catch(() => ({}));
72767
72996
  for (const [k, v] of Object.entries(all)) {
72768
72997
  if (k.startsWith("rec:")) this._recs.set(k.slice(4), v);
@@ -72787,15 +73016,13 @@ var BrowserFediAccounts = class extends FediAccounts {
72787
73016
  read(id) {
72788
73017
  let rec = null;
72789
73018
  let where = null;
72790
- 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)) {
72791
73024
  rec = this._recs.get(id);
72792
73025
  where = "browser";
72793
- } else {
72794
- const p = this._podMap()[id];
72795
- if (p) {
72796
- rec = p;
72797
- where = "pod";
72798
- }
72799
73026
  }
72800
73027
  if (!rec) return null;
72801
73028
  if (rec.mintedFor && this.actorId && rec.mintedFor !== this.actorId) {
@@ -72805,27 +73032,28 @@ var BrowserFediAccounts = class extends FediAccounts {
72805
73032
  return { ...rec, storage: where };
72806
73033
  }
72807
73034
  write(rec) {
72808
- const where = rec.storage || "browser";
72809
- const clean = { ...rec, storage: where };
72810
- if (where === "pod") {
72811
- const map = { ...this._podMap(), [clean.id]: clean };
72812
- this.store.write(POD_DOC2, map);
72813
- this.store.flush?.().catch(() => {
72814
- });
72815
- this._recs.delete(clean.id);
72816
- 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(() => {
72817
73053
  });
72818
- } else {
72819
- this._recs.set(clean.id, clean);
72820
- kvPut("rec:" + clean.id, clean).catch((e) => this.log(`fediacct persist: ${e.message}`));
72821
- const map = this._podMap();
72822
- if (clean.id in map) {
72823
- delete map[clean.id];
72824
- this.store.write(POD_DOC2, map);
72825
- this.store.flush?.().catch(() => {
72826
- });
72827
- }
72828
73054
  }
73055
+ this.log(`${moving.length} connected account(s) moved from this browser to the pod`);
73056
+ return moving.length;
72829
73057
  }
72830
73058
  // Best-effort: tell the server to revoke our token (RFC 7009), so a
72831
73059
  // disconnect actually invalidates it there — not just locally. Mirrors
@@ -72878,15 +73106,6 @@ var BrowserFediAccounts = class extends FediAccounts {
72878
73106
  cooldownFor: Math.max(0, Math.round(((this.pausedUntil.get(r.host) || 0) - Date.now()) / 1e3))
72879
73107
  }));
72880
73108
  }
72881
- // Move a connection between backends (the Options menu's Storage control).
72882
- setStorage(id, where) {
72883
- const rec = this.read(id);
72884
- if (rec && (where === "pod" || where === "browser")) {
72885
- this.write({ ...rec, storage: where });
72886
- return this.roster().find((r) => r.id === id) || null;
72887
- }
72888
- return null;
72889
- }
72890
73109
  // One app registration per host, remembered — over IndexedDB.
72891
73110
  async appFor(host, redirectUri) {
72892
73111
  const cached = this._apps.get(host);
@@ -73062,8 +73281,11 @@ var AcctFeed = class {
73062
73281
  }
73063
73282
  return map;
73064
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.
73065
73286
  _rateGuard(id, handle7, res) {
73066
- 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);
73067
73289
  if (!Number.isFinite(left) || left > RATE_FLOOR) return false;
73068
73290
  const reset = Date.parse(res.headers.get("x-ratelimit-reset") || "");
73069
73291
  const wait = Number.isFinite(reset) ? Math.max(0, reset - Date.now()) : BACKOFF_MIN_MS3;
@@ -73173,7 +73395,8 @@ var fail2 = () => {
73173
73395
  var promises_default = new Proxy({}, { get: () => fail2 });
73174
73396
 
73175
73397
  // lib/core/pod-only.mjs
73176
- 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);
73177
73400
 
73178
73401
  // lib/core/storage.mjs
73179
73402
  var LDP2 = Namespace("http://www.w3.org/ns/ldp#");
@@ -73735,6 +73958,41 @@ var BrowserAgent = class _BrowserAgent {
73735
73958
  // (lib/gateway/held-mail.mjs: /api/push). Named a moment later, once written,
73736
73959
  // so a burst is one call. One from another network is not pushed.
73737
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
+ }
73738
73996
  notePush(n) {
73739
73997
  if (!this.gatewayApi || n?.bsky || n?.via) return;
73740
73998
  if (!Object.keys(this.store.read("webpush.json", { subs: {} }).subs || {}).length) return;
@@ -73795,6 +74053,10 @@ var BrowserAgent = class _BrowserAgent {
73795
74053
  const said = await this.tellGateway("here", { handle: this.doorKey, nextAt: this.store ? nextDue(this.store) : null });
73796
74054
  if (said?.status === 200) saveMeta(this.webId, { hereAt: Date.now() }).catch(() => {
73797
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
+ }
73798
74060
  if (said?.status === 200 && typeof said.hold === "boolean") {
73799
74061
  await followHold(this, this.frontOrigin, said.hold).catch((e) => this.log(`the gateway's hold: ${e.message}`));
73800
74062
  }
@@ -73866,6 +74128,11 @@ var BrowserAgent = class _BrowserAgent {
73866
74128
  this.store.onEvent = (type, n) => {
73867
74129
  if (type === "notification") this.notePush(n);
73868
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
+ }
73869
74136
  const now = Date.now();
73870
74137
  const hereDue = !warm || now - (warm.hereAt || 0) >= _BrowserAgent.HERE_EVERY_MS;
73871
74138
  const here = hereDue ? await this.hereAtGateway() : null;
@@ -73884,6 +74151,8 @@ var BrowserAgent = class _BrowserAgent {
73884
74151
  }, 3e4);
73885
74152
  const drainNow = !warm || now - (warm.drainedAt || 0) >= DRAIN_EVERY_MS || here?.flushed > 0;
73886
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}`));
73887
74156
  this.startBsky();
73888
74157
  this.startAccts();
73889
74158
  this.tagfeed?.start(this.mirrorStart("tags"));
@@ -74022,7 +74291,7 @@ var BrowserAgent = class _BrowserAgent {
74022
74291
  const fullLoad = () => this.store.load().catch((e) => {
74023
74292
  unread = e;
74024
74293
  });
74025
- if (this._warm) await this.store.refresh(this._warm.pending).catch(fullLoad);
74294
+ if (this._warm && warmFrom.listingEtag) await this.store.refresh(this._warm.pending).catch(fullLoad);
74026
74295
  else await fullLoad();
74027
74296
  const cfg = config || this.store.getConfig();
74028
74297
  if (!cfg) throw await accountNotRead({ unread, podBase: remotePod, state: this.urls.state, webId, actorUrl: this.urls.actor });
@@ -74097,6 +74366,16 @@ var BrowserAgent = class _BrowserAgent {
74097
74366
  lease: this.lease,
74098
74367
  ownerPost: (a, o) => this.c2s.dispatch(a, o)
74099
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
+ }
74100
74379
  this.masto = new MastoApi({
74101
74380
  agent: this,
74102
74381
  log: this.log,
@@ -74112,8 +74391,11 @@ var BrowserAgent = class _BrowserAgent {
74112
74391
  this.tagfeed = new TagFeed({ store: this.store, intake: this.intake, log: this.log, fetcher: relayGet });
74113
74392
  this.provisioning = (async () => {
74114
74393
  if (!this._warm) await provisionPrivate(this.remote, this.urls);
74115
- await this.atproto.load();
74116
- 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
+ });
74117
74399
  this.viewer = !(this._warm && !this._warm.renewDue ? true : await this.lease.acquire());
74118
74400
  if (this.viewer) {
74119
74401
  this._watched = true;
@@ -74157,7 +74439,8 @@ var BrowserAgent = class _BrowserAgent {
74157
74439
  podRequests: this.remote?.stats?.() || null,
74158
74440
  update: null,
74159
74441
  inboxCooldownFor: 0,
74160
- stateSkipped: this.store?.lastSkipped || []
74442
+ stateSkipped: this.store?.lastSkipped || [],
74443
+ trouble: this.store?.read("trouble.json", null) || null
74161
74444
  };
74162
74445
  }
74163
74446
  // Rotate the signing key (POST /rotate-key). Mint a fresh keypair, replace the
@@ -74619,6 +74902,28 @@ async function bridge(request, url, { sameOrigin: sameOrigin2 = false } = {}) {
74619
74902
  // web/app/sw-src.mjs
74620
74903
  var agent = null;
74621
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
+ };
74622
74927
  var FACADE = /^\/(api\/v[12]\/|oauth\/|nodeinfo\/)/;
74623
74928
  var FACADE_EXACT = /* @__PURE__ */ new Set(["/.well-known/nodeinfo", "/.well-known/oauth-authorization-server", "/nodeinfo/2.0"]);
74624
74929
  var isFacade = (p) => FACADE.test(p) || FACADE_EXACT.has(p);
@@ -74657,7 +74962,8 @@ self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));
74657
74962
  async function bootFromSession(frontOrigin, restart) {
74658
74963
  const oidc = await getSession();
74659
74964
  if (!oidc) throw new Error("no session \u2014 sign in first");
74660
- const a = new BrowserAgent({ log: (m) => console.log("[sw-agent]", m) });
74965
+ const a = new BrowserAgent({ log: agentLog });
74966
+ a.recentLog = recentLog;
74661
74967
  await a.boot({ oidc, frontOrigin: frontOrigin || self.location.origin, restart });
74662
74968
  agent = a;
74663
74969
  return a;
@@ -74679,7 +74985,9 @@ self.addEventListener("message", (e) => {
74679
74985
  return;
74680
74986
  }
74681
74987
  if (e.data?.type !== "boot") return;
74682
- 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({
74683
74991
  type: "boot-error",
74684
74992
  error: err.message,
74685
74993
  code: err.code || null,
@@ -74690,8 +74998,28 @@ self.addEventListener("fetch", (e) => {
74690
74998
  const url = new URL(e.request.url);
74691
74999
  if (url.origin !== self.location.origin || !(isFacade(url.pathname) || isAdmin(url.pathname))) return;
74692
75000
  if (url.pathname.startsWith("/oauth/") && !sameOriginReferrer(e.request)) return;
74693
- e.respondWith(serve(e.request, url));
75001
+ const served = serve(e.request, url);
75002
+ e.respondWith(served);
75003
+ e.waitUntil(stillWorking(served));
74694
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
+ }
74695
75023
  async function serve(request, url) {
74696
75024
  const refuse = notAllowed(request, url);
74697
75025
  if (refuse) {