@oxy.so/federation 1.0.0 → 1.0.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.
@@ -337,6 +337,10 @@ export declare function upstreamHandleFromProfileField(options: {
337
337
  readonly hosts: readonly string[];
338
338
  /** Fixed path segments before the handle (`bsky.app/profile/<handle>` ⇒ `['profile']`). */
339
339
  readonly pathPrefix?: readonly string[];
340
+ /** Opt-in for the exact BirdsiteLive double-HTTPS serialization defect. */
341
+ readonly repairRepeatedHttpsScheme?: boolean;
342
+ /** Require the named assertion's link to explicitly carry rel=me. */
343
+ readonly requireRelMe?: boolean;
340
344
  }): BridgeDerivation;
341
345
  /**
342
346
  * Read the upstream handle out of `alsoKnownAs` profile URLs — the shape where an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/federation",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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",
@@ -61,6 +61,32 @@ function candidate(overrides: Partial<NetworkIdentityCandidate> = {}): NetworkId
61
61
  }
62
62
 
63
63
  describe('createBridgeRelabeller', () => {
64
+ it('refuses contradictory accepted profile assertions regardless of their order', () => {
65
+ const derive = upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['x.com'], requireRelMe: true });
66
+ const fields = ['alice', 'bob'].map(handle => ({ name: 'Official', value: `<a href="https://x.com/${handle}" rel="me">Official</a>` }));
67
+ expect(derive(candidate({ fields }))).toBeUndefined();
68
+ expect(derive(candidate({ fields: [...fields].reverse() }))).toBeUndefined();
69
+ expect(derive(candidate({ fields: [fields[0], fields[0]] }))).toBe('alice');
70
+ expect(derive(candidate({ fields: [{ name: 'Official', value: '<a href="https://x.com/alice">Official</a>' }] }))).toBeUndefined();
71
+ });
72
+ it('repairs the observed double-HTTPS Official assertion only with the reviewed opt-in', () => {
73
+ const actor = candidate({ fields: [{ name: 'Official', value: '<a href="https://https://twitter.com/jordievole" rel="me">Official</a>' }] });
74
+ expect(upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['twitter.com'] })(actor)).toBeUndefined();
75
+ expect(upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['twitter.com'], repairRepeatedHttpsScheme: true })(actor)).toBe('jordievole');
76
+ });
77
+
78
+ it.each([
79
+ 'https://https://attacker.example/jordievole',
80
+ 'https://https://twitter.com@attacker.example/jordievole',
81
+ 'https://https://attacker@twitter.com/jordievole',
82
+ 'https://https://twitter.com:444/jordievole',
83
+ 'https://https://https://twitter.com/jordievole',
84
+ 'http://https://twitter.com/jordievole',
85
+ 'https://https://twitter.com/i/status/123',
86
+ ])('does not widen the repair into an attribution for %s', href => {
87
+ const actor = candidate({ fields: [{ name: 'Official', value: `<a href="${href}" rel="me">Official</a>` }] });
88
+ expect(upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['twitter.com'], repairRepeatedHttpsScheme: true })(actor)).toBeUndefined();
89
+ });
64
90
  it('re-labels an actor onto the network its bridge mirrors', () => {
65
91
  const identity = createBridgeRelabeller([entry()]).deriveNetworkIdentity(candidate());
66
92
  expect(identity?.federatedUsername).toBe('wired@x.com');
@@ -164,6 +164,7 @@ export function parseUpstreamProfileUrl(
164
164
  return undefined;
165
165
  }
166
166
  if (url.protocol !== 'https:' && url.protocol !== 'http:') return undefined;
167
+ if (url.username || url.password || url.port) return undefined;
167
168
 
168
169
  const host = canonicalFederationHost(url.hostname);
169
170
  for (const network of networks) {
@@ -397,6 +398,7 @@ function profileUrlHandle(
397
398
  return undefined;
398
399
  }
399
400
  if (url.protocol !== 'https:' && url.protocol !== 'http:') return undefined;
401
+ if (url.username || url.password || url.port) return undefined;
400
402
  const host = canonicalFederationHost(url.hostname);
401
403
  if (!allowedHosts.some((allowed) => canonicalFederationHost(allowed) === host)) return undefined;
402
404
 
@@ -409,8 +411,17 @@ function profileUrlHandle(
409
411
  }
410
412
 
411
413
  /** Every `href="…"` in a sanitized field value, in document order. */
412
- function fieldHrefs(value: string): string[] {
414
+ function fieldHrefs(value: string, requireRelMe = false): string[] {
413
415
  const hrefs: string[] = [];
416
+ if (requireRelMe) {
417
+ for (const anchor of value.matchAll(/<a\b([^>]*)>/gi)) {
418
+ const rel = /\brel\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(anchor[1]);
419
+ if (!(rel?.[1] ?? rel?.[2] ?? '').toLowerCase().split(/\s+/).includes('me')) continue;
420
+ const href = /\bhref\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(anchor[1]);
421
+ if (href) hrefs.push(href[1] ?? href[2]);
422
+ }
423
+ return hrefs;
424
+ }
414
425
  const pattern = /href="([^"]*)"/gi;
415
426
  let match = pattern.exec(value);
416
427
  while (match !== null) {
@@ -431,18 +442,30 @@ export function upstreamHandleFromProfileField(options: {
431
442
  readonly hosts: readonly string[];
432
443
  /** Fixed path segments before the handle (`bsky.app/profile/<handle>` ⇒ `['profile']`). */
433
444
  readonly pathPrefix?: readonly string[];
445
+ /** Opt-in for the exact BirdsiteLive double-HTTPS serialization defect. */
446
+ readonly repairRepeatedHttpsScheme?: boolean;
447
+ /** Require the named assertion's link to explicitly carry rel=me. */
448
+ readonly requireRelMe?: boolean;
434
449
  }): BridgeDerivation {
435
450
  const wanted = options.fieldName.toLowerCase();
436
451
  const prefix = options.pathPrefix ?? [];
437
452
  return (candidate) => {
453
+ const handles = new Map<string, string>();
438
454
  for (const field of candidate.fields) {
439
455
  if (field.name.trim().toLowerCase() !== wanted) continue;
440
- for (const href of fieldHrefs(field.value)) {
441
- const handle = profileUrlHandle(href, options.hosts, prefix);
442
- if (handle !== undefined && handle.length > 0) return handle;
456
+ for (const href of fieldHrefs(field.value, options.requireRelMe)) {
457
+ // Observed bird.makeup Official assertion, 2026-09-13:
458
+ // https://https://twitter.com/jordievole. Repair exactly one duplicated
459
+ // HTTPS prefix, then run the ordinary host/path/credential validation.
460
+ const candidateHref = options.repairRepeatedHttpsScheme && href.startsWith('https://https://')
461
+ ? href.slice('https://'.length) : href;
462
+ let handle: string | undefined;
463
+ try { handle = profileUrlHandle(candidateHref, options.hosts, prefix); } catch { continue; }
464
+ if (handle !== undefined && handle.length > 0) handles.set(handle.toLowerCase(), handle);
443
465
  }
444
466
  }
445
- return undefined;
467
+ // Conflicting source assertions cannot be settled by document order.
468
+ return handles.size === 1 ? handles.values().next().value : undefined;
446
469
  };
447
470
  }
448
471