@takosjp/yurucommu-core 3.0.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.
Files changed (185) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +82 -0
  3. package/migrations/0001_init.sql +495 -0
  4. package/migrations/0002_social_remote_actor_edges.sql +92 -0
  5. package/migrations/0003_activity_remote_object_edges.sql +68 -0
  6. package/migrations/0004_blocklist.sql +26 -0
  7. package/migrations/0005_story_community_scope.sql +13 -0
  8. package/migrations/0006_dm_community_read_status.sql +19 -0
  9. package/migrations/0007_moderation_reports.sql +22 -0
  10. package/migrations/0008_actor_fields_aka.sql +18 -0
  11. package/migrations/0009_object_tags.sql +13 -0
  12. package/migrations/0010_object_recipients_drop_actor_fk.sql +34 -0
  13. package/migrations/0011_drop_remote_actor_fks.sql +205 -0
  14. package/migrations/0012_objects_content_fts.sql +39 -0
  15. package/migrations/0013_efficiency_indexes.sql +13 -0
  16. package/migrations/0014_inbox_actor_created_idx.sql +15 -0
  17. package/migrations/0015_community_bans.sql +16 -0
  18. package/migrations/0016_namespace_takos_oidc_subject.sql +19 -0
  19. package/migrations/0017_mobile_push_registrations.sql +22 -0
  20. package/migrations/README.md +122 -0
  21. package/package.json +75 -0
  22. package/packages/api/LICENSE +16 -0
  23. package/packages/api/package.json +30 -0
  24. package/packages/api/src/index.ts +4 -0
  25. package/packages/api/src/lib/api/account.ts +20 -0
  26. package/packages/api/src/lib/api/actors.ts +149 -0
  27. package/packages/api/src/lib/api/auth.ts +46 -0
  28. package/packages/api/src/lib/api/communities.ts +329 -0
  29. package/packages/api/src/lib/api/dm.test.ts +67 -0
  30. package/packages/api/src/lib/api/dm.ts +236 -0
  31. package/packages/api/src/lib/api/fetch.ts +111 -0
  32. package/packages/api/src/lib/api/follow.ts +30 -0
  33. package/packages/api/src/lib/api/media.ts +100 -0
  34. package/packages/api/src/lib/api/moderation.ts +98 -0
  35. package/packages/api/src/lib/api/normalize.ts +71 -0
  36. package/packages/api/src/lib/api/notifications.test.ts +63 -0
  37. package/packages/api/src/lib/api/notifications.ts +61 -0
  38. package/packages/api/src/lib/api/posts.test.ts +110 -0
  39. package/packages/api/src/lib/api/posts.ts +181 -0
  40. package/packages/api/src/lib/api/recommendations.ts +22 -0
  41. package/packages/api/src/lib/api/search.ts +88 -0
  42. package/packages/api/src/lib/api/stories.ts +80 -0
  43. package/packages/api/src/lib/api.ts +15 -0
  44. package/packages/api/src/lib/fetch-with-timeout.ts +42 -0
  45. package/packages/api/src/lib/transport.ts +40 -0
  46. package/packages/api/src/social-server.ts +47 -0
  47. package/packages/api/src/types/index.ts +185 -0
  48. package/scripts/apply-takosumi-migrations.ts +621 -0
  49. package/src/backend/federation-helpers.ts +36 -0
  50. package/src/backend/index.ts +872 -0
  51. package/src/backend/lib/account-migration.ts +106 -0
  52. package/src/backend/lib/activitypub-actor-cache.ts +238 -0
  53. package/src/backend/lib/activitypub-helpers.ts +131 -0
  54. package/src/backend/lib/activitypub-validators.ts +323 -0
  55. package/src/backend/lib/ap-context.ts +16 -0
  56. package/src/backend/lib/ap-ids.ts +101 -0
  57. package/src/backend/lib/ap-response.ts +30 -0
  58. package/src/backend/lib/ap-signing.ts +87 -0
  59. package/src/backend/lib/ap-verify.ts +670 -0
  60. package/src/backend/lib/auth-lockout.ts +230 -0
  61. package/src/backend/lib/backend-paths.ts +34 -0
  62. package/src/backend/lib/base64.ts +30 -0
  63. package/src/backend/lib/blocklist-purge.ts +109 -0
  64. package/src/backend/lib/blocklist.ts +279 -0
  65. package/src/backend/lib/chunk.ts +33 -0
  66. package/src/backend/lib/client-ip.ts +169 -0
  67. package/src/backend/lib/community-visibility.ts +230 -0
  68. package/src/backend/lib/crypto.ts +424 -0
  69. package/src/backend/lib/delivery/circuit.ts +265 -0
  70. package/src/backend/lib/delivery/metrics.ts +30 -0
  71. package/src/backend/lib/delivery/planner.ts +190 -0
  72. package/src/backend/lib/delivery/queue-batching.ts +626 -0
  73. package/src/backend/lib/delivery/queue-delivery.ts +641 -0
  74. package/src/backend/lib/delivery/queue.ts +576 -0
  75. package/src/backend/lib/delivery/transformers.ts +56 -0
  76. package/src/backend/lib/delivery/types.ts +139 -0
  77. package/src/backend/lib/errors.ts +114 -0
  78. package/src/backend/lib/federation-fetch.ts +296 -0
  79. package/src/backend/lib/feed-cursor.ts +57 -0
  80. package/src/backend/lib/feed-exclude.ts +48 -0
  81. package/src/backend/lib/hex.ts +8 -0
  82. package/src/backend/lib/log-mask.ts +213 -0
  83. package/src/backend/lib/logger.ts +285 -0
  84. package/src/backend/lib/mobile-contract.ts +137 -0
  85. package/src/backend/lib/oauth-providers.ts +324 -0
  86. package/src/backend/lib/oauth-utils.ts +148 -0
  87. package/src/backend/lib/oidc-id-token.ts +151 -0
  88. package/src/backend/lib/parse-helpers.ts +31 -0
  89. package/src/backend/lib/post-visibility.ts +190 -0
  90. package/src/backend/lib/session-actor.ts +61 -0
  91. package/src/backend/lib/ssrf.ts +428 -0
  92. package/src/backend/lib/strip-image-metadata.ts +191 -0
  93. package/src/backend/middleware/bearer-auth.ts +70 -0
  94. package/src/backend/middleware/body-limit.ts +212 -0
  95. package/src/backend/middleware/cache.ts +429 -0
  96. package/src/backend/middleware/csrf.ts +130 -0
  97. package/src/backend/middleware/error-handler.ts +77 -0
  98. package/src/backend/middleware/rate-limit.ts +308 -0
  99. package/src/backend/public.ts +21 -0
  100. package/src/backend/routes/account-teardown.ts +430 -0
  101. package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +354 -0
  102. package/src/backend/routes/activitypub/handlers/inbound-timestamp.ts +29 -0
  103. package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1634 -0
  104. package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +547 -0
  105. package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +497 -0
  106. package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +262 -0
  107. package/src/backend/routes/activitypub/handlers/user-inbox-handlers.ts +35 -0
  108. package/src/backend/routes/activitypub/inbox-types.ts +74 -0
  109. package/src/backend/routes/activitypub/inbox.ts +1191 -0
  110. package/src/backend/routes/activitypub/outbox.ts +0 -0
  111. package/src/backend/routes/activitypub/query-helpers.ts +227 -0
  112. package/src/backend/routes/activitypub.ts +616 -0
  113. package/src/backend/routes/actors-helpers.ts +487 -0
  114. package/src/backend/routes/actors.ts +1311 -0
  115. package/src/backend/routes/apps.ts +313 -0
  116. package/src/backend/routes/auth-helpers.ts +566 -0
  117. package/src/backend/routes/auth.ts +615 -0
  118. package/src/backend/routes/communities/membership-invites.ts +208 -0
  119. package/src/backend/routes/communities/membership-join.ts +335 -0
  120. package/src/backend/routes/communities/membership-members.ts +539 -0
  121. package/src/backend/routes/communities/membership-requests.ts +296 -0
  122. package/src/backend/routes/communities/membership-shared.ts +364 -0
  123. package/src/backend/routes/communities/messages.ts +479 -0
  124. package/src/backend/routes/communities/routes.ts +624 -0
  125. package/src/backend/routes/communities.ts +21 -0
  126. package/src/backend/routes/dm/contacts.ts +525 -0
  127. package/src/backend/routes/dm/conversations-helpers.ts +197 -0
  128. package/src/backend/routes/dm/conversations.ts +25 -0
  129. package/src/backend/routes/dm/messages.ts +658 -0
  130. package/src/backend/routes/dm/query-helpers.ts +85 -0
  131. package/src/backend/routes/dm/read-archive.ts +228 -0
  132. package/src/backend/routes/dm/requests.ts +222 -0
  133. package/src/backend/routes/dm/typing.ts +81 -0
  134. package/src/backend/routes/dm.ts +15 -0
  135. package/src/backend/routes/follow-helpers.ts +370 -0
  136. package/src/backend/routes/follow.ts +588 -0
  137. package/src/backend/routes/media.ts +692 -0
  138. package/src/backend/routes/mobile.ts +159 -0
  139. package/src/backend/routes/moderation.ts +373 -0
  140. package/src/backend/routes/notifications.ts +757 -0
  141. package/src/backend/routes/posts/delete-cascade.ts +330 -0
  142. package/src/backend/routes/posts/interactions.ts +795 -0
  143. package/src/backend/routes/posts/post-helpers.ts +847 -0
  144. package/src/backend/routes/posts/queries.ts +537 -0
  145. package/src/backend/routes/posts/routes.ts +865 -0
  146. package/src/backend/routes/posts/transformers.ts +161 -0
  147. package/src/backend/routes/posts.ts +17 -0
  148. package/src/backend/routes/recommendations.ts +88 -0
  149. package/src/backend/routes/search.ts +730 -0
  150. package/src/backend/routes/stories/interactions.ts +576 -0
  151. package/src/backend/routes/stories/query-helpers.ts +482 -0
  152. package/src/backend/routes/stories/routes.ts +906 -0
  153. package/src/backend/routes/stories.ts +13 -0
  154. package/src/backend/routes/takos-tools/dm.ts +249 -0
  155. package/src/backend/routes/takos-tools/follows.ts +225 -0
  156. package/src/backend/routes/takos-tools/posts.ts +292 -0
  157. package/src/backend/routes/takos-tools/search.ts +228 -0
  158. package/src/backend/routes/takos-tools/timeline.ts +132 -0
  159. package/src/backend/routes/takos-tools/types.ts +10 -0
  160. package/src/backend/routes/takos-tools-response.ts +178 -0
  161. package/src/backend/routes/takos-tools.ts +153 -0
  162. package/src/backend/routes/timeline.ts +755 -0
  163. package/src/backend/runtime/bun.ts +620 -0
  164. package/src/backend/runtime/cloudflare.ts +202 -0
  165. package/src/backend/runtime/compat-bun/types.ts +44 -0
  166. package/src/backend/runtime/memory-kv.ts +104 -0
  167. package/src/backend/runtime/shared.ts +142 -0
  168. package/src/backend/runtime/types.ts +205 -0
  169. package/src/backend/server.ts +636 -0
  170. package/src/backend/types.ts +143 -0
  171. package/src/db/index.ts +97 -0
  172. package/src/db/schema/actors.ts +129 -0
  173. package/src/db/schema/communities.ts +133 -0
  174. package/src/db/schema/date-utils.ts +17 -0
  175. package/src/db/schema/index.ts +17 -0
  176. package/src/db/schema/messaging.ts +241 -0
  177. package/src/db/schema/mobile.ts +37 -0
  178. package/src/db/schema/posts.ts +150 -0
  179. package/src/db/schema/relations.ts +266 -0
  180. package/src/db/schema/reports.ts +33 -0
  181. package/src/db/schema/social.ts +106 -0
  182. package/src/db/schema/stories.ts +70 -0
  183. package/src/db/schema.ts +15 -0
  184. package/src/plugin/public.ts +7 -0
  185. package/src/runtime/site-worker.ts +10 -0
