@fedify/lint 2.2.0-pr.695.16 → 2.2.0-pr.697.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/lint",
3
- "version": "2.2.0-pr.695.16+7a782334",
3
+ "version": "2.2.0-pr.697.18+b776632f",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts"
package/dist/index.cjs CHANGED
@@ -29,7 +29,7 @@ let _typescript_eslint_parser = require("@typescript-eslint/parser");
29
29
  _typescript_eslint_parser = __toESM(_typescript_eslint_parser);
30
30
  //#region deno.json
31
31
  var name = "@fedify/lint";
32
- var version = "2.2.0-pr.695.16+7a782334";
32
+ var version = "2.2.0-pr.697.18+b776632f";
33
33
  //#endregion
34
34
  //#region src/lib/const.ts
35
35
  /**
@@ -146,7 +146,8 @@ const RULE_IDS = {
146
146
  actorFeaturedTagsPropertyMismatch: "actor-featured-tags-property-mismatch",
147
147
  actorInboxPropertyMismatch: "actor-inbox-property-mismatch",
148
148
  actorSharedInboxPropertyMismatch: "actor-shared-inbox-property-mismatch",
149
- collectionFilteringNotImplemented: "collection-filtering-not-implemented"
149
+ collectionFilteringNotImplemented: "collection-filtering-not-implemented",
150
+ outboxListenerDeliveryRequired: "outbox-listener-delivery-required"
150
151
  };
151
152
  //#endregion
152
153
  //#region src/lib/messages.ts
@@ -191,6 +192,7 @@ function allOf(...predicates) {
191
192
  return (value) => predicates.every((predicate) => predicate(value));
192
193
  }
193
194
  const anyOf = (...predicates) => (value) => predicates.some((predicate) => predicate(value));
195
+ const isNode = (obj) => (0, _fxts_core.isObject)(obj) && "type" in obj;
194
196
  /**
195
197
  * Checks if a node is of a specific type.
196
198
  */
@@ -443,7 +445,7 @@ function createRequiredRuleEslint(config) {
443
445
  };
444
446
  }
445
447
  createRequiredRuleDeno(properties.assertionMethod);
446
- const eslint$20 = createRequiredRuleEslint(properties.assertionMethod);
448
+ const eslint$21 = createRequiredRuleEslint(properties.assertionMethod);
447
449
  //#endregion
448
450
  //#region src/lib/mismatch.ts
449
451
  const isIdentifierWithName = (name) => (node) => allOf(isNodeType("Identifier"), isNodeName(name))(node);
@@ -513,43 +515,43 @@ const createMismatchRuleEslint = (config) => ({
513
515
  }))
514
516
  });
515
517
  createMismatchRuleDeno(properties.featured);
516
- const eslint$19 = createMismatchRuleEslint(properties.featured);
518
+ const eslint$20 = createMismatchRuleEslint(properties.featured);
517
519
  createRequiredRuleDeno(properties.featured);
518
- const eslint$18 = createRequiredRuleEslint(properties.featured);
520
+ const eslint$19 = createRequiredRuleEslint(properties.featured);
519
521
  createMismatchRuleDeno(properties.featuredTags);
520
- const eslint$17 = createMismatchRuleEslint(properties.featuredTags);
522
+ const eslint$18 = createMismatchRuleEslint(properties.featuredTags);
521
523
  createRequiredRuleDeno(properties.featuredTags);
522
- const eslint$16 = createRequiredRuleEslint(properties.featuredTags);
524
+ const eslint$17 = createRequiredRuleEslint(properties.featuredTags);
523
525
  createMismatchRuleDeno(properties.followers);
524
- const eslint$15 = createMismatchRuleEslint(properties.followers);
526
+ const eslint$16 = createMismatchRuleEslint(properties.followers);
525
527
  createRequiredRuleDeno(properties.followers);
526
- const eslint$14 = createRequiredRuleEslint(properties.followers);
528
+ const eslint$15 = createRequiredRuleEslint(properties.followers);
527
529
  createMismatchRuleDeno(properties.following);
528
- const eslint$13 = createMismatchRuleEslint(properties.following);
530
+ const eslint$14 = createMismatchRuleEslint(properties.following);
529
531
  createRequiredRuleDeno(properties.following);
530
- const eslint$12 = createRequiredRuleEslint(properties.following);
532
+ const eslint$13 = createRequiredRuleEslint(properties.following);
531
533
  createMismatchRuleDeno(properties.id);
532
- const eslint$11 = createMismatchRuleEslint(properties.id);
534
+ const eslint$12 = createMismatchRuleEslint(properties.id);
533
535
  createRequiredRuleDeno(properties.id);
534
- const eslint$10 = createRequiredRuleEslint(properties.id);
536
+ const eslint$11 = createRequiredRuleEslint(properties.id);
535
537
  createMismatchRuleDeno(properties.inbox);
536
- const eslint$9 = createMismatchRuleEslint(properties.inbox);
538
+ const eslint$10 = createMismatchRuleEslint(properties.inbox);
537
539
  createRequiredRuleDeno(properties.inbox);
538
- const eslint$8 = createRequiredRuleEslint(properties.inbox);
540
+ const eslint$9 = createRequiredRuleEslint(properties.inbox);
539
541
  createMismatchRuleDeno(properties.liked);
540
- const eslint$7 = createMismatchRuleEslint(properties.liked);
542
+ const eslint$8 = createMismatchRuleEslint(properties.liked);
541
543
  createRequiredRuleDeno(properties.liked);
542
- const eslint$6 = createRequiredRuleEslint(properties.liked);
544
+ const eslint$7 = createRequiredRuleEslint(properties.liked);
543
545
  createMismatchRuleDeno(properties.outbox);
544
- const eslint$5 = createMismatchRuleEslint(properties.outbox);
546
+ const eslint$6 = createMismatchRuleEslint(properties.outbox);
545
547
  createRequiredRuleDeno(properties.outbox);
