@fedify/interaction-controls 2.5.0-dev.2339 → 2.5.0-dev.2355

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/dist/mod.cjs CHANGED
@@ -30,7 +30,7 @@ function createInteractionControl(config) {
30
30
  createRevocation: (options) => new _fedify_vocab.Delete({
31
31
  id: options.id,
32
32
  actor: options.actor,
33
- object: getRequiredId(options.authorization, "authorization"),
33
+ object: options.embedAuthorization && !(options.authorization instanceof URL) ? createEmbeddedAuthorization(options.authorization, config) : getRequiredId(options.authorization, "authorization"),
34
34
  ...audience(options)
35
35
  }),
36
36
  recognizeImpolite: config.recognizeImpolite,
@@ -59,6 +59,17 @@ function getRequiredId(value, name) {
59
59
  if (value.id == null) throw new TypeError(`The ${name} must have an id.`);
60
60
  return value.id;
61
61
  }
62
+ function createEmbeddedAuthorization(authorization, config) {
63
+ const { interactingObjectId, interactionTargetId } = authorization;
64
+ if (interactingObjectId == null) throw new TypeError("The authorization's interactingObject must have an id.");
65
+ if (interactionTargetId == null) throw new TypeError("The authorization's interactionTarget must have an id.");
66
+ return new config.authorizationClass({
67
+ id: getRequiredId(authorization, "authorization"),
68
+ attributions: authorization.attributionIds,
69
+ interactingObject: interactingObjectId,
70
+ interactionTarget: interactionTargetId
71
+ });
72
+ }
62
73
  function getId(value) {
63
74
  return value instanceof URL ? value : value.id;
64
75
  }
@@ -70,8 +81,9 @@ function getTypeId(object) {
70
81
  }
71
82
  async function verifyRequest(context, options, config) {
72
83
  const documentLoader = options.documentLoader ?? context.documentLoader;
84
+ const contextLoader = options.contextLoader ?? documentLoader;
73
85
  const expectedRequestId = options.request instanceof URL ? options.request : null;
74
- const requestResult = await materialize(options.request, config.requestClass, documentLoader);
86
+ const requestResult = await materialize(options.request, config.requestClass, documentLoader, contextLoader);
75
87
  if (!requestResult.ok) return {
76
88
  verified: false,
77
89
  failure: requestResult.failure
@@ -107,12 +119,17 @@ async function verifyRequest(context, options, config) {
107
119
  actual: request.id
108
120
  }
109
121
  };
110
- const dereferenceOptions = {
111
- documentLoader,
112
- contextLoader: documentLoader,
113
- suppressError: true
122
+ const targetResult = options.resolvedInteractionTarget != null && request.objectId != null ? {
123
+ ok: true,
124
+ value: options.resolvedInteractionTarget
125
+ } : await dereferenceField(request, request.objectId, config.getInteractionTarget, documentLoader, contextLoader);
126
+ if (!targetResult.ok) return {
127
+ verified: false,
128
+ request,
129
+ requestId: request.id,
130
+ failure: targetResult.failure
114
131
  };
115
- const interactionTarget = await config.getInteractionTarget(request, dereferenceOptions);
132
+ const interactionTarget = targetResult.value;
116
133
  if (interactionTarget == null) return {
117
134
  verified: false,
118
135
  request,
@@ -133,7 +150,17 @@ async function verifyRequest(context, options, config) {
133
150
  actualTypes: [getTypeId(interactionTarget)]
134
151
  }
135
152
  };
136
- const interactingObject = await config.getInteractingObject(request, dereferenceOptions);
153
+ const interactingResult = options.resolvedInteractingObject != null && request.instrumentId != null ? {
154
+ ok: true,
155
+ value: options.resolvedInteractingObject
156
+ } : await dereferenceField(request, request.instrumentId, config.getInteractingObject, documentLoader, contextLoader);
157
+ if (!interactingResult.ok) return {
158
+ verified: false,
159
+ request,
160
+ requestId: request.id,
161
+ failure: interactingResult.failure
162
+ };
163
+ const interactingObject = interactingResult.value;
137
164
  if (interactingObject == null) return {
138
165
  verified: false,
139
166
  request,
@@ -184,7 +211,7 @@ async function verifyRequest(context, options, config) {
184
211
  type: "missingObjectId"
185
212
  }
186
213
  };
187
- const validation = config.validateRequest(request, interactingObject, interactionTarget, requester);
214
+ const validation = config.validateRequest(request, interactingObject, interactionTarget, requester, options);
188
215
  if (validation != null) return {
189
216
  verified: false,
190
217
  request,
@@ -213,23 +240,35 @@ async function verifyRequest(context, options, config) {
213
240
  };
214
241
  }
215
242
  async function verifyAuthorization(context, options, config) {
216
- const embeddedAuthorization = !(options.authorization instanceof URL);
217
- const expectedAuthorizationId = options.authorization instanceof URL ? options.authorization : null;
243
+ const authorizationUrl = options.authorization instanceof URL ? options.authorization : null;
244
+ const embeddedAuthorization = authorizationUrl == null;
245
+ const expectedAuthorizationId = options.authorizationId ?? authorizationUrl;
246
+ const checkOrigin = !options.allowOffOrigin || options.verifyAuthenticity == null;
218
247
  const expectedAttribution = options.attributedTo ?? (!(options.interactionTarget instanceof URL) ? config.getSelfActor(options.interactionTarget) : null);
219
- if (expectedAuthorizationId != null && expectedAttribution == null) return {
248
+ if (authorizationUrl != null && options.authorizationId != null && !idsEqual(authorizationUrl, options.authorizationId)) return {
249
+ verified: false,
250
+ authorizationId: authorizationUrl,
251
+ failure: {
252
+ category: "unauthorized",
253
+ type: "idMismatch",
254
+ expected: options.authorizationId,
255
+ actual: authorizationUrl
256
+ }
257
+ };
258
+ if (authorizationUrl != null && expectedAttribution == null) return {
220
259
  verified: false,
221
- authorizationId: expectedAuthorizationId,
260
+ authorizationId: authorizationUrl,
222
261
  failure: {
223
262
  category: "unauthorized",
224
263
  type: "missingAttribution"
225
264
  }
226
265
  };
227
- if (expectedAuthorizationId != null && expectedAttribution != null) {
266
+ if (checkOrigin && authorizationUrl != null && expectedAttribution != null) {
228
267
  const expectedOrigin = getAuthorizationOrigin(expectedAttribution);
229
- const actualOrigin = getAuthorizationOrigin(expectedAuthorizationId);
268
+ const actualOrigin = getAuthorizationOrigin(authorizationUrl);
230
269
  if (expectedOrigin === "null" || actualOrigin === "null" || expectedOrigin !== actualOrigin) return {
231
270
  verified: false,
232
- authorizationId: expectedAuthorizationId,
271
+ authorizationId: authorizationUrl,
233
272
  failure: {
234
273
  category: "unauthorized",
235
274
  type: "originMismatch",
@@ -238,7 +277,8 @@ async function verifyAuthorization(context, options, config) {
238
277
  }
239
278
  };
240
279
  }
241
- const authorizationResult = await materialize(options.authorization, config.authorizationClass, options.documentLoader ?? context.documentLoader);
280
+ const documentLoader = options.documentLoader ?? context.documentLoader;
281
+ const authorizationResult = await materialize(options.authorization, config.authorizationClass, documentLoader, options.contextLoader ?? documentLoader);
242
282
  if (!authorizationResult.ok) return {
243
283
  verified: false,
244
284
  failure: authorizationResult.failure
@@ -362,7 +402,7 @@ async function verifyAuthorization(context, options, config) {
362
402
  };
363
403
  const expectedOrigin = getAuthorizationOrigin(expectedAttribution);
364
404
  const actualOrigin = getAuthorizationOrigin(authorization.id);
365
- if (expectedOrigin === "null" || actualOrigin === "null" || expectedOrigin !== actualOrigin) return {
405
+ if (checkOrigin && (expectedOrigin === "null" || actualOrigin === "null" || expectedOrigin !== actualOrigin)) return {
366
406
  verified: false,
367
407
  authorization,
368
408
  authorizationId: authorization.id,
@@ -415,60 +455,163 @@ async function verifyAuthorization(context, options, config) {
415
455
  authorizationId: authorization.id
416
456
  };
417
457
  }
418
- async function materialize(value, constructor, documentLoader) {
419
- if (!(value instanceof URL)) return {
420
- ok: true,
421
- object: value
458
+ /**
459
+ * Wraps the loaders for a single dereferencing operation so that errors they
460
+ * throw can be told apart from other errors afterwards. Parsed objects keep
461
+ * the loaders they were given, so the wrappers stop recording and pass calls
462
+ * through once the operation is released.
463
+ */
464
+ function trackLoaders(documentLoader, contextLoader) {
465
+ const records = /* @__PURE__ */ new Map();
466
+ let released = false;
467
+ const wrap = (loader) => {
468
+ if (loader == null) return void 0;
469
+ return async (url, options) => {
470
+ if (released) return await loader(url, options);
471
+ let remoteDocument;
472
+ try {
473
+ remoteDocument = await loader(url, options);
474
+ } catch (error) {
475
+ if (!released) records.set(error, url);
476
+ throw error;
477
+ }
478
+ if (remoteDocument == null) {
479
+ const message = "The document loader returned no document.";
480
+ let error;
481
+ try {
482
+ error = new _fedify_vocab_runtime.FetchError((0, _fedify_vocab_runtime.parseIri)(url), message);
483
+ } catch {
484
+ error = /* @__PURE__ */ new Error(`${url}: ${message}`);
485
+ }
486
+ if (!released) records.set(error, url);
487
+ throw error;
488
+ }
489
+ return remoteDocument;
490
+ };
422
491
  };
423
- const loader = documentLoader;
424
- if (loader == null) return {
425
- ok: false,
426
- failure: {
427
- category: "unverifiable",
428
- type: "notDereferenceable",
429
- url: value
492
+ const wrappedDocumentLoader = wrap(documentLoader);
493
+ return {
494
+ documentLoader: wrappedDocumentLoader,
495
+ contextLoader: contextLoader === documentLoader ? wrappedDocumentLoader : wrap(contextLoader),
496
+ records,
497
+ release: () => {
498
+ released = true;
499
+ records.clear();
500
+ }
501
+ };
502
+ }
503
+ const MAX_CAUSE_DEPTH = 8;
504
+ function collectCauses(error, causes = [], depth = 0) {
505
+ if (depth > MAX_CAUSE_DEPTH || causes.includes(error)) return causes;
506
+ causes.push(error);
507
+ if (typeof error !== "object" || error == null) return causes;
508
+ if (error instanceof AggregateError) for (const e of error.errors) collectCauses(e, causes, depth + 1);
509
+ if ("cause" in error) collectCauses(error.cause, causes, depth + 1);
510
+ const details = error.details;
511
+ if (typeof details === "object" && details != null && "cause" in details) collectCauses(details.cause, causes, depth + 1);
512
+ return causes;
513
+ }
514
+ /**
515
+ * Classifies the error thrown by a dereferencing operation. It is a fetch
516
+ * failure only if a loader error is among its causes; errors that loaders
517
+ * threw during attempts the operation recovered from are ignored.
518
+ */
519
+ function classifyFailure(error, tracker) {
520
+ const loaderErrors = collectCauses(error).filter((e) => tracker.records.has(e));
521
+ const transientIndex = loaderErrors.findIndex(_fedify_vocab_runtime.isTransientFetchError);
522
+ const selected = loaderErrors[transientIndex < 0 ? 0 : transientIndex];
523
+ const url = loaderErrors.length < 1 ? null : getFailedUrl(selected, tracker.records.get(selected));
524
+ if (url == null) return {
525
+ category: "unverifiable",
526
+ type: "invalidJsonLd",
527
+ cause: error,
528
+ transient: false
529
+ };
530
+ return {
531
+ category: "unverifiable",
532
+ type: "notDereferenceable",
533
+ url,
534
+ cause: loaderErrors.length === 1 ? selected : error,
535
+ transient: transientIndex >= 0
536
+ };
537
+ }
538
+ function getFailedUrl(error, requestedUrl) {
539
+ const url = error?.url;
540
+ if (url instanceof URL) return url;
541
+ for (const candidate of [url, requestedUrl]) {
542
+ if (typeof candidate !== "string") continue;
543
+ try {
544
+ return (0, _fedify_vocab_runtime.parseIri)(candidate);
545
+ } catch {
546
+ continue;
430
547
  }
548
+ }
549
+ return null;
550
+ }
551
+ function notDereferenceable(url) {
552
+ return {
553
+ category: "unverifiable",
554
+ type: "notDereferenceable",
555
+ url,
556
+ transient: false
431
557
  };
432
- let remoteDocument;
558
+ }
559
+ /**
560
+ * Dereferences a field of the request, such as its `object` or `instrument`.
561
+ * Returns `null` if the request has no such field at all.
562
+ */
563
+ async function dereferenceField(request, reference, dereference, documentLoader, contextLoader) {
564
+ const tracker = trackLoaders(documentLoader, contextLoader);
433
565
  try {
434
- remoteDocument = await loader(value.href);
435
- } catch (cause) {
566
+ const value = await dereference(request, {
567
+ documentLoader: tracker.documentLoader,
568
+ contextLoader: tracker.contextLoader,
569
+ suppressError: false
570
+ });
571
+ if (value != null || reference == null) return {
572
+ ok: true,
573
+ value
574
+ };
436
575
  return {
437
576
  ok: false,
438
- failure: {
439
- category: "unverifiable",
440
- type: "notDereferenceable",
441
- url: value,
442
- cause
443
- }
577
+ failure: notDereferenceable(reference)
444
578
  };
579
+ } catch (error) {
580
+ return {
581
+ ok: false,
582
+ failure: classifyFailure(error, tracker)
583
+ };
584
+ } finally {
585
+ tracker.release();
445
586
  }
446
- if (remoteDocument == null) return {
587
+ }
588
+ async function materialize(value, constructor, documentLoader, contextLoader) {
589
+ if (!(value instanceof URL)) return {
590
+ ok: true,
591
+ object: value
592
+ };
593
+ if (documentLoader == null) return {
447
594
  ok: false,
448
- failure: {
449
- category: "unverifiable",
450
- type: "notDereferenceable",
451
- url: value
452
- }
595
+ failure: notDereferenceable(value)
453
596
  };
597
+ const tracker = trackLoaders(documentLoader, contextLoader);
454
598
  try {
599
+ const remoteDocument = await tracker.documentLoader(value.href);
455
600
  return {
456
601
  ok: true,
457
602
  object: await constructor.fromJsonLd(remoteDocument.document, {
458
- documentLoader: loader,
459
- contextLoader: loader,
603
+ documentLoader: tracker.documentLoader,
604
+ contextLoader: tracker.contextLoader,
460
605
  baseUrl: value
461
606
  })
462
607
  };
463
- } catch (cause) {
608
+ } catch (error) {
464
609
  return {
465
610
  ok: false,
466
- failure: {
467
- category: "unverifiable",
468
- type: "invalidJsonLd",
469
- cause
470
- }
611
+ failure: classifyFailure(error, tracker)
471
612
  };
613
+ } finally {
614
+ tracker.release();
472
615
  }
473
616
  }
474
617
  async function evaluatePolicy(context, options, config) {
@@ -485,30 +628,60 @@ async function evaluatePolicy(context, options, config) {
485
628
  reason: implicitAutomatic
486
629
  };
487
630
  const policy = options.subject.interactionPolicy;
488
- if (policy == null) return missingPolicyDecision(config);
489
- const rule = policy[config.policyProperty];
490
- if (rule == null) return missingRuleDecision(config);
491
- const automaticApprovals = rule.automaticApprovals ?? [];
492
- const manualApprovals = rule.manualApprovals ?? [];
493
- if (automaticApprovals.length < 1 && manualApprovals.length < 1) return missingRuleDecision(config);
494
- const automatic = await matchRule(automaticApprovals, options.requester, context, options.matchesApprovalCollection, { actorOnly: true });
631
+ let rule = getApprovalRule(policy?.[config.policyProperty]);
632
+ if (rule == null) {
633
+ rule = getApprovalRule(options.fallbackRule);
634
+ if (rule == null) return policy == null ? missingPolicyDecision(config) : missingRuleDecision(config);
635
+ }
636
+ const { automaticApprovals, manualApprovals } = rule;
637
+ const matchOptions = {
638
+ requester: options.requester,
639
+ context,
640
+ matchesApprovalCollection: options.matchesApprovalCollection,
641
+ throwCollectionErrors: options.collectionErrors === "throw"
642
+ };
643
+ if (options.precedence === "automatic") {
644
+ const automatic = await matchRule(automaticApprovals, matchOptions);
645
+ if (automatic?.result === "matched") return {
646
+ result: "automatic",
647
+ reason: automatic.reason
648
+ };
649
+ else if (automatic?.result === "unverifiableCollection") return deniedUnverifiableCollection(automatic);
650
+ const manual = await matchRule(manualApprovals, matchOptions);
651
+ if (manual?.result === "matched") return {
652
+ result: "manual",
653
+ reason: manual.reason
654
+ };
655
+ else if (manual?.result === "unverifiableCollection") return deniedUnverifiableCollection(manual);
656
+ return {
657
+ result: "denied",
658
+ reason: { type: "noMatch" }
659
+ };
660
+ }
661
+ const automatic = await matchRule(automaticApprovals, {
662
+ ...matchOptions,
663
+ actorOnly: true
664
+ });
495
665
  if (automatic?.result === "matched") return {
496
666
  result: "automatic",
497
667
  reason: automatic.reason
498
668
  };
499
- const manual = await matchRule(manualApprovals, options.requester, context, options.matchesApprovalCollection, { actorOnly: true });
669
+ const manual = await matchRule(manualApprovals, {
670
+ ...matchOptions,
671
+ actorOnly: true
672
+ });
500
673
  if (manual?.result === "matched") return {
501
674
  result: "manual",
502
675
  reason: manual.reason
503
676
  };
504
- const broadAutomatic = await matchRule(automaticApprovals, options.requester, context, options.matchesApprovalCollection);
505
- const unverifiableAutomatic = broadAutomatic?.result === "unverifiableCollection" ? broadAutomatic.collection : null;
677
+ const broadAutomatic = await matchRule(automaticApprovals, matchOptions);
678
+ const unverifiableAutomatic = broadAutomatic?.result === "unverifiableCollection" ? broadAutomatic : null;
506
679
  if (broadAutomatic?.result === "matched") return {
507
680
  result: "automatic",
508
681
  reason: broadAutomatic.reason
509
682
  };
510
- const broadManual = await matchRule(manualApprovals, options.requester, context, options.matchesApprovalCollection);
511
- if (broadManual?.result === "unverifiableCollection") return deniedUnverifiableCollection(broadManual.collection);
683
+ const broadManual = await matchRule(manualApprovals, matchOptions);
684
+ if (broadManual?.result === "unverifiableCollection") return deniedUnverifiableCollection(broadManual);
512
685
  else if (broadManual?.result === "matched") return {
513
686
  result: "manual",
514
687
  reason: broadManual.reason
@@ -519,6 +692,16 @@ async function evaluatePolicy(context, options, config) {
519
692
  reason: { type: "noMatch" }
520
693
  };
521
694
  }
695
+ function getApprovalRule(rule) {
696
+ if (rule == null) return null;
697
+ const automaticApprovals = rule.automaticApprovals ?? [];
698
+ const manualApprovals = rule.manualApprovals ?? [];
699
+ if (automaticApprovals.length < 1 && manualApprovals.length < 1) return null;
700
+ return {
701
+ automaticApprovals,
702
+ manualApprovals
703
+ };
704
+ }
522
705
  async function matchImplicitAutomaticActors(subject, requester, options, config) {
523
706
  if (config.getImplicitAutomaticActors == null) return null;
524
707
  for await (const actor of config.getImplicitAutomaticActors(subject, options)) if (idsEqual(actor, requester)) return {
@@ -553,7 +736,8 @@ function missingRuleDecision(config) {
553
736
  reason: { type: "missingRule" }
554
737
  };
555
738
  }
556
- async function matchRule(entries, requester, context, matchesApprovalCollection, options = {}) {
739
+ async function matchRule(entries, options) {
740
+ const { requester, matchesApprovalCollection } = options;
557
741
  for (const entry of entries) if (entry.href === requester.href) return {
558
742
  result: "matched",
559
743
  reason: {
@@ -567,14 +751,18 @@ async function matchRule(entries, requester, context, matchesApprovalCollection,
567
751
  reason: { type: "public" }
568
752
  };
569
753
  if (matchesApprovalCollection != null) {
570
- let unverifiableCollection = null;
754
+ let unverifiable = null;
571
755
  for (const entry of entries) {
572
756
  if (entry.href === _fedify_vocab.PUBLIC_COLLECTION.href || entry.href === requester.href) continue;
573
757
  let matched;
574
758
  try {
575
- matched = await matchesApprovalCollection(entry, requester, context);
576
- } catch {
577
- unverifiableCollection ??= entry;
759
+ matched = await matchesApprovalCollection(entry, requester, options.context);
760
+ } catch (cause) {
761
+ if (options.throwCollectionErrors) throw cause;
762
+ unverifiable ??= {
763
+ collection: entry,
764
+ cause
765
+ };
578
766
  continue;
579
767
  }
580
768
  if (matched) return {
@@ -585,19 +773,20 @@ async function matchRule(entries, requester, context, matchesApprovalCollection,
585
773
  }
586
774
  };
587
775
  }
588
- if (unverifiableCollection != null) return {
776
+ if (unverifiable != null) return {
589
777
  result: "unverifiableCollection",
590
- collection: unverifiableCollection
778
+ ...unverifiable
591
779
  };
592
780
  }
593
781
  return null;
594
782
  }
595
- function deniedUnverifiableCollection(collection) {
783
+ function deniedUnverifiableCollection(failure) {
596
784
  return {
597
785
  result: "denied",
598
786
  reason: {
599
787
  type: "unverifiableCollection",
600
- collection
788
+ collection: failure.collection,
789
+ cause: failure.cause
601
790
  }
602
791
  };
603
792
  }
@@ -798,20 +987,23 @@ const quoteInteraction = createInteractionControl({
798
987
  _fedify_vocab.ChatMessage.typeId
799
988
  ],
800
989
  getInteractionTarget: (request, options) => request.getObject(options),
801
- validateRequest: (_request, quote, target, requester) => {
990
+ validateRequest: (_request, quote, target, requester, options) => {
802
991
  const targetId = getRequiredId(target, "interactionTarget");
803
- if (quote.quoteId != null && quote.quoteUrl != null && !idsEqual(quote.quoteUrl, quote.quoteId)) return {
992
+ const quoteReference = options.quoteReference ?? "strict";
993
+ if (quoteReference === "strict" && quote.quoteId != null && quote.quoteUrl != null && !idsEqual(quote.quoteUrl, quote.quoteId)) return {
804
994
  type: "objectMismatch",
805
995
  expected: quote.quoteId,
806
996
  actual: quote.quoteUrl
807
997
  };
808
998
  const quoteTargetId = getQuoteTargetId(quote);
809
- if (!idsEqual(quoteTargetId, targetId)) return {
999
+ if (!(quoteReference === "any" ? idsEqual(quote.quoteId, targetId) || idsEqual(quote.quoteUrl, targetId) : idsEqual(quoteTargetId, targetId))) return {
810
1000
  type: "objectMismatch",
811
1001
  expected: targetId,
812
1002
  actual: quoteTargetId ?? void 0
813
1003
  };
814
- if (!idsEqual(quote.attributionId, requester)) return {
1004
+ const attributionIds = quote.attributionIds;
1005
+ if (attributionIds.length < 1 && options.missingAttribution === "requester") return null;
1006
+ if (!(options.attribution === "any" ? attributionIds.some((id) => idsEqual(id, requester)) : idsEqual(quote.attributionId, requester))) return {
815
1007
  type: "requesterMismatch",
816
1008
  expected: requester,
817
1009
  actual: quote.attributionId ?? void 0