@takosjp/yurucommu-core 3.3.0 → 3.4.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.
Files changed (52) hide show
  1. package/migrations/0022_inbound_dispatch_claims.sql +17 -0
  2. package/package.json +3 -2
  3. package/packages/api/package.json +1 -1
  4. package/packages/api/src/index.ts +1 -0
  5. package/packages/api/src/lib/api/notifications.ts +1 -0
  6. package/packages/api/src/lib/api/posts.ts +1 -0
  7. package/packages/api/src/lib/rtc-client.ts +1 -3
  8. package/packages/api/src/types/call.ts +2 -10
  9. package/packages/api/src/types/index.ts +3 -0
  10. package/packages/api/src/types/realtime.ts +139 -0
  11. package/src/backend/index.ts +54 -12
  12. package/src/backend/lib/delivery/queue-batching.ts +53 -36
  13. package/src/backend/lib/delivery/queue-delivery.ts +3 -3
  14. package/src/backend/lib/delivery/queue.ts +17 -9
  15. package/src/backend/lib/delivery/types.ts +12 -0
  16. package/src/backend/lib/notification-push.ts +2 -2
  17. package/src/backend/lib/oauth-providers.ts +9 -0
  18. package/src/backend/lib/strip-image-metadata.ts +50 -30
  19. package/src/backend/lib/unread-counts.ts +63 -2
  20. package/src/backend/middleware/bearer-auth.ts +24 -9
  21. package/src/backend/public.ts +25 -1
  22. package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +4 -3
  23. package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +204 -5
  24. package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +78 -53
  25. package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +66 -2
  26. package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +35 -12
  27. package/src/backend/routes/activitypub/inbox-addressing.ts +236 -0
  28. package/src/backend/routes/activitypub/inbox-types.ts +8 -0
  29. package/src/backend/routes/activitypub/inbox.ts +410 -205
  30. package/src/backend/routes/activitypub/outbox.ts +0 -0
  31. package/src/backend/routes/auth.ts +2 -1
  32. package/src/backend/routes/communities/messages.ts +53 -17
  33. package/src/backend/routes/dm/messages.ts +47 -7
  34. package/src/backend/routes/dm/read-archive.ts +27 -0
  35. package/src/backend/routes/dm/typing.ts +16 -0
  36. package/src/backend/routes/notifications.ts +25 -0
  37. package/src/backend/routes/posts/post-helpers.ts +42 -23
  38. package/src/backend/routes/realtime/index.ts +67 -0
  39. package/src/backend/routes/rtc/index.ts +5 -1
  40. package/src/backend/runtime/call-hub-core.ts +13 -3
  41. package/src/backend/runtime/cloudflare.ts +63 -2
  42. package/src/backend/runtime/managed-relational.ts +197 -0
  43. package/src/backend/runtime/managed-runtime.ts +631 -0
  44. package/src/backend/runtime/queue.ts +40 -0
  45. package/src/backend/runtime/realtime-hub.ts +257 -0
  46. package/src/backend/runtime/realtime-stream-do.ts +323 -0
  47. package/src/backend/server.ts +15 -18
  48. package/src/backend/types.ts +13 -2
  49. package/src/db/d1-write.ts +270 -0
  50. package/src/db/index.ts +17 -0
  51. package/src/db/schema/federation.ts +19 -0
  52. package/src/db/schema/index.ts +1 -0
@@ -3,8 +3,12 @@
3
3
  * and the batch handler that dispatches to sub-modules.
4
4
  */
5
5
 
6
- import type { Message, MessageBatch, Queue } from "@cloudflare/workers-types";
7
6
  import type { Env } from "../../types.ts";
7
+ import type {
8
+ IQueueBatch,
9
+ IQueueMessage,
10
+ IQueueProducer,
11
+ } from "../../runtime/queue.ts";
8
12
  import type { Database } from "../../../db/index.ts";
9
13
  import { and, eq, notInArray, or, sql } from "drizzle-orm";
10
14
  import { actorCache, deliveryQueue } from "../../../db/index.ts";