546
- const eslint$4 = createRequiredRuleEslint(properties.outbox);
548
+ const eslint$5 = createRequiredRuleEslint(properties.outbox);
547
549
  createRequiredRuleDeno(properties.publicKey);
548
- const eslint$3 = createRequiredRuleEslint(properties.publicKey);
550
+ const eslint$4 = createRequiredRuleEslint(properties.publicKey);
549
551
  createMismatchRuleDeno(properties.sharedInbox);
550
- const eslint$2 = createMismatchRuleEslint(properties.sharedInbox);
552
+ const eslint$3 = createMismatchRuleEslint(properties.sharedInbox);
551
553
  createRequiredRuleDeno(properties.sharedInbox);
552
- const eslint$1 = createRequiredRuleEslint(properties.sharedInbox);
554
+ const eslint$2 = createRequiredRuleEslint(properties.sharedInbox);
553
555
  //#endregion
554
556
  //#region src/rules/collection-filtering-not-implemented.ts
555
557
  /**
@@ -568,7 +570,7 @@ const isFollowersDispatcherCall = (node) => "callee" in node && node.callee && n
568
570
  * CollectionDispatcher signature: (context, identifier, cursor, filter?) => ...
569
571
  */
570
572
  const hasFilterParameter = hasMinParams(4);
