@oxy.so/federation 2.12.0 → 2.14.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/cjs/index.js +5 -1
- package/dist/cjs/networkIdentity.js +42 -1
- package/dist/cjs/node/actorResolver.js +5 -0
- package/dist/cjs/node/inboundDispatch.js +6 -3
- package/dist/cjs/node/index.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/networkIdentity.js +38 -0
- package/dist/esm/node/actorResolver.js +5 -0
- package/dist/esm/node/inboundDispatch.js +6 -3
- package/dist/esm/node/index.js +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/networkIdentity.d.ts +32 -0
- package/dist/types/node/actorResolver.d.ts +7 -0
- package/dist/types/node/inboundDispatch.d.ts +5 -3
- package/dist/types/node/index.d.ts +1 -1
- package/package.json +8 -8
- package/src/__tests__/actorCollectionCounts.test.ts +16 -0
- package/src/__tests__/inboundDispatch.test.ts +2 -2
- package/src/__tests__/networkIdentity.test.ts +12 -0
- package/src/index.ts +4 -0
- package/src/networkIdentity.ts +42 -0
- package/src/node/actorResolver.ts +12 -0
- package/src/node/inboundDispatch.ts +9 -4
- package/src/node/index.ts +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* knowledge of any app's post shape.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.groupFollowersCollection = exports.groupOutboxCollection = exports.isGroupActorKind = exports.GROUP_OUTBOX_SIZE = exports.LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND = exports.AP_ACTOR_TYPES = exports.isApActorType = exports.accountKindForActor = exports.localActorTypeForAccountKind = exports.normalizeAlsoKnownAs = exports.createLocalActorBuilder = exports.createDomainPolicy = exports.extractActorUriFromActivityId = exports.isSameFederationHost = exports.canonicalFederationHost = exports.AP_CONTEXT = exports.readProxyDeclarations = exports.upstreamHandleFromProxyOf = exports.upstreamHandleFromPreferredUsername = exports.upstreamHandleFromAutomatedActor = exports.upstreamHandleFromAlsoKnownAs = exports.upstreamHandleFromProfileField = exports.federatedUsernameFromUpstreamUrl = exports.parseUpstreamProfileUrl = exports.upstreamProfileUrl = exports.stripBridgeBoilerplate = exports.createBridgeRelabeller = exports.blueskyUsernameFromHandle = exports.BSKY_NETWORK_DOMAIN = exports.FEDERATION_NETWORKS = exports.INSTANCE_ACTOR_USERNAME = exports.normalizeActorUsername = exports.createUrlBuilders = exports.DEFAULT_SIGNED_CONTENT_TYPE = exports.HTTP_SIGNATURE_ALGORITHM = exports.verifyHttpSignature = exports.signRequest = void 0;
|
|
25
|
+
exports.groupFollowersCollection = exports.groupOutboxCollection = exports.isGroupActorKind = exports.GROUP_OUTBOX_SIZE = exports.LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND = exports.AP_ACTOR_TYPES = exports.isApActorType = exports.accountKindForActor = exports.localActorTypeForAccountKind = exports.normalizeAlsoKnownAs = exports.createLocalActorBuilder = exports.createDomainPolicy = exports.extractActorUriFromActivityId = exports.isSameFederationHost = exports.canonicalFederationHost = exports.AP_CONTEXT = exports.readProxyDeclarations = exports.upstreamHandleFromProxyOf = exports.upstreamHandleFromPreferredUsername = exports.upstreamHandleFromAutomatedActor = exports.upstreamHandleFromAlsoKnownAs = exports.upstreamHandleFromProfileField = exports.federatedUsernameFromUpstreamUrl = exports.parseUpstreamProfileUrl = exports.upstreamProfileUrl = exports.stripBridgeBoilerplate = exports.createBridgeRelabeller = exports.atprotoIdentityHandle = exports.isUnresolvedAtprotoHandle = exports.UNRESOLVED_ATPROTO_HANDLE = exports.blueskyHandleFromUsername = exports.blueskyUsernameFromHandle = exports.BSKY_NETWORK_DOMAIN = exports.FEDERATION_NETWORKS = exports.INSTANCE_ACTOR_USERNAME = exports.normalizeActorUsername = exports.createUrlBuilders = exports.DEFAULT_SIGNED_CONTENT_TYPE = exports.HTTP_SIGNATURE_ALGORITHM = exports.verifyHttpSignature = exports.signRequest = void 0;
|
|
26
26
|
/**
|
|
27
27
|
* HTTP Signatures (draft-cavage) — the pure sign/verify crypto every Oxy app's
|
|
28
28
|
* ActivityPub federation shares. Private-key custody is injected; the key never
|
|
@@ -55,6 +55,10 @@ var networkIdentity_1 = require("./networkIdentity");
|
|
|
55
55
|
Object.defineProperty(exports, "FEDERATION_NETWORKS", { enumerable: true, get: function () { return networkIdentity_1.FEDERATION_NETWORKS; } });
|
|
56
56
|
Object.defineProperty(exports, "BSKY_NETWORK_DOMAIN", { enumerable: true, get: function () { return networkIdentity_1.BSKY_NETWORK_DOMAIN; } });
|
|
57
57
|
Object.defineProperty(exports, "blueskyUsernameFromHandle", { enumerable: true, get: function () { return networkIdentity_1.blueskyUsernameFromHandle; } });
|
|
58
|
+
Object.defineProperty(exports, "blueskyHandleFromUsername", { enumerable: true, get: function () { return networkIdentity_1.blueskyHandleFromUsername; } });
|
|
59
|
+
Object.defineProperty(exports, "UNRESOLVED_ATPROTO_HANDLE", { enumerable: true, get: function () { return networkIdentity_1.UNRESOLVED_ATPROTO_HANDLE; } });
|
|
60
|
+
Object.defineProperty(exports, "isUnresolvedAtprotoHandle", { enumerable: true, get: function () { return networkIdentity_1.isUnresolvedAtprotoHandle; } });
|
|
61
|
+
Object.defineProperty(exports, "atprotoIdentityHandle", { enumerable: true, get: function () { return networkIdentity_1.atprotoIdentityHandle; } });
|
|
58
62
|
Object.defineProperty(exports, "createBridgeRelabeller", { enumerable: true, get: function () { return networkIdentity_1.createBridgeRelabeller; } });
|
|
59
63
|
Object.defineProperty(exports, "stripBridgeBoilerplate", { enumerable: true, get: function () { return networkIdentity_1.stripBridgeBoilerplate; } });
|
|
60
64
|
Object.defineProperty(exports, "upstreamProfileUrl", { enumerable: true, get: function () { return networkIdentity_1.upstreamProfileUrl; } });
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
* means neither can be edited into the other by accident.
|
|
50
50
|
*/
|
|
51
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
-
exports.BSKY_NETWORK_DOMAIN = exports.FEDERATION_NETWORKS = void 0;
|
|
52
|
+
exports.UNRESOLVED_ATPROTO_HANDLE = exports.BSKY_NETWORK_DOMAIN = exports.FEDERATION_NETWORKS = void 0;
|
|
53
53
|
exports.upstreamProfileUrl = upstreamProfileUrl;
|
|
54
54
|
exports.parseUpstreamProfileUrl = parseUpstreamProfileUrl;
|
|
55
55
|
exports.readProxyDeclarations = readProxyDeclarations;
|
|
@@ -59,6 +59,9 @@ exports.upstreamHandleFromProxyOf = upstreamHandleFromProxyOf;
|
|
|
59
59
|
exports.upstreamHandleFromAutomatedActor = upstreamHandleFromAutomatedActor;
|
|
60
60
|
exports.upstreamHandleFromPreferredUsername = upstreamHandleFromPreferredUsername;
|
|
61
61
|
exports.blueskyUsernameFromHandle = blueskyUsernameFromHandle;
|
|
62
|
+
exports.blueskyHandleFromUsername = blueskyHandleFromUsername;
|
|
63
|
+
exports.isUnresolvedAtprotoHandle = isUnresolvedAtprotoHandle;
|
|
64
|
+
exports.atprotoIdentityHandle = atprotoIdentityHandle;
|
|
62
65
|
exports.createBridgeRelabeller = createBridgeRelabeller;
|
|
63
66
|
exports.stripBridgeBoilerplate = stripBridgeBoilerplate;
|
|
64
67
|
exports.federatedUsernameFromUpstreamUrl = federatedUsernameFromUpstreamUrl;
|
|
@@ -424,6 +427,44 @@ function blueskyUsernameFromHandle(handle) {
|
|
|
424
427
|
? handle.slice(0, -suffix.length)
|
|
425
428
|
: handle;
|
|
426
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* The inverse of {@link blueskyUsernameFromHandle}: the atproto handle a stored
|
|
432
|
+
* Bluesky username names. A username without a dot was a default
|
|
433
|
+
* `.bsky.social` handle; one with a dot is a custom domain, kept whole.
|
|
434
|
+
*/
|
|
435
|
+
function blueskyHandleFromUsername(username) {
|
|
436
|
+
return username.includes('.') ? username : `${username}.${exports.FEDERATION_NETWORKS.bluesky.domain}`;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* The literal handle a Bluesky AppView serves when a handle's bidirectional
|
|
440
|
+
* DNS/DID verification FAILS.
|
|
441
|
+
*
|
|
442
|
+
* It is an ERROR STRING, not an identity: every account whose handle cannot be
|
|
443
|
+
* verified gets the same one, so it is the single value in the atproto namespace
|
|
444
|
+
* guaranteed NOT to be unique. Keying an account on it collapses every such
|
|
445
|
+
* account onto one identity. Use {@link atprotoIdentityHandle}.
|
|
446
|
+
*/
|
|
447
|
+
exports.UNRESOLVED_ATPROTO_HANDLE = 'handle.invalid';
|
|
448
|
+
/**
|
|
449
|
+
* True when a handle is the unresolved-handle sentinel rather than a real one.
|
|
450
|
+
* Compared case-insensitively on the trimmed value: a handle is a DNS name.
|
|
451
|
+
*/
|
|
452
|
+
function isUnresolvedAtprotoHandle(handle) {
|
|
453
|
+
return handle.trim().toLowerCase() === exports.UNRESOLVED_ATPROTO_HANDLE;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* The handle an atproto account is IDENTIFIED by: its real handle, or its DID
|
|
457
|
+
* when the AppView could not verify one ({@link UNRESOLVED_ATPROTO_HANDLE}).
|
|
458
|
+
*
|
|
459
|
+
* The DID is the identifier atproto guarantees stable and unique. It contains a
|
|
460
|
+
* `:`, which no DNS handle may, so the substituted identity never collides with
|
|
461
|
+
* a real one, and `did:plc:…@bsky.social` splits on its `@` like any other
|
|
462
|
+
* acct. Nothing is stored under the sentinel, so the next refresh after the
|
|
463
|
+
* account fixes its DNS re-derives the real handle through the ordinary path.
|
|
464
|
+
*/
|
|
465
|
+
function atprotoIdentityHandle(handle, did) {
|
|
466
|
+
return isUnresolvedAtprotoHandle(handle) ? did : handle;
|
|
467
|
+
}
|
|
427
468
|
/**
|
|
428
469
|
* Build the readers for a set of reviewed bridge entries.
|
|
429
470
|
*
|
|
@@ -344,6 +344,9 @@ class ActorResolver {
|
|
|
344
344
|
const alsoKnownAs = Array.isArray(actor.alsoKnownAs)
|
|
345
345
|
? actor.alsoKnownAs.filter((v) => typeof v === 'string')
|
|
346
346
|
: undefined;
|
|
347
|
+
// A move target is an actor URL; anything else is not one.
|
|
348
|
+
const movedToValue = asString(actor.movedTo);
|
|
349
|
+
const movedTo = movedToValue && /^https?:\/\//.test(movedToValue) ? movedToValue : undefined;
|
|
347
350
|
// The IDENTITY the actor is stored under in Oxy, which is not necessarily the
|
|
348
351
|
// host it was fetched from — see `DeriveNetworkIdentity`. Everything below
|
|
349
352
|
// that addresses the actor over the PROTOCOL (`acct`, `uri`, `domain`) is
|
|
@@ -391,12 +394,14 @@ class ActorResolver {
|
|
|
391
394
|
type: asString(actor.type) || 'Person',
|
|
392
395
|
manuallyApprovesFollowers: actor.manuallyApprovesFollowers === true,
|
|
393
396
|
discoverable: actor.discoverable !== false,
|
|
397
|
+
...(typeof actor.indexable === 'boolean' && { indexable: actor.indexable }),
|
|
394
398
|
memorial: actor.memorial === true,
|
|
395
399
|
suspended: actor.suspended === true,
|
|
396
400
|
fields,
|
|
397
401
|
featuredUrl: asString(actor.featured) || undefined,
|
|
398
402
|
featuredTagsUrl: asString(actor.featuredTags) || undefined,
|
|
399
403
|
alsoKnownAs,
|
|
404
|
+
...(movedTo && { movedTo }),
|
|
400
405
|
networkAcct: networkIdentity?.federatedUsername,
|
|
401
406
|
remoteCreatedAt: typeof actor.published === 'string' ? new Date(actor.published) : undefined,
|
|
402
407
|
// Omitted, not written as `undefined`, when this refresh could not tell:
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* because those are identical across every Oxy app: they bridge a federated follow
|
|
8
8
|
* edge into the Oxy graph (via the identity adapter), record the AP-side follow
|
|
9
9
|
* row (via the store adapter), and send the Accept back (via the delivery
|
|
10
|
-
* service). Every CONTENT verb (Create / Announce / Like / Delete / Update
|
|
11
|
-
* non-follow Undo) is handed to the app-registered
|
|
10
|
+
* service). Every CONTENT verb (Create / Announce / Like / Delete / Update /
|
|
11
|
+
* Add / Remove, and a non-follow Undo) is handed to the app-registered
|
|
12
12
|
* {@link InboundDispatcherConfig.onContentActivity} callback, where the app's own
|
|
13
13
|
* post/engagement handlers live. The consent gate + notification side effects are
|
|
14
14
|
* injected so the engine holds no app knowledge.
|
|
@@ -299,12 +299,15 @@ function createInboundDispatcher(config) {
|
|
|
299
299
|
case 'Reject':
|
|
300
300
|
await handleReject(activity, verifiedActorUri);
|
|
301
301
|
break;
|
|
302
|
-
// Content verbs — the app owns these (posts, engagement, actor profile
|
|
302
|
+
// Content verbs — the app owns these (posts, engagement, actor profile
|
|
303
|
+
// edits, and collection changes such as pinned posts).
|
|
303
304
|
case 'Create':
|
|
304
305
|
case 'Delete':
|
|
305
306
|
case 'Like':
|
|
306
307
|
case 'Announce':
|
|
307
308
|
case 'Update':
|
|
309
|
+
case 'Add':
|
|
310
|
+
case 'Remove':
|
|
308
311
|
await config.onContentActivity(activity, verifiedActorUri);
|
|
309
312
|
break;
|
|
310
313
|
case 'Move': {
|
package/dist/cjs/node/index.js
CHANGED
|
@@ -54,7 +54,7 @@ Object.defineProperty(exports, "DeliveryAuthorizationDeniedError", { enumerable:
|
|
|
54
54
|
* Inbound ActivityPub dispatch — the untrusted-activity validator + switch, the
|
|
55
55
|
* follow-protocol handlers (Follow / Accept / Undo(Follow) / Reject) over the
|
|
56
56
|
* identity + store adapters, and the `onContentActivity` seam every content verb
|
|
57
|
-
* (Create / Announce / Like / Delete / Update, non-follow Undo) is handed to.
|
|
57
|
+
* (Create / Announce / Like / Delete / Update / Add / Remove, non-follow Undo) is handed to.
|
|
58
58
|
*/
|
|
59
59
|
var inboundDispatch_1 = require("./inboundDispatch");
|
|
60
60
|
Object.defineProperty(exports, "createInboundDispatcher", { enumerable: true, get: function () { return inboundDispatch_1.createInboundDispatcher; } });
|
package/dist/esm/index.js
CHANGED
|
@@ -41,7 +41,7 @@ export { createUrlBuilders, normalizeActorUsername, INSTANCE_ACTOR_USERNAME, } f
|
|
|
41
41
|
* app commits and answers for — `createBridgeRelabeller` takes them as a
|
|
42
42
|
* parameter so no app inherits another's.
|
|
43
43
|
*/
|
|
44
|
-
export { FEDERATION_NETWORKS, BSKY_NETWORK_DOMAIN, blueskyUsernameFromHandle, createBridgeRelabeller, stripBridgeBoilerplate, upstreamProfileUrl, parseUpstreamProfileUrl, federatedUsernameFromUpstreamUrl, upstreamHandleFromProfileField, upstreamHandleFromAlsoKnownAs, upstreamHandleFromAutomatedActor, upstreamHandleFromPreferredUsername, upstreamHandleFromProxyOf, readProxyDeclarations, } from './networkIdentity.js';
|
|
44
|
+
export { FEDERATION_NETWORKS, BSKY_NETWORK_DOMAIN, blueskyUsernameFromHandle, blueskyHandleFromUsername, UNRESOLVED_ATPROTO_HANDLE, isUnresolvedAtprotoHandle, atprotoIdentityHandle, createBridgeRelabeller, stripBridgeBoilerplate, upstreamProfileUrl, parseUpstreamProfileUrl, federatedUsernameFromUpstreamUrl, upstreamHandleFromProfileField, upstreamHandleFromAlsoKnownAs, upstreamHandleFromAutomatedActor, upstreamHandleFromPreferredUsername, upstreamHandleFromProxyOf, readProxyDeclarations, } from './networkIdentity.js';
|
|
45
45
|
/**
|
|
46
46
|
* The shared JSON-LD `@context` (load-bearing term declarations) and the
|
|
47
47
|
* ActivityPub URI helpers (actor-uri extraction + the per-instance domain policy:
|
|
@@ -409,6 +409,44 @@ export function blueskyUsernameFromHandle(handle) {
|
|
|
409
409
|
? handle.slice(0, -suffix.length)
|
|
410
410
|
: handle;
|
|
411
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* The inverse of {@link blueskyUsernameFromHandle}: the atproto handle a stored
|
|
414
|
+
* Bluesky username names. A username without a dot was a default
|
|
415
|
+
* `.bsky.social` handle; one with a dot is a custom domain, kept whole.
|
|
416
|
+
*/
|
|
417
|
+
export function blueskyHandleFromUsername(username) {
|
|
418
|
+
return username.includes('.') ? username : `${username}.${FEDERATION_NETWORKS.bluesky.domain}`;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* The literal handle a Bluesky AppView serves when a handle's bidirectional
|
|
422
|
+
* DNS/DID verification FAILS.
|
|
423
|
+
*
|
|
424
|
+
* It is an ERROR STRING, not an identity: every account whose handle cannot be
|
|
425
|
+
* verified gets the same one, so it is the single value in the atproto namespace
|
|
426
|
+
* guaranteed NOT to be unique. Keying an account on it collapses every such
|
|
427
|
+
* account onto one identity. Use {@link atprotoIdentityHandle}.
|
|
428
|
+
*/
|
|
429
|
+
export const UNRESOLVED_ATPROTO_HANDLE = 'handle.invalid';
|
|
430
|
+
/**
|
|
431
|
+
* True when a handle is the unresolved-handle sentinel rather than a real one.
|
|
432
|
+
* Compared case-insensitively on the trimmed value: a handle is a DNS name.
|
|
433
|
+
*/
|
|
434
|
+
export function isUnresolvedAtprotoHandle(handle) {
|
|
435
|
+
return handle.trim().toLowerCase() === UNRESOLVED_ATPROTO_HANDLE;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* The handle an atproto account is IDENTIFIED by: its real handle, or its DID
|
|
439
|
+
* when the AppView could not verify one ({@link UNRESOLVED_ATPROTO_HANDLE}).
|
|
440
|
+
*
|
|
441
|
+
* The DID is the identifier atproto guarantees stable and unique. It contains a
|
|
442
|
+
* `:`, which no DNS handle may, so the substituted identity never collides with
|
|
443
|
+
* a real one, and `did:plc:…@bsky.social` splits on its `@` like any other
|
|
444
|
+
* acct. Nothing is stored under the sentinel, so the next refresh after the
|
|
445
|
+
* account fixes its DNS re-derives the real handle through the ordinary path.
|
|
446
|
+
*/
|
|
447
|
+
export function atprotoIdentityHandle(handle, did) {
|
|
448
|
+
return isUnresolvedAtprotoHandle(handle) ? did : handle;
|
|
449
|
+
}
|
|
412
450
|
/**
|
|
413
451
|
* Build the readers for a set of reviewed bridge entries.
|
|
414
452
|
*
|
|
@@ -340,6 +340,9 @@ export class ActorResolver {
|
|
|
340
340
|
const alsoKnownAs = Array.isArray(actor.alsoKnownAs)
|
|
341
341
|
? actor.alsoKnownAs.filter((v) => typeof v === 'string')
|
|
342
342
|
: undefined;
|
|
343
|
+
// A move target is an actor URL; anything else is not one.
|
|
344
|
+
const movedToValue = asString(actor.movedTo);
|
|
345
|
+
const movedTo = movedToValue && /^https?:\/\//.test(movedToValue) ? movedToValue : undefined;
|
|
343
346
|
// The IDENTITY the actor is stored under in Oxy, which is not necessarily the
|
|
344
347
|
// host it was fetched from — see `DeriveNetworkIdentity`. Everything below
|
|
345
348
|
// that addresses the actor over the PROTOCOL (`acct`, `uri`, `domain`) is
|
|
@@ -387,12 +390,14 @@ export class ActorResolver {
|
|
|
387
390
|
type: asString(actor.type) || 'Person',
|
|
388
391
|
manuallyApprovesFollowers: actor.manuallyApprovesFollowers === true,
|
|
389
392
|
discoverable: actor.discoverable !== false,
|
|
393
|
+
...(typeof actor.indexable === 'boolean' && { indexable: actor.indexable }),
|
|
390
394
|
memorial: actor.memorial === true,
|
|
391
395
|
suspended: actor.suspended === true,
|
|
392
396
|
fields,
|
|
393
397
|
featuredUrl: asString(actor.featured) || undefined,
|
|
394
398
|
featuredTagsUrl: asString(actor.featuredTags) || undefined,
|
|
395
399
|
alsoKnownAs,
|
|
400
|
+
...(movedTo && { movedTo }),
|
|
396
401
|
networkAcct: networkIdentity?.federatedUsername,
|
|
397
402
|
remoteCreatedAt: typeof actor.published === 'string' ? new Date(actor.published) : undefined,
|
|
398
403
|
// Omitted, not written as `undefined`, when this refresh could not tell:
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* because those are identical across every Oxy app: they bridge a federated follow
|
|
7
7
|
* edge into the Oxy graph (via the identity adapter), record the AP-side follow
|
|
8
8
|
* row (via the store adapter), and send the Accept back (via the delivery
|
|
9
|
-
* service). Every CONTENT verb (Create / Announce / Like / Delete / Update
|
|
10
|
-
* non-follow Undo) is handed to the app-registered
|
|
9
|
+
* service). Every CONTENT verb (Create / Announce / Like / Delete / Update /
|
|
10
|
+
* Add / Remove, and a non-follow Undo) is handed to the app-registered
|
|
11
11
|
* {@link InboundDispatcherConfig.onContentActivity} callback, where the app's own
|
|
12
12
|
* post/engagement handlers live. The consent gate + notification side effects are
|
|
13
13
|
* injected so the engine holds no app knowledge.
|
|
@@ -294,12 +294,15 @@ export function createInboundDispatcher(config) {
|
|
|
294
294
|
case 'Reject':
|
|
295
295
|
await handleReject(activity, verifiedActorUri);
|
|
296
296
|
break;
|
|
297
|
-
// Content verbs — the app owns these (posts, engagement, actor profile
|
|
297
|
+
// Content verbs — the app owns these (posts, engagement, actor profile
|
|
298
|
+
// edits, and collection changes such as pinned posts).
|
|
298
299
|
case 'Create':
|
|
299
300
|
case 'Delete':
|
|
300
301
|
case 'Like':
|
|
301
302
|
case 'Announce':
|
|
302
303
|
case 'Update':
|
|
304
|
+
case 'Add':
|
|
305
|
+
case 'Remove':
|
|
303
306
|
await config.onContentActivity(activity, verifiedActorUri);
|
|
304
307
|
break;
|
|
305
308
|
case 'Move': {
|
package/dist/esm/node/index.js
CHANGED
|
@@ -44,7 +44,7 @@ export { createDeliveryService, DeliveryAuthorizationDeniedError, } from './deli
|
|
|
44
44
|
* Inbound ActivityPub dispatch — the untrusted-activity validator + switch, the
|
|
45
45
|
* follow-protocol handlers (Follow / Accept / Undo(Follow) / Reject) over the
|
|
46
46
|
* identity + store adapters, and the `onContentActivity` seam every content verb
|
|
47
|
-
* (Create / Announce / Like / Delete / Update, non-follow Undo) is handed to.
|
|
47
|
+
* (Create / Announce / Like / Delete / Update / Add / Remove, non-follow Undo) is handed to.
|
|
48
48
|
*/
|
|
49
49
|
export { createInboundDispatcher, ActorResolutionPendingError, } from './inboundDispatch.js';
|
|
50
50
|
/** The WebFinger + host-meta discovery router (domain-parameterized, consent-gated). */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export { createUrlBuilders, normalizeActorUsername, INSTANCE_ACTOR_USERNAME, typ
|
|
|
41
41
|
* app commits and answers for — `createBridgeRelabeller` takes them as a
|
|
42
42
|
* parameter so no app inherits another's.
|
|
43
43
|
*/
|
|
44
|
-
export { FEDERATION_NETWORKS, BSKY_NETWORK_DOMAIN, blueskyUsernameFromHandle, createBridgeRelabeller, stripBridgeBoilerplate, upstreamProfileUrl, parseUpstreamProfileUrl, federatedUsernameFromUpstreamUrl, upstreamHandleFromProfileField, upstreamHandleFromAlsoKnownAs, upstreamHandleFromAutomatedActor, upstreamHandleFromPreferredUsername, upstreamHandleFromProxyOf, readProxyDeclarations, type FederationNetwork, type FederationBridgeEntry, type BridgeRelabeller, type BridgeConsentModel, type BridgeDerivation, type BridgedActorField, type DeriveNetworkIdentity, type NetworkIdentity, type NetworkIdentityCandidate, type ProxyDeclaration, } from './networkIdentity';
|
|
44
|
+
export { FEDERATION_NETWORKS, BSKY_NETWORK_DOMAIN, blueskyUsernameFromHandle, blueskyHandleFromUsername, UNRESOLVED_ATPROTO_HANDLE, isUnresolvedAtprotoHandle, atprotoIdentityHandle, createBridgeRelabeller, stripBridgeBoilerplate, upstreamProfileUrl, parseUpstreamProfileUrl, federatedUsernameFromUpstreamUrl, upstreamHandleFromProfileField, upstreamHandleFromAlsoKnownAs, upstreamHandleFromAutomatedActor, upstreamHandleFromPreferredUsername, upstreamHandleFromProxyOf, readProxyDeclarations, type FederationNetwork, type FederationBridgeEntry, type BridgeRelabeller, type BridgeConsentModel, type BridgeDerivation, type BridgedActorField, type DeriveNetworkIdentity, type NetworkIdentity, type NetworkIdentityCandidate, type ProxyDeclaration, } from './networkIdentity';
|
|
45
45
|
/**
|
|
46
46
|
* The shared JSON-LD `@context` (load-bearing term declarations) and the
|
|
47
47
|
* ActivityPub URI helpers (actor-uri extraction + the per-instance domain policy:
|
|
@@ -464,6 +464,38 @@ export declare function upstreamHandleFromPreferredUsername(markers: readonly Re
|
|
|
464
464
|
* whole handle is kept.
|
|
465
465
|
*/
|
|
466
466
|
export declare function blueskyUsernameFromHandle(handle: string): string;
|
|
467
|
+
/**
|
|
468
|
+
* The inverse of {@link blueskyUsernameFromHandle}: the atproto handle a stored
|
|
469
|
+
* Bluesky username names. A username without a dot was a default
|
|
470
|
+
* `.bsky.social` handle; one with a dot is a custom domain, kept whole.
|
|
471
|
+
*/
|
|
472
|
+
export declare function blueskyHandleFromUsername(username: string): string;
|
|
473
|
+
/**
|
|
474
|
+
* The literal handle a Bluesky AppView serves when a handle's bidirectional
|
|
475
|
+
* DNS/DID verification FAILS.
|
|
476
|
+
*
|
|
477
|
+
* It is an ERROR STRING, not an identity: every account whose handle cannot be
|
|
478
|
+
* verified gets the same one, so it is the single value in the atproto namespace
|
|
479
|
+
* guaranteed NOT to be unique. Keying an account on it collapses every such
|
|
480
|
+
* account onto one identity. Use {@link atprotoIdentityHandle}.
|
|
481
|
+
*/
|
|
482
|
+
export declare const UNRESOLVED_ATPROTO_HANDLE = "handle.invalid";
|
|
483
|
+
/**
|
|
484
|
+
* True when a handle is the unresolved-handle sentinel rather than a real one.
|
|
485
|
+
* Compared case-insensitively on the trimmed value: a handle is a DNS name.
|
|
486
|
+
*/
|
|
487
|
+
export declare function isUnresolvedAtprotoHandle(handle: string): boolean;
|
|
488
|
+
/**
|
|
489
|
+
* The handle an atproto account is IDENTIFIED by: its real handle, or its DID
|
|
490
|
+
* when the AppView could not verify one ({@link UNRESOLVED_ATPROTO_HANDLE}).
|
|
491
|
+
*
|
|
492
|
+
* The DID is the identifier atproto guarantees stable and unique. It contains a
|
|
493
|
+
* `:`, which no DNS handle may, so the substituted identity never collides with
|
|
494
|
+
* a real one, and `did:plc:…@bsky.social` splits on its `@` like any other
|
|
495
|
+
* acct. Nothing is stored under the sentinel, so the next refresh after the
|
|
496
|
+
* account fixes its DNS re-derives the real handle through the ordinary path.
|
|
497
|
+
*/
|
|
498
|
+
export declare function atprotoIdentityHandle(handle: string, did: string): string;
|
|
467
499
|
/** A reviewed bridge registry, and the readers an app drives it with. */
|
|
468
500
|
export interface BridgeRelabeller {
|
|
469
501
|
/** The reviewed entry for a host, or `undefined` — most hosts are not bridges. */
|
|
@@ -60,12 +60,19 @@ export interface FederatedActorUpsert {
|
|
|
60
60
|
type: string;
|
|
61
61
|
manuallyApprovesFollowers: boolean;
|
|
62
62
|
discoverable: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Mastodon's opt-in to search engines and full-text search (`indexable`).
|
|
65
|
+
* Absent when the actor does not say, which Mastodon reads as "no".
|
|
66
|
+
*/
|
|
67
|
+
indexable?: boolean;
|
|
63
68
|
memorial: boolean;
|
|
64
69
|
suspended: boolean;
|
|
65
70
|
fields: FederatedActorField[];
|
|
66
71
|
featuredUrl?: string;
|
|
67
72
|
featuredTagsUrl?: string;
|
|
68
73
|
alsoKnownAs?: string[];
|
|
74
|
+
/** The account this actor moved to (`movedTo`), when it advertises one. */
|
|
75
|
+
movedTo?: string;
|
|
69
76
|
/**
|
|
70
77
|
* The `<handle>@<network-domain>` identity this actor was re-labelled onto, when
|
|
71
78
|
* it came from a bridge; absent for the ordinary actor whose identity is simply
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* because those are identical across every Oxy app: they bridge a federated follow
|
|
7
7
|
* edge into the Oxy graph (via the identity adapter), record the AP-side follow
|
|
8
8
|
* row (via the store adapter), and send the Accept back (via the delivery
|
|
9
|
-
* service). Every CONTENT verb (Create / Announce / Like / Delete / Update
|
|
10
|
-
* non-follow Undo) is handed to the app-registered
|
|
9
|
+
* service). Every CONTENT verb (Create / Announce / Like / Delete / Update /
|
|
10
|
+
* Add / Remove, and a non-follow Undo) is handed to the app-registered
|
|
11
11
|
* {@link InboundDispatcherConfig.onContentActivity} callback, where the app's own
|
|
12
12
|
* post/engagement handlers live. The consent gate + notification side effects are
|
|
13
13
|
* injected so the engine holds no app knowledge.
|
|
@@ -157,7 +157,9 @@ export interface InboundDispatcherConfig {
|
|
|
157
157
|
onOutboundFollowAccepted?(actorUri: string): Promise<void>;
|
|
158
158
|
/**
|
|
159
159
|
* Handle a CONTENT activity the engine does not own — Create / Announce / Like /
|
|
160
|
-
* Delete / Update, and a non-follow Undo. The app's
|
|
160
|
+
* Delete / Update / Add / Remove, and a non-follow Undo. The app's
|
|
161
|
+
* post/engagement handlers; `Add`/`Remove` carry collection changes such as a
|
|
162
|
+
* Mastodon pin (the status added to or removed from the actor's `featured`).
|
|
161
163
|
*/
|
|
162
164
|
onContentActivity(activity: Record<string, unknown>, verifiedActorUri: string): Promise<void>;
|
|
163
165
|
/**
|
|
@@ -44,7 +44,7 @@ export { createDeliveryService, DeliveryAuthorizationDeniedError, type DeliveryA
|
|
|
44
44
|
* Inbound ActivityPub dispatch — the untrusted-activity validator + switch, the
|
|
45
45
|
* follow-protocol handlers (Follow / Accept / Undo(Follow) / Reject) over the
|
|
46
46
|
* identity + store adapters, and the `onContentActivity` seam every content verb
|
|
47
|
-
* (Create / Announce / Like / Delete / Update, non-follow Undo) is handed to.
|
|
47
|
+
* (Create / Announce / Like / Delete / Update / Add / Remove, non-follow Undo) is handed to.
|
|
48
48
|
*/
|
|
49
49
|
export { createInboundDispatcher, ActorResolutionPendingError, type InboundMove, type InboundDispatcher, type InboundDispatcherConfig, type InboundDispatcherLogger, type InboundActivityValidation, type InboundLocalUser, type InboundIdentity, type InboundConsent, type InboundActorResolver, type InboundFollowStore, type InboundDelivery, } from './inboundDispatch';
|
|
50
50
|
/** The WebFinger + host-meta discovery router (domain-parameterized, consent-gated). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/federation",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"description": "Oxy Federation — the app-agnostic ActivityPub identity + follow engine substrate: the network-connector contract, normalized cross-network DTOs, HTTP signatures, actor resolution, the outbound delivery transport + follow lifecycle, the inbound dispatcher, and the webfinger/actor/inbox Express routers. Domain-parameterized so every Oxy app backend federates under its own domain.",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -90,13 +90,13 @@
|
|
|
90
90
|
}
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
|
-
"@types/express": "^
|
|
93
|
+
"@types/express": "^5.0.6",
|
|
94
94
|
"@types/jest": "^30.0.0",
|
|
95
|
-
"@types/node": "^
|
|
96
|
-
"express": "^
|
|
97
|
-
"jest": "^30.5.
|
|
98
|
-
"supertest": "^7.
|
|
99
|
-
"ts-jest": "^29.4.
|
|
100
|
-
"typescript": "
|
|
95
|
+
"@types/node": "^26.6.6",
|
|
96
|
+
"express": "^5.3.0",
|
|
97
|
+
"jest": "^30.5.2",
|
|
98
|
+
"supertest": "^7.3.1",
|
|
99
|
+
"ts-jest": "^29.4.15",
|
|
100
|
+
"typescript": "~6.0.3"
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -102,6 +102,22 @@ async function resolveWith(replies: {
|
|
|
102
102
|
return { upserts, bridged };
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
describe('fetchRemoteActor — search opt-in and moves', () => {
|
|
106
|
+
it('stores indexable and movedTo when the actor states them, and omits them when not', async () => {
|
|
107
|
+
const moved = await resolveWith({
|
|
108
|
+
actor: actorDocument({ indexable: true, movedTo: 'https://new.example/users/bob' }),
|
|
109
|
+
});
|
|
110
|
+
expect(moved.upserts[0]).toMatchObject({
|
|
111
|
+
indexable: true,
|
|
112
|
+
movedTo: 'https://new.example/users/bob',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const silent = await resolveWith({ actor: actorDocument({ movedTo: 'not a url' }) });
|
|
116
|
+
expect(silent.upserts[0]).not.toHaveProperty('indexable');
|
|
117
|
+
expect(silent.upserts[0]).not.toHaveProperty('movedTo');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
105
121
|
describe('fetchRemoteActor — remote collection counts', () => {
|
|
106
122
|
it('stores reported totals, and a real 0 stays 0', async () => {
|
|
107
123
|
const { upserts } = await resolveWith({
|
|
@@ -375,7 +375,7 @@ describe('Move → onMove', () => {
|
|
|
375
375
|
});
|
|
376
376
|
|
|
377
377
|
describe('content verbs → onContentActivity', () => {
|
|
378
|
-
it.each(['Create', 'Announce', 'Like', 'Delete', 'Update'])(
|
|
378
|
+
it.each(['Create', 'Announce', 'Like', 'Delete', 'Update', 'Add', 'Remove'])(
|
|
379
379
|
'routes %s to the app',
|
|
380
380
|
async (type) => {
|
|
381
381
|
const rig = makeRig();
|
|
@@ -404,7 +404,7 @@ describe('content verbs → onContentActivity', () => {
|
|
|
404
404
|
describe('blocked-origin domain policy', () => {
|
|
405
405
|
const BLOCKED_ACTOR = 'https://spam.example/users/mallory';
|
|
406
406
|
|
|
407
|
-
it.each(['Create', 'Announce', 'Like', 'Delete', 'Update'])(
|
|
407
|
+
it.each(['Create', 'Announce', 'Like', 'Delete', 'Update', 'Add', 'Remove'])(
|
|
408
408
|
'drops %s from a blocked origin without reaching the app',
|
|
409
409
|
async (type) => {
|
|
410
410
|
const rig = makeRig({ blockedHosts: ['spam.example'] });
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
FEDERATION_NETWORKS,
|
|
13
|
+
atprotoIdentityHandle,
|
|
14
|
+
blueskyHandleFromUsername,
|
|
13
15
|
blueskyUsernameFromHandle,
|
|
14
16
|
createBridgeRelabeller,
|
|
15
17
|
parseUpstreamProfileUrl,
|
|
@@ -390,6 +392,16 @@ describe('bsky.social is one network, whichever protocol an account arrives by',
|
|
|
390
392
|
expect(blueskyUsernameFromHandle('jay.bsky.team')).toBe('jay.bsky.team');
|
|
391
393
|
expect(blueskyUsernameFromHandle('bsky.social')).toBe('bsky.social');
|
|
392
394
|
});
|
|
395
|
+
|
|
396
|
+
it('maps a stored username back to its handle', () => {
|
|
397
|
+
for (const handle of ['skylee1.bsky.social', 'gothamist.com', 'bsky.social'])
|
|
398
|
+
expect(blueskyHandleFromUsername(blueskyUsernameFromHandle(handle))).toBe(handle);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("identifies an account whose handle does not verify by its DID, never Bluesky's sentinel", () => {
|
|
402
|
+
expect(atprotoIdentityHandle('alice.example', 'did:plc:abc')).toBe('alice.example');
|
|
403
|
+
expect(atprotoIdentityHandle(' Handle.Invalid ', 'did:plc:abc')).toBe('did:plc:abc');
|
|
404
|
+
});
|
|
393
405
|
});
|
|
394
406
|
|
|
395
407
|
describe('federatedUsernameFromUpstreamUrl — the search direction', () => {
|
package/src/index.ts
CHANGED
|
@@ -65,6 +65,10 @@ export {
|
|
|
65
65
|
FEDERATION_NETWORKS,
|
|
66
66
|
BSKY_NETWORK_DOMAIN,
|
|
67
67
|
blueskyUsernameFromHandle,
|
|
68
|
+
blueskyHandleFromUsername,
|
|
69
|
+
UNRESOLVED_ATPROTO_HANDLE,
|
|
70
|
+
isUnresolvedAtprotoHandle,
|
|
71
|
+
atprotoIdentityHandle,
|
|
68
72
|
createBridgeRelabeller,
|
|
69
73
|
stripBridgeBoilerplate,
|
|
70
74
|
upstreamProfileUrl,
|
package/src/networkIdentity.ts
CHANGED
|
@@ -646,6 +646,48 @@ export function blueskyUsernameFromHandle(handle: string): string {
|
|
|
646
646
|
: handle;
|
|
647
647
|
}
|
|
648
648
|
|
|
649
|
+
/**
|
|
650
|
+
* The inverse of {@link blueskyUsernameFromHandle}: the atproto handle a stored
|
|
651
|
+
* Bluesky username names. A username without a dot was a default
|
|
652
|
+
* `.bsky.social` handle; one with a dot is a custom domain, kept whole.
|
|
653
|
+
*/
|
|
654
|
+
export function blueskyHandleFromUsername(username: string): string {
|
|
655
|
+
return username.includes('.') ? username : `${username}.${FEDERATION_NETWORKS.bluesky.domain}`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* The literal handle a Bluesky AppView serves when a handle's bidirectional
|
|
660
|
+
* DNS/DID verification FAILS.
|
|
661
|
+
*
|
|
662
|
+
* It is an ERROR STRING, not an identity: every account whose handle cannot be
|
|
663
|
+
* verified gets the same one, so it is the single value in the atproto namespace
|
|
664
|
+
* guaranteed NOT to be unique. Keying an account on it collapses every such
|
|
665
|
+
* account onto one identity. Use {@link atprotoIdentityHandle}.
|
|
666
|
+
*/
|
|
667
|
+
export const UNRESOLVED_ATPROTO_HANDLE = 'handle.invalid';
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* True when a handle is the unresolved-handle sentinel rather than a real one.
|
|
671
|
+
* Compared case-insensitively on the trimmed value: a handle is a DNS name.
|
|
672
|
+
*/
|
|
673
|
+
export function isUnresolvedAtprotoHandle(handle: string): boolean {
|
|
674
|
+
return handle.trim().toLowerCase() === UNRESOLVED_ATPROTO_HANDLE;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* The handle an atproto account is IDENTIFIED by: its real handle, or its DID
|
|
679
|
+
* when the AppView could not verify one ({@link UNRESOLVED_ATPROTO_HANDLE}).
|
|
680
|
+
*
|
|
681
|
+
* The DID is the identifier atproto guarantees stable and unique. It contains a
|
|
682
|
+
* `:`, which no DNS handle may, so the substituted identity never collides with
|
|
683
|
+
* a real one, and `did:plc:…@bsky.social` splits on its `@` like any other
|
|
684
|
+
* acct. Nothing is stored under the sentinel, so the next refresh after the
|
|
685
|
+
* account fixes its DNS re-derives the real handle through the ordinary path.
|
|
686
|
+
*/
|
|
687
|
+
export function atprotoIdentityHandle(handle: string, did: string): string {
|
|
688
|
+
return isUnresolvedAtprotoHandle(handle) ? did : handle;
|
|
689
|
+
}
|
|
690
|
+
|
|
649
691
|
/** A reviewed bridge registry, and the readers an app drives it with. */
|
|
650
692
|
export interface BridgeRelabeller {
|
|
651
693
|
/** The reviewed entry for a host, or `undefined` — most hosts are not bridges. */
|
|
@@ -148,12 +148,19 @@ export interface FederatedActorUpsert {
|
|
|
148
148
|
type: string;
|
|
149
149
|
manuallyApprovesFollowers: boolean;
|
|
150
150
|
discoverable: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Mastodon's opt-in to search engines and full-text search (`indexable`).
|
|
153
|
+
* Absent when the actor does not say, which Mastodon reads as "no".
|
|
154
|
+
*/
|
|
155
|
+
indexable?: boolean;
|
|
151
156
|
memorial: boolean;
|
|
152
157
|
suspended: boolean;
|
|
153
158
|
fields: FederatedActorField[];
|
|
154
159
|
featuredUrl?: string;
|
|
155
160
|
featuredTagsUrl?: string;
|
|
156
161
|
alsoKnownAs?: string[];
|
|
162
|
+
/** The account this actor moved to (`movedTo`), when it advertises one. */
|
|
163
|
+
movedTo?: string;
|
|
157
164
|
/**
|
|
158
165
|
* The `<handle>@<network-domain>` identity this actor was re-labelled onto, when
|
|
159
166
|
* it came from a bridge; absent for the ordinary actor whose identity is simply
|
|
@@ -598,6 +605,9 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
|
|
|
598
605
|
const alsoKnownAs = Array.isArray(actor.alsoKnownAs)
|
|
599
606
|
? actor.alsoKnownAs.filter((v): v is string => typeof v === 'string')
|
|
600
607
|
: undefined;
|
|
608
|
+
// A move target is an actor URL; anything else is not one.
|
|
609
|
+
const movedToValue = asString(actor.movedTo);
|
|
610
|
+
const movedTo = movedToValue && /^https?:\/\//.test(movedToValue) ? movedToValue : undefined;
|
|
601
611
|
|
|
602
612
|
// The IDENTITY the actor is stored under in Oxy, which is not necessarily the
|
|
603
613
|
// host it was fetched from — see `DeriveNetworkIdentity`. Everything below
|
|
@@ -647,12 +657,14 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
|
|
|
647
657
|
type: asString(actor.type) || 'Person',
|
|
648
658
|
manuallyApprovesFollowers: actor.manuallyApprovesFollowers === true,
|
|
649
659
|
discoverable: actor.discoverable !== false,
|
|
660
|
+
...(typeof actor.indexable === 'boolean' && { indexable: actor.indexable }),
|
|
650
661
|
memorial: actor.memorial === true,
|
|
651
662
|
suspended: actor.suspended === true,
|
|
652
663
|
fields,
|
|
653
664
|
featuredUrl: asString(actor.featured) || undefined,
|
|
654
665
|
featuredTagsUrl: asString(actor.featuredTags) || undefined,
|
|
655
666
|
alsoKnownAs,
|
|
667
|
+
...(movedTo && { movedTo }),
|
|
656
668
|
networkAcct: networkIdentity?.federatedUsername,
|
|
657
669
|
remoteCreatedAt:
|
|
658
670
|
typeof actor.published === 'string' ? new Date(actor.published) : undefined,
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* because those are identical across every Oxy app: they bridge a federated follow
|
|
7
7
|
* edge into the Oxy graph (via the identity adapter), record the AP-side follow
|
|
8
8
|
* row (via the store adapter), and send the Accept back (via the delivery
|
|
9
|
-
* service). Every CONTENT verb (Create / Announce / Like / Delete / Update
|
|
10
|
-
* non-follow Undo) is handed to the app-registered
|
|
9
|
+
* service). Every CONTENT verb (Create / Announce / Like / Delete / Update /
|
|
10
|
+
* Add / Remove, and a non-follow Undo) is handed to the app-registered
|
|
11
11
|
* {@link InboundDispatcherConfig.onContentActivity} callback, where the app's own
|
|
12
12
|
* post/engagement handlers live. The consent gate + notification side effects are
|
|
13
13
|
* injected so the engine holds no app knowledge.
|
|
@@ -186,7 +186,9 @@ export interface InboundDispatcherConfig {
|
|
|
186
186
|
onOutboundFollowAccepted?(actorUri: string): Promise<void>;
|
|
187
187
|
/**
|
|
188
188
|
* Handle a CONTENT activity the engine does not own — Create / Announce / Like /
|
|
189
|
-
* Delete / Update, and a non-follow Undo. The app's
|
|
189
|
+
* Delete / Update / Add / Remove, and a non-follow Undo. The app's
|
|
190
|
+
* post/engagement handlers; `Add`/`Remove` carry collection changes such as a
|
|
191
|
+
* Mastodon pin (the status added to or removed from the actor's `featured`).
|
|
190
192
|
*/
|
|
191
193
|
onContentActivity(activity: Record<string, unknown>, verifiedActorUri: string): Promise<void>;
|
|
192
194
|
/**
|
|
@@ -499,12 +501,15 @@ export function createInboundDispatcher(config: InboundDispatcherConfig): Inboun
|
|
|
499
501
|
case 'Reject':
|
|
500
502
|
await handleReject(activity, verifiedActorUri);
|
|
501
503
|
break;
|
|
502
|
-
// Content verbs — the app owns these (posts, engagement, actor profile
|
|
504
|
+
// Content verbs — the app owns these (posts, engagement, actor profile
|
|
505
|
+
// edits, and collection changes such as pinned posts).
|
|
503
506
|
case 'Create':
|
|
504
507
|
case 'Delete':
|
|
505
508
|
case 'Like':
|
|
506
509
|
case 'Announce':
|
|
507
510
|
case 'Update':
|
|
511
|
+
case 'Add':
|
|
512
|
+
case 'Remove':
|
|
508
513
|
await config.onContentActivity(activity, verifiedActorUri);
|
|
509
514
|
break;
|
|
510
515
|
case 'Move': {
|
package/src/node/index.ts
CHANGED
|
@@ -109,7 +109,7 @@ export {
|
|
|
109
109
|
* Inbound ActivityPub dispatch — the untrusted-activity validator + switch, the
|
|
110
110
|
* follow-protocol handlers (Follow / Accept / Undo(Follow) / Reject) over the
|
|
111
111
|
* identity + store adapters, and the `onContentActivity` seam every content verb
|
|
112
|
-
* (Create / Announce / Like / Delete / Update, non-follow Undo) is handed to.
|
|
112
|
+
* (Create / Announce / Like / Delete / Update / Add / Remove, non-follow Undo) is handed to.
|
|
113
113
|
*/
|
|
114
114
|
export {
|
|
115
115
|
createInboundDispatcher,
|