@@ -119,8 +123,8 @@ export function nowIso(): string {
119
123
  }
120
124
 
121
125
  export type QueueEnv = Env & {
122
- DELIVERY_QUEUE: Queue<DeliveryQueueMessageV1>;
123
- DELIVERY_DLQ: Queue<DeliveryDlqMessageV1>;
126
+ DELIVERY_QUEUE: IQueueProducer<DeliveryQueueMessageV1>;
127
+ DELIVERY_DLQ: IQueueProducer<DeliveryDlqMessageV1>;
124
128
  };
125
129
 
126
130
  function queueAvailable(env: Env): env is QueueEnv {
@@ -130,7 +134,7 @@ function queueAvailable(env: Env): env is QueueEnv {
130
134
  export function requireQueue(
131
135
  env: Env,
132
136
  label: string,
133
- message: Message<DeliveryQueueMessageV1>,
137
+ message: IQueueMessage<DeliveryQueueMessageV1>,
134
138
  ): env is QueueEnv {
135
139
  if (queueAvailable(env)) return true;
136
140
  log.warn("Missing DELIVERY_QUEUE/DELIVERY_DLQ bindings; dropping job", {
@@ -426,7 +430,7 @@ export async function enqueueFanoutToCommunity(
426
430
  // ---------------------------------------------------------------------------
427
431
 
428
432
  export async function handleDeliveryQueueBatch(
429
- batch: MessageBatch<DeliveryQueueMessageV1>,
433
+ batch: IQueueBatch<DeliveryQueueMessageV1>,
430
434
  env: Env,
431
435
  ): Promise<void> {
432
436
  const db = env.DB_INSTANCE;
@@ -494,13 +498,13 @@ export async function handleDeliveryQueueBatch(
494
498
 
495
499
  // Deliver endpoint messages with bulkhead+concurrency.
496
500
  const deliveryMessages = batch.messages.filter(
497
- (m: Message<DeliveryQueueMessageV1>) =>
501
+ (m: IQueueMessage<DeliveryQueueMessageV1>) =>
498
502
  isDeliveryQueueMessageV1(m.body) && m.body.type === "deliver_endpoint",
499
- ) as Array<Message<DeliveryQueueMessageV1>>;
503
+ ) as Array<IQueueMessage<DeliveryQueueMessageV1>>;
500
504
  await runWithConcurrency(
501
505
  deliveryMessages,
502
506
  BULKHEAD_GLOBAL_CONCURRENCY,
503
- async (m: Message<DeliveryQueueMessageV1>) => {
507
+ async (m: IQueueMessage<DeliveryQueueMessageV1>) => {
504
508
  try {
505
509
  await processDeliverEndpoint(
506
510
  db,
@@ -531,10 +535,14 @@ export async function handleDeliveryQueueBatch(
531
535
  error,
532
536
  });
533
537
  }
538
+ // Same choke point feeds the realtime stream (federated + fanout inserts).
539
+ const { sweepRealtimeNotifications } =
540
+ await import("../../runtime/realtime-hub.ts");
541
+ await sweepRealtimeNotifications(env);
534
542
  }
535
543
 
536
544
  export async function handleDeliveryDlqBatch(
537
- batch: MessageBatch<DeliveryDlqMessageV1>,
545
+ batch: IQueueBatch<DeliveryDlqMessageV1>,
538
546
  env: Env,
539
547
  ): Promise<void> {
540
548
  for (const message of batch.messages) {
@@ -5,6 +5,8 @@ export type DeliveryFanoutFollowersMessageV1 = {
5
5
  type: "fanout_followers";
6
6
  activityId: string;
7
7
  followeeApId: string;
8
+ /** Stable keyset cursor used when a large graph spans queue invocations. */
9
+ cursor?: string;
8
10
  scheduledAt: string; // ISO8601 UTC
9
11
  };
10
12
 
@@ -24,6 +26,10 @@ export type DeliveryFanoutCommunityMessageV1 = {
24
26
  // receive `activityId`. Absent for non-Create activities (edit/delete relay
25
27
  // the activity directly).
26
28
  announceActivityId?: string;
29
+ /** Current bounded fan-out phase; absent means start with local members. */
30
+ stage?: "local_members" | "remote_members" | "remote_followers";
31
+ /** Stable actor-id keyset cursor within `stage`. */
32
+ cursor?: string;
27
33
  scheduledAt: string; // ISO8601 UTC
28
34
  };
29
35
 
@@ -106,12 +112,18 @@ export function isDeliveryQueueMessageV1(
106
112
  return (
107
113
  typeof v.activityId === "string" &&
108
114
  typeof v.followeeApId === "string" &&
115
+ (v.cursor === undefined || typeof v.cursor === "string") &&
109
116
  typeof v.scheduledAt === "string"
110
117
  );
111
118
  case "fanout_community":
112
119
  return (
113
120
  typeof v.activityId === "string" &&
114
121
  typeof v.communityApId === "string" &&
122
+ (v.stage === undefined ||
123
+ v.stage === "local_members" ||
124
+ v.stage === "remote_members" ||
125
+ v.stage === "remote_followers") &&
126
+ (v.cursor === undefined || typeof v.cursor === "string") &&
115
127
  typeof v.scheduledAt === "string"
116
128
  );
117
129
  case "resolve_actor":
@@ -1,4 +1,4 @@
1
- import type { Message } from "@cloudflare/workers-types";
1
+ import type { IQueueMessage } from "../runtime/queue.ts";
2
2
  import { and, asc, eq, exists, inArray, lte, ne, sql } from "drizzle-orm";
3
3
 
4
4
  import {
@@ -501,7 +501,7 @@ export async function purgeExpiredNotificationPushJobs(
501
501
  export async function processNotificationPushJob(
502
502
  env: Env,
503
503
  body: DeliveryNotificationPushMessageV1,
504
- message: Message<DeliveryQueueMessageV1>,
504
+ message: IQueueMessage<DeliveryQueueMessageV1>,
505
505
  ): Promise<void> {
506
506
  const db = env.DB_INSTANCE;
507
507
  let job = await db
@@ -61,6 +61,15 @@ export function getOidcClientCredentials(env: Env): {
61
61
  };
62
62
  }
63
63
 
64
+ /**
65
+ * Audience advertised to native clients and accepted by the host exchange.
66
+ * A Capsule has one public OIDC client with multiple exact redirect URIs; web
67
+ * and native clients must therefore converge on this same client id.
68
+ */
69
+ export function getMobileOidcAudience(env: Env): string {
70
+ return getOidcClientCredentials(env).clientId;
71
+ }
72
+
64
73
  export function issuerEndpoint(issuer: string, path: string): string {
65
74
  return `${issuer.replace(/\/$/, "")}${path}`;
66
75
  }
@@ -10,9 +10,10 @@
10
10
  * re-encoding the pixels, so the visible image is unchanged.
11
11
  *
12
12
  * Pure byte-surgery (no native image library): it walks the container structure
13
- * and drops only metadata segments/chunks, copying everything else verbatim. It
14
- * NEVER corrupts: on any structural surprise it returns the original bytes
15
- * unchanged (the magic-byte validation has already confirmed the declared type).
13
+ * and drops only metadata segments/chunks, copying everything else verbatim.
14
+ * Supported containers fail closed on structural surprises: returning their
15
+ * original bytes would silently publish the metadata the parser failed to
16
+ * understand.
16
17
  *
17
18
  * Covered: JPEG (APP1 EXIF/XMP, APP13 IPTC, COM), PNG (tEXt/zTXt/iTXt/eXIf/tIME),
18
19
  * WebP (EXIF / XMP chunks). GIF and video are passed through unchanged (GIF
@@ -24,36 +25,37 @@ export function stripImageMetadata(
24
25
  bytes: Uint8Array,
25
26
  mimeType: string,
26
27
  ): Uint8Array {
27
- try {
28
- switch (mimeType) {
29
- case "image/jpeg":
30
- return stripJpeg(bytes);
31
- case "image/png":
32
- return stripPng(bytes);
33
- case "image/webp":
34
- return stripWebp(bytes);
35
- default:
36
- return bytes; // gif / video / unknown: unchanged
37
- }
38
- } catch {
39
- // Never let a parsing surprise corrupt or drop the upload.
40
- return bytes;
28
+ switch (mimeType) {
29
+ case "image/jpeg":
30
+ return stripJpeg(bytes);
31
+ case "image/png":
32
+ return stripPng(bytes);
33
+ case "image/webp":
34
+ return stripWebp(bytes);
35
+ default:
36
+ return bytes; // gif / video / unknown: unchanged
41
37
  }
42
38
  }
43
39
 
40
+ function malformed(format: "JPEG" | "PNG" | "WebP"): never {
41
+ throw new Error(`Malformed ${format} image`);
42
+ }
43
+
44
44
  // ---------------------------------------------------------------------------
45
45
  // JPEG: a stream of marker segments. Drop APP1 (EXIF + XMP), APP13 (IPTC /
46
46
  // Photoshop) and COM (comment); keep APP0 (JFIF), APP2 (ICC), APP14 (Adobe
47
47
  // color transform), the quantization/Huffman tables, and the scan data verbatim.
48
48
  // ---------------------------------------------------------------------------
49
49
  function stripJpeg(bytes: Uint8Array): Uint8Array {
50
- if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return bytes;
50
+ if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) {
51
+ return malformed("JPEG");
52
+ }
51
53
 
52
54
  const out: number[] = [0xff, 0xd8]; // SOI
53
55
  let i = 2;
54
56
 
55
57
  while (i + 1 < bytes.length) {
56
- if (bytes[i] !== 0xff) return bytes; // not at a marker — bail unchanged
58
+ if (bytes[i] !== 0xff) return malformed("JPEG");
57
59
  const marker = bytes[i + 1];
58
60
 
59
61
  // Start of Scan: entropy-coded data runs to EOI — copy the rest verbatim.
@@ -72,9 +74,9 @@ function stripJpeg(bytes: Uint8Array): Uint8Array {
72
74
  i += 2;
73
75
  continue;
74
76
  }
75
- if (i + 3 >= bytes.length) return bytes; // truncated length — bail unchanged
77
+ if (i + 3 >= bytes.length) return malformed("JPEG");
76
78
  const len = (bytes[i + 2] << 8) | bytes[i + 3]; // includes the 2 length bytes
77
- if (len < 2 || i + 2 + len > bytes.length) return bytes; // malformed — bail
79
+ if (len < 2 || i + 2 + len > bytes.length) return malformed("JPEG");
78
80
 
79
81
  const drop =
80
82
  marker === 0xe1 || // APP1: EXIF + XMP (the GPS carriers)
@@ -85,7 +87,7 @@ function stripJpeg(bytes: Uint8Array): Uint8Array {
85
87
  }
86
88
  i += 2 + len;
87
89
  }
88
- return Uint8Array.from(out);
90
+ return malformed("JPEG");
89
91
  }
90
92
 
91
93
  // ---------------------------------------------------------------------------
@@ -96,8 +98,10 @@ const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
96
98
  const PNG_DROP_CHUNKS = new Set(["tEXt", "zTXt", "iTXt", "eXIf", "tIME"]);
97
99
 
98
100
  function stripPng(bytes: Uint8Array): Uint8Array {
99
- if (bytes.length < 8) return bytes;
100
- for (let i = 0; i < 8; i++) if (bytes[i] !== PNG_SIGNATURE[i]) return bytes;
101
+ if (bytes.length < 8) return malformed("PNG");
102
+ for (let i = 0; i < 8; i++) {
103
+ if (bytes[i] !== PNG_SIGNATURE[i]) return malformed("PNG");
104
+ }
101
105
 
102
106
  const out: number[] = [...PNG_SIGNATURE];
103
107
  let i = 8;
@@ -107,7 +111,7 @@ function stripPng(bytes: Uint8Array): Uint8Array {
107
111
  (bytes[i + 1] << 16) |
108
112
  (bytes[i + 2] << 8) |
109
113
  bytes[i + 3];
110
- if (len < 0) return bytes; // overflow / malformed — bail unchanged
114
+ if (len < 0) return malformed("PNG");
111
115
  const type = String.fromCharCode(
112
116
  bytes[i + 4],
113
117
  bytes[i + 5],
@@ -115,7 +119,7 @@ function stripPng(bytes: Uint8Array): Uint8Array {
115
119
  bytes[i + 7],
116
120
  );
117
121
  const chunkEnd = i + 12 + len; // length(4) + type(4) + data(len) + crc(4)
118
- if (chunkEnd > bytes.length) return bytes; // truncated — bail unchanged
122
+ if (chunkEnd > bytes.length) return malformed("PNG");
119
123
 
120
124
  if (!PNG_DROP_CHUNKS.has(type)) {
121
125
  for (let k = i; k < chunkEnd; k++) out.push(bytes[k]);
@@ -123,9 +127,22 @@ function stripPng(bytes: Uint8Array): Uint8Array {
123
127
  i = chunkEnd;
124
128
  if (type === "IEND") break;
125
129
  }
130
+ if (i !== bytes.length || !containsPngChunk(out, "IEND")) {
131
+ return malformed("PNG");
132
+ }
126
133
  return Uint8Array.from(out);
127
134
  }
128
135
 
136
+ function containsPngChunk(bytes: readonly number[], type: string): boolean {
137
+ const needle = [...type].map((character) => character.charCodeAt(0));
138
+ for (let i = 8; i + needle.length <= bytes.length; i++) {
139
+ if (needle.every((value, offset) => bytes[i + offset] === value)) {
140
+ return true;
141
+ }
142
+ }
143
+ return false;
144
+ }
145
+
129
146
  // ---------------------------------------------------------------------------
130
147
  // WebP: a RIFF container ("RIFF" <size> "WEBP" <chunks>). Drop the "EXIF" and
131
148
  // "XMP " chunks, clear the matching VP8X feature-flag bits, and rewrite the RIFF
@@ -142,8 +159,10 @@ function fourCC(bytes: Uint8Array, off: number): string {
142
159
  }
143
160
 
144
161
  function stripWebp(bytes: Uint8Array): Uint8Array {
145
- if (bytes.length < 16) return bytes;
146
- if (fourCC(bytes, 0) !== "RIFF" || fourCC(bytes, 8) !== "WEBP") return bytes;
162
+ if (bytes.length < 16) return malformed("WebP");
163
+ if (fourCC(bytes, 0) !== "RIFF" || fourCC(bytes, 8) !== "WEBP") {
164
+ return malformed("WebP");
165
+ }
147
166
 
148
167
  const head: number[] = [];
149
168
  for (let k = 0; k < 12; k++) head.push(bytes[k]); // RIFF + size + WEBP
@@ -158,10 +177,10 @@ function stripWebp(bytes: Uint8Array): Uint8Array {
158
177
  (bytes[i + 5] << 8) |
159
178
  (bytes[i + 6] << 16) |
160
179
  (bytes[i + 7] << 24);
161
- if (size < 0) return bytes; // overflow — bail unchanged
180
+ if (size < 0) return malformed("WebP");
162
181
  const padded = size + (size & 1); // chunks pad to an even length
163
182
  const chunkEnd = i + 8 + padded;
164
- if (chunkEnd > bytes.length) return bytes; // truncated — bail unchanged
183
+ if (chunkEnd > bytes.length) return malformed("WebP");
165
184
 
166
185
  if (cc === "EXIF" || cc === "XMP ") {
167
186
  removed = true; // skip this chunk entirely
@@ -170,6 +189,7 @@ function stripWebp(bytes: Uint8Array): Uint8Array {
170
189
  }
171
190
  i = chunkEnd;
172
191
  }
192
+ if (i !== bytes.length) return malformed("WebP");
173
193
  if (!removed) return bytes; // nothing to strip — keep original bytes
174
194
 
175
195
  // Clear the EXIF (bit 3) / XMP (bit 2) feature flags in a VP8X header so the
@@ -17,8 +17,14 @@
17
17
  * own, after the later of the per-community read time and the join time.
18
18
  */
19
19
 
20
- import { sql } from "drizzle-orm";
21
- import type { Database } from "../../db/index.ts";
20
+ import { and, count, eq, sql } from "drizzle-orm";
21
+ import {
22
+ activities,
23
+ inbox as inboxTable,
24
+ objects,
25
+ type Database,
26
+ } from "../../db/index.ts";
27
+ import { notificationEligibilityWhere } from "./notification-eligibility.ts";
22
28
 
23
29
  export interface YurumeUnreadCounts {
24
30
  readonly dm: number;
@@ -77,3 +83,58 @@ export async function yurumeUnreadCounts(
77
83
  const community = Number(communityRow?.c ?? 0);
78
84
  return { dm, community, total: dm + community };
79
85
  }
86
+
87
+ /**
88
+ * Unread social-notification count. SAME shared eligibility builder as the
89
+ * notifications list, the badge endpoint, and push delivery (not-self,
90
+ * user-facing types, archive exclusion, direct-DM exclusion, block/mute
91
+ * suppression) so a realtime-pushed badge can never drift from the badge the
92
+ * client fetches.
93
+ */
94
+ export async function notificationUnreadCount(
95
+ db: Database,
96
+ actorApId: string,
97
+ ): Promise<number> {
98
+ const result = await db
99
+ .select({ count: count() })
100
+ .from(inboxTable)
101
+ .innerJoin(activities, eq(inboxTable.activityApId, activities.apId))
102
+ .leftJoin(objects, eq(activities.objectApId, objects.apId))
103
+ .where(
104
+ and(
105
+ eq(inboxTable.actorApId, actorApId),
106
+ eq(inboxTable.read, 0),
107
+ ...notificationEligibilityWhere(db, actorApId, { direct: "exclude" }),
108
+ ),
109
+ )
110
+ .get();
111
+ return Number(result?.count ?? 0);
112
+ }
113
+
114
+ export interface UnreadSnapshot {
115
+ readonly dm: number;
116
+ readonly community: number;
117
+ readonly talkTotal: number;
118
+ readonly notifications: number;
119
+ }
120
+
121
+ /**
122
+ * One authoritative unread snapshot (talk + notifications) for the realtime
123
+ * `unread` event. Server-computed on every emit so clients never derive or
124
+ * increment counters themselves.
125
+ */
126
+ export async function computeUnreadSnapshot(
127
+ db: Database,
128
+ actorApId: string,
129
+ ): Promise<UnreadSnapshot> {
130
+ const [talk, notifications] = await Promise.all([
131
+ yurumeUnreadCounts(db, actorApId),
132
+ notificationUnreadCount(db, actorApId),
133
+ ]);
134
+ return {
135
+ dm: talk.dm,
136
+ community: talk.community,
137
+ talkTotal: talk.total,
138
+ notifications,
139
+ };
140
+ }
@@ -6,6 +6,8 @@ import {
6
6
  issuerEndpoint,
7
7
  } from "../lib/oauth-providers.ts";
8
8
 
9
+ const INTROSPECTION_TIMEOUT_MS = 5_000;
10
+
9
11
  export function requireBearerAuth(
10
12
  requiredScope: string,
11
13
  ): MiddlewareHandler<{ Bindings: Env; Variables: Variables }> {
@@ -27,15 +29,28 @@ export function requireBearerAuth(
27
29
  );
28
30
  }
29
31
 
30
- const res = await fetch(issuerEndpoint(issuer, "/oauth/introspect"), {
31
- method: "POST",
32
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
33
- body: new URLSearchParams({
34
- token,
35
- client_id: clientId,
36
- client_secret: clientSecret,
37
- }).toString(),
38
- });
32
+ let res: Response;
33
+ try {
34
+ res = await fetch(issuerEndpoint(issuer, "/oauth/introspect"), {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
37
+ body: new URLSearchParams({
38
+ token,
39
+ client_id: clientId,
40
+ client_secret: clientSecret,
41
+ }).toString(),
42
+ signal: AbortSignal.timeout(INTROSPECTION_TIMEOUT_MS),
43
+ });
44
+ } catch {
45
+ c.header("Retry-After", "5");
46
+ return c.json(
47
+ {
48
+ error: "temporarily_unavailable",
49
+ error_description: "Introspection request failed",
50
+ },
51
+ 503,
52
+ );
53
+ }
39
54
  if (!res.ok) {
40
55
  return c.json(
41
56
  {
@@ -12,10 +12,34 @@ export {
12
12
  export { default } from "./index.ts";
13
13
  export { default as app } from "./index.ts";
14
14
  export { type Database, getDb, getDbSQLite } from "../db/index.ts";
15
- export { wrapCloudflareBindings } from "./runtime/cloudflare.ts";
15
+ export {
16
+ wrapCloudflareBindings,
17
+ wrapCloudflareMessageBatch,
18
+ wrapCloudflareQueue,
19
+ } from "./runtime/cloudflare.ts";
20
+ export {
21
+ ManagedRuntimeGatewayError,
22
+ createManagedRuntimeQueueProducer,
23
+ type ManagedRuntimeGateway,
24
+ type ManagedRuntimeQueueProducerOptions,
25
+ } from "./runtime/managed-runtime.ts";
26
+ export {
27
+ createManagedRelationalDatabase,
28
+ type ManagedRelationalDatabaseOptions,
29
+ } from "./runtime/managed-relational.ts";
30
+ export type {
31
+ IQueueBatch,
32
+ IQueueMessage,
33
+ IQueueProducer,
34
+ QueueBatchItem,
35
+ QueueSendOptions,
36
+ } from "./runtime/queue.ts";
16
37
  // Call feature: the signaling Durable Object class each product's generated
17
38
  // worker entry must re-export so Wrangler can bind CALL_SIGNALING to it.
18
39
  export { CallSignalingDurableObject } from "./runtime/call-signaling-do.ts";
40
+ // Realtime stream: the per-user fanout Durable Object class each product's
41
+ // generated worker entry must re-export so Wrangler can bind REALTIME_STREAM.
42
+ export { RealtimeStreamDO } from "./runtime/realtime-stream-do.ts";
19
43
  export type { Env, EnvVars } from "./types.ts";
20
44
  export type {
21
45
  DeliveryDlqMessageV1,
@@ -59,6 +59,7 @@ export async function handleGroupFollow(
59
59
  actorApIdStr: string,
60
60
  baseUrl: string,
61
61
  activityId: string,
62
+ sourceActivityId: string = activityId,
62
63
  ) {
63
64
  const db = c.get("db");
64
65
  const followerKey = {
@@ -130,7 +131,7 @@ export async function handleGroupFollow(
130
131
  where: and(
131
132
  eq(activities.type, responseType),
132
133
  eq(activities.actorApId, group.apId),
133
- eq(activities.objectApId, activityId),
134
+ eq(activities.objectApId, sourceActivityId),
134
135
  ),
135
136
  });
136
137
  if (existingResponse) {
@@ -144,14 +145,14 @@ export async function handleGroupFollow(
144
145
  id: responseId,
145
146
  type: responseType,
146
147
  actor: group.apId,
147
- object: activityId,
148
+ object: sourceActivityId,
148
149
  };
149
150
 
150
151
  await db.insert(activities).values({
151
152
  apId: responseId,
152
153
  type: responseType,
153
154
  actorApId: group.apId,
154
- objectApId: activityId,
155
+ objectApId: sourceActivityId,
155
156
  rawJson: JSON.stringify(responseActivity),
156
157
  direction: "outbound",
157
158
  });