@oxy.so/federation 2.0.0 → 2.1.1

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.
@@ -171,6 +171,31 @@ function buildActorImage(config, banner) {
171
171
  }
172
172
  return apImageObject(resolved);
173
173
  }
174
+ /**
175
+ * The publishable subset of an `alsoKnownAs` input: absolute `https:` URIs,
176
+ * first occurrence wins, input order kept. Exported so every actor builder
177
+ * (Oxy's own included) applies the same rule.
178
+ */
179
+ export function normalizeAlsoKnownAs(values) {
180
+ if (!values)
181
+ return [];
182
+ const seen = new Set();
183
+ const result = [];
184
+ for (const value of values) {
185
+ if (typeof value !== 'string' || seen.has(value))
186
+ continue;
187
+ try {
188
+ if (new URL(value).protocol !== 'https:')
189
+ continue;
190
+ }
191
+ catch {
192
+ continue;
193
+ }
194
+ seen.add(value);
195
+ result.push(value);
196
+ }
197
+ return result;
198
+ }
174
199
  /**
175
200
  * Build the per-instance local-actor builder. Bind it once with an app's domain +
176
201
  * media resolver; call the returned function per user.
@@ -201,6 +226,10 @@ export function createLocalActorBuilder(config) {
201
226
  publicKeyPem: publicKey.publicKeyPem,
202
227
  },
203
228
  };
229
+ const aliases = normalizeAlsoKnownAs(params.alsoKnownAs);
230
+ if (aliases.length > 0) {
231
+ actorObject.alsoKnownAs = aliases;
232
+ }
204
233
  // `published` (account creation date) is advertised when the API provides it.
205
234
  if (createdAt) {
206
235
  actorObject.published = new Date(createdAt).toISOString();
@@ -30,7 +30,12 @@ export const AP_CONTEXT = [
30
30
  // is typed `@id` (an IRI, not a literal); the `misskey`/`fedibird` namespaces
31
31
  // and the AS2 `Link` type back the FEP-e232 `Link` quote tag. Without these
32
32
  // declarations a strict JSON-LD consumer DROPS the quote fields.
33
+ //
34
+ // `alsoKnownAs` is the account-alias term a Mastodon `Move` verifies. It is
35
+ // `as:alsoKnownAs` typed `@id`, exactly as Mastodon declares it; without the
36
+ // declaration a strict consumer drops the aliases and the move is refused.
33
37
  {
38
+ alsoKnownAs: { '@id': 'as:alsoKnownAs', '@type': '@id' },
34
39
  sensitive: 'as:sensitive',
35
40
  toot: 'http://joinmastodon.org/ns#',
36
41
  votersCount: 'toot:votersCount',
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, localActorTypeForAccountKind, isApActorType, AP_ACTOR_TYPES, LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND, } from './actorObject.js';
65
+ export { createLocalActorBuilder, normalizeAlsoKnownAs, localActorTypeForAccountKind, isApActorType, AP_ACTOR_TYPES, LOCAL_ACTOR_TYPE_BY_ACCOUNT_KIND, } from './actorObject.js';
@@ -263,6 +263,7 @@ export function createActorRouter(config) {
263
263
  profileHeaderImage,
264
264
  publicKey,
265
265
  createdAt: user.createdAt,
266
+ alsoKnownAs: user.alsoKnownAs,
266
267
  });
267
268
  res.set('Content-Type', apContentType);
268
269
  res.set('Cache-Control', 'max-age=1800');
@@ -380,6 +380,7 @@ export function createDeliveryService(config) {
380
380
  profileHeaderImage,
381
381
  publicKey,
382
382
  createdAt: user.createdAt,
383
+ alsoKnownAs: user.alsoKnownAs,
383
384
  });
384
385
  const actor = urls.actor(username);
385
386
  const now = new Date();
@@ -12,6 +12,12 @@
12
12
  * post/engagement handlers live. The consent gate + notification side effects are
13
13
  * injected so the engine holds no app knowledge.
14
14
  *
15
+ * `Move` (account migration) is handed to {@link InboundDispatcherConfig.onMove}
16
+ * after a SHAPE check only: the engine proves the activity is the signing actor
17
+ * moving itself, and the app forwards it to Oxy (`POST /federation/move`), which
18
+ * owns the identity decision — the alias check and the re-fetch of the old
19
+ * actor's `movedTo`.
20
+ *
15
21
  * Extracted behaviour-identically from Mention's former `InboxProcessingService`
16
22
  * dispatcher + `handleIncomingFollow` / `handleUndo(Follow)` / `handleAccept` /
17
23
  * `handleReject`.
@@ -38,6 +44,38 @@ export class ActorResolutionPendingError extends Error {
38
44
  this.actorUri = actorUri;
39
45
  }
40
46
  }
47
+ /**
48
+ * The shape check for an inbound `Move`.
49
+ *
50
+ * A Move is only meaningful as an actor moving ITSELF: `actor` and `object` must
51
+ * both be the actor whose HTTP signature was verified, so a relay or a third
52
+ * party cannot move somebody else's followers. `target` must be an absolute
53
+ * https URI, and differ from the old actor.
54
+ */
55
+ function parseInboundMove(activity, verifiedActorUri) {
56
+ const activityId = typeof activity.id === 'string' ? activity.id : undefined;
57
+ if (!activityId)
58
+ return { ok: false, reason: 'missing id' };
59
+ const actor = objectTargetUri(activity.actor);
60
+ if (actor !== verifiedActorUri)
61
+ return { ok: false, reason: 'actor is not the signer' };
62
+ const object = objectTargetUri(activity.object);
63
+ if (object !== verifiedActorUri)
64
+ return { ok: false, reason: 'object is not the moving actor' };
65
+ const target = objectTargetUri(activity.target);
66
+ if (!target)
67
+ return { ok: false, reason: 'missing target' };
68
+ try {
69
+ if (new URL(target).protocol !== 'https:')
70
+ return { ok: false, reason: 'target is not https' };
71
+ }
72
+ catch {
73
+ return { ok: false, reason: 'target is not a URL' };
74
+ }
75
+ if (target === verifiedActorUri)
76
+ return { ok: false, reason: 'target is the moving actor' };
77
+ return { ok: true, move: { activityId, oldActorUri: verifiedActorUri, targetActorUri: target } };
78
+ }
41
79
  /**
42
80
  * The lowercased host of an actor URI, or null when it is not a parseable absolute
43
81
  * URL. Callers treat null as blocked: an origin whose host cannot be determined
@@ -255,6 +293,19 @@ export function createInboundDispatcher(config) {
255
293
  case 'Update':
256
294
  await config.onContentActivity(activity, verifiedActorUri);
257
295
  break;
296
+ case 'Move': {
297
+ const parsed = parseInboundMove(activity, verifiedActorUri);
298
+ if (!parsed.ok) {
299
+ logger.warn(`[Federation] dropping Move from ${verifiedActorUri}: ${parsed.reason}`);
300
+ break;
301
+ }
302
+ if (!config.onMove) {
303
+ logger.debug(`Unhandled Move from ${verifiedActorUri} (no onMove handler)`);
304
+ break;
305
+ }
306
+ await config.onMove(parsed.move);
307
+ break;
308
+ }
258
309
  default:
259
310
  logger.debug(`Unhandled activity type: ${validation.type}`);
260
311
  }