@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/LICENSE +20 -0
- package/README.md +13 -0
- package/dist/mod.cjs +899 -0
- package/dist/mod.d.cts +762 -0
- package/dist/mod.d.ts +321 -0
- package/dist/mod.js +892 -0
- package/package.json +72 -0
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 };
|