571
- const eslint = {
573
+ const eslint$1 = {
572
574
  meta: {
573
575
  type: "suggestion",
574
576
  docs: { description: "Ensure followers dispatcher implements filtering" },
@@ -593,33 +595,287 @@ const eslint = {
593
595
  }
594
596
  };
595
597
  //#endregion
598
+ //#region src/rules/outbox-listener-delivery-required.ts
599
+ const MESSAGE = "Outbox listeners should deliver posted activities explicitly with ctx.sendActivity() or ctx.forwardActivity().";
600
+ const isChainedFromOutboxListeners = (expr, federationTracker) => {
601
+ if (expr.type !== "CallExpression") return false;
602
+ if (!hasMemberExpressionCallee(expr) || !hasIdentifierProperty(expr)) return false;
603
+ const methodName = expr.callee.property.name;
604
+ if (methodName === "setOutboxListeners") return federationTracker.isFederationObject(expr.callee.object);
605
+ if (methodName === "authorize" || methodName === "onError" || methodName === "on") return isChainedFromOutboxListeners(expr.callee.object, federationTracker);
606
+ return false;
607
+ };
608
+ const DELIVERY_METHOD_NAMES = new Set(["sendActivity", "forwardActivity"]);
609
+ const getMemberPropertyName = (expr) => {
610
+ if (expr.type !== "MemberExpression") return null;
611
+ const property = expr.property;
612
+ if (property.type === "Identifier") return property.name;
613
+ if (property.type === "Literal" && typeof property.value === "string") return property.value;
614
+ return null;
615
+ };
616
+ function unwrapContextParam(node) {
617
+ let current = node ?? null;
618
+ while (current?.type === "AssignmentPattern") current = current.left;
619
+ return current;
620
+ }
621
+ function escapeRegExp(value) {
622
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
623
+ }
624
+ function stripCommentsAndStrings(code) {
625
+ let result = "";
626
+ let index = 0;
627
+ const skipQuotedString = (quote) => {
628
+ const start = index;
629
+ index += 1;
630
+ while (index < code.length) {
631
+ const char = code[index];
632
+ if (char === "\\") {
633
+ index += 2;
634
+ continue;
635
+ }
636
+ index += 1;
637
+ if (char === quote) break;
638
+ }
639
+ const literal = code.slice(start, index);
640
+ const value = literal.slice(1, -1);
641
+ result += DELIVERY_METHOD_NAMES.has(value) ? literal : `${quote}${quote}`;
642
+ };
643
+ const stripTemplateLiteral = () => {
644
+ const start = index;
645
+ index += 1;
646
+ let raw = "";
647
+ let hasExpression = false;
648
+ while (index < code.length) {
649
+ const char = code[index];
650
+ if (char === "\\") {
651
+ raw += char;
652
+ raw += code[index + 1] ?? "";
653
+ index += 2;
654
+ continue;
655
+ }
656
+ if (char === "`") {
657
+ index += 1;
658
+ if (!hasExpression && DELIVERY_METHOD_NAMES.has(raw)) result += code.slice(start, index);
659
+ else result += "``";
660
+ return;
661
+ }
662
+ if (char === "$" && code[index + 1] === "{") {
663
+ hasExpression = true;
664
+ result += "`${";
665
+ index += 2;
666
+ let depth = 1;
667
+ while (index < code.length && depth > 0) {
668
+ const exprChar = code[index];
669
+ const next = code[index + 1];
670
+ if (exprChar === "'" || exprChar === "\"") {
671
+ skipQuotedString(exprChar);
672
+ continue;
673
+ }
674
+ if (exprChar === "`") {
675
+ stripTemplateLiteral();
676
+ continue;
677
+ }
678
+ if (exprChar === "/" && next === "*") {
679
+ index += 2;
680
+ while (index < code.length) {
681
+ if (code[index] === "*" && code[index + 1] === "/") {
682
+ index += 2;
683
+ break;
684
+ }
685
+ index += 1;
686
+ }
687
+ continue;
688
+ }
689
+ if (exprChar === "/" && next === "/") {
690
+ index += 2;
691
+ while (index < code.length && code[index] !== "\n") index += 1;
692
+ continue;
693
+ }
694
+ result += exprChar;
695
+ index += 1;
696
+ if (exprChar === "{") depth += 1;
697
+ else if (exprChar === "}") depth -= 1;
698
+ }
699
+ continue;
700
+ }
701
+ raw += char;
702
+ index += 1;
703
+ }
704
+ result += "``";
705
+ };
706
+ while (index < code.length) {
707
+ const char = code[index];
708
+ const next = code[index + 1];
709
+ if (char === "/" && next === "*") {
710
+ index += 2;
711
+ while (index < code.length) {
712
+ if (code[index] === "*" && code[index + 1] === "/") {
713
+ index += 2;
714
+ break;
715
+ }
716
+ index += 1;
717
+ }
718
+ continue;
719
+ }
720
+ if (char === "/" && next === "/") {
721
+ index += 2;
722
+ while (index < code.length && code[index] !== "\n") index += 1;
723
+ continue;
724
+ }
725
+ if (char === "'" || char === "\"") {
726
+ skipQuotedString(char);
727
+ continue;
728
+ }
729
+ if (char === "`") {
730
+ stripTemplateLiteral();
731
+ continue;
732
+ }
733
+ result += char;
734
+ index += 1;
735
+ }
736
+ return result;
737
+ }
738
+ function getDeliveryAliasName(node) {
739
+ if (node.type === "Identifier") return node.name;
740
+ if (node.type === "AssignmentPattern" && node.left.type === "Identifier") return node.left.name;
741
+ return null;
742
+ }
743
+ function buildContextExpressionPattern(contextName) {
744
+ const name = escapeRegExp(contextName);
745
+ const boundedName = String.raw`(?<![\w$])${name}(?![\w$])`;
746
+ return String.raw`(?:${boundedName}|\(\s*${boundedName}(?:\s+as\s+[^)]+)?\s*\))`;
747
+ }
748
+ const resolveListenerReference = (expr, bindings, seen = /* @__PURE__ */ new Set()) => {
749
+ if (isFunction(expr)) return expr;
750
+ if (expr.type === "Identifier") {
751
+ if (seen.has(expr.name)) return null;
752
+ seen.add(expr.name);
753
+ const binding = bindings.get(expr.name);
754
+ if (binding == null || !isNode(binding)) return null;
755
+ if (isFunction(binding) || binding.type === "FunctionDeclaration") return binding;
756
+ if (binding.type === "Identifier") return resolveListenerReference(binding, bindings, seen);
757
+ return null;
758
+ }
759
+ if (expr.type === "MemberExpression" && expr.object.type === "Identifier" && !expr.computed) {
760
+ const binding = bindings.get(expr.object.name);
761
+ if (binding == null || !isNode(binding) || binding.type !== "ObjectExpression") return null;
762
+ const propertyName = getMemberPropertyName(expr);
763
+ if (propertyName == null) return null;
764
+ for (const prop of binding.properties) {
765
+ if (!isNode(prop) || prop.type !== "Property") continue;
766
+ if ((prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" && typeof prop.key.value === "string" ? prop.key.value : null) !== propertyName || !isNode(prop.value)) continue;
767
+ const value = prop.value;
768
+ if (isFunction(value) || value.type === "FunctionDeclaration") return value;
769
+ }
770
+ }
771
+ return null;
772
+ };
773
+ const listenerCallsDeliveryMethod = (sourceCode, listener) => {
774
+ const code = stripCommentsAndStrings(sourceCode.getText(listener));
775
+ const aliases = /* @__PURE__ */ new Set();
776
+ const contextParam = unwrapContextParam(listener.params[0]);
777
+ const contextName = contextParam?.type === "Identifier" ? contextParam.name : null;
778
+ if (contextParam?.type === "ObjectPattern") for (const prop of contextParam.properties) {
779
+ if (!isNode(prop) || prop.type !== "Property") continue;
780
+ const keyName = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" && typeof prop.key.value === "string" ? prop.key.value : null;
781
+ if (keyName == null || !DELIVERY_METHOD_NAMES.has(keyName)) continue;
782
+ const alias = getDeliveryAliasName(prop.value);
783
+ if (alias != null) aliases.add(alias);
784
+ }
785
+ if (contextName != null) {
786
+ const contextExpr = buildContextExpressionPattern(contextName);
787
+ if (new RegExp(String.raw`${contextExpr}\s*(?:\?\s*\.\s*(?:sendActivity|forwardActivity)|\.\s*(?:sendActivity|forwardActivity)|\?\s*\.\s*\[\s*["'\`](?:sendActivity|forwardActivity)["'\`]\s*\]|\[\s*["'\`](?:sendActivity|forwardActivity)["'\`]\s*\])\s*\(`).test(code)) return true;
788
+ const destructuringPattern = new RegExp(String.raw`(?:const|let|var)\s*{([^}]*)}\s*=\s*${contextExpr}`, "g");
789
+ for (const match of code.matchAll(destructuringPattern)) {
790
+ const fields = match[1].split(",").map((field) => field.trim()).filter(Boolean);
791
+ for (const field of fields) {
792
+ const [sourceName, aliasName] = field.split(":").map((part) => part.trim());
793
+ if (!DELIVERY_METHOD_NAMES.has(sourceName)) continue;
794
+ aliases.add(aliasName ?? sourceName);
795
+ }
796
+ }
797
+ const aliasPattern = new RegExp(String.raw`(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*${contextExpr}\s*(?:\?\s*\.\s*(sendActivity|forwardActivity)|\.\s*(sendActivity|forwardActivity)|\?\s*\.\s*\[\s*["'\`](sendActivity|forwardActivity)["'\`]\s*\]|\[\s*["'\`](sendActivity|forwardActivity)["'\`]\s*\])`, "g");
798
+ for (const match of code.matchAll(aliasPattern)) aliases.add(match[1]);
799
+ }
800
+ return globalThis.Array.from(aliases).some((alias) => new RegExp(String.raw`\b${escapeRegExp(alias)}\s*\(`).test(code));
801
+ };
802
+ function createRule(buildReport) {
803
+ return (context) => {
804
+ const federationTracker = trackFederationVariables();
805
+ const bindings = /* @__PURE__ */ new Map();
806
+ const pendingCalls = [];
807
+ const sourceCode = context.sourceCode;
808
+ const inspectCall = (node) => {
809
+ if (!hasMemberExpressionCallee(node) || !hasIdentifierProperty(node) || !hasMethodName("on")(node) || node.arguments.length < 2) return;
810
+ if (!isChainedFromOutboxListeners(node.callee.object, federationTracker)) return;
811
+ const listener = node.arguments[1];
812
+ const resolvedListener = isNode(listener) && isFunction(listener) ? listener : isNode(listener) ? resolveListenerReference(listener, bindings) : null;
813
+ if (resolvedListener == null) return;
814
+ if (listenerCallsDeliveryMethod(sourceCode, resolvedListener)) return;
815
+ context.report({
816
+ node: resolvedListener,
817
+ ...buildReport
818
+ });
819
+ };
820
+ return {
821
+ VariableDeclarator(node) {
822
+ federationTracker.VariableDeclarator(node);
823
+ if (node.id.type === "Identifier" && node.init != null) bindings.set(node.id.name, node.init);
824
+ },
825
+ FunctionDeclaration(node) {
826
+ if (node.id != null) bindings.set(node.id.name, node);
827
+ },
828
+ CallExpression(node) {
829
+ pendingCalls.push(node);
830
+ },
831
+ "Program:exit"() {
832
+ for (const node of pendingCalls) inspectCall(node);
833
+ }
834
+ };
835
+ };
836
+ }
837
+ createRule({ message: MESSAGE });
838
+ const eslint = {
839
+ meta: {
840
+ type: "suggestion",
841
+ docs: { description: "Warn when an outbox listener omits explicit delivery methods" },
842
+ schema: [],
843
+ messages: { required: "{{ message }}" }
844
+ },
845
+ create: createRule({
846
+ messageId: "required",
847
+ data: { message: MESSAGE }
848
+ })
849
+ };
850
+ //#endregion
596
851
  //#region src/index.ts
597
852
  /**
598
853
  * ESLint plugin for Fedify.
599
854
  * Provides lint rules for validating Fedify federation code.
600
855
  */
601
856
  const rules = {
602
- [RULE_IDS.actorIdMismatch]: eslint$11,
603
- [RULE_IDS.actorIdRequired]: eslint$10,
604
- [RULE_IDS.actorFollowingPropertyRequired]: eslint$12,
605
- [RULE_IDS.actorFollowingPropertyMismatch]: eslint$13,
606
- [RULE_IDS.actorFollowersPropertyRequired]: eslint$14,
607
- [RULE_IDS.actorFollowersPropertyMismatch]: eslint$15,
608
- [RULE_IDS.actorOutboxPropertyRequired]: eslint$4,
609
- [RULE_IDS.actorOutboxPropertyMismatch]: eslint$5,
610
- [RULE_IDS.actorLikedPropertyRequired]: eslint$6,
611
- [RULE_IDS.actorLikedPropertyMismatch]: eslint$7,
612
- [RULE_IDS.actorFeaturedPropertyRequired]: eslint$18,
613
- [RULE_IDS.actorFeaturedPropertyMismatch]: eslint$19,
614
- [RULE_IDS.actorFeaturedTagsPropertyRequired]: eslint$16,
615
- [RULE_IDS.actorFeaturedTagsPropertyMismatch]: eslint$17,
616
- [RULE_IDS.actorInboxPropertyRequired]: eslint$8,
617
- [RULE_IDS.actorInboxPropertyMismatch]: eslint$9,
618
- [RULE_IDS.actorSharedInboxPropertyRequired]: eslint$1,
619
- [RULE_IDS.actorSharedInboxPropertyMismatch]: eslint$2,
620
- [RULE_IDS.actorPublicKeyRequired]: eslint$3,
621
- [RULE_IDS.actorAssertionMethodRequired]: eslint$20,
622
- [RULE_IDS.collectionFilteringNotImplemented]: eslint
857
+ [RULE_IDS.actorIdMismatch]: eslint$12,
858
+ [RULE_IDS.actorIdRequired]: eslint$11,
859
+ [RULE_IDS.actorFollowingPropertyRequired]: eslint$13,
860
+ [RULE_IDS.actorFollowingPropertyMismatch]: eslint$14,
861
+ [RULE_IDS.actorFollowersPropertyRequired]: eslint$15,
862
+ [RULE_IDS.actorFollowersPropertyMismatch]: eslint$16,
863
+ [RULE_IDS.actorOutboxPropertyRequired]: eslint$5,
864
+ [RULE_IDS.actorOutboxPropertyMismatch]: eslint$6,
865
+ [RULE_IDS.actorLikedPropertyRequired]: eslint$7,
866
+ [RULE_IDS.actorLikedPropertyMismatch]: eslint$8,
867
+ [RULE_IDS.actorFeaturedPropertyRequired]: eslint$19,
868
+ [RULE_IDS.actorFeaturedPropertyMismatch]: eslint$20,
869
+ [RULE_IDS.actorFeaturedTagsPropertyRequired]: eslint$17,
870
+ [RULE_IDS.actorFeaturedTagsPropertyMismatch]: eslint$18,
871
+ [RULE_IDS.actorInboxPropertyRequired]: eslint$9,
872
+ [RULE_IDS.actorInboxPropertyMismatch]: eslint$10,
873
+ [RULE_IDS.actorSharedInboxPropertyRequired]: eslint$2,
874
+ [RULE_IDS.actorSharedInboxPropertyMismatch]: eslint$3,
875
+ [RULE_IDS.actorPublicKeyRequired]: eslint$4,
876
+ [RULE_IDS.actorAssertionMethodRequired]: eslint$21,
877
+ [RULE_IDS.collectionFilteringNotImplemented]: eslint$1,
878
+ [RULE_IDS.outboxListenerDeliveryRequired]: eslint
623
879
  };
624
880
  const recommendedRuleIds = [RULE_IDS.actorIdMismatch, RULE_IDS.actorIdRequired];
625
881
  /**
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { always, every, fromEntries, head, isEmpty, isObject, keys, map, negate,
2
2
  import parser from "@typescript-eslint/parser";
3
3
  //#region deno.json
4
4
  var name = "@fedify/lint";
5
- var version = "2.2.0-pr.695.16+7a782334";
5
+ var version = "2.2.0-pr.697.18+b776632f";
6
6
  //#endregion
7
7
  //#region src/lib/const.ts
8
8
  /**
@@ -119,7 +119,8 @@ const RULE_IDS = {
119
119
  actorFeaturedTagsPropertyMismatch: "actor-featured-tags-property-mismatch",
120
120
  actorInboxPropertyMismatch: "actor-inbox-property-mismatch",
121
121
  actorSharedInboxPropertyMismatch: "actor-shared-inbox-property-mismatch",
122
- collectionFilteringNotImplemented: "collection-filtering-not-implemented"
122
+ collectionFilteringNotImplemented: "collection-filtering-not-implemented",
123
+ outboxListenerDeliveryRequired: "outbox-listener-delivery-required"
123
124
  };
124
125
  //#endregion
125
126
  //#region src/lib/messages.ts
@@ -164,6 +165,7 @@ function allOf(...predicates) {
164
165
  return (value) => predicates.every((predicate) => predicate(value));
165
166
  }
166
167
  const anyOf = (...predicates) => (value) => predicates.some((predicate) => predicate(value));
168
+ const isNode = (obj) => isObject(obj) && "type" in obj;
167
169
  /**
168
170
  * Checks if a node is of a specific type.
169
171
  */
@@ -416,7 +418,7 @@ function createRequiredRuleEslint(config) {
416
418
  };
417
419
  }
418
420
  createRequiredRuleDeno(properties.assertionMethod);
419
- const eslint$20 = createRequiredRuleEslint(properties.assertionMethod);
421
+ const eslint$21 = createRequiredRuleEslint(properties.assertionMethod);
420
422
  //#endregion
421
423
  //#region src/lib/mismatch.ts
422
424
  const isIdentifierWithName = (name) => (node) => allOf(isNodeType("Identifier"), isNodeName(name))(node);
@@ -486,43 +488,43 @@ const createMismatchRuleEslint = (config) => ({
486
488
  }))
487
489
  });
