@oxy.so/federation 2.11.0 → 2.13.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/esm/index.js +1 -1
- package/dist/esm/networkIdentity.js +38 -0
- package/dist/esm/node/actorResolver.js +5 -0
- 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/package.json +11 -12
- package/src/__tests__/actorCollectionCounts.test.ts +16 -0
- 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/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:
|
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:
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/federation",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.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",
|
|
@@ -75,12 +75,11 @@
|
|
|
75
75
|
"clean": "rm -rf dist",
|
|
76
76
|
"typescript": "tsc --noEmit",
|
|
77
77
|
"test": "jest --passWithNoTests",
|
|
78
|
-
"lint": "biome lint --error-on-warnings ./src"
|
|
79
|
-
"prepublishOnly": "node ../../scripts/assert-bun-publish.mjs && bun run clean && bun run build"
|
|
78
|
+
"lint": "biome lint --error-on-warnings ./src"
|
|
80
79
|
},
|
|
81
80
|
"dependencies": {
|
|
82
|
-
"@oxy.so/contracts": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || 4.11.0-oxy-one.0",
|
|
83
|
-
"@oxy.so/core": "^6.14.0"
|
|
81
|
+
"@oxy.so/contracts": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || 4.11.0-oxy-one.0",
|
|
82
|
+
"@oxy.so/core": "^6.14.0 || ^7.0.0"
|
|
84
83
|
},
|
|
85
84
|
"peerDependencies": {
|
|
86
85
|
"express": "^4.18.0 || ^5.0.0"
|
|
@@ -91,13 +90,13 @@
|
|
|
91
90
|
}
|
|
92
91
|
},
|
|
93
92
|
"devDependencies": {
|
|
94
|
-
"@types/express": "^
|
|
93
|
+
"@types/express": "^5.0.6",
|
|
95
94
|
"@types/jest": "^30.0.0",
|
|
96
|
-
"@types/node": "^
|
|
97
|
-
"express": "^
|
|
98
|
-
"jest": "^30.5.
|
|
99
|
-
"supertest": "^7.
|
|
100
|
-
"ts-jest": "^29.4.
|
|
101
|
-
"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"
|
|
102
101
|
}
|
|
103
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({
|
|
@@ -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,
|