···11import type { CommitEvt } from "@atproto/sync";
22import { didOwnsUri } from "../lib/at-uri.ts";
33import { COLLECTIONS, normalizeRole } from "../lib/constants.ts";
44+import { LIMITS } from "../lib/limits.ts";
45import {
56 applyRevisionFromFirehose,
67 deleteBookmarkByUri,
···6667 createdAt: string;
6768}
68697070+// --- Size guards ---
7171+//
7272+// Other PDSes are not trusted to enforce our lexicon maxima. Records that
7373+// exceed the declared limits are logged and skipped — never throw, so one bad
7474+// record can't kill the subscriber.
7575+7676+function logDrop(atUri: string, reason: string): void {
7777+ console.warn(`[firehose] dropping record ${atUri}: ${reason}`);
7878+}
7979+8080+function wikiExceedsLimits(atUri: string, r: WikiRecord): boolean {
8181+ if (r.name.length > LIMITS.wiki.name) {
8282+ logDrop(atUri, "wiki.name exceeds limit");
8383+ return true;
8484+ }
8585+ if (r.visibility.length > LIMITS.wiki.visibility) {
8686+ logDrop(atUri, "wiki.visibility exceeds limit");
8787+ return true;
8888+ }
8989+ if (r.language !== undefined && r.language.length > LIMITS.wiki.language) {
9090+ logDrop(atUri, "wiki.language exceeds limit");
9191+ return true;
9292+ }
9393+ if (
9494+ r.description !== undefined &&
9595+ r.description.length > LIMITS.wiki.description
9696+ ) {
9797+ logDrop(atUri, "wiki.description exceeds limit");
9898+ return true;
9999+ }
100100+ return false;
101101+}
102102+103103+function noteExceedsLimits(atUri: string, r: NoteRecord): boolean {
104104+ if (r.slug.length > LIMITS.note.slug) {
105105+ logDrop(atUri, "note.slug exceeds limit");
106106+ return true;
107107+ }
108108+ if (r.title.length > LIMITS.note.title) {
109109+ logDrop(atUri, "note.title exceeds limit");
110110+ return true;
111111+ }
112112+ return false;
113113+}
114114+115115+function revisionExceedsLimits(atUri: string, r: RevisionRecord): boolean {
116116+ if (r.diff.length > LIMITS.revision.diff) {
117117+ logDrop(atUri, "revision.diff exceeds limit");
118118+ return true;
119119+ }
120120+ if (r.diffFormat.length > LIMITS.revision.diffFormat) {
121121+ logDrop(atUri, "revision.diffFormat exceeds limit");
122122+ return true;
123123+ }
124124+ if (r.message !== undefined && r.message.length > LIMITS.revision.message) {
125125+ logDrop(atUri, "revision.message exceeds limit");
126126+ return true;
127127+ }
128128+ if (r.blobs !== undefined && r.blobs.length > LIMITS.revision.blobCount) {
129129+ logDrop(atUri, "revision.blobs exceeds count limit");
130130+ return true;
131131+ }
132132+ return false;
133133+}
134134+135135+function membershipExceedsLimits(atUri: string, r: MembershipRecord): boolean {
136136+ if (r.memberDid.length > LIMITS.membership.memberDid) {
137137+ logDrop(atUri, "membership.memberDid exceeds limit");
138138+ return true;
139139+ }
140140+ if (r.role.length > LIMITS.membership.role) {
141141+ logDrop(atUri, "membership.role exceeds limit");
142142+ return true;
143143+ }
144144+ return false;
145145+}
146146+69147// --- Type guards ---
7014871149type Rec = Record<string, unknown>;
···134212135213 switch (evt.collection) {
136214 case COLLECTIONS.wiki:
137137- if (isWikiRecord(r)) handleWiki(evt.did, evt.rkey, atUri, r);
215215+ if (isWikiRecord(r) && !wikiExceedsLimits(atUri, r))
216216+ handleWiki(evt.did, evt.rkey, atUri, r);
138217 break;
139218 case COLLECTIONS.note:
140140- if (isNoteRecord(r)) handleNote(evt.did, atUri, r);
219219+ if (isNoteRecord(r) && !noteExceedsLimits(atUri, r))
220220+ handleNote(evt.did, atUri, r);
141221 break;
142222 case COLLECTIONS.noteRevision:
143143- if (isRevisionRecord(r)) handleRevision(evt.did, atUri, r);
223223+ if (isRevisionRecord(r) && !revisionExceedsLimits(atUri, r))
224224+ handleRevision(evt.did, atUri, r);
144225 break;
145226 case COLLECTIONS.membership:
146146- if (isMembershipRecord(r)) handleMembership(evt.did, atUri, r);
227227+ if (isMembershipRecord(r) && !membershipExceedsLimits(atUri, r))
228228+ handleMembership(evt.did, atUri, r);
147229 break;
148230 case COLLECTIONS.memberRequest:
149231 if (isMemberRequestRecord(r)) handleMemberRequest(evt.did, atUri, r);
+4
src/lib/i18n/en.ts
···146146 wikiNameRequired: "Wiki name is required.",
147147 wikiSlugExists: 'A wiki with slug "{slug}" already exists.',
148148 wikiLanguageRequired: "Language is required.",
149149+ wikiNameTooLong: "Wiki name is too long (max {max} characters).",
150150+ titleTooLong: "Title is too long (max {max} characters).",
151151+ contentTooLong: "Content is too long (max {max} characters).",
152152+ messageTooLong: "Edit summary is too long (max {max} characters).",
149153 invalidZip: "Invalid or corrupt zip file.",
150154 tooManyFiles: "Too many files in zip (max 100 markdown files).",
151155 zipTooLarge: "Zip content exceeds the 50MB limit.",
+4
src/lib/i18n/fr.ts
···148148 wikiNameRequired: "Le nom du wiki est requis.",
149149 wikiSlugExists: 'Un wiki avec le slug "{slug}" existe déjà.',
150150 wikiLanguageRequired: "La langue est requise.",
151151+ wikiNameTooLong: "Le nom du wiki est trop long (max {max} caractères).",
152152+ titleTooLong: "Le titre est trop long (max {max} caractères).",
153153+ contentTooLong: "Le contenu est trop long (max {max} caractères).",
154154+ messageTooLong: "Le résumé est trop long (max {max} caractères).",
151155 invalidZip: "Fichier zip invalide ou corrompu.",
152156 tooManyFiles: "Trop de fichiers dans le zip (max 100 fichiers markdown).",
153157 zipTooLarge: "Le contenu du zip dépasse la limite de 50 Mo.",