@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,308 @@
1
+ import type { Context, Next } from "hono";
2
+
3
+ import { getClientIP } from "../lib/client-ip.ts";
4
+ import type { IKeyValueStore } from "../runtime/types.ts";
5
+ import type { Env, Variables } from "../types.ts";
6
+ import { logger } from "../lib/logger.ts";
7
+
8
+ const log = logger.child({ component: "middleware.rate_limit" });
9
+
10
+ interface RateLimitEntry {
11
+ count: number;
12
+ resetAt: number;
13
+ }
14
+
15
+ const RATE_LIMIT_KV_PREFIX = "rate-limit:v1";
16
+
17
+ /**
18
+ * In-memory fallback rate-limit store used only when distributed KV is
19
+ * unavailable. Entries expire once `resetAt` passes; we sweep expired
20
+ * entries on every access. A hard cap also bounds worst-case growth
21
+ * when many distinct keys hit the limiter within a single window.
22
+ *
23
+ * When the cap is hit, the store uses LRU eviction (Map iteration order
24
+ * is insertion order — by re-inserting an entry every time it is touched
25
+ * the oldest live entries naturally end up at the front). This is more
26
+ * forgiving than the previous `.clear()` behaviour (which reset every
27
+ * counter and effectively bypassed the limiter for one window).
28
+ */
29
+ const FALLBACK_RATE_LIMIT_MAX_ENTRIES = 10_000;
30
+ const fallbackRateLimitStore = new Map<string, RateLimitEntry>();
31
+
32
+ function evictExpiredFallbackEntries(now: number): void {
33
+ for (const [key, entry] of fallbackRateLimitStore) {
34
+ if (entry.resetAt <= now) {
35
+ fallbackRateLimitStore.delete(key);
36
+ }
37
+ }
38
+ enforceFallbackLruCap();
39
+ }
40
+
41
+ function enforceFallbackLruCap(): void {
42
+ while (fallbackRateLimitStore.size > FALLBACK_RATE_LIMIT_MAX_ENTRIES) {
43
+ // Map preserves insertion order; the first key is the least-recently
44
+ // touched entry under the LRU re-insertion strategy used below.
45
+ const iter = fallbackRateLimitStore.keys().next();
46
+ if (iter.done) break;
47
+ fallbackRateLimitStore.delete(iter.value);
48
+ }
49
+ }
50
+
51
+ function touchFallbackEntry(key: string, entry: RateLimitEntry): void {
52
+ // Re-inserting moves the key to the end of the Map iteration order,
53
+ // which our LRU eviction relies on.
54
+ fallbackRateLimitStore.delete(key);
55
+ fallbackRateLimitStore.set(key, entry);
56
+ }
57
+
58
+ /** @internal Test-only inspector for the fallback store. */
59
+ export const __fallbackRateLimitInternals = {
60
+ size: () => fallbackRateLimitStore.size,
61
+ clear: () => fallbackRateLimitStore.clear(),
62
+ set: (key: string, entry: RateLimitEntry) =>
63
+ fallbackRateLimitStore.set(key, entry),
64
+ touch: touchFallbackEntry,
65
+ evict: evictExpiredFallbackEntries,
66
+ enforceCap: enforceFallbackLruCap,
67
+ maxEntries: FALLBACK_RATE_LIMIT_MAX_ENTRIES,
68
+ };
69
+
70
+ let hasWarnedKvFailure = false;
71
+
72
+ function getRateLimitStorageKey(key: string): string {
73
+ return `${RATE_LIMIT_KV_PREFIX}:${encodeURIComponent(key)}`;
74
+ }
75
+
76
+ function isValidEntry(v: unknown): v is RateLimitEntry {
77
+ if (typeof v !== "object" || v === null) return false;
78
+ const entry = v as Record<string, unknown>;
79
+ return typeof entry.count === "number" && typeof entry.resetAt === "number";
80
+ }
81
+
82
+ function parseRateLimitEntry(
83
+ raw: string | null,
84
+ now: number,
85
+ ): RateLimitEntry | null {
86
+ if (!raw) return null;
87
+
88
+ try {
89
+ const parsed: unknown = JSON.parse(raw);
90
+ if (!isValidEntry(parsed) || parsed.resetAt <= now) return null;
91
+ return parsed;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ function consumeLocalFallback(
98
+ key: string,
99
+ windowMs: number,
100
+ now: number,
101
+ ): RateLimitEntry {
102
+ evictExpiredFallbackEntries(now);
103
+ const existing = fallbackRateLimitStore.get(key);
104
+ if (!existing || existing.resetAt <= now) {
105
+ const created = { count: 1, resetAt: now + windowMs };
106
+ touchFallbackEntry(key, created);
107
+ enforceFallbackLruCap();
108
+ return created;
109
+ }
110
+
111
+ const next = { ...existing, count: existing.count + 1 };
112
+ touchFallbackEntry(key, next);
113
+ enforceFallbackLruCap();
114
+ return next;
115
+ }
116
+
117
+ async function consumeDistributed(
118
+ kv: IKeyValueStore,
119
+ key: string,
120
+ windowMs: number,
121
+ now: number,
122
+ ): Promise<RateLimitEntry> {
123
+ const storageKey = getRateLimitStorageKey(key);
124
+ const current = parseRateLimitEntry(await kv.get(storageKey), now);
125
+
126
+ const next: RateLimitEntry = current
127
+ ? { count: current.count + 1, resetAt: current.resetAt }
128
+ : { count: 1, resetAt: now + windowMs };
129
+
130
+ const expirationTtl = Math.max(
131
+ 60,
132
+ Math.ceil((next.resetAt - now) / 1000) + 5,
133
+ );
134
+ await kv.put(storageKey, JSON.stringify(next), { expirationTtl });
135
+ return next;
136
+ }
137
+
138
+ async function consumeRateLimit(
139
+ kv: IKeyValueStore | undefined,
140
+ key: string,
141
+ windowMs: number,
142
+ now: number,
143
+ ): Promise<RateLimitEntry> {
144
+ if (!kv) {
145
+ return consumeLocalFallback(key, windowMs, now);
146
+ }
147
+
148
+ try {
149
+ return await consumeDistributed(kv, key, windowMs, now);
150
+ } catch (err) {
151
+ if (!hasWarnedKvFailure) {
152
+ hasWarnedKvFailure = true;
153
+ log.warn("KV unavailable, falling back to in-memory limiter", {
154
+ event: "rate_limit.kv.unavailable",
155
+ error: err,
156
+ });
157
+ }
158
+ return consumeLocalFallback(key, windowMs, now);
159
+ }
160
+ }
161
+
162
+ interface RateLimitConfig {
163
+ windowMs: number;
164
+ maxRequests: number;
165
+ keyPrefix?: string;
166
+ }
167
+
168
+ function buildKey(prefix: string | undefined, clientId: string): string {
169
+ return `${prefix ?? ""}${clientId}`;
170
+ }
171
+
172
+ const RATE_LIMIT_WINDOW_MS = 60_000;
173
+
174
+ /**
175
+ * Threshold reasoning (per-minute, bucketed by client IP unless a logged-in
176
+ * actor is present, in which case the actor AP-ID is used):
177
+ *
178
+ * - `general` (1k/min): generic page load + asset traffic. Even an SPA
179
+ * that fans out fetches on first paint stays well under 1k for a real
180
+ * user. Anything beyond is a scraper or a runaway client and should
181
+ * cool off.
182
+ * - `auth` (20/min): credential endpoints. Lower bound on guessing
183
+ * (a real user reauthenticating after a token rotation still
184
+ * fits inside 20). Kept tight because a brute-force window matters
185
+ * more than a smooth-UX false positive.
186
+ * - `postCreate` (200/min): publish/edit. A single user cannot meaningfully
187
+ * post 200 times per minute; multi-window scheduled jobs / bulk imports
188
+ * should authenticate as a service account with a higher quota.
189
+ * - `search` (3k/min): timeline search; this includes typeahead-style
190
+ * incremental queries so the budget is intentionally generous.
191
+ * - `mediaUpload` (2k/min): R2/media uploads; bounded by upstream
192
+ * egress, but we keep a sub-budget here for abuse detection.
193
+ * - `dm` (600/min): direct message send. Tight enough to make a spam
194
+ * bot visible without breaking an active human conversation.
195
+ * - `communityMessage` (600/min): community chat send. A publish-like write
196
+ * (inserts an object + recipient + Create activity); mirrors `dm` so a spam
197
+ * bot is visible without breaking an active human conversation, instead of
198
+ * sharing the generous `general` read budget.
199
+ * - `storyWrite` (200/min): story create + interactions (vote/like/share).
200
+ * Story create fans out to followers, so it is publish-like; matched to
201
+ * `postCreate` rather than left in the `general` read bucket.
202
+ * - `inbox` (1k/min): per-IP inbound federation. Previously 20k which
203
+ * effectively disabled the limiter — a misbehaving peer instance
204
+ * behind a single egress IP could DoS us. 1k/min is well above any
205
+ * legitimate peer throughput; per-domain caps (`inboxDomain` below)
206
+ * add a second-level guard for peers behind multiple egresses.
207
+ * - `inboxDomain` (1k/min): per-source-domain inbound federation.
208
+ * Same rationale as `inbox` but bucketed by the peer's hostname so
209
+ * that a domain with many egress IPs cannot multiply its budget.
210
+ * - `federationDiscovery` (60/min): WebFinger / NodeInfo / actor doc
211
+ * lookups. Public discovery should be slow enough to discourage
212
+ * enumeration.
213
+ */
214
+ const RATE_LIMITS = {
215
+ general: { windowMs: RATE_LIMIT_WINDOW_MS, maxRequests: 1_000 },
216
+ auth: { windowMs: RATE_LIMIT_WINDOW_MS, maxRequests: 20 },
217
+ postCreate: { windowMs: RATE_LIMIT_WINDOW_MS, maxRequests: 200 },
218
+ search: { windowMs: RATE_LIMIT_WINDOW_MS, maxRequests: 3_000 },
219
+ mediaUpload: { windowMs: RATE_LIMIT_WINDOW_MS, maxRequests: 2_000 },
220
+ dm: { windowMs: RATE_LIMIT_WINDOW_MS, maxRequests: 600 },
221
+ communityMessage: { windowMs: RATE_LIMIT_WINDOW_MS, maxRequests: 600 },
222
+ storyWrite: { windowMs: RATE_LIMIT_WINDOW_MS, maxRequests: 200 },
223
+ inbox: { windowMs: RATE_LIMIT_WINDOW_MS, maxRequests: 1_000 },
224
+ inboxDomain: { windowMs: RATE_LIMIT_WINDOW_MS, maxRequests: 1_000 },
225
+ federationDiscovery: { windowMs: RATE_LIMIT_WINDOW_MS, maxRequests: 60 },
226
+ } as const;
227
+
228
+ export const RateLimitConfigs = {
229
+ general: { ...RATE_LIMITS.general },
230
+ auth: { ...RATE_LIMITS.auth, keyPrefix: "auth:" },
231
+ postCreate: { ...RATE_LIMITS.postCreate, keyPrefix: "post:" },
232
+ search: { ...RATE_LIMITS.search, keyPrefix: "search:" },
233
+ mediaUpload: { ...RATE_LIMITS.mediaUpload, keyPrefix: "media:" },
234
+ dm: { ...RATE_LIMITS.dm, keyPrefix: "dm:" },
235
+ communityMessage: {
236
+ ...RATE_LIMITS.communityMessage,
237
+ keyPrefix: "community-msg:",
238
+ },
239
+ storyWrite: { ...RATE_LIMITS.storyWrite, keyPrefix: "story-write:" },
240
+ inbox: { ...RATE_LIMITS.inbox, keyPrefix: "inbox:" },
241
+ inboxDomain: { ...RATE_LIMITS.inboxDomain, keyPrefix: "inbox-domain:" },
242
+ federationDiscovery: {
243
+ ...RATE_LIMITS.federationDiscovery,
244
+ keyPrefix: "fed-disc:",
245
+ },
246
+ } as const satisfies Record<string, RateLimitConfig>;
247
+
248
+ /**
249
+ * Create a rate limiting middleware
250
+ */
251
+ export function rateLimit(config: RateLimitConfig) {
252
+ return async (
253
+ c: Context<{ Bindings: Env; Variables: Variables }>,
254
+ next: Next,
255
+ ) => {
256
+ // Get client identifier (IP or authenticated actor)
257
+ const actor = c.get("actor");
258
+ const ip = getClientIP(c);
259
+ const clientId = actor?.ap_id || ip;
260
+
261
+ const key = buildKey(config.keyPrefix, clientId);
262
+ const now = Date.now();
263
+ const entry = await consumeRateLimit(c.env.KV, key, config.windowMs, now);
264
+
265
+ // Set rate limit headers
266
+ const remaining = Math.max(0, config.maxRequests - entry.count);
267
+ const resetAt = Math.ceil(entry.resetAt / 1000);
268
+
269
+ c.header("X-RateLimit-Limit", config.maxRequests.toString());
270
+ c.header("X-RateLimit-Remaining", remaining.toString());
271
+ c.header("X-RateLimit-Reset", resetAt.toString());
272
+
273
+ // Check if rate limited
274
+ if (entry.count > config.maxRequests) {
275
+ const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
276
+ c.header("Retry-After", retryAfter.toString());
277
+
278
+ return c.json(
279
+ {
280
+ error: "Too many requests",
281
+ retry_after: retryAfter,
282
+ },
283
+ 429,
284
+ );
285
+ }
286
+
287
+ await next();
288
+ };
289
+ }
290
+
291
+ /**
292
+ * Programmatic rate-limit consume helper used outside the middleware path.
293
+ * Returns the updated entry so the caller can decide how to respond. Used by
294
+ * the ActivityPub inbox to apply per-domain throttling alongside the
295
+ * per-IP middleware.
296
+ */
297
+ export async function consumeRateLimitProgrammatic(
298
+ kv: IKeyValueStore | undefined,
299
+ config: RateLimitConfig,
300
+ bucketKey: string,
301
+ ): Promise<{ entry: RateLimitEntry; limited: boolean; retryAfter: number }> {
302
+ const key = buildKey(config.keyPrefix, bucketKey);
303
+ const now = Date.now();
304
+ const entry = await consumeRateLimit(kv, key, config.windowMs, now);
305
+ const limited = entry.count > config.maxRequests;
306
+ const retryAfter = Math.max(0, Math.ceil((entry.resetAt - now) / 1000));
307
+ return { entry, limited, retryAfter };
308
+ }
@@ -0,0 +1,21 @@
1
+ export {
2
+ backendApp,
3
+ type BackendPluginContextV1,
4
+ createYurucommuBackendApp,
5
+ type CreateYurucommuBackendAppOptionsV1,
6
+ handleYurucommuQueueBatch,
7
+ YURUCOMMU_BACKEND_PLUGIN_API_VERSION,
8
+ type YurucommuBackendDiscoveryClientV1,
9
+ type YurucommuBackendDiscoveryOptionsV1,
10
+ type YurucommuBackendPluginV1,
11
+ } from "./index.ts";
12
+ export { default } from "./index.ts";
13
+ export { default as app } from "./index.ts";
14
+ export { type Database, getDb, getDbSQLite } from "../db/index.ts";
15
+ export { wrapCloudflareBindings } from "./runtime/cloudflare.ts";
16
+ export type { Env, EnvVars } from "./types.ts";
17
+ export type {
18
+ DeliveryDlqMessageV1,
19
+ DeliveryQueueMessageV1,
20
+ } from "./lib/delivery/types.ts";
21
+ export type { D1Database } from "@cloudflare/workers-types";