@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,227 @@
1
+ import type { Context } from "hono";
2
+ import type { Env, Variables } from "../../types.ts";
3
+ import { and, eq, isNull } from "drizzle-orm";
4
+ import { communities, instanceActor } from "../../../db/index.ts";
5
+ import type { Database } from "../../../db/index.ts";
6
+ import { generateKeyPair } from "../../federation-helpers.ts";
7
+ import type { RemoteFetchSigner } from "../../lib/activitypub-actor-cache.ts";
8
+ import { logger } from "../../lib/logger.ts";
9
+
10
+ const log = logger.child({ component: "activitypub.query_helpers" });
11
+
12
+ export const INSTANCE_ACTOR_USERNAME = "community";
13
+
14
+ /** Columns of a community needed to serve its ActivityPub Group actor doc. */
15
+ export interface FederatedCommunityActor {
16
+ apId: string;
17
+ preferredUsername: string;
18
+ name: string;
19
+ summary: string | null;
20
+ iconUrl: string | null;
21
+ inbox: string;
22
+ outbox: string;
23
+ followersUrl: string;
24
+ publicKeyPem: string;
25
+ joinPolicy: string;
26
+ memberCount: number;
27
+ createdAt: string;
28
+ }
29
+
30
+ /**
31
+ * Load a community that is servable over federation as a Group actor: it must
32
+ * exist, not be deleted, and be PUBLIC. A private community's existence /
33
+ * name / summary is members-only (mirroring the discovery-list gate), so it is
34
+ * NOT exposed as a federated actor — callers 404 a null result.
35
+ */
36
+ export async function loadFederatedCommunity(
37
+ db: Database,
38
+ apId: string,
39
+ ): Promise<FederatedCommunityActor | null> {
40
+ const row = await db
41
+ .select({
42
+ apId: communities.apId,
43
+ preferredUsername: communities.preferredUsername,
44
+ name: communities.name,
45
+ summary: communities.summary,
46
+ iconUrl: communities.iconUrl,
47
+ inbox: communities.inbox,
48
+ outbox: communities.outbox,
49
+ followersUrl: communities.followersUrl,
50
+ publicKeyPem: communities.publicKeyPem,
51
+ joinPolicy: communities.joinPolicy,
52
+ memberCount: communities.memberCount,
53
+ createdAt: communities.createdAt,
54
+ })
55
+ .from(communities)
56
+ .where(
57
+ and(
58
+ eq(communities.apId, apId),
59
+ isNull(communities.deletedAt),
60
+ eq(communities.visibility, "public"),
61
+ ),
62
+ )
63
+ .get();
64
+ return row ?? null;
65
+ }
66
+
67
+ export type InstanceActorResult = {
68
+ apId: string;
69
+ preferredUsername: string;
70
+ name: string | null;
71
+ summary: string | null;
72
+ publicKeyPem: string;
73
+ privateKeyPem: string;
74
+ joinPolicy: string;
75
+ postingPolicy: string;
76
+ visibility: string;
77
+ };
78
+
79
+ /**
80
+ * Process-local in-flight lock for `getInstanceActor`. Two concurrent
81
+ * cold-start callers used to race: each would observe a missing row,
82
+ * each would generate its own RSA keypair, and the loser of the insert
83
+ * would simply drop its key. That is correct (because `onConflictDoNothing`
84
+ * keeps the winner's row) but wasteful — RSA generation is ~250ms.
85
+ *
86
+ * Cloudflare D1 does not support `SELECT FOR UPDATE`, so the synthetic
87
+ * advisory lock here is a single in-process Promise keyed by AP-ID. Across
88
+ * processes / D1 shards the race is still resolved by the database's
89
+ * conflict-do-nothing semantics, but the local lock keeps the hot path
90
+ * cheap when the same worker isolate receives a burst of concurrent
91
+ * requests.
92
+ */
93
+ const inFlightInstanceActor = new Map<string, Promise<InstanceActorResult>>();
94
+
95
+ export async function getInstanceActor(
96
+ c: Context<{ Bindings: Env; Variables: Variables }>,
97
+ ): Promise<InstanceActorResult> {
98
+ const db = c.get("db");
99
+ const baseUrl = c.env.APP_URL;
100
+ const apId = `${baseUrl}/ap/actor`;
101
+
102
+ // Hot path: row exists, no lock acquisition needed.
103
+ const existing = await db.query.instanceActor.findFirst({
104
+ where: eq(instanceActor.apId, apId),
105
+ });
106
+ if (existing) {
107
+ return {
108
+ apId: existing.apId,
109
+ preferredUsername: existing.preferredUsername,
110
+ name: existing.name,
111
+ summary: existing.summary,
112
+ publicKeyPem: existing.publicKeyPem,
113
+ privateKeyPem: existing.privateKeyPem,
114
+ joinPolicy: existing.joinPolicy,
115
+ postingPolicy: existing.postingPolicy,
116
+ visibility: existing.visibility,
117
+ };
118
+ }
119
+
120
+ // Cold path: serialise lazy-create through the synthetic advisory lock.
121
+ const inflight = inFlightInstanceActor.get(apId);
122
+ if (inflight) return await inflight;
123
+
124
+ const promise = (async (): Promise<InstanceActorResult> => {
125
+ try {
126
+ // Re-check under lock. Some other in-isolate caller may have created
127
+ // the row between our initial findFirst and lock acquisition.
128
+ const racy = await db.query.instanceActor.findFirst({
129
+ where: eq(instanceActor.apId, apId),
130
+ });
131
+ if (racy) {
132
+ return {
133
+ apId: racy.apId,
134
+ preferredUsername: racy.preferredUsername,
135
+ name: racy.name,
136
+ summary: racy.summary,
137
+ publicKeyPem: racy.publicKeyPem,
138
+ privateKeyPem: racy.privateKeyPem,
139
+ joinPolicy: racy.joinPolicy,
140
+ postingPolicy: racy.postingPolicy,
141
+ visibility: racy.visibility,
142
+ };
143
+ }
144
+
145
+ // NO interactive transaction: the production runtime is Cloudflare D1,
146
+ // whose drizzle driver does not support `db.transaction()` (it throws),
147
+ // which previously made this cold path 500 on every call — the instance
148
+ // actor row could never be lazily created, breaking `/ap/actor` and any
149
+ // path that signs as the instance actor. The insert is idempotent on the
150
+ // natural primary key (`ap_id`) via `onConflictDoNothing`, so concurrent
151
+ // creators race safely: the loser's insert is a no-op and BOTH re-read
152
+ // the winner's row (and thus the winner's keypair) via the trailing
153
+ // findFirst — no transaction is needed for correctness here.
154
+ const { publicKeyPem, privateKeyPem } = await generateKeyPair();
155
+ const now = new Date().toISOString();
156
+ await db
157
+ .insert(instanceActor)
158
+ .values({
159
+ apId,
160
+ preferredUsername: INSTANCE_ACTOR_USERNAME,
161
+ name: "Yurucommu",
162
+ summary: "Yurucommu Community",
163
+ publicKeyPem,
164
+ privateKeyPem,
165
+ joinPolicy: "open",
166
+ postingPolicy: "members",
167
+ visibility: "public",
168
+ createdAt: now,
169
+ updatedAt: now,
170
+ })
171
+ .onConflictDoNothing({ target: instanceActor.apId });
172
+
173
+ const created = await db.query.instanceActor.findFirst({
174
+ where: eq(instanceActor.apId, apId),
175
+ });
176
+ if (!created) {
177
+ throw new Error("Failed to load instance actor after lazy-create");
178
+ }
179
+
180
+ return {
181
+ apId: created.apId,
182
+ preferredUsername: created.preferredUsername,
183
+ name: created.name,
184
+ summary: created.summary,
185
+ publicKeyPem: created.publicKeyPem,
186
+ privateKeyPem: created.privateKeyPem,
187
+ joinPolicy: created.joinPolicy,
188
+ postingPolicy: created.postingPolicy,
189
+ visibility: created.visibility,
190
+ };
191
+ } catch (err) {
192
+ log.error("getInstanceActor failed", {
193
+ event: "ap.instance_actor.lazy_create_failed",
194
+ apId,
195
+ error: err,
196
+ });
197
+ throw err;
198
+ } finally {
199
+ inFlightInstanceActor.delete(apId);
200
+ }
201
+ })();
202
+
203
+ inFlightInstanceActor.set(apId, promise);
204
+ return await promise;
205
+ }
206
+
207
+ /**
208
+ * Build the authorized-fetch signing identity from the instance actor. Used to
209
+ * HTTP-sign outbound actor/object GETs so secure-mode remotes serve the
210
+ * document. The `keyId` matches the `#main-key` fragment the served instance
211
+ * actor doc (`GET /ap/actor`) exposes, so the remote can fetch + verify it.
212
+ */
213
+ export async function getInstanceFetchSigner(
214
+ c: Context<{ Bindings: Env; Variables: Variables }>,
215
+ ): Promise<RemoteFetchSigner> {
216
+ const instance = await getInstanceActor(c);
217
+ return {
218
+ keyId: `${instance.apId}#main-key`,
219
+ privateKeyPem: instance.privateKeyPem,
220
+ };
221
+ }
222
+
223
+ /** @internal Test-only helper for inspecting the synthetic lock. */
224
+ export const __instanceActorInternals = {
225
+ inflightSize: () => inFlightInstanceActor.size,
226
+ clear: () => inFlightInstanceActor.clear(),
227
+ };