488
490
  createMismatchRuleDeno(properties.featured);
489
- const eslint$19 = createMismatchRuleEslint(properties.featured);
491
+ const eslint$20 = createMismatchRuleEslint(properties.featured);
490
492
  createRequiredRuleDeno(properties.featured);
491
- const eslint$18 = createRequiredRuleEslint(properties.featured);
493
+ const eslint$19 = createRequiredRuleEslint(properties.featured);
492
494
  createMismatchRuleDeno(properties.featuredTags);
493
- const eslint$17 = createMismatchRuleEslint(properties.featuredTags);
495
+ const eslint$18 = createMismatchRuleEslint(properties.featuredTags);
494
496
  createRequiredRuleDeno(properties.featuredTags);
495
- const eslint$16 = createRequiredRuleEslint(properties.featuredTags);
497
+ const eslint$17 = createRequiredRuleEslint(properties.featuredTags);
496
498
  createMismatchRuleDeno(properties.followers);
497
- const eslint$15 = createMismatchRuleEslint(properties.followers);
499
+ const eslint$16 = createMismatchRuleEslint(properties.followers);
498
500
  createRequiredRuleDeno(properties.followers);
499
- const eslint$14 = createRequiredRuleEslint(properties.followers);
501
+ const eslint$15 = createRequiredRuleEslint(properties.followers);
500
502
  createMismatchRuleDeno(properties.following);
