@fedify/interaction-controls 2.4.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mod.js ADDED
@@ -0,0 +1,892 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { Accept, Announce, AnnounceAuthorization, AnnounceRequest, Application, Article, ChatMessage, Create, Delete, FeatureAuthorization, FeatureRequest, FeaturedCollection, Group, Like, LikeAuthorization, LikeRequest, Mention, Note, Object as Object$1, Organization, PUBLIC_COLLECTION, Person, Question, QuoteAuthorization, QuoteRequest, Reject, ReplyAuthorization, ReplyRequest, Service, isActor } from "@fedify/vocab";
3
+ import { getFe34Origin } from "@fedify/vocab-runtime";
4
+ //#region src/control.ts
5
+ function createInteractionControl(config) {
6
+ return {
7
+ name: config.name,
8
+ policyProperty: config.policyProperty,
9
+ requestTypeId: config.requestClass.typeId,
10
+ authorizationTypeId: config.authorizationClass.typeId,
11
+ verifyRequest: (context, options) => verifyRequest(context, options, config),
12
+ evaluatePolicy: (context, options) => evaluatePolicy(context, options, config),
13
+ createRequest: (options) => new config.requestClass({
14
+ id: options.id,
15
+ actor: options.actor,
16
+ object: options.object,
17
+ instrument: options.instrument,
18
+ ...audience(options)
19
+ }),
20
+ createAuthorization: (options) => new config.authorizationClass({
21
+ id: options.id,
22
+ attribution: options.attributedTo,
23
+ interactingObject: getRequiredId(options.interactingObject, "interactingObject"),
24
+ interactionTarget: getRequiredId(options.interactionTarget, "interactionTarget")
25
+ }),
26
+ verifyAuthorization: (context, options) => verifyAuthorization(context, options, config),
27
+ createAccept: (options) => createAccept(options),
28
+ createReject: (options) => createReject(options),
29
+ createRevocation: (options) => new Delete({
30
+ id: options.id,
31
+ actor: options.actor,
32
+ object: getRequiredId(options.authorization, "authorization"),
33
+ ...audience(options)
34
+ }),
35
+ recognizeImpolite: config.recognizeImpolite,
36
+ getInteractionKey: (input) => ({
37
+ interaction: config.name,
38
+ requester: input.requester,
39
+ interactingObjectId: getRequiredId(input.interactingObject, "interactingObject"),
40
+ interactionTargetId: getRequiredId(input.interactionTarget, "interactionTarget")
41
+ }),
42
+ getAuthorizationKey: (input) => ({
43
+ interaction: config.name,
44
+ authorizationId: getRequiredId(input.authorization, "authorization")
45
+ })
46
+ };
47
+ }
48
+ function audience(options) {
49
+ const to = options.to;
50
+ const cc = options.cc;
51
+ return {
52
+ ...to == null ? {} : Array.isArray(to) ? { tos: to } : { to },
53
+ ...cc == null ? {} : Array.isArray(cc) ? { ccs: cc } : { cc }
54
+ };
55
+ }
56
+ function getRequiredId(value, name) {
57
+ if (value instanceof URL) return value;
58
+ if (value.id == null) throw new TypeError(`The ${name} must have an id.`);
59
+ return value.id;
60
+ }
61
+ function getId(value) {
62
+ return value instanceof URL ? value : value.id;
63
+ }
64
+ function idsEqual(left, right) {
65
+ return left != null && right != null && left.href === right.href;
66
+ }
67
+ function getTypeId(object) {
68
+ return object.constructor.typeId;
69
+ }
70
+ async function verifyRequest(context, options, config) {
71
+ const documentLoader = options.documentLoader ?? context.documentLoader;
72
+ const expectedRequestId = options.request instanceof URL ? options.request : null;
73
+ const requestResult = await materialize(options.request, config.requestClass, documentLoader);
74
+ if (!requestResult.ok) return {
75
+ verified: false,
76
+ failure: requestResult.failure
77
+ };
78
+ const request = requestResult.object;
79
+ if (!(request instanceof config.requestClass)) return {
80
+ verified: false,
81
+ request,
82
+ requestId: request.id ?? void 0,
83
+ failure: {
84
+ category: "invalid",
85
+ type: "wrongType",
86
+ expectedType: config.requestClass.typeId,
87
+ actualTypes: [getTypeId(request)]
88
+ }
89
+ };
90
+ if (request.id == null) return {
91
+ verified: false,
92
+ request,
93
+ failure: {
94
+ category: "invalid",
95
+ type: "missingId"
96
+ }
97
+ };
98
+ if (expectedRequestId != null && !idsEqual(request.id, expectedRequestId)) return {
99
+ verified: false,
100
+ request,
101
+ requestId: request.id,
102
+ failure: {
103
+ category: "invalid",
104
+ type: "idMismatch",
105
+ expected: expectedRequestId,
106
+ actual: request.id
107
+ }
108
+ };
109
+ const dereferenceOptions = {
110
+ documentLoader,
111
+ contextLoader: documentLoader,
112
+ suppressError: true
113
+ };
114
+ const interactionTarget = await config.getInteractionTarget(request, dereferenceOptions);
115
+ if (interactionTarget == null) return {
116
+ verified: false,
117
+ request,
118
+ requestId: request.id,
119
+ failure: {
120
+ category: "invalid",
121
+ type: "missingObject"
122
+ }
123
+ };
124
+ if (config.isInteractionTarget != null && !config.isInteractionTarget(interactionTarget)) return {
125
+ verified: false,
126
+ request,
127
+ requestId: request.id,
128
+ failure: {
129
+ category: "invalid",
130
+ type: "wrongObjectType",
131
+ expectedTypes: config.interactionTargetTypes ?? [],
132
+ actualTypes: [getTypeId(interactionTarget)]
133
+ }
134
+ };
135
+ const interactingObject = await config.getInteractingObject(request, dereferenceOptions);
136
+ if (interactingObject == null) return {
137
+ verified: false,
138
+ request,
139
+ requestId: request.id,
140
+ failure: {
141
+ category: "invalid",
142
+ type: "missingInstrument"
143
+ }
144
+ };
145
+ if (config.isInteractingObject != null && !config.isInteractingObject(interactingObject)) return {
146
+ verified: false,
147
+ request,
148
+ requestId: request.id,
149
+ failure: {
150
+ category: "invalid",
151
+ type: "wrongInstrumentType",
152
+ expectedTypes: config.interactingObjectTypes ?? [],
153
+ actualTypes: [getTypeId(interactingObject)]
154
+ }
155
+ };
156
+ const requester = request.actorId ?? config.getRequester?.(request, interactingObject, interactionTarget);
157
+ if (requester == null) return {
158
+ verified: false,
159
+ request,
160
+ requestId: request.id,
161
+ failure: {
162
+ category: "invalid",
163
+ type: "missingActor"
164
+ }
165
+ };
166
+ const interactingObjectId = getId(interactingObject);
167
+ if (interactingObjectId == null) return {
168
+ verified: false,
169
+ request,
170
+ requestId: request.id,
171
+ failure: {
172
+ category: "invalid",
173
+ type: "missingInstrumentId"
174
+ }
175
+ };
176
+ const interactionTargetId = getId(interactionTarget);
177
+ if (interactionTargetId == null) return {
178
+ verified: false,
179
+ request,
180
+ requestId: request.id,
181
+ failure: {
182
+ category: "invalid",
183
+ type: "missingObjectId"
184
+ }
185
+ };
186
+ const validation = config.validateRequest(request, interactingObject, interactionTarget, requester);
187
+ if (validation != null) return {
188
+ verified: false,
189
+ request,
190
+ requestId: request.id,
191
+ failure: validation.type === "requesterMismatch" ? {
192
+ category: "unauthorized",
193
+ type: validation.type,
194
+ expected: validation.expected,
195
+ actual: validation.actual
196
+ } : {
197
+ category: "invalid",
198
+ type: validation.type,
199
+ expected: validation.expected,
200
+ actual: validation.actual
201
+ }
202
+ };
203
+ return {
204
+ verified: true,
205
+ request,
206
+ requestId: request.id,
207
+ requester,
208
+ interactingObject,
209
+ interactingObjectId,
210
+ interactionTarget,
211
+ interactionTargetId
212
+ };
213
+ }
214
+ async function verifyAuthorization(context, options, config) {
215
+ const embeddedAuthorization = !(options.authorization instanceof URL);
216
+ const expectedAuthorizationId = options.authorization instanceof URL ? options.authorization : null;
217
+ const expectedAttribution = options.attributedTo ?? (!(options.interactionTarget instanceof URL) ? config.getSelfActor(options.interactionTarget) : null);
218
+ if (expectedAuthorizationId != null && expectedAttribution == null) return {
219
+ verified: false,
220
+ authorizationId: expectedAuthorizationId,
221
+ failure: {
222
+ category: "unauthorized",
223
+ type: "missingAttribution"
224
+ }
225
+ };
226
+ if (expectedAuthorizationId != null && expectedAttribution != null) {
227
+ const expectedOrigin = getAuthorizationOrigin(expectedAttribution);
228
+ const actualOrigin = getAuthorizationOrigin(expectedAuthorizationId);
229
+ if (expectedOrigin === "null" || actualOrigin === "null" || expectedOrigin !== actualOrigin) return {
230
+ verified: false,
231
+ authorizationId: expectedAuthorizationId,
232
+ failure: {
233
+ category: "unauthorized",
234
+ type: "originMismatch",
235
+ expectedOrigin,
236
+ actualOrigin
237
+ }
238
+ };
239
+ }
240
+ const authorizationResult = await materialize(options.authorization, config.authorizationClass, options.documentLoader ?? context.documentLoader);
241
+ if (!authorizationResult.ok) return {
242
+ verified: false,
243
+ failure: authorizationResult.failure
244
+ };
245
+ const authorization = authorizationResult.object;
246
+ if (!(authorization instanceof config.authorizationClass)) return {
247
+ verified: false,
248
+ authorization,
249
+ authorizationId: authorization.id ?? void 0,
250
+ failure: {
251
+ category: "unauthorized",
252
+ type: "wrongType",
253
+ expectedType: config.authorizationClass.typeId,
254
+ actualTypes: [getTypeId(authorization)]
255
+ }
256
+ };
257
+ if (authorization.id == null) return {
258
+ verified: false,
259
+ authorization,
260
+ failure: {
261
+ category: "unauthorized",
262
+ type: "missingId"
263
+ }
264
+ };
265
+ if (expectedAuthorizationId != null && !idsEqual(authorization.id, expectedAuthorizationId)) return {
266
+ verified: false,
267
+ authorization,
268
+ authorizationId: authorization.id,
269
+ failure: {
270
+ category: "unauthorized",
271
+ type: "idMismatch",
272
+ expected: expectedAuthorizationId,
273
+ actual: authorization.id
274
+ }
275
+ };
276
+ const revocation = await options.getRevocation?.(authorization.id, context);
277
+ if (revocation != null) return {
278
+ verified: false,
279
+ authorization,
280
+ authorizationId: authorization.id,
281
+ failure: {
282
+ category: "revoked",
283
+ type: "deleted",
284
+ ...revocation
285
+ }
286
+ };
287
+ const interactingObjectId = getId(options.interactingObject);
288
+ if (interactingObjectId == null) return {
289
+ verified: false,
290
+ authorization,
291
+ authorizationId: authorization.id,
292
+ failure: {
293
+ category: "unauthorized",
294
+ type: "missingObjectId"
295
+ }
296
+ };
297
+ const interactionTargetId = getId(options.interactionTarget);
298
+ if (interactionTargetId == null) return {
299
+ verified: false,
300
+ authorization,
301
+ authorizationId: authorization.id,
302
+ failure: {
303
+ category: "unauthorized",
304
+ type: "missingTargetId"
305
+ }
306
+ };
307
+ const actualInteractingId = authorization.interactingObjectId;
308
+ if (!idsEqual(actualInteractingId, interactingObjectId)) return {
309
+ verified: false,
310
+ authorization,
311
+ authorizationId: authorization.id,
312
+ failure: {
313
+ category: "unauthorized",
314
+ type: "objectMismatch",
315
+ expected: interactingObjectId,
316
+ actual: actualInteractingId ?? void 0
317
+ }
318
+ };
319
+ const actualTargetId = authorization.interactionTargetId;
320
+ if (!idsEqual(actualTargetId, interactionTargetId)) return {
321
+ verified: false,
322
+ authorization,
323
+ authorizationId: authorization.id,
324
+ failure: {
325
+ category: "unauthorized",
326
+ type: "targetMismatch",
327
+ expected: interactionTargetId,
328
+ actual: actualTargetId ?? void 0
329
+ }
330
+ };
331
+ if (expectedAttribution == null) return {
332
+ verified: false,
333
+ authorization,
334
+ authorizationId: authorization.id,
335
+ failure: {
336
+ category: "unauthorized",
337
+ type: "missingAttribution"
338
+ }
339
+ };
340
+ if (config.authorizationAttribution !== "optional" && !idsEqual(authorization.attributionId, expectedAttribution)) return {
341
+ verified: false,
342
+ authorization,
343
+ authorizationId: authorization.id,
344
+ failure: {
345
+ category: "unauthorized",
346
+ type: "attributionMismatch",
347
+ expected: expectedAttribution,
348
+ actual: authorization.attributionId ?? void 0
349
+ }
350
+ };
351
+ if (authorization.attributionId != null && !idsEqual(authorization.attributionId, expectedAttribution)) return {
352
+ verified: false,
353
+ authorization,
354
+ authorizationId: authorization.id,
355
+ failure: {
356
+ category: "unauthorized",
357
+ type: "attributionMismatch",
358
+ expected: expectedAttribution,
359
+ actual: authorization.attributionId
360
+ }
361
+ };
362
+ const expectedOrigin = getAuthorizationOrigin(expectedAttribution);
363
+ const actualOrigin = getAuthorizationOrigin(authorization.id);
364
+ if (expectedOrigin === "null" || actualOrigin === "null" || expectedOrigin !== actualOrigin) return {
365
+ verified: false,
366
+ authorization,
367
+ authorizationId: authorization.id,
368
+ failure: {
369
+ category: "unauthorized",
370
+ type: "originMismatch",
371
+ expectedOrigin,
372
+ actualOrigin
373
+ }
374
+ };
375
+ if (embeddedAuthorization && options.verifyAuthenticity == null) return {
376
+ verified: false,
377
+ authorization,
378
+ authorizationId: authorization.id,
379
+ failure: {
380
+ category: "unauthorized",
381
+ type: "notAuthentic",
382
+ detail: "Embedded authorizations require authenticity verification."
383
+ }
384
+ };
385
+ if (options.verifyAuthenticity != null) {
386
+ let authentic;
387
+ try {
388
+ authentic = await options.verifyAuthenticity(authorization, context);
389
+ } catch (cause) {
390
+ return {
391
+ verified: false,
392
+ authorization,
393
+ authorizationId: authorization.id,
394
+ failure: {
395
+ category: "unauthorized",
396
+ type: "notAuthentic",
397
+ cause
398
+ }
399
+ };
400
+ }
401
+ if (!authentic) return {
402
+ verified: false,
403
+ authorization,
404
+ authorizationId: authorization.id,
405
+ failure: {
406
+ category: "unauthorized",
407
+ type: "notAuthentic"
408
+ }
409
+ };
410
+ }
411
+ return {
412
+ verified: true,
413
+ authorization,
414
+ authorizationId: authorization.id
415
+ };
416
+ }
417
+ async function materialize(value, constructor, documentLoader) {
418
+ if (!(value instanceof URL)) return {
419
+ ok: true,
420
+ object: value
421
+ };
422
+ const loader = documentLoader;
423
+ if (loader == null) return {
424
+ ok: false,
425
+ failure: {
426
+ category: "unverifiable",
427
+ type: "notDereferenceable",
428
+ url: value
429
+ }
430
+ };
431
+ let remoteDocument;
432
+ try {
433
+ remoteDocument = await loader(value.href);
434
+ } catch (cause) {
435
+ return {
436
+ ok: false,
437
+ failure: {
438
+ category: "unverifiable",
439
+ type: "notDereferenceable",
440
+ url: value,
441
+ cause
442
+ }
443
+ };
444
+ }
445
+ if (remoteDocument == null) return {
446
+ ok: false,
447
+ failure: {
448
+ category: "unverifiable",
449
+ type: "notDereferenceable",
450
+ url: value
451
+ }
452
+ };
453
+ try {
454
+ return {
455
+ ok: true,
456
+ object: await constructor.fromJsonLd(remoteDocument.document, {
457
+ documentLoader: loader,
458
+ contextLoader: loader,
459
+ baseUrl: value
460
+ })
461
+ };
462
+ } catch (cause) {
463
+ return {
464
+ ok: false,
465
+ failure: {
466
+ category: "unverifiable",
467
+ type: "invalidJsonLd",
468
+ cause
469
+ }
470
+ };
471
+ }
472
+ }
473
+ async function evaluatePolicy(context, options, config) {
474
+ if (idsEqual(config.getSelfActor(options.subject), options.requester)) return {
475
+ result: "automatic",
476
+ reason: { type: "self" }
477
+ };
478
+ const implicitAutomatic = await matchImplicitAutomaticActors(options.subject, options.requester, {
479
+ documentLoader: options.documentLoader ?? context.documentLoader,
480
+ suppressError: true
481
+ }, config);
482
+ if (implicitAutomatic != null) return {
483
+ result: "automatic",
484
+ reason: implicitAutomatic
485
+ };
486
+ const policy = options.subject.interactionPolicy;
487
+ if (policy == null) return missingPolicyDecision(config);
488
+ const rule = policy[config.policyProperty];
489
+ if (rule == null) return missingRuleDecision(config);
490
+ const automaticApprovals = rule.automaticApprovals ?? [];
491
+ const manualApprovals = rule.manualApprovals ?? [];
492
+ if (automaticApprovals.length < 1 && manualApprovals.length < 1) return missingRuleDecision(config);
493
+ const automatic = await matchRule(automaticApprovals, options.requester, context, options.matchesApprovalCollection, { actorOnly: true });
494
+ if (automatic?.result === "matched") return {
495
+ result: "automatic",
496
+ reason: automatic.reason
497
+ };
498
+ const manual = await matchRule(manualApprovals, options.requester, context, options.matchesApprovalCollection, { actorOnly: true });
499
+ if (manual?.result === "matched") return {
500
+ result: "manual",
501
+ reason: manual.reason
502
+ };
503
+ const broadAutomatic = await matchRule(automaticApprovals, options.requester, context, options.matchesApprovalCollection);
504
+ const unverifiableAutomatic = broadAutomatic?.result === "unverifiableCollection" ? broadAutomatic.collection : null;
505
+ if (broadAutomatic?.result === "matched") return {
506
+ result: "automatic",
507
+ reason: broadAutomatic.reason
508
+ };
509
+ const broadManual = await matchRule(manualApprovals, options.requester, context, options.matchesApprovalCollection);
510
+ if (broadManual?.result === "unverifiableCollection") return deniedUnverifiableCollection(broadManual.collection);
511
+ else if (broadManual?.result === "matched") return {
512
+ result: "manual",
513
+ reason: broadManual.reason
514
+ };
515
+ if (unverifiableAutomatic != null) return deniedUnverifiableCollection(unverifiableAutomatic);
516
+ return {
517
+ result: "denied",
518
+ reason: { type: "noMatch" }
519
+ };
520
+ }
521
+ async function matchImplicitAutomaticActors(subject, requester, options, config) {
522
+ if (config.getImplicitAutomaticActors == null) return null;
523
+ for await (const actor of config.getImplicitAutomaticActors(subject, options)) if (idsEqual(actor, requester)) return {
524
+ type: "actor",
525
+ actor
526
+ };
527
+ return null;
528
+ }
529
+ function missingPolicyDecision(config) {
530
+ if (config.defaultMissingPolicy === "automatic") return {
531
+ result: "automatic",
532
+ reason: {
533
+ type: "default",
534
+ default: "publicAutomatic"
535
+ }
536
+ };
537
+ return {
538
+ result: "denied",
539
+ reason: { type: "missingPolicy" }
540
+ };
541
+ }
542
+ function missingRuleDecision(config) {
543
+ if (config.defaultMissingPolicy === "automatic") return {
544
+ result: "automatic",
545
+ reason: {
546
+ type: "default",
547
+ default: "publicAutomatic"
548
+ }
549
+ };
550
+ return {
551
+ result: "denied",
552
+ reason: { type: "missingRule" }
553
+ };
554
+ }
555
+ async function matchRule(entries, requester, context, matchesApprovalCollection, options = {}) {
556
+ for (const entry of entries) if (entry.href === requester.href) return {
557
+ result: "matched",
558
+ reason: {
559
+ type: "actor",
560
+ actor: entry
561
+ }
562
+ };
563
+ if (options.actorOnly) return null;
564
+ for (const entry of entries) if (entry.href === PUBLIC_COLLECTION.href) return {
565
+ result: "matched",
566
+ reason: { type: "public" }
567
+ };
568
+ if (matchesApprovalCollection != null) {
569
+ let unverifiableCollection = null;
570
+ for (const entry of entries) {
571
+ if (entry.href === PUBLIC_COLLECTION.href || entry.href === requester.href) continue;
572
+ let matched;
573
+ try {
574
+ matched = await matchesApprovalCollection(entry, requester, context);
575
+ } catch {
576
+ unverifiableCollection ??= entry;
577
+ continue;
578
+ }
579
+ if (matched) return {
580
+ result: "matched",
581
+ reason: {
582
+ type: "collection",
583
+ collection: entry
584
+ }
585
+ };
586
+ }
587
+ if (unverifiableCollection != null) return {
588
+ result: "unverifiableCollection",
589
+ collection: unverifiableCollection
590
+ };
591
+ }
592
+ return null;
593
+ }
594
+ function deniedUnverifiableCollection(collection) {
595
+ return {
596
+ result: "denied",
597
+ reason: {
598
+ type: "unverifiableCollection",
599
+ collection
600
+ }
601
+ };
602
+ }
603
+ function getAuthorizationOrigin(id) {
604
+ try {
605
+ return getFe34Origin(id);
606
+ } catch {
607
+ return id.origin;
608
+ }
609
+ }
610
+ function createAccept(options) {
611
+ if (options.mode === "polite") return new Accept({
612
+ id: options.id,
613
+ actor: options.actor,
614
+ object: options.request,
615
+ result: options.authorization,
616
+ ...audience(options)
617
+ });
618
+ return new Accept({
619
+ id: options.id,
620
+ actor: options.actor,
621
+ object: getRequiredId(options.interactingObject, "interactingObject"),
622
+ target: getRequiredId(options.interactionTarget, "interactionTarget"),
623
+ result: getRequiredId(options.authorization, "authorization"),
624
+ ...audience(options)
625
+ });
626
+ }
627
+ function createReject(options) {
628
+ if (options.mode === "polite") return new Reject({
629
+ id: options.id,
630
+ actor: options.actor,
631
+ object: options.request,
632
+ ...audience(options)
633
+ });
634
+ return new Reject({
635
+ id: options.id,
636
+ actor: options.actor,
637
+ object: getRequiredId(options.interactingObject, "interactingObject"),
638
+ target: getRequiredId(options.interactionTarget, "interactionTarget"),
639
+ ...audience(options)
640
+ });
641
+ }
642
+ function recognized(values) {
643
+ if (values.requester == null || values.interactionTargetId == null) return null;
644
+ const interactingObjectId = values.interactingObject.id;
645
+ if (interactingObjectId == null) return null;
646
+ return {
647
+ requester: values.requester,
648
+ interactingObject: values.interactingObject,
649
+ interactingObjectId,
650
+ interactionTarget: values.interactionTarget,
651
+ interactionTargetId: values.interactionTargetId,
652
+ source: values.source,
653
+ evidence: values.evidence
654
+ };
655
+ }
656
+ //#endregion
657
+ //#region src/announce.ts
658
+ const announceInteraction = createInteractionControl({
659
+ name: "announce",
660
+ policyProperty: "canAnnounce",
661
+ requestClass: AnnounceRequest,
662
+ authorizationClass: AnnounceAuthorization,
663
+ getInteractingObject: (request, options) => request.getInstrument(options),
664
+ isInteractingObject: (object) => object instanceof Announce,
665
+ interactingObjectTypes: [Announce.typeId],
666
+ getInteractionTarget: (request, options) => request.getObject(options),
667
+ validateRequest: (_request, announce, target, requester) => {
668
+ const targetId = getRequiredId(target, "interactionTarget");
669
+ if (!idsEqual(announce.objectId, targetId)) return {
670
+ type: "objectMismatch",
671
+ expected: targetId,
672
+ actual: announce.objectId ?? void 0
673
+ };
674
+ if (!idsEqual(announce.actorId, requester)) return {
675
+ type: "requesterMismatch",
676
+ expected: requester,
677
+ actual: announce.actorId ?? void 0
678
+ };
679
+ return null;
680
+ },
681
+ getSelfActor: (subject) => subject.attributionId,
682
+ defaultMissingPolicy: "automatic",
683
+ recognizeImpolite: (source) => recognized({
684
+ requester: source.actorId,
685
+ interactingObject: source,
686
+ interactionTarget: void 0,
687
+ interactionTargetId: source.objectId,
688
+ source,
689
+ evidence: {
690
+ type: "activity",
691
+ activityType: Announce.typeId
692
+ }
693
+ })
694
+ });
695
+ //#endregion
696
+ //#region src/feature.ts
697
+ const featureInteraction = createInteractionControl({
698
+ name: "feature",
699
+ policyProperty: "canFeature",
700
+ requestClass: FeatureRequest,
701
+ authorizationClass: FeatureAuthorization,
702
+ getInteractingObject: (request, options) => request.getInstrument(options),
703
+ isInteractingObject: (object) => object instanceof FeaturedCollection,
704
+ interactingObjectTypes: [FeaturedCollection.typeId],
705
+ getInteractionTarget: (request, options) => request.getObject(options),
706
+ isInteractionTarget: isActor,
707
+ interactionTargetTypes: [
708
+ Application.typeId,
709
+ Group.typeId,
710
+ Organization.typeId,
711
+ Person.typeId,
712
+ Service.typeId
713
+ ],
714
+ getRequester: (_request, collection) => collection.attributionId,
715
+ validateRequest: (_request, collection, _target, requester) => {
716
+ if (!idsEqual(collection.attributionId, requester)) return {
717
+ type: "requesterMismatch",
718
+ expected: requester,
719
+ actual: collection.attributionId ?? void 0
720
+ };
721
+ return null;
722
+ },
723
+ authorizationAttribution: "optional",
724
+ getSelfActor: (subject) => subject.id,
725
+ defaultMissingPolicy: "denied",
726
+ recognizeImpolite: () => null
727
+ });
728
+ //#endregion
729
+ //#region src/key.ts
730
+ function formatInteractionKey(key) {
731
+ return JSON.stringify([
732
+ key.interaction,
733
+ key.requester.href,
734
+ key.interactingObjectId.href,
735
+ key.interactionTargetId.href
736
+ ]);
737
+ }
738
+ function formatAuthorizationKey(key) {
739
+ return JSON.stringify([key.interaction, key.authorizationId.href]);
740
+ }
741
+ //#endregion
742
+ //#region src/like.ts
743
+ const likeInteraction = createInteractionControl({
744
+ name: "like",
745
+ policyProperty: "canLike",
746
+ requestClass: LikeRequest,
747
+ authorizationClass: LikeAuthorization,
748
+ getInteractingObject: (request, options) => request.getInstrument(options),
749
+ isInteractingObject: (object) => object instanceof Like,
750
+ interactingObjectTypes: [Like.typeId],
751
+ getInteractionTarget: (request, options) => request.getObject(options),
752
+ validateRequest: (_request, like, target, requester) => {
753
+ const targetId = getRequiredId(target, "interactionTarget");
754
+ if (!idsEqual(like.objectId, targetId)) return {
755
+ type: "objectMismatch",
756
+ expected: targetId,
757
+ actual: like.objectId ?? void 0
758
+ };
759
+ if (!idsEqual(like.actorId, requester)) return {
760
+ type: "requesterMismatch",
761
+ expected: requester,
762
+ actual: like.actorId ?? void 0
763
+ };
764
+ return null;
765
+ },
766
+ getSelfActor: (subject) => subject.attributionId,
767
+ defaultMissingPolicy: "automatic",
768
+ recognizeImpolite: (source) => recognized({
769
+ requester: source.actorId,
770
+ interactingObject: source,
771
+ interactionTarget: void 0,
772
+ interactionTargetId: source.objectId,
773
+ source,
774
+ evidence: {
775
+ type: "activity",
776
+ activityType: Like.typeId
777
+ }
778
+ })
779
+ });
780
+ //#endregion
781
+ //#region src/quote.ts
782
+ const QUOTE = new URL("https://w3id.org/fep/044f#quote");
783
+ function getQuoteTargetId(quote) {
784
+ return quote.quoteId ?? quote.quoteUrl;
785
+ }
786
+ const quoteInteraction = createInteractionControl({
787
+ name: "quote",
788
+ policyProperty: "canQuote",
789
+ requestClass: QuoteRequest,
790
+ authorizationClass: QuoteAuthorization,
791
+ getInteractingObject: (request, options) => request.getInstrument(options),
792
+ isInteractingObject: (object) => object instanceof Note || object instanceof Article || object instanceof Question || object instanceof ChatMessage,
793
+ interactingObjectTypes: [
794
+ Note.typeId,
795
+ Article.typeId,
796
+ Question.typeId,
797
+ ChatMessage.typeId
798
+ ],
799
+ getInteractionTarget: (request, options) => request.getObject(options),
800
+ validateRequest: (_request, quote, target, requester) => {
801
+ const targetId = getRequiredId(target, "interactionTarget");
802
+ if (quote.quoteId != null && quote.quoteUrl != null && !idsEqual(quote.quoteUrl, quote.quoteId)) return {
803
+ type: "objectMismatch",
804
+ expected: quote.quoteId,
805
+ actual: quote.quoteUrl
806
+ };
807
+ const quoteTargetId = getQuoteTargetId(quote);
808
+ if (!idsEqual(quoteTargetId, targetId)) return {
809
+ type: "objectMismatch",
810
+ expected: targetId,
811
+ actual: quoteTargetId ?? void 0
812
+ };
813
+ if (!idsEqual(quote.attributionId, requester)) return {
814
+ type: "requesterMismatch",
815
+ expected: requester,
816
+ actual: quote.attributionId ?? void 0
817
+ };
818
+ return null;
819
+ },
820
+ getSelfActor: (subject) => subject.attributionId,
821
+ defaultMissingPolicy: "denied",
822
+ recognizeImpolite: (source) => {
823
+ if (source instanceof Create) return null;
824
+ return recognized({
825
+ requester: source.attributionId,
826
+ interactingObject: source,
827
+ interactionTarget: void 0,
828
+ interactionTargetId: getQuoteTargetId(source),
829
+ source,
830
+ evidence: {
831
+ type: "property",
832
+ property: QUOTE
833
+ }
834
+ });
835
+ }
836
+ });
837
+ //#endregion
838
+ //#region src/reply.ts
839
+ const IN_REPLY_TO = new URL("https://www.w3.org/ns/activitystreams#inReplyTo");
840
+ const replyInteraction = createInteractionControl({
841
+ name: "reply",
842
+ policyProperty: "canReply",
843
+ requestClass: ReplyRequest,
844
+ authorizationClass: ReplyAuthorization,
845
+ getInteractingObject: (request, options) => request.getInstrument(options),
846
+ isInteractingObject: (object) => object instanceof Note || object instanceof Article || object instanceof Question || object instanceof ChatMessage,
847
+ interactingObjectTypes: [
848
+ Note.typeId,
849
+ Article.typeId,
850
+ Question.typeId,
851
+ ChatMessage.typeId
852
+ ],
853
+ getInteractionTarget: (request, options) => request.getObject(options),
854
+ validateRequest: (_request, reply, target, requester) => {
855
+ const targetId = getRequiredId(target, "interactionTarget");
856
+ if (!idsEqual(reply.replyTargetId, targetId)) return {
857
+ type: "objectMismatch",
858
+ expected: targetId,
859
+ actual: reply.replyTargetId ?? void 0
860
+ };
861
+ if (!idsEqual(reply.attributionId, requester)) return {
862
+ type: "requesterMismatch",
863
+ expected: requester,
864
+ actual: reply.attributionId ?? void 0
865
+ };
866
+ return null;
867
+ },
868
+ getSelfActor: (subject) => subject.attributionId,
869
+ getImplicitAutomaticActors: getReplyParticipants,
870
+ defaultMissingPolicy: "automatic",
871
+ recognizeImpolite: (source) => {
872
+ if (source instanceof Create) return null;
873
+ return recognized({
874
+ requester: source.attributionId,
875
+ interactingObject: source,
876
+ interactionTarget: void 0,
877
+ interactionTargetId: source.replyTargetId,
878
+ source,
879
+ evidence: {
880
+ type: "property",
881
+ property: IN_REPLY_TO
882
+ }
883
+ });
884
+ }
885
+ });
886
+ async function* getReplyParticipants(subject, options) {
887
+ const replyTarget = await subject.getReplyTarget(options);
888
+ if (replyTarget instanceof Object$1 && replyTarget.attributionId != null) yield replyTarget.attributionId;
889
+ for await (const tag of subject.getTags(options)) if (tag instanceof Mention && tag.href != null) yield tag.href;
890
+ }
891
+ //#endregion
892
+ export { announceInteraction, featureInteraction, formatAuthorizationKey, formatInteractionKey, likeInteraction, quoteInteraction, replyInteraction };