@@ -0,0 +1,262 @@
1
+ import type { Database } from "../../../../db/index.ts";
2
+ import { and, eq, sql } from "drizzle-orm";
3
+ import type { BatchItem } from "drizzle-orm/batch";
4
+ import {
5
+ activities,
6
+ announces,
7
+ follows,
8
+ inbox as inboxTable,
9
+ likes,
10
+ objects,
11
+ } from "../../../../db/index.ts";
12
+ import { isLocal } from "../../../federation-helpers.ts";
13
+ import type { Activity } from "../inbox-types.ts";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Atomic multi-statement commit (mirrors posts/interactions.ts `runBatch`)
17
+ //
18
+ // D1 has no interactive transactions, but both the D1 and libsql drivers
19
+ // expose `db.batch([...])`, which commits a list of prepared statements
20
+ // atomically. The shared `Database` union aliases the abstract
21
+ // `BaseSQLiteDatabase` base (which does not surface `batch`), so we narrow to
22
+ // the concrete batch surface here rather than weakening the shared type.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ type BatchStatement = BatchItem<"sqlite">;
26
+ interface BatchableDb {
27
+ batch(
28
+ statements: readonly [BatchStatement, ...BatchStatement[]],
29
+ ): Promise<unknown>;
30
+ }
31
+
32
+ export async function runBatch(
33
+ db: Database,
34
+ statements: readonly [BatchStatement, ...BatchStatement[]],
35
+ ): Promise<void> {
36
+ await (db as unknown as BatchableDb).batch(statements);
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Shared helpers used by multiple inbox handler files
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /** Upsert an activity record and create an inbox entry for a local actor. */
44
+ export async function upsertActivityAndNotify(
45
+ db: Database,
46
+ activityId: string,
47
+ type: string,
48
+ actorApId: string,
49
+ objectApIdValue: string,
50
+ rawActivity: Activity,
51
+ recipientApId: string,
52
+ ): Promise<void> {
53
+ const now = new Date().toISOString();
54
+
55
+ await db
56
+ .insert(activities)
57
+ .values({
58
+ apId: activityId,
59
+ type,
60
+ actorApId,
61
+ objectApId: objectApIdValue,
62
+ rawJson: JSON.stringify(rawActivity),
63
+ })
64
+ .onConflictDoNothing();
65
+
66
+ // Idempotent like the activities insert above: a replayed delivery (remote
67
+ // retry) must not create a duplicate inbox/notification row or 500 on the
68
+ // (actor, activity) unique constraint.
69
+ await db
70
+ .insert(inboxTable)
71
+ .values({
72
+ actorApId: recipientApId,
73
+ activityApId: activityId,
74
+ read: 0,
75
+ createdAt: now,
76
+ })
77
+ .onConflictDoNothing();
78
+ }
79
+
80
+ /**
81
+ * Interaction table descriptor used by generic helpers.
82
+ * Both `likes` and `announces` share the same column shape.
83
+ */
84
+ type InteractionTable = typeof likes | typeof announces;
85
+
86
+ const INTERACTION_TABLES: Record<"like" | "announce", InteractionTable> = {
87
+ like: likes,
88
+ announce: announces,
89
+ };
90
+
91
+ /** Find a follow by activityApId and return it (or null). */
92
+ export async function findFollowByActivityId(
93
+ db: Database,
94
+ activityApIdValue: string,
95
+ ): Promise<{
96
+ followerApId: string;
97
+ followingApId: string;
98
+ status: string;
99
+ } | null> {
100
+ const row = await db
101
+ .select({
102
+ followerApId: follows.followerApId,
103
+ followingApId: follows.followingApId,
104
+ status: follows.status,
105
+ })
106
+ .from(follows)
107
+ .where(eq(follows.activityApId, activityApIdValue))
108
+ .get();
109
+ return row ?? null;
110
+ }
111
+
112
+ /**
113
+ * Delete a follow using its compound key. Returns the deleted rows (with their
114
+ * prior status) so callers can gate denormalized count updates on a row
115
+ * actually being removed AND on whether it had been counted (status
116
+ * 'accepted'). `.returning()` yields the deleted rows across both D1 and
117
+ * libsql backends.
118
+ */
119
+ export async function deleteFollowByCompoundKey(
120
+ db: Database,
121
+ followerApId: string,
122
+ followingApId: string,
123
+ ): Promise<Array<{ status: string }>> {
124
+ return await db
125
+ .delete(follows)
126
+ .where(
127
+ and(
128
+ eq(follows.followerApId, followerApId),
129
+ eq(follows.followingApId, followingApId),
130
+ ),
131
+ )
132
+ .returning({ status: follows.status });
133
+ }
134
+
135
+ /**
136
+ * Lookup the owner of an object and, if local, notify via upsertActivityAndNotify.
137
+ * Returns the attributedTo value, or null if the object was not found.
138
+ */
139
+ export async function notifyLocalObjectOwner(
140
+ db: Database,
141
+ objectApIdValue: string,
142
+ activityId: string,
143
+ activityType: string,
144
+ actorApIdValue: string,
145
+ rawActivity: Activity,
146
+ baseUrl: string,
147
+ ): Promise<string | null> {
148
+ const obj = await db
149
+ .select({ attributedTo: objects.attributedTo })
150
+ .from(objects)
151
+ .where(eq(objects.apId, objectApIdValue))
152
+ .get();
153
+ if (!obj) return null;
154
+
155
+ if (isLocal(obj.attributedTo, baseUrl)) {
156
+ await upsertActivityAndNotify(
157
+ db,
158
+ activityId,
159
+ activityType,
160
+ actorApIdValue,
161
+ objectApIdValue,
162
+ rawActivity,
163
+ obj.attributedTo,
164
+ );
165
+ }
166
+ return obj.attributedTo;
167
+ }
168
+
169
+ /** Map interaction kind to the corresponding count column on `objects`. */
170
+ const COUNT_FIELDS: Record<"like" | "announce", "likeCount" | "announceCount"> =
171
+ {
172
+ like: "likeCount",
173
+ announce: "announceCount",
174
+ };
175
+
176
+ /**
177
+ * Generic undo for Like or Announce: delete by direct object ID or fallback
178
+ * to the activityId lookup, then RECOMPUTE the count field.
179
+ * Returns true if the deletion was handled, false otherwise.
180
+ *
181
+ * #COUNTER-SYM (crash-retry convergence): the edge delete and the counter
182
+ * maintenance MUST commit together. Previously the edge was deleted in one
183
+ * statement and the counter decremented (`- 1`) in a SEPARATE statement; under
184
+ * the claim/processed re-dispatch model an interruption between the two left
185
+ * the edge gone but the counter un-decremented, and the peer's retry matched 0
186
+ * rows so the decrement was SKIPPED → a permanent OVER-count. Group the delete
187
+ * and the counter update into a single atomic `db.batch`, and derive the
188
+ * counter from `COUNT(*)` of the edge table (idempotent recompute, mirrors the
189
+ * inbound insert path in handleInteraction) rather than a blind `- 1`, so a
190
+ * retry after a mid-write crash CONVERGES to the true edge count.
191
+ */
192
+ export async function undoInteraction(
193
+ db: Database,
194
+ kind: "like" | "announce",
195
+ countField: "likeCount" | "announceCount",
196
+ directObjectId: string | undefined,
197
+ activityId: string | null,
198
+ actor: string,
199
+ ): Promise<boolean> {
200
+ const table = INTERACTION_TABLES[kind];
201
+ const cf = countField ?? COUNT_FIELDS[kind];
202
+
203
+ if (directObjectId) {
204
+ // Delete the edge and recompute the counter from the remaining edge rows
205
+ // atomically. A duplicate Undo (or an Undo of an interaction we never
206
+ // recorded) deletes 0 rows and the recompute is a no-op against the
207
+ // already-correct edge set, so the count cannot drift in either direction.
208
+ await runBatch(db, [
209
+ db
210
+ .delete(table)
211
+ .where(
212
+ and(eq(table.actorApId, actor), eq(table.objectApId, directObjectId)),
213
+ ),
214
+ db
215
+ .update(objects)
216
+ .set({
217
+ [cf]: sql`(SELECT COUNT(*) FROM ${table} WHERE ${table.objectApId} = ${directObjectId})`,
218
+ })
219
+ .where(eq(objects.apId, directObjectId)),
220
+ ]);
221
+ return true;
222
+ }
223
+
224
+ if (!activityId) return false;
225
+
226
+ // Resolve the edge by its activity ID, then commit the delete + recompute in
227
+ // one batch keyed on the resolved objectApId.
228
+ const record = await db
229
+ .select({
230
+ actorApId: table.actorApId,
231
+ objectApId: table.objectApId,
232
+ })
233
+ .from(table)
234
+ .where(eq(table.activityApId, activityId))
235
+ .get();
236
+ if (record) {
237
+ // Bind the undo to the VERIFIED signer. The activity id is public, so a
238
+ // resolved edge whose owner != the signing actor is a cross-actor forgery
239
+ // (a remote attacker undoing someone else's like/announce by id). The
240
+ // directObjectId branch above already keys its delete on `actor`; mirror it.
241
+ if (record.actorApId !== actor) return false;
242
+ await runBatch(db, [
243
+ db
244
+ .delete(table)
245
+ .where(
246
+ and(
247
+ eq(table.actorApId, record.actorApId),
248
+ eq(table.objectApId, record.objectApId),
249
+ ),
250
+ ),
251
+ db
252
+ .update(objects)
253
+ .set({
254
+ [cf]: sql`(SELECT COUNT(*) FROM ${table} WHERE ${table.objectApId} = ${record.objectApId})`,
255
+ })
256
+ .where(eq(objects.apId, record.objectApId)),
257
+ ]);
258
+ return true;
259
+ }
260
+
261
+ return false;
262
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * User Inbox Handlers - Orchestrator
3
+ *
4
+ * Re-exports all ActivityPub user inbox handlers from domain-specific modules:
5
+ * - inbox-follow-handlers: Follow/Accept/Reject/Undo
6
+ * - inbox-content-handlers: Create/Delete/Update/Move
7
+ * - inbox-interaction-handlers: Like/Announce/Add/Remove/Block/Flag
8
+ */
9
+
10
+ // Follow/Accept/Reject/Undo
11
+ export {
12
+ handleAccept,
13
+ handleFollow,
14
+ handleReject,
15
+ handleUndo,
16
+ } from "./inbox-follow-handlers.ts";
17
+
18
+ // Create/Delete/Update/Move + CreateStory
19
+ export {
20
+ handleCreate,
21
+ handleCreateStory,
22
+ handleDelete,
23
+ handleMove,
24
+ handleUpdate,
25
+ } from "./inbox-content-handlers.ts";
26
+
27
+ // Like/Announce/Add/Remove/Block/Flag
28
+ export {
29
+ handleAdd,
30
+ handleAnnounce,
31
+ handleBlock,
32
+ handleFlag,
33
+ handleLike,
34
+ handleRemove,
35
+ } from "./inbox-interaction-handlers.ts";
@@ -0,0 +1,74 @@
1
+ import type { Context } from "hono";
2
+ import type { Env, Variables } from "../../types.ts";
3
+
4
+ export type ActivityContext = Context<{ Bindings: Env; Variables: Variables }>;
5
+
6
+ export type ActivityObject = {
7
+ id?: string;
8
+ type?: string | string[];
9
+ object?: string;
10
+ inReplyTo?: string;
11
+ to?: string[];
12
+ cc?: string[];
13
+ conversation?: string;
14
+ content?: string;
15
+ summary?: string | null;
16
+ attachment?: unknown;
17
+ tag?: unknown;
18
+ overlays?: unknown;
19
+ endTime?: string;
20
+ displayDuration?: string;
21
+ published?: string;
22
+ room?: string;
23
+ };
24
+
25
+ export type Activity = {
26
+ id?: string;
27
+ type?: string;
28
+ actor?: string;
29
+ object?: string | ActivityObject;
30
+ target?: string | ActivityObject;
31
+ room?: string;
32
+ };
33
+
34
+ export type RemoteActor = {
35
+ id: string;
36
+ type?: string;
37
+ preferredUsername?: string;
38
+ name?: string;
39
+ summary?: string;
40
+ icon?: { url?: string };
41
+ inbox?: string;
42
+ outbox?: string;
43
+ publicKey?: { id?: string; publicKeyPem?: string };
44
+ };
45
+
46
+ export type StoryOverlay = {
47
+ type?: string;
48
+ position?: {
49
+ x?: number;
50
+ y?: number;
51
+ width?: number;
52
+ height?: number;
53
+ };
54
+ };
55
+
56
+ // AS2 `type` may be a string or an array; this matches a name against either
57
+ // shape so `=== "Note"` comparisons keep working once a remote sends an array.
58
+ export function typeIncludes(
59
+ type: string | string[] | undefined,
60
+ name: string,
61
+ ): boolean {
62
+ return Array.isArray(type) ? type.includes(name) : type === name;
63
+ }
64
+
65
+ export function getActivityObject(activity: Activity): ActivityObject | null {
66
+ if (!activity.object || typeof activity.object === "string") return null;
67
+ return activity.object;
68
+ }
69
+
70
+ export function getActivityObjectId(activity: Activity): string | null {
71
+ if (!activity.object) return null;
72
+ if (typeof activity.object === "string") return activity.object;
73
+ return activity.object.id || null;
74
+ }