@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.
- package/LICENSE +16 -0
- package/README.md +82 -0
- package/migrations/0001_init.sql +495 -0
- package/migrations/0002_social_remote_actor_edges.sql +92 -0
- package/migrations/0003_activity_remote_object_edges.sql +68 -0
- package/migrations/0004_blocklist.sql +26 -0
- package/migrations/0005_story_community_scope.sql +13 -0
- package/migrations/0006_dm_community_read_status.sql +19 -0
- package/migrations/0007_moderation_reports.sql +22 -0
- package/migrations/0008_actor_fields_aka.sql +18 -0
- package/migrations/0009_object_tags.sql +13 -0
- package/migrations/0010_object_recipients_drop_actor_fk.sql +34 -0
- package/migrations/0011_drop_remote_actor_fks.sql +205 -0
- package/migrations/0012_objects_content_fts.sql +39 -0
- package/migrations/0013_efficiency_indexes.sql +13 -0
- package/migrations/0014_inbox_actor_created_idx.sql +15 -0
- package/migrations/0015_community_bans.sql +16 -0
- package/migrations/0016_namespace_takos_oidc_subject.sql +19 -0
- package/migrations/0017_mobile_push_registrations.sql +22 -0
- package/migrations/README.md +122 -0
- package/package.json +75 -0
- package/packages/api/LICENSE +16 -0
- package/packages/api/package.json +30 -0
- package/packages/api/src/index.ts +4 -0
- package/packages/api/src/lib/api/account.ts +20 -0
- package/packages/api/src/lib/api/actors.ts +149 -0
- package/packages/api/src/lib/api/auth.ts +46 -0
- package/packages/api/src/lib/api/communities.ts +329 -0
- package/packages/api/src/lib/api/dm.test.ts +67 -0
- package/packages/api/src/lib/api/dm.ts +236 -0
- package/packages/api/src/lib/api/fetch.ts +111 -0
- package/packages/api/src/lib/api/follow.ts +30 -0
- package/packages/api/src/lib/api/media.ts +100 -0
- package/packages/api/src/lib/api/moderation.ts +98 -0
- package/packages/api/src/lib/api/normalize.ts +71 -0
- package/packages/api/src/lib/api/notifications.test.ts +63 -0
- package/packages/api/src/lib/api/notifications.ts +61 -0
- package/packages/api/src/lib/api/posts.test.ts +110 -0
- package/packages/api/src/lib/api/posts.ts +181 -0
- package/packages/api/src/lib/api/recommendations.ts +22 -0
- package/packages/api/src/lib/api/search.ts +88 -0
- package/packages/api/src/lib/api/stories.ts +80 -0
- package/packages/api/src/lib/api.ts +15 -0
- package/packages/api/src/lib/fetch-with-timeout.ts +42 -0
- package/packages/api/src/lib/transport.ts +40 -0
- package/packages/api/src/social-server.ts +47 -0
- package/packages/api/src/types/index.ts +185 -0
- package/scripts/apply-takosumi-migrations.ts +621 -0
- package/src/backend/federation-helpers.ts +36 -0
- package/src/backend/index.ts +872 -0
- package/src/backend/lib/account-migration.ts +106 -0
- package/src/backend/lib/activitypub-actor-cache.ts +238 -0
- package/src/backend/lib/activitypub-helpers.ts +131 -0
- package/src/backend/lib/activitypub-validators.ts +323 -0
- package/src/backend/lib/ap-context.ts +16 -0
- package/src/backend/lib/ap-ids.ts +101 -0
- package/src/backend/lib/ap-response.ts +30 -0
- package/src/backend/lib/ap-signing.ts +87 -0
- package/src/backend/lib/ap-verify.ts +670 -0
- package/src/backend/lib/auth-lockout.ts +230 -0
- package/src/backend/lib/backend-paths.ts +34 -0
- package/src/backend/lib/base64.ts +30 -0
- package/src/backend/lib/blocklist-purge.ts +109 -0
- package/src/backend/lib/blocklist.ts +279 -0
- package/src/backend/lib/chunk.ts +33 -0
- package/src/backend/lib/client-ip.ts +169 -0
- package/src/backend/lib/community-visibility.ts +230 -0
- package/src/backend/lib/crypto.ts +424 -0
- package/src/backend/lib/delivery/circuit.ts +265 -0
- package/src/backend/lib/delivery/metrics.ts +30 -0
- package/src/backend/lib/delivery/planner.ts +190 -0
- package/src/backend/lib/delivery/queue-batching.ts +626 -0
- package/src/backend/lib/delivery/queue-delivery.ts +641 -0
- package/src/backend/lib/delivery/queue.ts +576 -0
- package/src/backend/lib/delivery/transformers.ts +56 -0
- package/src/backend/lib/delivery/types.ts +139 -0
- package/src/backend/lib/errors.ts +114 -0
- package/src/backend/lib/federation-fetch.ts +296 -0
- package/src/backend/lib/feed-cursor.ts +57 -0
- package/src/backend/lib/feed-exclude.ts +48 -0
- package/src/backend/lib/hex.ts +8 -0
- package/src/backend/lib/log-mask.ts +213 -0
- package/src/backend/lib/logger.ts +285 -0
- package/src/backend/lib/mobile-contract.ts +137 -0
- package/src/backend/lib/oauth-providers.ts +324 -0
- package/src/backend/lib/oauth-utils.ts +148 -0
- package/src/backend/lib/oidc-id-token.ts +151 -0
- package/src/backend/lib/parse-helpers.ts +31 -0
- package/src/backend/lib/post-visibility.ts +190 -0
- package/src/backend/lib/session-actor.ts +61 -0
- package/src/backend/lib/ssrf.ts +428 -0
- package/src/backend/lib/strip-image-metadata.ts +191 -0
- package/src/backend/middleware/bearer-auth.ts +70 -0
- package/src/backend/middleware/body-limit.ts +212 -0
- package/src/backend/middleware/cache.ts +429 -0
- package/src/backend/middleware/csrf.ts +130 -0
- package/src/backend/middleware/error-handler.ts +77 -0
- package/src/backend/middleware/rate-limit.ts +308 -0
- package/src/backend/public.ts +21 -0
- package/src/backend/routes/account-teardown.ts +430 -0
- package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +354 -0
- package/src/backend/routes/activitypub/handlers/inbound-timestamp.ts +29 -0
- package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1634 -0
- package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +547 -0
- package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +497 -0
- package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +262 -0
- package/src/backend/routes/activitypub/handlers/user-inbox-handlers.ts +35 -0
- package/src/backend/routes/activitypub/inbox-types.ts +74 -0
- package/src/backend/routes/activitypub/inbox.ts +1191 -0
- package/src/backend/routes/activitypub/outbox.ts +0 -0
- package/src/backend/routes/activitypub/query-helpers.ts +227 -0
- package/src/backend/routes/activitypub.ts +616 -0
- package/src/backend/routes/actors-helpers.ts +487 -0
- package/src/backend/routes/actors.ts +1311 -0
- package/src/backend/routes/apps.ts +313 -0
- package/src/backend/routes/auth-helpers.ts +566 -0
- package/src/backend/routes/auth.ts +615 -0
- package/src/backend/routes/communities/membership-invites.ts +208 -0
- package/src/backend/routes/communities/membership-join.ts +335 -0
- package/src/backend/routes/communities/membership-members.ts +539 -0
- package/src/backend/routes/communities/membership-requests.ts +296 -0
- package/src/backend/routes/communities/membership-shared.ts +364 -0
- package/src/backend/routes/communities/messages.ts +479 -0
- package/src/backend/routes/communities/routes.ts +624 -0
- package/src/backend/routes/communities.ts +21 -0
- package/src/backend/routes/dm/contacts.ts +525 -0
- package/src/backend/routes/dm/conversations-helpers.ts +197 -0
- package/src/backend/routes/dm/conversations.ts +25 -0
- package/src/backend/routes/dm/messages.ts +658 -0
- package/src/backend/routes/dm/query-helpers.ts +85 -0
- package/src/backend/routes/dm/read-archive.ts +228 -0
- package/src/backend/routes/dm/requests.ts +222 -0
- package/src/backend/routes/dm/typing.ts +81 -0
- package/src/backend/routes/dm.ts +15 -0
- package/src/backend/routes/follow-helpers.ts +370 -0
- package/src/backend/routes/follow.ts +588 -0
- package/src/backend/routes/media.ts +692 -0
- package/src/backend/routes/mobile.ts +159 -0
- package/src/backend/routes/moderation.ts +373 -0
- package/src/backend/routes/notifications.ts +757 -0
- package/src/backend/routes/posts/delete-cascade.ts +330 -0
- package/src/backend/routes/posts/interactions.ts +795 -0
- package/src/backend/routes/posts/post-helpers.ts +847 -0
- package/src/backend/routes/posts/queries.ts +537 -0
- package/src/backend/routes/posts/routes.ts +865 -0
- package/src/backend/routes/posts/transformers.ts +161 -0
- package/src/backend/routes/posts.ts +17 -0
- package/src/backend/routes/recommendations.ts +88 -0
- package/src/backend/routes/search.ts +730 -0
- package/src/backend/routes/stories/interactions.ts +576 -0
- package/src/backend/routes/stories/query-helpers.ts +482 -0
- package/src/backend/routes/stories/routes.ts +906 -0
- package/src/backend/routes/stories.ts +13 -0
- package/src/backend/routes/takos-tools/dm.ts +249 -0
- package/src/backend/routes/takos-tools/follows.ts +225 -0
- package/src/backend/routes/takos-tools/posts.ts +292 -0
- package/src/backend/routes/takos-tools/search.ts +228 -0
- package/src/backend/routes/takos-tools/timeline.ts +132 -0
- package/src/backend/routes/takos-tools/types.ts +10 -0
- package/src/backend/routes/takos-tools-response.ts +178 -0
- package/src/backend/routes/takos-tools.ts +153 -0
- package/src/backend/routes/timeline.ts +755 -0
- package/src/backend/runtime/bun.ts +620 -0
- package/src/backend/runtime/cloudflare.ts +202 -0
- package/src/backend/runtime/compat-bun/types.ts +44 -0
- package/src/backend/runtime/memory-kv.ts +104 -0
- package/src/backend/runtime/shared.ts +142 -0
- package/src/backend/runtime/types.ts +205 -0
- package/src/backend/server.ts +636 -0
- package/src/backend/types.ts +143 -0
- package/src/db/index.ts +97 -0
- package/src/db/schema/actors.ts +129 -0
- package/src/db/schema/communities.ts +133 -0
- package/src/db/schema/date-utils.ts +17 -0
- package/src/db/schema/index.ts +17 -0
- package/src/db/schema/messaging.ts +241 -0
- package/src/db/schema/mobile.ts +37 -0
- package/src/db/schema/posts.ts +150 -0
- package/src/db/schema/relations.ts +266 -0
- package/src/db/schema/reports.ts +33 -0
- package/src/db/schema/social.ts +106 -0
- package/src/db/schema/stories.ts +70 -0
- package/src/db/schema.ts +15 -0
- package/src/plugin/public.ts +7 -0
- 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";
|