@oxy.so/federation 2.4.1 → 2.5.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/actorObject.js +51 -1
- package/dist/cjs/index.js +2 -1
- package/dist/esm/actorObject.js +50 -1
- package/dist/esm/index.js +1 -1
- package/dist/types/actorObject.d.ts +33 -16
- package/dist/types/index.d.ts +1 -1
- package/package.json +3 -3
- package/src/__tests__/actorObject.test.ts +25 -5
- package/src/actorObject.ts +72 -16
- package/src/index.ts +2 -0
package/dist/cjs/actorObject.js
CHANGED
|
@@ -22,6 +22,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
22
22
|
exports.LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND = exports.AP_ACTOR_TYPES = void 0;
|
|
23
23
|
exports.isApActorType = isApActorType;
|
|
24
24
|
exports.localActorTypeForAccountKind = localActorTypeForAccountKind;
|
|
25
|
+
exports.accountKindForActor = accountKindForActor;
|
|
25
26
|
exports.normalizeAlsoKnownAs = normalizeAlsoKnownAs;
|
|
26
27
|
exports.createLocalActorBuilder = createLocalActorBuilder;
|
|
27
28
|
const contracts_1 = require("@oxy.so/contracts");
|
|
@@ -46,7 +47,10 @@ function isApActorType(value) {
|
|
|
46
47
|
return typeof value === 'string' && exports.AP_ACTOR_TYPES.includes(value);
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
|
-
* Oxy account kind → the AS2 actor type the fediverse is told about it
|
|
50
|
+
* Oxy account kind → the AS2 actor type the fediverse is told about it — the
|
|
51
|
+
* ONE table both directions read: what Oxy announces for its own accounts here,
|
|
52
|
+
* and, through {@link accountKindForActor}, what it records a remote actor as.
|
|
53
|
+
* Changing how a kind federates is an edit to this table and nowhere else.
|
|
50
54
|
*
|
|
51
55
|
* `satisfies Record<AccountKind, LocalActorType>` is the load-bearing part: a
|
|
52
56
|
* kind added to `@oxy.so/contracts` fails THIS build rather than silently
|
|
@@ -78,6 +82,8 @@ function isApActorType(value) {
|
|
|
78
82
|
* accepted by all four implementations' whitelists and compared in none of
|
|
79
83
|
* them — neither `bot?` nor `group?` in Mastodon, `bot_account = false` in
|
|
80
84
|
* Lemmy, `isBot` false in Misskey, an ordinary account in PeerTube.
|
|
85
|
+
* - **`community` → `Group`.** A forum its members post into and that
|
|
86
|
+
* re-broadcasts them: exactly what Lemmy, Mbin and Friendica mean by `Group`.
|
|
81
87
|
*
|
|
82
88
|
* What this does NOT do: it does not stop a remote instance offering a reply box
|
|
83
89
|
* under a channel's post. NO actor type gates that in any of the four — Mastodon
|
|
@@ -92,6 +98,7 @@ exports.LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND = {
|
|
|
92
98
|
project: 'Organization',
|
|
93
99
|
bot: 'Service',
|
|
94
100
|
channel: 'Organization',
|
|
101
|
+
community: 'Group',
|
|
95
102
|
};
|
|
96
103
|
/**
|
|
97
104
|
* The AS2 actor type for an Oxy account kind, defaulting to `Person`.
|
|
@@ -108,6 +115,49 @@ exports.LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND = {
|
|
|
108
115
|
function localActorTypeForAccountKind(kind) {
|
|
109
116
|
return (0, contracts_1.isAccountKind)(kind) ? exports.LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND[kind] : 'Person';
|
|
110
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* The inverse of {@link LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND}, derived from it. Three
|
|
120
|
+
* kinds share `Organization`, so a type reads back as the FIRST kind the table
|
|
121
|
+
* lists for it (`Organization` → `organization`); nothing on the wire tells a
|
|
122
|
+
* remote project from a remote organization.
|
|
123
|
+
*/
|
|
124
|
+
const ACCOUNT_KIND_BY_LOCAL_ACTOR_TYPE = (() => {
|
|
125
|
+
const inverse = new Map();
|
|
126
|
+
for (const [kind, type] of Object.entries(exports.LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND)) {
|
|
127
|
+
if (!inverse.has(type))
|
|
128
|
+
inverse.set(type, kind);
|
|
129
|
+
}
|
|
130
|
+
return inverse;
|
|
131
|
+
})();
|
|
132
|
+
/**
|
|
133
|
+
* AS2 actor types Oxy never announces but receives, and the announced type each
|
|
134
|
+
* is read as. `Application` is a non-human software actor — what `Service`
|
|
135
|
+
* already says, and half of Mastodon's own `bot?`.
|
|
136
|
+
*/
|
|
137
|
+
const INBOUND_ACTOR_TYPE_ALIASES = new Map([
|
|
138
|
+
['Application', 'Service'],
|
|
139
|
+
]);
|
|
140
|
+
/**
|
|
141
|
+
* The kind a `Group` that only its moderators may post into is recorded as.
|
|
142
|
+
* Such a group forwards nobody: it PUBLISHES (a PeerTube video channel, a
|
|
143
|
+
* WordPress multi-author blog), which is what an Oxy channel is. The one
|
|
144
|
+
* inbound signal the table above has no column for.
|
|
145
|
+
*/
|
|
146
|
+
const MODERATED_GROUP_KIND = 'channel';
|
|
147
|
+
/**
|
|
148
|
+
* The Oxy account kind a REMOTE actor is recorded as, read from the same table
|
|
149
|
+
* the local announcement uses ({@link LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND}). A type
|
|
150
|
+
* the table does not know (absent, misspelt, an extension type) is `personal`,
|
|
151
|
+
* the same default {@link localActorTypeForAccountKind} announces.
|
|
152
|
+
*/
|
|
153
|
+
function accountKindForActor(actor) {
|
|
154
|
+
if (typeof actor.type !== 'string')
|
|
155
|
+
return 'personal';
|
|
156
|
+
const type = INBOUND_ACTOR_TYPE_ALIASES.get(actor.type) ?? actor.type;
|
|
157
|
+
if (type === exports.LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND.community && actor.postingRestrictedToMods === true)
|
|
158
|
+
return MODERATED_GROUP_KIND;
|
|
159
|
+
return ACCOUNT_KIND_BY_LOCAL_ACTOR_TYPE.get(type) ?? 'personal';
|
|
160
|
+
}
|
|
111
161
|
/** Map common image extensions to a MIME type for an actor image `mediaType`. */
|
|
112
162
|
const IMAGE_MEDIA_TYPE_BY_EXT = {
|
|
113
163
|
png: 'image/png',
|
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.LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND = exports.AP_ACTOR_TYPES = exports.isApActorType = 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.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;
|
|
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
|
|
@@ -95,6 +95,7 @@ var actorObject_1 = require("./actorObject");
|
|
|
95
95
|
Object.defineProperty(exports, "createLocalActorBuilder", { enumerable: true, get: function () { return actorObject_1.createLocalActorBuilder; } });
|
|
96
96
|
Object.defineProperty(exports, "normalizeAlsoKnownAs", { enumerable: true, get: function () { return actorObject_1.normalizeAlsoKnownAs; } });
|
|
97
97
|
Object.defineProperty(exports, "localActorTypeForAccountKind", { enumerable: true, get: function () { return actorObject_1.localActorTypeForAccountKind; } });
|
|
98
|
+
Object.defineProperty(exports, "accountKindForActor", { enumerable: true, get: function () { return actorObject_1.accountKindForActor; } });
|
|
98
99
|
Object.defineProperty(exports, "isApActorType", { enumerable: true, get: function () { return actorObject_1.isApActorType; } });
|
|
99
100
|
Object.defineProperty(exports, "AP_ACTOR_TYPES", { enumerable: true, get: function () { return actorObject_1.AP_ACTOR_TYPES; } });
|
|
100
101
|
Object.defineProperty(exports, "LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND", { enumerable: true, get: function () { return actorObject_1.LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND; } });
|
package/dist/esm/actorObject.js
CHANGED
|
@@ -39,7 +39,10 @@ export function isApActorType(value) {
|
|
|
39
39
|
return typeof value === 'string' && AP_ACTOR_TYPES.includes(value);
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Oxy account kind → the AS2 actor type the fediverse is told about it
|
|
42
|
+
* Oxy account kind → the AS2 actor type the fediverse is told about it — the
|
|
43
|
+
* ONE table both directions read: what Oxy announces for its own accounts here,
|
|
44
|
+
* and, through {@link accountKindForActor}, what it records a remote actor as.
|
|
45
|
+
* Changing how a kind federates is an edit to this table and nowhere else.
|
|
43
46
|
*
|
|
44
47
|
* `satisfies Record<AccountKind, LocalActorType>` is the load-bearing part: a
|
|
45
48
|
* kind added to `@oxy.so/contracts` fails THIS build rather than silently
|
|
@@ -71,6 +74,8 @@ export function isApActorType(value) {
|
|
|
71
74
|
* accepted by all four implementations' whitelists and compared in none of
|
|
72
75
|
* them — neither `bot?` nor `group?` in Mastodon, `bot_account = false` in
|
|
73
76
|
* Lemmy, `isBot` false in Misskey, an ordinary account in PeerTube.
|
|
77
|
+
* - **`community` → `Group`.** A forum its members post into and that
|
|
78
|
+
* re-broadcasts them: exactly what Lemmy, Mbin and Friendica mean by `Group`.
|
|
74
79
|
*
|
|
75
80
|
* What this does NOT do: it does not stop a remote instance offering a reply box
|
|
76
81
|
* under a channel's post. NO actor type gates that in any of the four — Mastodon
|
|
@@ -85,6 +90,7 @@ export const LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND = {
|
|
|
85
90
|
project: 'Organization',
|
|
86
91
|
bot: 'Service',
|
|
87
92
|
channel: 'Organization',
|
|
93
|
+
community: 'Group',
|
|
88
94
|
};
|
|
89
95
|
/**
|
|
90
96
|
* The AS2 actor type for an Oxy account kind, defaulting to `Person`.
|
|
@@ -101,6 +107,49 @@ export const LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND = {
|
|
|
101
107
|
export function localActorTypeForAccountKind(kind) {
|
|
102
108
|
return isAccountKind(kind) ? LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND[kind] : 'Person';
|
|
103
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* The inverse of {@link LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND}, derived from it. Three
|
|
112
|
+
* kinds share `Organization`, so a type reads back as the FIRST kind the table
|
|
113
|
+
* lists for it (`Organization` → `organization`); nothing on the wire tells a
|
|
114
|
+
* remote project from a remote organization.
|
|
115
|
+
*/
|
|
116
|
+
const ACCOUNT_KIND_BY_LOCAL_ACTOR_TYPE = (() => {
|
|
117
|
+
const inverse = new Map();
|
|
118
|
+
for (const [kind, type] of Object.entries(LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND)) {
|
|
119
|
+
if (!inverse.has(type))
|
|
120
|
+
inverse.set(type, kind);
|
|
121
|
+
}
|
|
122
|
+
return inverse;
|
|
123
|
+
})();
|
|
124
|
+
/**
|
|
125
|
+
* AS2 actor types Oxy never announces but receives, and the announced type each
|
|
126
|
+
* is read as. `Application` is a non-human software actor — what `Service`
|
|
127
|
+
* already says, and half of Mastodon's own `bot?`.
|
|
128
|
+
*/
|
|
129
|
+
const INBOUND_ACTOR_TYPE_ALIASES = new Map([
|
|
130
|
+
['Application', 'Service'],
|
|
131
|
+
]);
|
|
132
|
+
/**
|
|
133
|
+
* The kind a `Group` that only its moderators may post into is recorded as.
|
|
134
|
+
* Such a group forwards nobody: it PUBLISHES (a PeerTube video channel, a
|
|
135
|
+
* WordPress multi-author blog), which is what an Oxy channel is. The one
|
|
136
|
+
* inbound signal the table above has no column for.
|
|
137
|
+
*/
|
|
138
|
+
const MODERATED_GROUP_KIND = 'channel';
|
|
139
|
+
/**
|
|
140
|
+
* The Oxy account kind a REMOTE actor is recorded as, read from the same table
|
|
141
|
+
* the local announcement uses ({@link LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND}). A type
|
|
142
|
+
* the table does not know (absent, misspelt, an extension type) is `personal`,
|
|
143
|
+
* the same default {@link localActorTypeForAccountKind} announces.
|
|
144
|
+
*/
|
|
145
|
+
export function accountKindForActor(actor) {
|
|
146
|
+
if (typeof actor.type !== 'string')
|
|
147
|
+
return 'personal';
|
|
148
|
+
const type = INBOUND_ACTOR_TYPE_ALIASES.get(actor.type) ?? actor.type;
|
|
149
|
+
if (type === LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND.community && actor.postingRestrictedToMods === true)
|
|
150
|
+
return MODERATED_GROUP_KIND;
|
|
151
|
+
return ACCOUNT_KIND_BY_LOCAL_ACTOR_TYPE.get(type) ?? 'personal';
|
|
152
|
+
}
|
|
104
153
|
/** Map common image extensions to a MIME type for an actor image `mediaType`. */
|
|
105
154
|
const IMAGE_MEDIA_TYPE_BY_EXT = {
|
|
106
155
|
png: 'image/png',
|
package/dist/esm/index.js
CHANGED
|
@@ -62,4 +62,4 @@ export { canonicalFederationHost, isSameFederationHost, extractActorUriFromActiv
|
|
|
62
62
|
* byte-identical across apps, with media resolution injected. The actor `type`
|
|
63
63
|
* follows the Oxy account kind ({@link LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND}).
|
|
64
64
|
*/
|
|
65
|
-
export { createLocalActorBuilder, normalizeAlsoKnownAs, localActorTypeForAccountKind, isApActorType, AP_ACTOR_TYPES, LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND, } from './actorObject.js';
|
|
65
|
+
export { createLocalActorBuilder, normalizeAlsoKnownAs, localActorTypeForAccountKind, accountKindForActor, isApActorType, AP_ACTOR_TYPES, LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND, } from './actorObject.js';
|
|
@@ -36,28 +36,29 @@ export declare function isApActorType(value: unknown): value is ApActorType;
|
|
|
36
36
|
/**
|
|
37
37
|
* The AS2 actor types this engine will announce for a LOCAL user actor.
|
|
38
38
|
*
|
|
39
|
-
* Narrower than AS2's five actor types, and narrow ON PURPOSE
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* `fe0da961`), not against the spec:
|
|
39
|
+
* Narrower than AS2's five actor types, and narrow ON PURPOSE. Both decisions
|
|
40
|
+
* were checked against real receiving implementations (mastodon `a3649295`,
|
|
41
|
+
* lemmy `4ce92433`, misskey `b95e4841`, peertube `fe0da961`), not against the
|
|
42
|
+
* spec:
|
|
44
43
|
*
|
|
45
44
|
* - **`Application`** is the INSTANCE actor's type (see the actor router's
|
|
46
45
|
* `/ap/users/instance` branch), reserved by convention for the software
|
|
47
46
|
* itself. No account is the software.
|
|
48
|
-
* - **`Group`** is
|
|
49
|
-
* "a collective of actors" but as a FORWARDING
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* `Group` would advertise a protocol this engine does not implement.
|
|
47
|
+
* - **`Group`** is announced ONLY for a `community`, because in the deployed
|
|
48
|
+
* fediverse it is not read as "a collective of actors" but as a FORWARDING
|
|
49
|
+
* actor. Lemmy reclassifies a remote `Group` as a community: followable, the
|
|
50
|
+
* Follow is Accepted, and then it never shows a post unless the actor
|
|
51
|
+
* `Announce`s its members' posts. PeerTube rejects a `Group` outright unless
|
|
52
|
+
* it carries `attributedTo` naming a `Person`. An account that authors its
|
|
53
|
+
* own posts (a project, a channel) must therefore never claim it; a community
|
|
54
|
+
* is the one kind whose nature IS forwarding.
|
|
57
55
|
*/
|
|
58
|
-
export type LocalActorType = Extract<ApActorType, 'Person' | 'Organization' | 'Service'>;
|
|
56
|
+
export type LocalActorType = Extract<ApActorType, 'Person' | 'Organization' | 'Service' | 'Group'>;
|
|
59
57
|
/**
|
|
60
|
-
* Oxy account kind → the AS2 actor type the fediverse is told about it
|
|
58
|
+
* Oxy account kind → the AS2 actor type the fediverse is told about it — the
|
|
59
|
+
* ONE table both directions read: what Oxy announces for its own accounts here,
|
|
60
|
+
* and, through {@link accountKindForActor}, what it records a remote actor as.
|
|
61
|
+
* Changing how a kind federates is an edit to this table and nowhere else.
|
|
61
62
|
*
|
|
62
63
|
* `satisfies Record<AccountKind, LocalActorType>` is the load-bearing part: a
|
|
63
64
|
* kind added to `@oxy.so/contracts` fails THIS build rather than silently
|
|
@@ -89,6 +90,8 @@ export type LocalActorType = Extract<ApActorType, 'Person' | 'Organization' | 'S
|
|
|
89
90
|
* accepted by all four implementations' whitelists and compared in none of
|
|
90
91
|
* them — neither `bot?` nor `group?` in Mastodon, `bot_account = false` in
|
|
91
92
|
* Lemmy, `isBot` false in Misskey, an ordinary account in PeerTube.
|
|
93
|
+
* - **`community` → `Group`.** A forum its members post into and that
|
|
94
|
+
* re-broadcasts them: exactly what Lemmy, Mbin and Friendica mean by `Group`.
|
|
92
95
|
*
|
|
93
96
|
* What this does NOT do: it does not stop a remote instance offering a reply box
|
|
94
97
|
* under a channel's post. NO actor type gates that in any of the four — Mastodon
|
|
@@ -103,6 +106,7 @@ export declare const LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND: {
|
|
|
103
106
|
readonly project: "Organization";
|
|
104
107
|
readonly bot: "Service";
|
|
105
108
|
readonly channel: "Organization";
|
|
109
|
+
readonly community: "Group";
|
|
106
110
|
};
|
|
107
111
|
/**
|
|
108
112
|
* The AS2 actor type for an Oxy account kind, defaulting to `Person`.
|
|
@@ -117,6 +121,19 @@ export declare const LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND: {
|
|
|
117
121
|
* existed.
|
|
118
122
|
*/
|
|
119
123
|
export declare function localActorTypeForAccountKind(kind: unknown): LocalActorType;
|
|
124
|
+
/** The fields of a remote actor document that decide its kind. */
|
|
125
|
+
export interface ActorKindSignals {
|
|
126
|
+
type?: unknown;
|
|
127
|
+
/** Lemmy's and PeerTube's flag: only moderators may post into this `Group`. */
|
|
128
|
+
postingRestrictedToMods?: unknown;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The Oxy account kind a REMOTE actor is recorded as, read from the same table
|
|
132
|
+
* the local announcement uses ({@link LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND}). A type
|
|
133
|
+
* the table does not know (absent, misspelt, an extension type) is `personal`,
|
|
134
|
+
* the same default {@link localActorTypeForAccountKind} announces.
|
|
135
|
+
*/
|
|
136
|
+
export declare function accountKindForActor(actor: ActorKindSignals): AccountKind;
|
|
120
137
|
/**
|
|
121
138
|
* App-supplied media resolution for the actor `icon`/`image`. Each function
|
|
122
139
|
* resolves a stored reference (Oxy file id or URL) to a FINAL, ready-to-serve
|
package/dist/types/index.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export { canonicalFederationHost, isSameFederationHost, extractActorUriFromActiv
|
|
|
62
62
|
* byte-identical across apps, with media resolution injected. The actor `type`
|
|
63
63
|
* follows the Oxy account kind ({@link LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND}).
|
|
64
64
|
*/
|
|
65
|
-
export { createLocalActorBuilder, normalizeAlsoKnownAs, localActorTypeForAccountKind, isApActorType, AP_ACTOR_TYPES, LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND, type ApActorType, type LocalActorType, type LocalActorBuilder, type LocalActorBuilderConfig, type BuildLocalActorParams, type ActorMediaResolver, } from './actorObject';
|
|
65
|
+
export { createLocalActorBuilder, normalizeAlsoKnownAs, localActorTypeForAccountKind, accountKindForActor, type ActorKindSignals, isApActorType, AP_ACTOR_TYPES, LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND, type ApActorType, type LocalActorType, type LocalActorBuilder, type LocalActorBuilderConfig, type BuildLocalActorParams, type ActorMediaResolver, } from './actorObject';
|
|
66
66
|
/**
|
|
67
67
|
* Supported external networks, keyed by the transport a connector speaks.
|
|
68
68
|
* `instagram-graph` is Meta's Graph API Business Discovery: a read-only
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/federation",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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",
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
"prepublishOnly": "node ../../scripts/assert-bun-publish.mjs && bun run clean && bun run build"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@oxy.so/contracts": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || 4.11.0-oxy-one.0",
|
|
83
|
-
"@oxy.so/core": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0"
|
|
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": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
86
|
"express": "^4.18.0 || ^5.0.0"
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isApActorType,
|
|
8
8
|
LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND,
|
|
9
9
|
localActorTypeForAccountKind,
|
|
10
|
+
accountKindForActor,
|
|
10
11
|
type ActorMediaResolver,
|
|
11
12
|
} from '../index';
|
|
12
13
|
|
|
@@ -198,6 +199,7 @@ describe('actor type by Oxy account kind', () => {
|
|
|
198
199
|
expect(localActorTypeForAccountKind('project')).toBe('Organization');
|
|
199
200
|
expect(localActorTypeForAccountKind('bot')).toBe('Service');
|
|
200
201
|
expect(localActorTypeForAccountKind('channel')).toBe('Organization');
|
|
202
|
+
expect(localActorTypeForAccountKind('community')).toBe('Group');
|
|
201
203
|
});
|
|
202
204
|
|
|
203
205
|
it('never announces a human-curated channel as automated', () => {
|
|
@@ -207,10 +209,11 @@ describe('actor type by Oxy account kind', () => {
|
|
|
207
209
|
// A channel is curated by people; only `bot` may claim automation.
|
|
208
210
|
expect(localActorTypeForAccountKind('channel')).not.toBe('Service');
|
|
209
211
|
expect(localActorTypeForAccountKind('bot')).toBe('Service');
|
|
210
|
-
// `Group` is
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
// `Group` is announced for a community alone: Lemmy reads it as a forwarding
|
|
213
|
+
// community, and PeerTube rejects a `Group` lacking `attributedTo` outright.
|
|
214
|
+
expect(ACCOUNT_KINDS.filter((kind) => localActorTypeForAccountKind(kind) === 'Group')).toEqual([
|
|
215
|
+
'community',
|
|
216
|
+
]);
|
|
214
217
|
});
|
|
215
218
|
|
|
216
219
|
it('covers every account kind contracts defines, with more than one answer', () => {
|
|
@@ -219,13 +222,30 @@ describe('actor type by Oxy account kind', () => {
|
|
|
219
222
|
// Vacuity floor: a mapping that returned one constant would pass every
|
|
220
223
|
// single-kind assertion above and fail this.
|
|
221
224
|
const answers = ACCOUNT_KINDS.map((kind) => localActorTypeForAccountKind(kind));
|
|
222
|
-
expect(new Set(answers).size).toBe(
|
|
225
|
+
expect(new Set(answers).size).toBe(4);
|
|
223
226
|
// And specifically: the two kinds either side of the channel decision differ.
|
|
224
227
|
expect(localActorTypeForAccountKind('channel')).not.toBe(
|
|
225
228
|
localActorTypeForAccountKind('personal'),
|
|
226
229
|
);
|
|
227
230
|
});
|
|
228
231
|
|
|
232
|
+
it('reads every announced type back as a kind announced the same way', () => {
|
|
233
|
+
for (const kind of ACCOUNT_KINDS) {
|
|
234
|
+
const type = localActorTypeForAccountKind(kind);
|
|
235
|
+
expect(localActorTypeForAccountKind(accountKindForActor({ type }))).toBe(type);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('reads the actor types and flags Oxy never announces', () => {
|
|
240
|
+
expect(accountKindForActor({ type: 'Application' })).toBe('bot');
|
|
241
|
+
expect(accountKindForActor({ type: 'Group', postingRestrictedToMods: true })).toBe('channel');
|
|
242
|
+
expect(accountKindForActor({ type: 'Group', postingRestrictedToMods: false })).toBe(
|
|
243
|
+
'community',
|
|
244
|
+
);
|
|
245
|
+
expect(accountKindForActor({ type: 'Tombstone' })).toBe('personal');
|
|
246
|
+
expect(accountKindForActor({})).toBe('personal');
|
|
247
|
+
});
|
|
248
|
+
|
|
229
249
|
it('falls back to Person for an absent, null, or unrecognized kind', () => {
|
|
230
250
|
expect(localActorTypeForAccountKind(undefined)).toBe('Person');
|
|
231
251
|
expect(localActorTypeForAccountKind(null)).toBe('Person');
|
package/src/actorObject.ts
CHANGED
|
@@ -49,29 +49,30 @@ export function isApActorType(value: unknown): value is ApActorType {
|
|
|
49
49
|
/**
|
|
50
50
|
* The AS2 actor types this engine will announce for a LOCAL user actor.
|
|
51
51
|
*
|
|
52
|
-
* Narrower than AS2's five actor types, and narrow ON PURPOSE
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* `fe0da961`), not against the spec:
|
|
52
|
+
* Narrower than AS2's five actor types, and narrow ON PURPOSE. Both decisions
|
|
53
|
+
* were checked against real receiving implementations (mastodon `a3649295`,
|
|
54
|
+
* lemmy `4ce92433`, misskey `b95e4841`, peertube `fe0da961`), not against the
|
|
55
|
+
* spec:
|
|
57
56
|
*
|
|
58
57
|
* - **`Application`** is the INSTANCE actor's type (see the actor router's
|
|
59
58
|
* `/ap/users/instance` branch), reserved by convention for the software
|
|
60
59
|
* itself. No account is the software.
|
|
61
|
-
* - **`Group`** is
|
|
62
|
-
* "a collective of actors" but as a FORWARDING
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* `Group` would advertise a protocol this engine does not implement.
|
|
60
|
+
* - **`Group`** is announced ONLY for a `community`, because in the deployed
|
|
61
|
+
* fediverse it is not read as "a collective of actors" but as a FORWARDING
|
|
62
|
+
* actor. Lemmy reclassifies a remote `Group` as a community: followable, the
|
|
63
|
+
* Follow is Accepted, and then it never shows a post unless the actor
|
|
64
|
+
* `Announce`s its members' posts. PeerTube rejects a `Group` outright unless
|
|
65
|
+
* it carries `attributedTo` naming a `Person`. An account that authors its
|
|
66
|
+
* own posts (a project, a channel) must therefore never claim it; a community
|
|
67
|
+
* is the one kind whose nature IS forwarding.
|
|
70
68
|
*/
|
|
71
|
-
export type LocalActorType = Extract<ApActorType, 'Person' | 'Organization' | 'Service'>;
|
|
69
|
+
export type LocalActorType = Extract<ApActorType, 'Person' | 'Organization' | 'Service' | 'Group'>;
|
|
72
70
|
|
|
73
71
|
/**
|
|
74
|
-
* Oxy account kind → the AS2 actor type the fediverse is told about it
|
|
72
|
+
* Oxy account kind → the AS2 actor type the fediverse is told about it — the
|
|
73
|
+
* ONE table both directions read: what Oxy announces for its own accounts here,
|
|
74
|
+
* and, through {@link accountKindForActor}, what it records a remote actor as.
|
|
75
|
+
* Changing how a kind federates is an edit to this table and nowhere else.
|
|
75
76
|
*
|
|
76
77
|
* `satisfies Record<AccountKind, LocalActorType>` is the load-bearing part: a
|
|
77
78
|
* kind added to `@oxy.so/contracts` fails THIS build rather than silently
|
|
@@ -103,6 +104,8 @@ export type LocalActorType = Extract<ApActorType, 'Person' | 'Organization' | 'S
|
|
|
103
104
|
* accepted by all four implementations' whitelists and compared in none of
|
|
104
105
|
* them — neither `bot?` nor `group?` in Mastodon, `bot_account = false` in
|
|
105
106
|
* Lemmy, `isBot` false in Misskey, an ordinary account in PeerTube.
|
|
107
|
+
* - **`community` → `Group`.** A forum its members post into and that
|
|
108
|
+
* re-broadcasts them: exactly what Lemmy, Mbin and Friendica mean by `Group`.
|
|
106
109
|
*
|
|
107
110
|
* What this does NOT do: it does not stop a remote instance offering a reply box
|
|
108
111
|
* under a channel's post. NO actor type gates that in any of the four — Mastodon
|
|
@@ -117,6 +120,7 @@ export const LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND = {
|
|
|
117
120
|
project: 'Organization',
|
|
118
121
|
bot: 'Service',
|
|
119
122
|
channel: 'Organization',
|
|
123
|
+
community: 'Group',
|
|
120
124
|
} as const satisfies Record<AccountKind, LocalActorType>;
|
|
121
125
|
|
|
122
126
|
/**
|
|
@@ -135,6 +139,58 @@ export function localActorTypeForAccountKind(kind: unknown): LocalActorType {
|
|
|
135
139
|
return isAccountKind(kind) ? LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND[kind] : 'Person';
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The inverse of {@link LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND}, derived from it. Three
|
|
144
|
+
* kinds share `Organization`, so a type reads back as the FIRST kind the table
|
|
145
|
+
* lists for it (`Organization` → `organization`); nothing on the wire tells a
|
|
146
|
+
* remote project from a remote organization.
|
|
147
|
+
*/
|
|
148
|
+
const ACCOUNT_KIND_BY_LOCAL_ACTOR_TYPE: ReadonlyMap<string, AccountKind> = (() => {
|
|
149
|
+
const inverse = new Map<string, AccountKind>();
|
|
150
|
+
for (const [kind, type] of Object.entries(LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND)) {
|
|
151
|
+
if (!inverse.has(type)) inverse.set(type, kind as AccountKind);
|
|
152
|
+
}
|
|
153
|
+
return inverse;
|
|
154
|
+
})();
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* AS2 actor types Oxy never announces but receives, and the announced type each
|
|
158
|
+
* is read as. `Application` is a non-human software actor — what `Service`
|
|
159
|
+
* already says, and half of Mastodon's own `bot?`.
|
|
160
|
+
*/
|
|
161
|
+
const INBOUND_ACTOR_TYPE_ALIASES: ReadonlyMap<string, LocalActorType> = new Map([
|
|
162
|
+
['Application', 'Service'],
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The kind a `Group` that only its moderators may post into is recorded as.
|
|
167
|
+
* Such a group forwards nobody: it PUBLISHES (a PeerTube video channel, a
|
|
168
|
+
* WordPress multi-author blog), which is what an Oxy channel is. The one
|
|
169
|
+
* inbound signal the table above has no column for.
|
|
170
|
+
*/
|
|
171
|
+
const MODERATED_GROUP_KIND: AccountKind = 'channel';
|
|
172
|
+
|
|
173
|
+
/** The fields of a remote actor document that decide its kind. */
|
|
174
|
+
export interface ActorKindSignals {
|
|
175
|
+
type?: unknown;
|
|
176
|
+
/** Lemmy's and PeerTube's flag: only moderators may post into this `Group`. */
|
|
177
|
+
postingRestrictedToMods?: unknown;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The Oxy account kind a REMOTE actor is recorded as, read from the same table
|
|
182
|
+
* the local announcement uses ({@link LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND}). A type
|
|
183
|
+
* the table does not know (absent, misspelt, an extension type) is `personal`,
|
|
184
|
+
* the same default {@link localActorTypeForAccountKind} announces.
|
|
185
|
+
*/
|
|
186
|
+
export function accountKindForActor(actor: ActorKindSignals): AccountKind {
|
|
187
|
+
if (typeof actor.type !== 'string') return 'personal';
|
|
188
|
+
const type = INBOUND_ACTOR_TYPE_ALIASES.get(actor.type) ?? actor.type;
|
|
189
|
+
if (type === LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND.community && actor.postingRestrictedToMods === true)
|
|
190
|
+
return MODERATED_GROUP_KIND;
|
|
191
|
+
return ACCOUNT_KIND_BY_LOCAL_ACTOR_TYPE.get(type) ?? 'personal';
|
|
192
|
+
}
|
|
193
|
+
|
|
138
194
|
/** Map common image extensions to a MIME type for an actor image `mediaType`. */
|
|
139
195
|
const IMAGE_MEDIA_TYPE_BY_EXT: Record<string, string> = {
|
|
140
196
|
png: 'image/png',
|
package/src/index.ts
CHANGED