@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,30 @@
1
+ import { logger } from "../logger.ts";
2
+
3
+ type MetricPayload = {
4
+ metric: string;
5
+ value: number;
6
+ at: string;
7
+ tags?: Record<string, string | number | boolean | null | undefined>;
8
+ };
9
+
10
+ const log = logger.child({ component: "delivery.metrics" });
11
+
12
+ export function emitMetric(
13
+ metric: string,
14
+ value: number,
15
+ tags?: MetricPayload["tags"],
16
+ ): void {
17
+ const payload: MetricPayload = {
18
+ metric,
19
+ value,
20
+ at: new Date().toISOString(),
21
+ tags,
22
+ };
23
+ // Logs are the portable, zero-dependency metrics channel across Workers + local runtimes.
24
+ // Downstream (Cloudflare / log shipper) can derive p50/p95/p99 and rates from these points.
25
+ log.info("metric", {
26
+ event: "delivery.metric",
27
+ type: "metric",
28
+ ...payload,
29
+ });
30
+ }
@@ -0,0 +1,190 @@
1
+ import type { Database } from "../../../db/index.ts";
2
+ import { inArray } from "drizzle-orm";
3
+ import { actorCache } from "../../../db/index.ts";
4
+ import { emitMetric } from "./metrics.ts";
5
+ import {
6
+ DELIVERY_ENDPOINT_CACHE_TTL_MS,
7
+ safeParseIsoTimeMs,
8
+ } from "./transformers.ts";
9
+ import { isSafeRemoteUrl } from "../../federation-helpers.ts";
10
+ import { filterBlockedActorApIds } from "../blocklist.ts";
11
+
12
+ export type PlannedEndpointGroup = {
13
+ endpoint: string;
14
+ recipientCount: number;
15
+ };
16
+
17
+ export type PlanEndpointsResult = {
18
+ groups: PlannedEndpointGroup[];
19
+ unknownRecipients: string[];
20
+ totalRecipients: number;
21
+ sharedInboxRecipients: number;
22
+ /**
23
+ * Recipients dropped because the operator has blocked their actor AP-ID or
24
+ * hostname. These receive no outbound delivery and no resolve_actor enqueue
25
+ * (mirrors the inbound blocklist enforcement in the inbox handler).
26
+ */
27
+ blockedRecipients: string[];
28
+ };
29
+
30
+ type ActorCacheRow = {
31
+ apId: string;
32
+ inbox: string;
33
+ sharedInbox: string | null;
34
+ lastFetchedAt: string;
35
+ };
36
+
37
+ // Max ids per IN(...) lookup. Cloudflare D1 caps a query at 100 bound
38
+ // parameters (libsql/better-sqlite3 — what the tests run on — allow ~32k, so an
39
+ // over-large chunk passes CI but throws "too many SQL variables" on production
40
+ // D1). Each chunk element binds one parameter, so keep this <=90: a large
41
+ // recipient set is loaded in chunks rather than throwing.
42
+ const PLANNER_IN_CHUNK = 90;
43
+
44
+ function isActorCacheFresh(row: ActorCacheRow, nowMs: number): boolean {
45
+ const lastFetched = safeParseIsoTimeMs(row.lastFetchedAt);
46
+ if (lastFetched === null) return false;
47
+ return nowMs - lastFetched < DELIVERY_ENDPOINT_CACHE_TTL_MS;
48
+ }
49
+
50
+ function chooseEndpoint(
51
+ row: ActorCacheRow,
52
+ ): { endpoint: string; usedSharedInbox: boolean } | null {
53
+ const shared = row.sharedInbox;
54
+ if (shared && isSafeRemoteUrl(shared)) {
55
+ return { endpoint: shared, usedSharedInbox: true };
56
+ }
57
+ if (row.inbox && isSafeRemoteUrl(row.inbox)) {
58
+ return { endpoint: row.inbox, usedSharedInbox: false };
59
+ }
60
+ return null;
61
+ }
62
+
63
+ export async function planEndpointsFromActorCache(
64
+ db: Database,
65
+ recipientActorApIds: string[],
66
+ options?: {
67
+ metricTags?: Record<string, string | number | boolean | null | undefined>;
68
+ },
69
+ ): Promise<PlanEndpointsResult> {
70
+ const nowMs = Date.now();
71
+ const totalRecipients = recipientActorApIds.length;
72
+ if (totalRecipients === 0) {
73
+ return {
74
+ groups: [],
75
+ unknownRecipients: [],
76
+ totalRecipients: 0,
77
+ sharedInboxRecipients: 0,
78
+ blockedRecipients: [],
79
+ };
80
+ }
81
+
82
+ // Enforce the operator blocklist on the OUTBOUND side too: drop any
83
+ // recipient whose actor AP-ID or hostname is defederated BEFORE grouping
84
+ // endpoints, so a blocked domain/actor never receives our posts or DMs.
85
+ // Uses the same isActorBlocked check (which also covers the hostname
86
+ // transitively) that the inbox handler uses inbound.
87
+ const allowedRecipients: string[] = [];
88
+ const blockedRecipients: string[] = [];
89
+ // Batched blocklist filter (2 queries) instead of a serial isActorBlocked
90
+ // per recipient (2 queries each → up to ~400 round-trips per fan-out page).
91
+ const blockedSet = await filterBlockedActorApIds(db, recipientActorApIds);
92
+ for (const apId of recipientActorApIds) {
93
+ if (blockedSet.has(apId)) {
94
+ blockedRecipients.push(apId);
95
+ } else {
96
+ allowedRecipients.push(apId);
97
+ }
98
+ }
99
+
100
+ // After blocklist filtering, nothing left to plan.
101
+ if (allowedRecipients.length === 0) {
102
+ emitMetric("delivery_shared_inbox_aggregation_ratio", 0, {
103
+ total_recipients: totalRecipients,
104
+ shared_inbox_recipients: 0,
105
+ endpoints: 0,
106
+ unknown_recipients: 0,
107
+ blocked_recipients: blockedRecipients.length,
108
+ ...(options?.metricTags ?? {}),
109
+ });
110
+ return {
111
+ groups: [],
112
+ unknownRecipients: [],
113
+ totalRecipients,
114
+ sharedInboxRecipients: 0,
115
+ blockedRecipients,
116
+ };
117
+ }
118
+
119
+ // Batch-load actor_cache for the recipient set, chunked so a large fan-out
120
+ // (a big community's whole remote audience) can't exceed SQLite's bound-
121
+ // parameter ceiling and throw — an un-acked throw here would poison the fanout
122
+ // queue message into an infinite retry loop.
123
+ const rows: ActorCacheRow[] = [];
124
+ for (let i = 0; i < allowedRecipients.length; i += PLANNER_IN_CHUNK) {
125
+ const chunkRows = await db
126
+ .select({
127
+ apId: actorCache.apId,
128
+ inbox: actorCache.inbox,
129
+ sharedInbox: actorCache.sharedInbox,
130
+ lastFetchedAt: actorCache.lastFetchedAt,
131
+ })
132
+ .from(actorCache)
133
+ .where(
134
+ inArray(
135
+ actorCache.apId,
136
+ allowedRecipients.slice(i, i + PLANNER_IN_CHUNK),
137
+ ),
138
+ );
139
+ rows.push(...(chunkRows as ActorCacheRow[]));
140
+ }
141
+
142
+ const byApId = new Map(rows.map((r) => [r.apId, r as ActorCacheRow]));
143
+ const unknownRecipients: string[] = [];
144
+ const endpointCounts = new Map<string, number>();
145
+ let sharedInboxRecipients = 0;
146
+
147
+ for (const apId of allowedRecipients) {
148
+ const row = byApId.get(apId);
149
+ // Treat missing, stale, or unresolvable actors as unknown for resolve_actor jobs.
150
+ const chosen =
151
+ row && isActorCacheFresh(row, nowMs) ? chooseEndpoint(row) : null;
152
+ if (!chosen) {
153
+ unknownRecipients.push(apId);
154
+ continue;
155
+ }
156
+
157
+ if (chosen.usedSharedInbox) sharedInboxRecipients++;
158
+ endpointCounts.set(
159
+ chosen.endpoint,
160
+ (endpointCounts.get(chosen.endpoint) ?? 0) + 1,
161
+ );
162
+ }
163
+
164
+ const groups: PlannedEndpointGroup[] = Array.from(
165
+ endpointCounts.entries(),
166
+ ).map(([endpoint, recipientCount]) => ({
167
+ endpoint,
168
+ recipientCount,
169
+ }));
170
+
171
+ // Observability: sharedInbox aggregation ratio.
172
+ const ratio =
173
+ totalRecipients > 0 ? sharedInboxRecipients / totalRecipients : 0;
174
+ emitMetric("delivery_shared_inbox_aggregation_ratio", ratio, {
175
+ total_recipients: totalRecipients,
176
+ shared_inbox_recipients: sharedInboxRecipients,
177
+ endpoints: groups.length,
178
+ unknown_recipients: unknownRecipients.length,
179
+ blocked_recipients: blockedRecipients.length,
180
+ ...(options?.metricTags ?? {}),
181
+ });
182
+
183
+ return {
184
+ groups,
185
+ unknownRecipients,
186
+ totalRecipients,
187
+ sharedInboxRecipients,
188
+ blockedRecipients,
189
+ };
190
+ }