501
- const eslint$13 = createMismatchRuleEslint(properties.following);
503
+ const eslint$14 = createMismatchRuleEslint(properties.following);
502
504
  createRequiredRuleDeno(properties.following);
503
- const eslint$12 = createRequiredRuleEslint(properties.following);
505
+ const eslint$13 = createRequiredRuleEslint(properties.following);
504
506
  createMismatchRuleDeno(properties.id);
505
- const eslint$11 = createMismatchRuleEslint(properties.id);
507
+ const eslint$12 = createMismatchRuleEslint(properties.id);
506
508
  createRequiredRuleDeno(properties.id);
507
- const eslint$10 = createRequiredRuleEslint(properties.id);
509
+ const eslint$11 = createRequiredRuleEslint(properties.id);
508
510
  createMismatchRuleDeno(properties.inbox);
509
- const eslint$9 = createMismatchRuleEslint(properties.inbox);
511
+ const eslint$10 = createMismatchRuleEslint(properties.inbox);
510
512
  createRequiredRuleDeno(properties.inbox);
511
- const eslint$8 = createRequiredRuleEslint(properties.inbox);
513
+ const eslint$9 = createRequiredRuleEslint(properties.inbox);
512
514
  createMismatchRuleDeno(properties.liked);
513
- const eslint$7 = createMismatchRuleEslint(properties.liked);
515
+ const eslint$8 = createMismatchRuleEslint(properties.liked);
514
516
  createRequiredRuleDeno(properties.liked);
515
- const eslint$6 = createRequiredRuleEslint(properties.liked);
517
+ const eslint$7 = createRequiredRuleEslint(properties.liked);
516
518
  createMismatchRuleDeno(properties.outbox);
517
- const eslint$5 = createMismatchRuleEslint(properties.outbox);
519
+ const eslint$6 = createMismatchRuleEslint(properties.outbox);
518
520
  createRequiredRuleDeno(properties.outbox);
519
- const eslint$4 = createRequiredRuleEslint(properties.outbox);
521
+ const eslint$5 = createRequiredRuleEslint(properties.outbox);
520
522
  createRequiredRuleDeno(properties.publicKey);
521
- const eslint$3 = createRequiredRuleEslint(properties.publicKey);
523
+ const eslint$4 = createRequiredRuleEslint(properties.publicKey);
522
524
  createMismatchRuleDeno(properties.sharedInbox);
523
- const eslint$2 = createMismatchRuleEslint(properties.sharedInbox);
525
+ const eslint$3 = createMismatchRuleEslint(properties.sharedInbox);
524
526
  createRequiredRuleDeno(properties.sharedInbox);
525
- const eslint$1 = createRequiredRuleEslint(properties.sharedInbox);
527
+ const eslint$2 = createRequiredRuleEslint(properties.sharedInbox);
526
528
  //#endregion
527
529
  //#region src/rules/collection-filtering-not-implemented.ts
528
530
  /**
@@ -541,7 +543,7 @@ const isFollowersDispatcherCall = (node) => "callee" in node && node.callee && n
541
543
  * CollectionDispatcher signature: (context, identifier, cursor, filter?) => ...
542
544
  */
543
545
  const hasFilterParameter = hasMinParams(4);
544
- const eslint = {
546
+ const eslint$1 = {
545
547
  meta: {
546
548
  type: "suggestion",
547
549
  docs: { description: "Ensure followers dispatcher implements filtering" },
@@ -566,33 +568,287 @@ const eslint = {
566
568
  }
567
569
  };
568
570
  //#endregion
571
+ //#region src/rules/outbox-listener-delivery-required.ts
572
+ const MESSAGE = "Outbox listeners should deliver posted activities explicitly with ctx.sendActivity() or ctx.forwardActivity().";
573
+ const isChainedFromOutboxListeners = (expr, federationTracker) => {
574
+ if (expr.type !== "CallExpression") return false;
575
+ if (!hasMemberExpressionCallee(expr) || !hasIdentifierProperty(expr)) return false;
576
+ const methodName = expr.callee.property.name;
577
+ if (methodName === "setOutboxListeners") return federationTracker.isFederationObject(expr.callee.object);
578
+ if (methodName === "authorize" || methodName === "onError" || methodName === "on") return isChainedFromOutboxListeners(expr.callee.object, federationTracker);
579
+ return false;
580
+ };
581
+ const DELIVERY_METHOD_NAMES = new Set(["sendActivity", "forwardActivity"]);
582
+ const getMemberPropertyName = (expr) => {
583
+ if (expr.type !== "MemberExpression") return null;
584
+ const property = expr.property;
585
+ if (property.type === "Identifier") return property.name;
586
+ if (property.type === "Literal" && typeof property.value === "string") return property.value;
587
+ return null;
588
+ };
589
+ function unwrapContextParam(node) {
590
+ let current = node ?? null;
591
+ while (current?.type === "AssignmentPattern") current = current.left;
592
+ return current;
593
+ }
594
+ function escapeRegExp(value) {
595
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
596
+ }
597
+ function stripCommentsAndStrings(code) {
598
+ let result = "";
599
+ let index = 0;
600
+ const skipQuotedString = (quote) => {
601
+ const start = index;
602
+ index += 1;
603
+ while (index < code.length) {
604
+ const char = code[index];
605
+ if (char === "\\") {
606
+ index += 2;
607
+ continue;
608
+ }
609
+ index += 1;
610
+ if (char === quote) break;
611
+ }
612
+ const literal = code.slice(start, index);
613
+ const value = literal.slice(1, -1);
614
+ result += DELIVERY_METHOD_NAMES.has(value) ? literal : `${quote}${quote}`;
615
+ };
616
+ const stripTemplateLiteral = () => {
617
+ const start = index;
618
+ index += 1;
619
+ let raw = "";
620
+ let hasExpression = false;
621
+ while (index < code.length) {
622
+ const char = code[index];
623
+ if (char === "\\") {
624
+ raw += char;
625
+ raw += code[index + 1] ?? "";
626
+ index += 2;
627
+ continue;
628
+ }
629
+ if (char === "`") {
630
+ index += 1;
631
+ if (!hasExpression && DELIVERY_METHOD_NAMES.has(raw)) result += code.slice(start, index);
632
+ else result += "``";
633
+ return;
634
+ }
635
+ if (char === "$" && code[index + 1] === "{") {
636
+ hasExpression = true;
637
+ result += "`${";
638
+ index += 2;
639
+ let depth = 1;
640
+ while (index < code.length && depth > 0) {
641
+ const exprChar = code[index];
642
+ const next = code[index + 1];
643
+ if (exprChar === "'" || exprChar === "\"") {
644
+ skipQuotedString(exprChar);
645
+ continue;
646
+ }
647
+ if (exprChar === "`") {
648
+ stripTemplateLiteral();
649
+ continue;
650
+ }
651
+ if (exprChar === "/" && next === "*") {
652
+ index += 2;
653
+ while (index < code.length) {
654
+ if (code[index] === "*" && code[index + 1] === "/") {
655
+ index += 2;
656
+ break;
657
+ }
658
+ index += 1;
659
+ }
660
+ continue;
661
+ }
662
+ if (exprChar === "/" && next === "/") {
663
+ index += 2;
664
+ while (index < code.length && code[index] !== "\n") index += 1;
665
+ continue;
666
+ }
667
+ result += exprChar;
668
+ index += 1;
669
+ if (exprChar === "{") depth += 1;
670
+ else if (exprChar === "}") depth -= 1;
671
+ }
672
+ continue;
673
+ }
674
+ raw += char;
675
+ index += 1;
676
+ }
677
+ result += "``";
678
+ };
679
+ while (index < code.length) {
680
+ const char = code[index];
681
+ const next = code[index + 1];
682
+ if (char === "/" && next === "*") {
683
+ index += 2;
684
+ while (index < code.length) {
685
+ if (code[index] === "*" && code[index + 1] === "/") {
686
+ index += 2;
687
+ break;
688
+ }
689
+ index += 1;
690
+ }
691
+ continue;
692
+ }
693
+ if (char === "/" && next === "/") {
694
+ index += 2;
695
+ while (index < code.length && code[index] !== "\n") index += 1;
696
+ continue;
697
+ }
698
+ if (char === "'" || char === "\"") {
699
+ skipQuotedString(char);
700
+ continue;
701
+ }
702
+ if (char === "`") {
703
+ stripTemplateLiteral();
704
+ continue;
705
+ }
706
+ result += char;
707
+ index += 1;
708
+ }
709
+ return result;
710
+ }
711
+ function getDeliveryAliasName(node) {
712
+ if (node.type === "Identifier") return node.name;
713
+ if (node.type === "AssignmentPattern" && node.left.type === "Identifier") return node.left.name;
714
+ return null;
715
+ }
716
+ function buildContextExpressionPattern(contextName) {
717
+ const name = escapeRegExp(contextName);
718
+ const boundedName = String.raw`(?<![\w$])${name}(?![\w$])`;
719
+ return String.raw`(?:${boundedName}|\(\s*${boundedName}(?:\s+as\s+[^)]+)?\s*\))`;
720
+ }
721
+ const resolveListenerReference = (expr, bindings, seen = /* @__PURE__ */ new Set()) => {
722
+ if (isFunction(expr)) return expr;
723
+ if (expr.type === "Identifier") {
724
+ if (seen.has(expr.name)) return null;
725
+ seen.add(expr.name);
726
+ const binding = bindings.get(expr.name);
727
+ if (binding == null || !isNode(binding)) return null;
728
+ if (isFunction(binding) || binding.type === "FunctionDeclaration") return binding;
729
+ if (binding.type === "Identifier") return resolveListenerReference(binding, bindings, seen);
730
+ return null;
731
+ }
732
+ if (expr.type === "MemberExpression" && expr.object.type === "Identifier" && !expr.computed) {
733
+ const binding = bindings.get(expr.object.name);
734
+ if (binding == null || !isNode(binding) || binding.type !== "ObjectExpression") return null;
735
+ const propertyName = getMemberPropertyName(expr);
736
+ if (propertyName == null) return null;
737
+ for (const prop of binding.properties) {
738
+ if (!isNode(prop) || prop.type !== "Property") continue;
739
+ if ((prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" && typeof prop.key.value === "string" ? prop.key.value : null) !== propertyName || !isNode(prop.value)) continue;
740
+ const value = prop.value;
741
+ if (isFunction(value) || value.type === "FunctionDeclaration") return value;
742
+ }
743
+ }
744
+ return null;
745
+ };
746
+ const listenerCallsDeliveryMethod = (sourceCode, listener) => {
747
+ const code = stripCommentsAndStrings(sourceCode.getText(listener));
748
+ const aliases = /* @__PURE__ */ new Set();
749
+ const contextParam = unwrapContextParam(listener.params[0]);
750
+ const contextName = contextParam?.type === "Identifier" ? contextParam.name : null;
751
+ if (contextParam?.type === "ObjectPattern") for (const prop of contextParam.properties) {
752
+ if (!isNode(prop) || prop.type !== "Property") continue;
753
+ const keyName = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" && typeof prop.key.value === "string" ? prop.key.value : null;
754
+ if (keyName == null || !DELIVERY_METHOD_NAMES.has(keyName)) continue;
755
+ const alias = getDeliveryAliasName(prop.value);
756
+ if (alias != null) aliases.add(alias);
757
+ }
758
+ if (contextName != null) {
759
+ const contextExpr = buildContextExpressionPattern(contextName);
760
+ if (new RegExp(String.raw`${contextExpr}\s*(?:\?\s*\.\s*(?:sendActivity|forwardActivity)|\.\s*(?:sendActivity|forwardActivity)|\?\s*\.\s*\[\s*["'\`](?:sendActivity|forwardActivity)["'\`]\s*\]|\[\s*["'\`](?:sendActivity|forwardActivity)["'\`]\s*\])\s*\(`).test(code)) return true;
761
+ const destructuringPattern = new RegExp(String.raw`(?:const|let|var)\s*{([^}]*)}\s*=\s*${contextExpr}`, "g");
762
+ for (const match of code.matchAll(destructuringPattern)) {
763
+ const fields = match[1].split(",").map((field) => field.trim()).filter(Boolean);
764
+ for (const field of fields) {
765
+ const [sourceName, aliasName] = field.split(":").map((part) => part.trim());
766
+ if (!DELIVERY_METHOD_NAMES.has(sourceName)) continue;
767
+ aliases.add(aliasName ?? sourceName);
768
+ }
769
+ }
770
+ const aliasPattern = new RegExp(String.raw`(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*${contextExpr}\s*(?:\?\s*\.\s*(sendActivity|forwardActivity)|\.\s*(sendActivity|forwardActivity)|\?\s*\.\s*\[\s*["'\`](sendActivity|forwardActivity)["'\`]\s*\]|\[\s*["'\`](sendActivity|forwardActivity)["'\`]\s*\])`, "g");
771
+ for (const match of code.matchAll(aliasPattern)) aliases.add(match[1]);
772
+ }
773
+ return globalThis.Array.from(aliases).some((alias) => new RegExp(String.raw`\b${escapeRegExp(alias)}\s*\(`).test(code));
774
+ };
775
+ function createRule(buildReport) {
776
+ return (context) => {
777
+ const federationTracker = trackFederationVariables();
778
+ const bindings = /* @__PURE__ */ new Map();
779
+ const pendingCalls = [];
780
+ const sourceCode = context.sourceCode;
781
+ const inspectCall = (node) => {
782
+ if (!hasMemberExpressionCallee(node) || !hasIdentifierProperty(node) || !hasMethodName("on")(node) || node.arguments.length < 2) return;
783
+ if (!isChainedFromOutboxListeners(node.callee.object, federationTracker)) return;
784
+ const listener = node.arguments[1];
785
+ const resolvedListener = isNode(listener) && isFunction(listener) ? listener : isNode(listener) ? resolveListenerReference(listener, bindings) : null;
786
+ if (resolvedListener == null) return;
787
+ if (listenerCallsDeliveryMethod(sourceCode, resolvedListener)) return;
788
+ context.report({
789
+ node: resolvedListener,
790
+ ...buildReport
791
+ });
792
+ };
793
+ return {
794
+ VariableDeclarator(node) {
795
+ federationTracker.VariableDeclarator(node);
796
+ if (node.id.type === "Identifier" && node.init != null) bindings.set(node.id.name, node.init);
797
+ },
798
+ FunctionDeclaration(node) {
799
+ if (node.id != null) bindings.set(node.id.name, node);
800
+ },
801
+ CallExpression(node) {
802
+ pendingCalls.push(node);
803
+ },
804
+ "Program:exit"() {
805
+ for (const node of pendingCalls) inspectCall(node);
806
+ }
807
+ };
808
+ };
809
+ }
810
+ createRule({ message: MESSAGE });
811
+ const eslint = {
812
+ meta: {
813
+ type: "suggestion",
814
+ docs: { description: "Warn when an outbox listener omits explicit delivery methods" },
815
+ schema: [],
816
+ messages: { required: "{{ message }}" }
817
+ },
818
+ create: createRule({
819
+ messageId: "required",
820
+ data: { message: MESSAGE }
821
+ })
822
+ };
823
+ //#endregion
569
824
  //#region src/index.ts
570
825
  /**
571
826
  * ESLint plugin for Fedify.
572
827
  * Provides lint rules for validating Fedify federation code.
573
828
  */
574
829
  const rules = {
575
- [RULE_IDS.actorIdMismatch]: eslint$11,
576
- [RULE_IDS.actorIdRequired]: eslint$10,
577
- [RULE_IDS.actorFollowingPropertyRequired]: eslint$12,
578
- [RULE_IDS.actorFollowingPropertyMismatch]: eslint$13,
579
- [RULE_IDS.actorFollowersPropertyRequired]: eslint$14,
580
- [RULE_IDS.actorFollowersPropertyMismatch]: eslint$15,
581
- [RULE_IDS.actorOutboxPropertyRequired]: eslint$4,
582
- [RULE_IDS.actorOutboxPropertyMismatch]: eslint$5,
583
- [RULE_IDS.actorLikedPropertyRequired]: eslint$6,
584
- [RULE_IDS.actorLikedPropertyMismatch]: eslint$7,
585
- [RULE_IDS.actorFeaturedPropertyRequired]: eslint$18,
586
- [RULE_IDS.actorFeaturedPropertyMismatch]: eslint$19,
587
- [RULE_IDS.actorFeaturedTagsPropertyRequired]: eslint$16,
588
- [RULE_IDS.actorFeaturedTagsPropertyMismatch]: eslint$17,
589
- [RULE_IDS.actorInboxPropertyRequired]: eslint$8,
590
- [RULE_IDS.actorInboxPropertyMismatch]: eslint$9,
591
- [RULE_IDS.actorSharedInboxPropertyRequired]: eslint$1,
592
- [RULE_IDS.actorSharedInboxPropertyMismatch]: eslint$2,
593
- [RULE_IDS.actorPublicKeyRequired]: eslint$3,
594
- [RULE_IDS.actorAssertionMethodRequired]: eslint$20,
595
- [RULE_IDS.collectionFilteringNotImplemented]: eslint
830
+ [RULE_IDS.actorIdMismatch]: eslint$12,
831
+ [RULE_IDS.actorIdRequired]: eslint$11,
832
+ [RULE_IDS.actorFollowingPropertyRequired]: eslint$13,
833
+ [RULE_IDS.actorFollowingPropertyMismatch]: eslint$14,
834
+ [RULE_IDS.actorFollowersPropertyRequired]: eslint$15,
835
+ [RULE_IDS.actorFollowersPropertyMismatch]: eslint$16,
836
+ [RULE_IDS.actorOutboxPropertyRequired]: eslint$5,
837
+ [RULE_IDS.actorOutboxPropertyMismatch]: eslint$6,
838
+ [RULE_IDS.actorLikedPropertyRequired]: eslint$7,
839
+ [RULE_IDS.actorLikedPropertyMismatch]: eslint$8,
840
+ [RULE_IDS.actorFeaturedPropertyRequired]: eslint$19,
841
+ [RULE_IDS.actorFeaturedPropertyMismatch]: eslint$20,
842
+ [RULE_IDS.actorFeaturedTagsPropertyRequired]: eslint$17,
843
+ [RULE_IDS.actorFeaturedTagsPropertyMismatch]: eslint$18,
844
+ [RULE_IDS.actorInboxPropertyRequired]: eslint$9,
845
+ [RULE_IDS.actorInboxPropertyMismatch]: eslint$10,
846
+ [RULE_IDS.actorSharedInboxPropertyRequired]: eslint$2,
847
+ [RULE_IDS.actorSharedInboxPropertyMismatch]: eslint$3,
848
+ [RULE_IDS.actorPublicKeyRequired]: eslint$4,
849
+ [RULE_IDS.actorAssertionMethodRequired]: eslint$21,
850
+ [RULE_IDS.collectionFilteringNotImplemented]: eslint$1,
851
+ [RULE_IDS.outboxListenerDeliveryRequired]: eslint
596
852
  };
597
853
  const recommendedRuleIds = [RULE_IDS.actorIdMismatch, RULE_IDS.actorIdRequired];
598
854
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/lint",
3
- "version": "2.2.0-pr.695.16+7a782334",
3
+ "version": "2.2.0-pr.697.18+b776632f",
4
4
  "description": "Fedify linting rules and plugins",
5
5
  "keywords": [
6
6
  "Fedify",
@@ -47,7 +47,7 @@
47
47
  ],
48
48
  "peerDependencies": {
49
49
  "eslint": ">=9.0.0",
50
- "@fedify/fedify": "^2.2.0-pr.695.16+7a782334"
50
+ "@fedify/fedify": "^2.2.0-pr.697.18+b776632f"
51
51
  },
52
52
  "peerDependenciesMeta": {
53
53
  "eslint": {