ocean-brain 0.2.2 → 0.3.1
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/dist/index.js +73 -41
- package/dist/mcp.js +587 -26
- package/package.json +6 -1
- package/scripts/postinstall-prisma.mjs +41 -0
- package/server/client/dist/assets/Calendar-DynpmB3D.js +25 -0
- package/server/client/dist/assets/Callout-tNHgPhLM.js +1 -0
- package/server/client/dist/assets/Graph-DMXLUfCv.js +1 -0
- package/server/client/dist/assets/Image-ZxF459sC.js +1 -0
- package/server/client/dist/assets/Image.es-Dnn4SMzG.js +1 -0
- package/server/client/dist/assets/ModalActionRow-B3vNjcDc.js +1 -0
- package/server/client/dist/assets/Note-PDMuj62M.js +1 -0
- package/server/client/dist/assets/Note-PEevDKBh.css +1 -0
- package/server/client/dist/assets/Reminders-ComtTLuN.js +1 -0
- package/server/client/dist/assets/{Search-BANPp2ZV.js → Search-BJeZTbVu.js} +1 -1
- package/server/client/dist/assets/SurfaceCard-DAcVMY4f.js +1 -0
- package/server/client/dist/assets/Tag-BuNCDGD5.js +1 -0
- package/server/client/dist/assets/TagNotes-ajFOP5pZ.js +1 -0
- package/server/client/dist/assets/Trash.es-DLiHgjNo.js +1 -0
- package/server/client/dist/assets/{image.api-rBH0OSWJ.js → image.api-CjJgNbC0.js} +1 -1
- package/server/client/dist/assets/index-CDrZmLC2.js +1 -0
- package/server/client/dist/assets/index-DAZMX8bQ.css +1 -0
- package/server/client/dist/assets/index-DqFkOWbi.js +205 -0
- package/server/client/dist/assets/manage-image-CVyZTDT_.js +1 -0
- package/server/client/dist/assets/manage-image-detail-Bp17tylS.js +1 -0
- package/server/client/dist/assets/mcp-BpD7VH-d.js +1 -0
- package/server/client/dist/assets/placeholder-DSf4QF4v.js +34 -0
- package/server/client/dist/assets/trash-Ceb17h_s.js +1 -0
- package/server/client/dist/assets/useReminderMutate-CzyOQ_VB.js +82 -0
- package/server/client/dist/index.html +2 -2
- package/server/dist/app.js +97 -9
- package/server/dist/app.js.map +1 -1
- package/server/dist/main.js +16 -2
- package/server/dist/main.js.map +1 -1
- package/server/dist/modules/auth-guard.js +73 -0
- package/server/dist/modules/auth-guard.js.map +1 -0
- package/server/dist/modules/auth-mode.js +51 -0
- package/server/dist/modules/auth-mode.js.map +1 -0
- package/server/dist/modules/auth.js +9 -0
- package/server/dist/modules/auth.js.map +1 -1
- package/server/dist/modules/blocknote.js +154 -2
- package/server/dist/modules/blocknote.js.map +1 -1
- package/server/dist/modules/error-handler.js +41 -0
- package/server/dist/modules/error-handler.js.map +1 -0
- package/server/dist/modules/image-upload.js +114 -0
- package/server/dist/modules/image-upload.js.map +1 -0
- package/server/dist/modules/mcp-admin.js +88 -0
- package/server/dist/modules/mcp-admin.js.map +1 -0
- package/server/dist/modules/mcp-auth.js +71 -0
- package/server/dist/modules/mcp-auth.js.map +1 -0
- package/server/dist/modules/mcp-token.js +28 -0
- package/server/dist/modules/mcp-token.js.map +1 -0
- package/server/dist/modules/note-authoring.js +153 -0
- package/server/dist/modules/note-authoring.js.map +1 -0
- package/server/dist/modules/note-cleanup.js +207 -0
- package/server/dist/modules/note-cleanup.js.map +1 -0
- package/server/dist/modules/note-snapshot.js +223 -0
- package/server/dist/modules/note-snapshot.js.map +1 -0
- package/server/dist/modules/note-trash.js +262 -0
- package/server/dist/modules/note-trash.js.map +1 -0
- package/server/dist/modules/prisma-runtime.js +46 -0
- package/server/dist/modules/prisma-runtime.js.map +1 -0
- package/server/dist/modules/recovery-retention.js +26 -0
- package/server/dist/modules/recovery-retention.js.map +1 -0
- package/server/dist/modules/remote-image.js +176 -0
- package/server/dist/modules/remote-image.js.map +1 -0
- package/server/dist/modules/tag-organization.js +66 -0
- package/server/dist/modules/tag-organization.js.map +1 -0
- package/server/dist/modules/use-async.js +2 -4
- package/server/dist/modules/use-async.js.map +1 -1
- package/server/dist/schema/note/index.js +150 -6
- package/server/dist/schema/note/index.js.map +1 -1
- package/server/dist/start.js +11 -0
- package/server/dist/start.js.map +1 -0
- package/server/dist/types/index.js.map +1 -1
- package/server/dist/urls.js +3 -2
- package/server/dist/urls.js.map +1 -1
- package/server/dist/views/auth.js +305 -0
- package/server/dist/views/auth.js.map +1 -0
- package/server/dist/views/image.js +24 -81
- package/server/dist/views/image.js.map +1 -1
- package/server/dist/views/index.js +4 -0
- package/server/dist/views/index.js.map +1 -1
- package/server/dist/views/mcp-admin.js +42 -0
- package/server/dist/views/mcp-admin.js.map +1 -0
- package/server/dist/views/note.js +124 -0
- package/server/dist/views/note.js.map +1 -0
- package/server/dist/views/tag.js +23 -0
- package/server/dist/views/tag.js.map +1 -0
- package/server/prisma/migrations/20260331183000_0010_note_snapshot/migration.sql +13 -0
- package/server/prisma/migrations/20260331195000_0011_note_trash_entities/migration.sql +35 -0
- package/server/prisma/migrations/20260331213000_0012_image_hash_unique/migration.sql +49 -0
- package/server/prisma/migrations/20260404000000_0013_mcp_admin_control/migration.sql +9 -0
- package/server/prisma/schema.prisma +66 -1
- package/server/client/dist/assets/Calendar-ST5q_8KT.js +0 -48
- package/server/client/dist/assets/Callout-CrWW3bg0.js +0 -1
- package/server/client/dist/assets/Graph-CzJjt6rP.js +0 -1
- package/server/client/dist/assets/Image-Dxg1qqZw.js +0 -1
- package/server/client/dist/assets/Image.es-DZ0H6wH1.js +0 -1
- package/server/client/dist/assets/Note-Bu7fN3B6.js +0 -6
- package/server/client/dist/assets/Note-CCvXqPzv.css +0 -1
- package/server/client/dist/assets/Plus.es-CS3WVMDq.js +0 -1
- package/server/client/dist/assets/PriorityLegend-D61iBwNW.js +0 -1
- package/server/client/dist/assets/Reminders-BjirJMOR.css +0 -1
- package/server/client/dist/assets/Reminders-DjWRombU.js +0 -1
- package/server/client/dist/assets/Tag-DbKSLTj0.js +0 -1
- package/server/client/dist/assets/TagNotes-B6s7jtL7.js +0 -1
- package/server/client/dist/assets/index-BzWwEDoL.js +0 -1
- package/server/client/dist/assets/index-CF5_ZCYN.js +0 -162
- package/server/client/dist/assets/index-DjZk2Gpg.css +0 -1
- package/server/client/dist/assets/manage-image-detail-DOx6QsCl.js +0 -1
- package/server/client/dist/assets/manage-image-g-MC6Fkc.js +0 -1
- package/server/client/dist/assets/placeholder-DV3u6MrA.js +0 -34
- package/server/client/dist/assets/useReminderMutate-CU-UinGn.js +0 -82
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import models from "../models.js";
|
|
2
|
+
import {
|
|
3
|
+
createRetentionCutoff,
|
|
4
|
+
RECOVERY_CLEANUP_BATCH_LIMIT,
|
|
5
|
+
TRASH_RETENTION_DAYS
|
|
6
|
+
} from "./recovery-retention.js";
|
|
7
|
+
class NoteRestoreConflictError extends Error {
|
|
8
|
+
constructor(message = "A live note with the same id already exists.") {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "NoteRestoreConflictError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const serializeTrashedNote = (note) => ({
|
|
14
|
+
id: String(note.id),
|
|
15
|
+
title: note.title,
|
|
16
|
+
createdAt: note.createdAt.toISOString(),
|
|
17
|
+
updatedAt: note.updatedAt.toISOString(),
|
|
18
|
+
deletedAt: note.deletedAt.toISOString(),
|
|
19
|
+
pinned: note.pinned,
|
|
20
|
+
order: note.order,
|
|
21
|
+
layout: note.layout,
|
|
22
|
+
tagNames: note.tags.map((tag) => tag.name)
|
|
23
|
+
});
|
|
24
|
+
const restoreTagIdsInContent = (content, tagIdByName) => {
|
|
25
|
+
const rewriteNodes = (nodes) => {
|
|
26
|
+
return nodes.map((node) => ({
|
|
27
|
+
...node,
|
|
28
|
+
content: node.content?.map((contentNode) => {
|
|
29
|
+
if (contentNode.type !== "tag") {
|
|
30
|
+
return contentNode;
|
|
31
|
+
}
|
|
32
|
+
const tagName = typeof contentNode.props?.tag === "string" ? contentNode.props.tag : null;
|
|
33
|
+
const restoredTagId = tagName ? tagIdByName.get(tagName) : null;
|
|
34
|
+
if (!restoredTagId) {
|
|
35
|
+
return contentNode;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
...contentNode,
|
|
39
|
+
props: {
|
|
40
|
+
...contentNode.props,
|
|
41
|
+
id: String(restoredTagId)
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
children: node.children ? rewriteNodes(node.children) : node.children
|
|
46
|
+
}));
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(content);
|
|
50
|
+
return JSON.stringify(rewriteNodes(parsed));
|
|
51
|
+
} catch {
|
|
52
|
+
return content;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const createNoteTrashService = (deps) => ({
|
|
56
|
+
listTrashedNotes: async (input) => {
|
|
57
|
+
await deps.purgeExpiredDeletedNotes(
|
|
58
|
+
createRetentionCutoff(TRASH_RETENTION_DAYS),
|
|
59
|
+
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
60
|
+
);
|
|
61
|
+
const limit = Math.max(1, Number(input?.limit ?? 25));
|
|
62
|
+
const offset = Math.max(0, Number(input?.offset ?? 0));
|
|
63
|
+
const [totalCount, notes] = await Promise.all([
|
|
64
|
+
deps.countDeletedNotes(),
|
|
65
|
+
deps.listDeletedNotes(offset, limit)
|
|
66
|
+
]);
|
|
67
|
+
return {
|
|
68
|
+
totalCount,
|
|
69
|
+
notes: notes.map(serializeTrashedNote)
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
trashNoteById: async (id) => {
|
|
73
|
+
await deps.purgeExpiredDeletedNotes(
|
|
74
|
+
createRetentionCutoff(TRASH_RETENTION_DAYS),
|
|
75
|
+
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
76
|
+
);
|
|
77
|
+
const note = await deps.findLiveNote(id);
|
|
78
|
+
if (!note) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const trashedNote = await deps.moveNoteToTrash(note);
|
|
82
|
+
return serializeTrashedNote(trashedNote);
|
|
83
|
+
},
|
|
84
|
+
restoreNoteById: async (id) => {
|
|
85
|
+
await deps.purgeExpiredDeletedNotes(
|
|
86
|
+
createRetentionCutoff(TRASH_RETENTION_DAYS),
|
|
87
|
+
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
88
|
+
);
|
|
89
|
+
const deletedNote = await deps.findDeletedNote(id);
|
|
90
|
+
if (!deletedNote) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (await deps.liveNoteExists(id)) {
|
|
94
|
+
throw new NoteRestoreConflictError();
|
|
95
|
+
}
|
|
96
|
+
return deps.restoreDeletedNote(deletedNote);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const includeDeletedNote = {
|
|
100
|
+
reminders: { orderBy: { reminderDate: "asc" } },
|
|
101
|
+
tags: { orderBy: { name: "asc" } }
|
|
102
|
+
};
|
|
103
|
+
const defaultPurgeExpiredDeletedNotes = async (before, limit) => {
|
|
104
|
+
const expiredNotes = await models.deletedNote.findMany({
|
|
105
|
+
where: { deletedAt: { lt: before } },
|
|
106
|
+
orderBy: { deletedAt: "asc" },
|
|
107
|
+
take: limit,
|
|
108
|
+
select: { id: true }
|
|
109
|
+
});
|
|
110
|
+
if (expiredNotes.length === 0) {
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
const deleted = await models.deletedNote.deleteMany({ where: { id: { in: expiredNotes.map((note) => note.id) } } });
|
|
114
|
+
return deleted.count;
|
|
115
|
+
};
|
|
116
|
+
const noteTrashService = createNoteTrashService({
|
|
117
|
+
countDeletedNotes: async () => models.deletedNote.count(),
|
|
118
|
+
findDeletedNote: async (id) => {
|
|
119
|
+
return models.deletedNote.findUnique({
|
|
120
|
+
where: { id },
|
|
121
|
+
include: includeDeletedNote
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
findLiveNote: async (id) => {
|
|
125
|
+
return models.note.findUnique({
|
|
126
|
+
where: { id },
|
|
127
|
+
include: {
|
|
128
|
+
reminders: { orderBy: { reminderDate: "asc" } },
|
|
129
|
+
tags: { orderBy: { name: "asc" } }
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
listDeletedNotes: async (skip, take) => {
|
|
134
|
+
return models.deletedNote.findMany({
|
|
135
|
+
skip,
|
|
136
|
+
take,
|
|
137
|
+
orderBy: { deletedAt: "desc" },
|
|
138
|
+
include: includeDeletedNote
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
liveNoteExists: async (id) => {
|
|
142
|
+
const note = await models.note.findUnique({
|
|
143
|
+
where: { id },
|
|
144
|
+
select: { id: true }
|
|
145
|
+
});
|
|
146
|
+
return Boolean(note);
|
|
147
|
+
},
|
|
148
|
+
purgeExpiredDeletedNotes: defaultPurgeExpiredDeletedNotes,
|
|
149
|
+
moveNoteToTrash: async (note) => {
|
|
150
|
+
return models.$transaction(async (tx) => {
|
|
151
|
+
await tx.deletedNote.create({
|
|
152
|
+
data: {
|
|
153
|
+
id: note.id,
|
|
154
|
+
title: note.title,
|
|
155
|
+
content: note.content,
|
|
156
|
+
createdAt: note.createdAt,
|
|
157
|
+
updatedAt: note.updatedAt,
|
|
158
|
+
pinned: note.pinned,
|
|
159
|
+
order: note.order,
|
|
160
|
+
layout: note.layout,
|
|
161
|
+
tags: { create: note.tags.map((tag) => ({ name: tag.name })) },
|
|
162
|
+
reminders: {
|
|
163
|
+
create: note.reminders.map((reminder) => ({
|
|
164
|
+
originalId: reminder.id,
|
|
165
|
+
reminderDate: reminder.reminderDate,
|
|
166
|
+
completed: reminder.completed,
|
|
167
|
+
priority: reminder.priority,
|
|
168
|
+
content: reminder.content,
|
|
169
|
+
createdAt: reminder.createdAt,
|
|
170
|
+
updatedAt: reminder.updatedAt
|
|
171
|
+
}))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
await tx.note.delete({ where: { id: note.id } });
|
|
176
|
+
if (note.tags.length > 0) {
|
|
177
|
+
await tx.tag.deleteMany({
|
|
178
|
+
where: {
|
|
179
|
+
id: { in: note.tags.map((tag) => tag.id) },
|
|
180
|
+
notes: { none: {} }
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return tx.deletedNote.findUniqueOrThrow({
|
|
185
|
+
where: { id: note.id },
|
|
186
|
+
include: includeDeletedNote
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
restoreDeletedNote: async (deletedNote) => {
|
|
191
|
+
return models.$transaction(async (tx) => {
|
|
192
|
+
const tagNames = Array.from(new Set(deletedNote.tags.map((tag) => tag.name).filter(Boolean)));
|
|
193
|
+
if (tagNames.length > 0) {
|
|
194
|
+
const existingTags = await tx.tag.findMany({
|
|
195
|
+
where: { name: { in: tagNames } },
|
|
196
|
+
select: { name: true }
|
|
197
|
+
});
|
|
198
|
+
const existingTagNames = new Set(existingTags.map((tag) => tag.name));
|
|
199
|
+
const missingTagNames = tagNames.filter((tagName) => !existingTagNames.has(tagName));
|
|
200
|
+
for (const tagName of missingTagNames) {
|
|
201
|
+
await tx.tag.create({ data: { name: tagName } });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const restoreTagIds = tagNames.length > 0 ? await tx.tag.findMany({
|
|
205
|
+
where: { name: { in: tagNames } },
|
|
206
|
+
select: {
|
|
207
|
+
id: true,
|
|
208
|
+
name: true
|
|
209
|
+
}
|
|
210
|
+
}) : [];
|
|
211
|
+
const restoredContent = restoreTagIds.length > 0 ? restoreTagIdsInContent(
|
|
212
|
+
deletedNote.content,
|
|
213
|
+
new Map(restoreTagIds.map((tag) => [tag.name, tag.id]))
|
|
214
|
+
) : deletedNote.content;
|
|
215
|
+
const note = await tx.note.create({
|
|
216
|
+
data: {
|
|
217
|
+
id: deletedNote.id,
|
|
218
|
+
title: deletedNote.title,
|
|
219
|
+
content: restoredContent,
|
|
220
|
+
createdAt: deletedNote.createdAt,
|
|
221
|
+
updatedAt: deletedNote.updatedAt,
|
|
222
|
+
pinned: deletedNote.pinned,
|
|
223
|
+
order: deletedNote.order,
|
|
224
|
+
layout: deletedNote.layout,
|
|
225
|
+
...restoreTagIds.length > 0 ? { tags: { connect: restoreTagIds.map((tag) => ({ id: tag.id })) } } : {},
|
|
226
|
+
...deletedNote.reminders.length > 0 ? {
|
|
227
|
+
reminders: {
|
|
228
|
+
create: deletedNote.reminders.map((reminder) => ({
|
|
229
|
+
reminderDate: reminder.reminderDate,
|
|
230
|
+
completed: reminder.completed,
|
|
231
|
+
priority: reminder.priority,
|
|
232
|
+
content: reminder.content,
|
|
233
|
+
createdAt: reminder.createdAt,
|
|
234
|
+
updatedAt: reminder.updatedAt
|
|
235
|
+
}))
|
|
236
|
+
}
|
|
237
|
+
} : {}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
await tx.deletedNote.delete({ where: { id: deletedNote.id } });
|
|
241
|
+
return note;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
const listTrashedNotes = noteTrashService.listTrashedNotes;
|
|
246
|
+
const trashNoteById = noteTrashService.trashNoteById;
|
|
247
|
+
const restoreTrashedNoteById = noteTrashService.restoreNoteById;
|
|
248
|
+
const purgeExpiredTrashedNotes = async () => {
|
|
249
|
+
return defaultPurgeExpiredDeletedNotes(
|
|
250
|
+
createRetentionCutoff(TRASH_RETENTION_DAYS),
|
|
251
|
+
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
export {
|
|
255
|
+
NoteRestoreConflictError,
|
|
256
|
+
createNoteTrashService,
|
|
257
|
+
listTrashedNotes,
|
|
258
|
+
purgeExpiredTrashedNotes,
|
|
259
|
+
restoreTrashedNoteById,
|
|
260
|
+
trashNoteById
|
|
261
|
+
};
|
|
262
|
+
//# sourceMappingURL=note-trash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/note-trash.ts"],"sourcesContent":["import models, { type NoteLayout, type ReminderPriority } from '~/models.js';\nimport {\n createRetentionCutoff,\n RECOVERY_CLEANUP_BATCH_LIMIT,\n TRASH_RETENTION_DAYS\n} from './recovery-retention.js';\n\ninterface LiveTagRecord {\n id: number;\n name: string;\n}\n\ninterface LiveReminderRecord {\n id: number;\n reminderDate: Date;\n completed: boolean;\n priority: ReminderPriority;\n content: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\ninterface LiveNoteRecord {\n id: number;\n title: string;\n content: string;\n createdAt: Date;\n updatedAt: Date;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n tags: LiveTagRecord[];\n reminders: LiveReminderRecord[];\n}\n\ninterface RestoredNoteRecord {\n id: number;\n title: string;\n content: string;\n createdAt: Date;\n updatedAt: Date;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n}\n\ninterface DeletedTagRecord {\n name: string;\n}\n\ninterface DeletedReminderRecord {\n originalId: number | null;\n reminderDate: Date;\n completed: boolean;\n priority: ReminderPriority;\n content: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\ninterface DeletedNoteRecord {\n id: number;\n title: string;\n content: string;\n createdAt: Date;\n updatedAt: Date;\n deletedAt: Date;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n tags: DeletedTagRecord[];\n reminders: DeletedReminderRecord[];\n}\n\ninterface BlockNoteNode {\n type: string;\n props?: Record<string, unknown>;\n content?: BlockNoteNode[];\n children?: BlockNoteNode[];\n}\n\nexport interface TrashedNoteSummary {\n id: string;\n title: string;\n createdAt: string;\n updatedAt: string;\n deletedAt: string;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n tagNames: string[];\n}\n\nexport interface TrashedNotesResult {\n totalCount: number;\n notes: TrashedNoteSummary[];\n}\n\nexport class NoteRestoreConflictError extends Error {\n constructor(message = 'A live note with the same id already exists.') {\n super(message);\n this.name = 'NoteRestoreConflictError';\n }\n}\n\ninterface NoteTrashServiceDeps {\n countDeletedNotes: () => Promise<number>;\n findDeletedNote: (id: number) => Promise<DeletedNoteRecord | null>;\n findLiveNote: (id: number) => Promise<LiveNoteRecord | null>;\n listDeletedNotes: (skip: number, take: number) => Promise<DeletedNoteRecord[]>;\n liveNoteExists: (id: number) => Promise<boolean>;\n moveNoteToTrash: (note: LiveNoteRecord) => Promise<DeletedNoteRecord>;\n purgeExpiredDeletedNotes: (before: Date, limit: number) => Promise<number>;\n restoreDeletedNote: (note: DeletedNoteRecord) => Promise<RestoredNoteRecord>;\n}\n\nconst serializeTrashedNote = (note: DeletedNoteRecord): TrashedNoteSummary => ({\n id: String(note.id),\n title: note.title,\n createdAt: note.createdAt.toISOString(),\n updatedAt: note.updatedAt.toISOString(),\n deletedAt: note.deletedAt.toISOString(),\n pinned: note.pinned,\n order: note.order,\n layout: note.layout,\n tagNames: note.tags.map((tag) => tag.name)\n});\n\nconst restoreTagIdsInContent = (content: string, tagIdByName: Map<string, number>) => {\n const rewriteNodes = (nodes: BlockNoteNode[]): BlockNoteNode[] => {\n return nodes.map((node) => ({\n ...node,\n content: node.content?.map((contentNode) => {\n if (contentNode.type !== 'tag') {\n return contentNode;\n }\n\n const tagName = typeof contentNode.props?.tag === 'string'\n ? contentNode.props.tag\n : null;\n const restoredTagId = tagName ? tagIdByName.get(tagName) : null;\n\n if (!restoredTagId) {\n return contentNode;\n }\n\n return {\n ...contentNode,\n props: {\n ...contentNode.props,\n id: String(restoredTagId)\n }\n };\n }),\n children: node.children ? rewriteNodes(node.children) : node.children\n }));\n };\n\n try {\n const parsed = JSON.parse(content) as BlockNoteNode[];\n return JSON.stringify(rewriteNodes(parsed));\n } catch {\n return content;\n }\n};\n\nexport const createNoteTrashService = (deps: NoteTrashServiceDeps) => ({\n listTrashedNotes: async (input?: {\n limit?: number;\n offset?: number;\n }): Promise<TrashedNotesResult> => {\n await deps.purgeExpiredDeletedNotes(\n createRetentionCutoff(TRASH_RETENTION_DAYS),\n RECOVERY_CLEANUP_BATCH_LIMIT\n );\n\n const limit = Math.max(1, Number(input?.limit ?? 25));\n const offset = Math.max(0, Number(input?.offset ?? 0));\n const [totalCount, notes] = await Promise.all([\n deps.countDeletedNotes(),\n deps.listDeletedNotes(offset, limit)\n ]);\n\n return {\n totalCount,\n notes: notes.map(serializeTrashedNote)\n };\n },\n\n trashNoteById: async (id: number): Promise<TrashedNoteSummary | null> => {\n await deps.purgeExpiredDeletedNotes(\n createRetentionCutoff(TRASH_RETENTION_DAYS),\n RECOVERY_CLEANUP_BATCH_LIMIT\n );\n\n const note = await deps.findLiveNote(id);\n\n if (!note) {\n return null;\n }\n\n const trashedNote = await deps.moveNoteToTrash(note);\n return serializeTrashedNote(trashedNote);\n },\n\n restoreNoteById: async (id: number) => {\n await deps.purgeExpiredDeletedNotes(\n createRetentionCutoff(TRASH_RETENTION_DAYS),\n RECOVERY_CLEANUP_BATCH_LIMIT\n );\n\n const deletedNote = await deps.findDeletedNote(id);\n\n if (!deletedNote) {\n return null;\n }\n\n if (await deps.liveNoteExists(id)) {\n throw new NoteRestoreConflictError();\n }\n\n return deps.restoreDeletedNote(deletedNote);\n }\n});\n\nconst includeDeletedNote = {\n reminders: { orderBy: { reminderDate: 'asc' as const } },\n tags: { orderBy: { name: 'asc' as const } }\n};\n\nconst defaultPurgeExpiredDeletedNotes = async (before: Date, limit: number) => {\n const expiredNotes = await models.deletedNote.findMany({\n where: { deletedAt: { lt: before } },\n orderBy: { deletedAt: 'asc' },\n take: limit,\n select: { id: true }\n });\n\n if (expiredNotes.length === 0) {\n return 0;\n }\n\n const deleted = await models.deletedNote.deleteMany({ where: { id: { in: expiredNotes.map((note) => note.id) } } });\n\n return deleted.count;\n};\n\nconst noteTrashService = createNoteTrashService({\n countDeletedNotes: async () => models.deletedNote.count(),\n findDeletedNote: async (id) => {\n return models.deletedNote.findUnique({\n where: { id },\n include: includeDeletedNote\n });\n },\n findLiveNote: async (id) => {\n return models.note.findUnique({\n where: { id },\n include: {\n reminders: { orderBy: { reminderDate: 'asc' } },\n tags: { orderBy: { name: 'asc' } }\n }\n });\n },\n listDeletedNotes: async (skip, take) => {\n return models.deletedNote.findMany({\n skip,\n take,\n orderBy: { deletedAt: 'desc' },\n include: includeDeletedNote\n });\n },\n liveNoteExists: async (id) => {\n const note = await models.note.findUnique({\n where: { id },\n select: { id: true }\n });\n return Boolean(note);\n },\n purgeExpiredDeletedNotes: defaultPurgeExpiredDeletedNotes,\n moveNoteToTrash: async (note) => {\n return models.$transaction(async (tx) => {\n await tx.deletedNote.create({\n data: {\n id: note.id,\n title: note.title,\n content: note.content,\n createdAt: note.createdAt,\n updatedAt: note.updatedAt,\n pinned: note.pinned,\n order: note.order,\n layout: note.layout,\n tags: { create: note.tags.map((tag) => ({ name: tag.name })) },\n reminders: {\n create: note.reminders.map((reminder) => ({\n originalId: reminder.id,\n reminderDate: reminder.reminderDate,\n completed: reminder.completed,\n priority: reminder.priority,\n content: reminder.content,\n createdAt: reminder.createdAt,\n updatedAt: reminder.updatedAt\n }))\n }\n }\n });\n\n await tx.note.delete({ where: { id: note.id } });\n\n if (note.tags.length > 0) {\n await tx.tag.deleteMany({\n where: {\n id: { in: note.tags.map((tag) => tag.id) },\n notes: { none: {} }\n }\n });\n }\n\n return tx.deletedNote.findUniqueOrThrow({\n where: { id: note.id },\n include: includeDeletedNote\n });\n });\n },\n restoreDeletedNote: async (deletedNote) => {\n return models.$transaction(async (tx) => {\n const tagNames = Array.from(new Set(deletedNote.tags.map((tag) => tag.name).filter(Boolean)));\n\n if (tagNames.length > 0) {\n const existingTags = await tx.tag.findMany({\n where: { name: { in: tagNames } },\n select: { name: true }\n });\n const existingTagNames = new Set(existingTags.map((tag) => tag.name));\n const missingTagNames = tagNames.filter((tagName) => !existingTagNames.has(tagName));\n\n for (const tagName of missingTagNames) {\n await tx.tag.create({ data: { name: tagName } });\n }\n }\n\n const restoreTagIds = tagNames.length > 0\n ? await tx.tag.findMany({\n where: { name: { in: tagNames } },\n select: {\n id: true,\n name: true\n }\n })\n : [];\n\n const restoredContent = restoreTagIds.length > 0\n ? restoreTagIdsInContent(\n deletedNote.content,\n new Map(restoreTagIds.map((tag) => [tag.name, tag.id]))\n )\n : deletedNote.content;\n\n const note = await tx.note.create({\n data: {\n id: deletedNote.id,\n title: deletedNote.title,\n content: restoredContent,\n createdAt: deletedNote.createdAt,\n updatedAt: deletedNote.updatedAt,\n pinned: deletedNote.pinned,\n order: deletedNote.order,\n layout: deletedNote.layout,\n ...(restoreTagIds.length > 0\n ? { tags: { connect: restoreTagIds.map((tag) => ({ id: tag.id })) } }\n : {}),\n ...(deletedNote.reminders.length > 0\n ? {\n reminders: {\n create: deletedNote.reminders.map((reminder) => ({\n reminderDate: reminder.reminderDate,\n completed: reminder.completed,\n priority: reminder.priority,\n content: reminder.content,\n createdAt: reminder.createdAt,\n updatedAt: reminder.updatedAt\n }))\n }\n }\n : {})\n }\n });\n\n await tx.deletedNote.delete({ where: { id: deletedNote.id } });\n\n return note;\n });\n }\n});\n\nexport const listTrashedNotes = noteTrashService.listTrashedNotes;\nexport const trashNoteById = noteTrashService.trashNoteById;\nexport const restoreTrashedNoteById = noteTrashService.restoreNoteById;\nexport const purgeExpiredTrashedNotes = async () => {\n return defaultPurgeExpiredDeletedNotes(\n createRetentionCutoff(TRASH_RETENTION_DAYS),\n RECOVERY_CLEANUP_BATCH_LIMIT\n );\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
const resolveServerRoot = () => {
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
return process.env.OCEAN_BRAIN_PACKAGE_ROOT || path.resolve(__dirname, "../..");
|
|
8
|
+
};
|
|
9
|
+
const resolvePrismaSchemaPath = () => {
|
|
10
|
+
return path.resolve(resolveServerRoot(), "prisma/schema.prisma");
|
|
11
|
+
};
|
|
12
|
+
const resolvePrismaCliEntry = () => {
|
|
13
|
+
const require2 = createRequire(import.meta.url);
|
|
14
|
+
return require2.resolve("prisma/build/index.js");
|
|
15
|
+
};
|
|
16
|
+
const createPrismaRuntime = (deps) => ({
|
|
17
|
+
runMigrateDeploy: () => {
|
|
18
|
+
deps.execFileSync(
|
|
19
|
+
process.execPath,
|
|
20
|
+
[
|
|
21
|
+
deps.resolvePrismaCliEntry(),
|
|
22
|
+
"migrate",
|
|
23
|
+
"deploy",
|
|
24
|
+
`--schema=${resolvePrismaSchemaPath()}`
|
|
25
|
+
],
|
|
26
|
+
{
|
|
27
|
+
stdio: "inherit",
|
|
28
|
+
env: { ...process.env }
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const defaultPrismaRuntime = createPrismaRuntime({
|
|
34
|
+
execFileSync,
|
|
35
|
+
resolvePrismaCliEntry
|
|
36
|
+
});
|
|
37
|
+
const runPrismaMigrateDeploy = () => {
|
|
38
|
+
defaultPrismaRuntime.runMigrateDeploy();
|
|
39
|
+
};
|
|
40
|
+
export {
|
|
41
|
+
createPrismaRuntime,
|
|
42
|
+
resolvePrismaCliEntry,
|
|
43
|
+
resolvePrismaSchemaPath,
|
|
44
|
+
runPrismaMigrateDeploy
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=prisma-runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/prisma-runtime.ts"],"sourcesContent":["import { execFileSync } from 'child_process';\nimport path from 'path';\nimport { createRequire } from 'module';\nimport { fileURLToPath } from 'url';\n\ninterface PrismaRuntimeDeps {\n execFileSync: (\n file: string,\n args: string[],\n options: {\n env: NodeJS.ProcessEnv;\n stdio: 'inherit';\n }\n ) => void;\n resolvePrismaCliEntry: () => string;\n}\n\nconst resolveServerRoot = () => {\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n return process.env.OCEAN_BRAIN_PACKAGE_ROOT || path.resolve(__dirname, '../..');\n};\n\nexport const resolvePrismaSchemaPath = () => {\n return path.resolve(resolveServerRoot(), 'prisma/schema.prisma');\n};\n\nexport const resolvePrismaCliEntry = () => {\n const require = createRequire(import.meta.url);\n return require.resolve('prisma/build/index.js');\n};\n\nexport const createPrismaRuntime = (deps: PrismaRuntimeDeps) => ({\n runMigrateDeploy: () => {\n deps.execFileSync(\n process.execPath,\n [\n deps.resolvePrismaCliEntry(),\n 'migrate',\n 'deploy',\n `--schema=${resolvePrismaSchemaPath()}`\n ],\n {\n stdio: 'inherit',\n env: { ...process.env }\n }\n );\n }\n});\n\nconst defaultPrismaRuntime = createPrismaRuntime({\n execFileSync,\n resolvePrismaCliEntry\n});\n\nexport const runPrismaMigrateDeploy = () => {\n defaultPrismaRuntime.runMigrateDeploy();\n};\n"],"mappings":"AAAA,SAAS,oBAAoB;AAC7B,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAc9B,MAAM,oBAAoB,MAAM;AAC5B,QAAM,YAAY,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAC7D,SAAO,QAAQ,IAAI,4BAA4B,KAAK,QAAQ,WAAW,OAAO;AAClF;AAEO,MAAM,0BAA0B,MAAM;AACzC,SAAO,KAAK,QAAQ,kBAAkB,GAAG,sBAAsB;AACnE;AAEO,MAAM,wBAAwB,MAAM;AACvC,QAAMA,WAAU,cAAc,YAAY,GAAG;AAC7C,SAAOA,SAAQ,QAAQ,uBAAuB;AAClD;AAEO,MAAM,sBAAsB,CAAC,UAA6B;AAAA,EAC7D,kBAAkB,MAAM;AACpB,SAAK;AAAA,MACD,QAAQ;AAAA,MACR;AAAA,QACI,KAAK,sBAAsB;AAAA,QAC3B;AAAA,QACA;AAAA,QACA,YAAY,wBAAwB,CAAC;AAAA,MACzC;AAAA,MACA;AAAA,QACI,OAAO;AAAA,QACP,KAAK,EAAE,GAAG,QAAQ,IAAI;AAAA,MAC1B;AAAA,IACJ;AAAA,EACJ;AACJ;AAEA,MAAM,uBAAuB,oBAAoB;AAAA,EAC7C;AAAA,EACA;AACJ,CAAC;AAEM,MAAM,yBAAyB,MAAM;AACxC,uBAAqB,iBAAiB;AAC1C;","names":["require"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const NOTE_SNAPSHOT_RETENTION_DAYS = 14;
|
|
2
|
+
const NOTE_SNAPSHOT_MAX_PER_NOTE = 10;
|
|
3
|
+
const NOTE_TRASH_RETENTION_DAYS = 30;
|
|
4
|
+
const RECOVERY_RETENTION_BATCH_LIMIT = 100;
|
|
5
|
+
const SNAPSHOT_RETENTION_DAYS = NOTE_SNAPSHOT_RETENTION_DAYS;
|
|
6
|
+
const SNAPSHOT_MAX_PER_NOTE = NOTE_SNAPSHOT_MAX_PER_NOTE;
|
|
7
|
+
const TRASH_RETENTION_DAYS = NOTE_TRASH_RETENTION_DAYS;
|
|
8
|
+
const RECOVERY_CLEANUP_BATCH_LIMIT = RECOVERY_RETENTION_BATCH_LIMIT;
|
|
9
|
+
const DAY_IN_MS = 24 * 60 * 60 * 1e3;
|
|
10
|
+
const resolveRetentionCutoff = (retentionDays, now = /* @__PURE__ */ new Date()) => {
|
|
11
|
+
return new Date(now.getTime() - retentionDays * DAY_IN_MS);
|
|
12
|
+
};
|
|
13
|
+
const createRetentionCutoff = resolveRetentionCutoff;
|
|
14
|
+
export {
|
|
15
|
+
NOTE_SNAPSHOT_MAX_PER_NOTE,
|
|
16
|
+
NOTE_SNAPSHOT_RETENTION_DAYS,
|
|
17
|
+
NOTE_TRASH_RETENTION_DAYS,
|
|
18
|
+
RECOVERY_CLEANUP_BATCH_LIMIT,
|
|
19
|
+
RECOVERY_RETENTION_BATCH_LIMIT,
|
|
20
|
+
SNAPSHOT_MAX_PER_NOTE,
|
|
21
|
+
SNAPSHOT_RETENTION_DAYS,
|
|
22
|
+
TRASH_RETENTION_DAYS,
|
|
23
|
+
createRetentionCutoff,
|
|
24
|
+
resolveRetentionCutoff
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=recovery-retention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/recovery-retention.ts"],"sourcesContent":["export const NOTE_SNAPSHOT_RETENTION_DAYS = 14;\nexport const NOTE_SNAPSHOT_MAX_PER_NOTE = 10;\nexport const NOTE_TRASH_RETENTION_DAYS = 30;\nexport const RECOVERY_RETENTION_BATCH_LIMIT = 100;\n\nexport const SNAPSHOT_RETENTION_DAYS = NOTE_SNAPSHOT_RETENTION_DAYS;\nexport const SNAPSHOT_MAX_PER_NOTE = NOTE_SNAPSHOT_MAX_PER_NOTE;\nexport const TRASH_RETENTION_DAYS = NOTE_TRASH_RETENTION_DAYS;\nexport const RECOVERY_CLEANUP_BATCH_LIMIT = RECOVERY_RETENTION_BATCH_LIMIT;\n\nconst DAY_IN_MS = 24 * 60 * 60 * 1000;\n\nexport const resolveRetentionCutoff = (retentionDays: number, now = new Date()) => {\n return new Date(now.getTime() - (retentionDays * DAY_IN_MS));\n};\n\nexport const createRetentionCutoff = resolveRetentionCutoff;\n"],"mappings":"AAAO,MAAM,+BAA+B;AACrC,MAAM,6BAA6B;AACnC,MAAM,4BAA4B;AAClC,MAAM,iCAAiC;AAEvC,MAAM,0BAA0B;AAChC,MAAM,wBAAwB;AAC9B,MAAM,uBAAuB;AAC7B,MAAM,+BAA+B;AAE5C,MAAM,YAAY,KAAK,KAAK,KAAK;AAE1B,MAAM,yBAAyB,CAAC,eAAuB,MAAM,oBAAI,KAAK,MAAM;AAC/E,SAAO,IAAI,KAAK,IAAI,QAAQ,IAAK,gBAAgB,SAAU;AAC/D;AAEO,MAAM,wBAAwB;","names":[]}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
class RemoteImageFetchError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
status;
|
|
6
|
+
constructor(code, status, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "RemoteImageFetchError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.status = status;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
15
|
+
const SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Map([
|
|
16
|
+
["image/png", "png"],
|
|
17
|
+
["image/jpeg", "jpg"],
|
|
18
|
+
["image/gif", "gif"],
|
|
19
|
+
["image/webp", "webp"],
|
|
20
|
+
["image/bmp", "bmp"],
|
|
21
|
+
["image/avif", "avif"]
|
|
22
|
+
]);
|
|
23
|
+
const createRemoteImageFetchError = (code, status, message) => {
|
|
24
|
+
return new RemoteImageFetchError(code, status, message);
|
|
25
|
+
};
|
|
26
|
+
const normalizeIp = (address) => {
|
|
27
|
+
return address.toLowerCase().replace(/^::ffff:/, "");
|
|
28
|
+
};
|
|
29
|
+
const isBlockedIpv4 = (address) => {
|
|
30
|
+
const octets = address.split(".").map((segment) => Number(segment));
|
|
31
|
+
if (octets.length !== 4 || octets.some((segment) => Number.isNaN(segment))) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (octets[0] === 127 || octets[0] === 10) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (octets[0] === 192 && octets[1] === 168) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return octets[0] === 169 && octets[1] === 254;
|
|
44
|
+
};
|
|
45
|
+
const isBlockedIpv6 = (address) => {
|
|
46
|
+
if (address === "::1") {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const firstSegment = address.split(":")[0];
|
|
50
|
+
if (!firstSegment) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const firstValue = Number.parseInt(firstSegment, 16);
|
|
54
|
+
if (Number.isNaN(firstValue)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return (firstValue & 65024) === 64512 || (firstValue & 65472) === 65152;
|
|
58
|
+
};
|
|
59
|
+
const isBlockedAddress = (address) => {
|
|
60
|
+
const normalizedAddress = normalizeIp(address);
|
|
61
|
+
const family = isIP(normalizedAddress);
|
|
62
|
+
if (family === 4) {
|
|
63
|
+
return isBlockedIpv4(normalizedAddress);
|
|
64
|
+
}
|
|
65
|
+
if (family === 6) {
|
|
66
|
+
return isBlockedIpv6(normalizedAddress);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
const assertSafeRemoteUrl = async (src, lookupHostname) => {
|
|
71
|
+
let remoteUrl;
|
|
72
|
+
try {
|
|
73
|
+
remoteUrl = new URL(src);
|
|
74
|
+
} catch {
|
|
75
|
+
throw createRemoteImageFetchError("INVALID_REMOTE_URL", 400, "Remote image URL is invalid.");
|
|
76
|
+
}
|
|
77
|
+
if (!["http:", "https:"].includes(remoteUrl.protocol)) {
|
|
78
|
+
throw createRemoteImageFetchError("INVALID_REMOTE_URL", 400, "Remote image URL must use http or https.");
|
|
79
|
+
}
|
|
80
|
+
if (!remoteUrl.hostname || remoteUrl.username || remoteUrl.password) {
|
|
81
|
+
throw createRemoteImageFetchError("INVALID_REMOTE_URL", 400, "Remote image URL is invalid.");
|
|
82
|
+
}
|
|
83
|
+
if (remoteUrl.port && !(remoteUrl.protocol === "http:" && remoteUrl.port === "80" || remoteUrl.protocol === "https:" && remoteUrl.port === "443")) {
|
|
84
|
+
throw createRemoteImageFetchError("REMOTE_URL_BLOCKED", 403, "Remote image host is not allowed.");
|
|
85
|
+
}
|
|
86
|
+
if (remoteUrl.hostname.toLowerCase() === "localhost") {
|
|
87
|
+
throw createRemoteImageFetchError("REMOTE_URL_BLOCKED", 403, "Remote image host is not allowed.");
|
|
88
|
+
}
|
|
89
|
+
if (isIP(remoteUrl.hostname)) {
|
|
90
|
+
if (isBlockedAddress(remoteUrl.hostname)) {
|
|
91
|
+
throw createRemoteImageFetchError("REMOTE_URL_BLOCKED", 403, "Remote image host is not allowed.");
|
|
92
|
+
}
|
|
93
|
+
return remoteUrl;
|
|
94
|
+
}
|
|
95
|
+
let addresses;
|
|
96
|
+
try {
|
|
97
|
+
addresses = await lookupHostname(remoteUrl.hostname);
|
|
98
|
+
} catch {
|
|
99
|
+
throw createRemoteImageFetchError("REMOTE_FETCH_FAILED", 502, "Remote image could not be fetched.");
|
|
100
|
+
}
|
|
101
|
+
if (addresses.length === 0 || addresses.some(({ address }) => isBlockedAddress(address))) {
|
|
102
|
+
throw createRemoteImageFetchError("REMOTE_URL_BLOCKED", 403, "Remote image host is not allowed.");
|
|
103
|
+
}
|
|
104
|
+
return remoteUrl;
|
|
105
|
+
};
|
|
106
|
+
const readImageBody = async (response, maxBytes) => {
|
|
107
|
+
if (!response.body) {
|
|
108
|
+
throw createRemoteImageFetchError("REMOTE_FETCH_FAILED", 502, "Remote image response body was empty.");
|
|
109
|
+
}
|
|
110
|
+
const reader = response.body.getReader();
|
|
111
|
+
const chunks = [];
|
|
112
|
+
let totalBytes = 0;
|
|
113
|
+
while (true) {
|
|
114
|
+
const { done, value } = await reader.read();
|
|
115
|
+
if (done) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
totalBytes += value.byteLength;
|
|
119
|
+
if (totalBytes > maxBytes) {
|
|
120
|
+
await reader.cancel();
|
|
121
|
+
throw createRemoteImageFetchError("REMOTE_IMAGE_TOO_LARGE", 413, "Remote image exceeded the size limit.");
|
|
122
|
+
}
|
|
123
|
+
chunks.push(Buffer.from(value));
|
|
124
|
+
}
|
|
125
|
+
return Buffer.concat(chunks);
|
|
126
|
+
};
|
|
127
|
+
const fetchRemoteImage = async (src, options = {}) => {
|
|
128
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
129
|
+
const lookupHostname = options.lookupHostname ?? ((hostname) => {
|
|
130
|
+
return dnsLookup(hostname, {
|
|
131
|
+
all: true,
|
|
132
|
+
verbatim: true
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
136
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
137
|
+
const remoteUrl = await assertSafeRemoteUrl(src, lookupHostname);
|
|
138
|
+
let response;
|
|
139
|
+
try {
|
|
140
|
+
response = await fetchImpl(remoteUrl, {
|
|
141
|
+
method: "GET",
|
|
142
|
+
redirect: "error",
|
|
143
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error instanceof RemoteImageFetchError) {
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
|
|
150
|
+
throw createRemoteImageFetchError("REMOTE_FETCH_TIMEOUT", 504, "Remote image fetch timed out.");
|
|
151
|
+
}
|
|
152
|
+
throw createRemoteImageFetchError("REMOTE_FETCH_FAILED", 502, "Remote image could not be fetched.");
|
|
153
|
+
}
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw createRemoteImageFetchError("REMOTE_FETCH_FAILED", 502, "Remote image could not be fetched.");
|
|
156
|
+
}
|
|
157
|
+
const contentTypeHeader = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
158
|
+
const extension = SUPPORTED_IMAGE_TYPES.get(contentTypeHeader);
|
|
159
|
+
if (!extension) {
|
|
160
|
+
throw createRemoteImageFetchError(
|
|
161
|
+
"REMOTE_IMAGE_UNSUPPORTED_CONTENT_TYPE",
|
|
162
|
+
415,
|
|
163
|
+
"Remote image content type is not supported."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
buffer: await readImageBody(response, maxBytes),
|
|
168
|
+
contentType: contentTypeHeader,
|
|
169
|
+
extension
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
export {
|
|
173
|
+
RemoteImageFetchError,
|
|
174
|
+
fetchRemoteImage
|
|
175
|
+
};
|
|
176
|
+
//# sourceMappingURL=remote-image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/remote-image.ts"],"sourcesContent":["import { lookup as dnsLookup } from 'node:dns/promises';\nimport { isIP } from 'node:net';\n\nexport interface RemoteImage {\n buffer: Buffer;\n contentType: string;\n extension: string;\n}\n\nexport interface RemoteImageFetchOptions {\n fetchImpl?: typeof fetch;\n lookupHostname?: (hostname: string) => Promise<Array<{ address: string }>>;\n maxBytes?: number;\n timeoutMs?: number;\n}\n\nexport class RemoteImageFetchError extends Error {\n public readonly code: string;\n public readonly status: number;\n\n constructor(code: string, status: number, message: string) {\n super(message);\n this.name = 'RemoteImageFetchError';\n this.code = code;\n this.status = status;\n }\n}\n\nconst DEFAULT_MAX_BYTES = 10 * 1024 * 1024;\nconst DEFAULT_TIMEOUT_MS = 5_000;\nconst SUPPORTED_IMAGE_TYPES = new Map([\n ['image/png', 'png'],\n ['image/jpeg', 'jpg'],\n ['image/gif', 'gif'],\n ['image/webp', 'webp'],\n ['image/bmp', 'bmp'],\n ['image/avif', 'avif']\n]);\n\nconst createRemoteImageFetchError = (code: string, status: number, message: string) => {\n return new RemoteImageFetchError(code, status, message);\n};\n\nconst normalizeIp = (address: string) => {\n return address.toLowerCase().replace(/^::ffff:/, '');\n};\n\nconst isBlockedIpv4 = (address: string) => {\n const octets = address.split('.').map((segment) => Number(segment));\n\n if (octets.length !== 4 || octets.some((segment) => Number.isNaN(segment))) {\n return false;\n }\n\n if (octets[0] === 127 || octets[0] === 10) {\n return true;\n }\n\n if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {\n return true;\n }\n\n if (octets[0] === 192 && octets[1] === 168) {\n return true;\n }\n\n return octets[0] === 169 && octets[1] === 254;\n};\n\nconst isBlockedIpv6 = (address: string) => {\n if (address === '::1') {\n return true;\n }\n\n const firstSegment = address.split(':')[0];\n\n if (!firstSegment) {\n return false;\n }\n\n const firstValue = Number.parseInt(firstSegment, 16);\n\n if (Number.isNaN(firstValue)) {\n return false;\n }\n\n return (firstValue & 0xfe00) === 0xfc00 || (firstValue & 0xffc0) === 0xfe80;\n};\n\nconst isBlockedAddress = (address: string) => {\n const normalizedAddress = normalizeIp(address);\n const family = isIP(normalizedAddress);\n\n if (family === 4) {\n return isBlockedIpv4(normalizedAddress);\n }\n\n if (family === 6) {\n return isBlockedIpv6(normalizedAddress);\n }\n\n return false;\n};\n\nconst assertSafeRemoteUrl = async (\n src: string,\n lookupHostname: NonNullable<RemoteImageFetchOptions['lookupHostname']>\n) => {\n let remoteUrl: URL;\n\n try {\n remoteUrl = new URL(src);\n } catch {\n throw createRemoteImageFetchError('INVALID_REMOTE_URL', 400, 'Remote image URL is invalid.');\n }\n\n if (!['http:', 'https:'].includes(remoteUrl.protocol)) {\n throw createRemoteImageFetchError('INVALID_REMOTE_URL', 400, 'Remote image URL must use http or https.');\n }\n\n if (!remoteUrl.hostname || remoteUrl.username || remoteUrl.password) {\n throw createRemoteImageFetchError('INVALID_REMOTE_URL', 400, 'Remote image URL is invalid.');\n }\n\n if (\n remoteUrl.port\n && !(\n (remoteUrl.protocol === 'http:' && remoteUrl.port === '80')\n || (remoteUrl.protocol === 'https:' && remoteUrl.port === '443')\n )\n ) {\n throw createRemoteImageFetchError('REMOTE_URL_BLOCKED', 403, 'Remote image host is not allowed.');\n }\n\n if (remoteUrl.hostname.toLowerCase() === 'localhost') {\n throw createRemoteImageFetchError('REMOTE_URL_BLOCKED', 403, 'Remote image host is not allowed.');\n }\n\n if (isIP(remoteUrl.hostname)) {\n if (isBlockedAddress(remoteUrl.hostname)) {\n throw createRemoteImageFetchError('REMOTE_URL_BLOCKED', 403, 'Remote image host is not allowed.');\n }\n\n return remoteUrl;\n }\n\n let addresses: Array<{ address: string }>;\n\n try {\n addresses = await lookupHostname(remoteUrl.hostname);\n } catch {\n throw createRemoteImageFetchError('REMOTE_FETCH_FAILED', 502, 'Remote image could not be fetched.');\n }\n\n if (addresses.length === 0 || addresses.some(({ address }) => isBlockedAddress(address))) {\n throw createRemoteImageFetchError('REMOTE_URL_BLOCKED', 403, 'Remote image host is not allowed.');\n }\n\n return remoteUrl;\n};\n\nconst readImageBody = async (response: Response, maxBytes: number) => {\n if (!response.body) {\n throw createRemoteImageFetchError('REMOTE_FETCH_FAILED', 502, 'Remote image response body was empty.');\n }\n\n const reader = response.body.getReader();\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n totalBytes += value.byteLength;\n\n if (totalBytes > maxBytes) {\n await reader.cancel();\n throw createRemoteImageFetchError('REMOTE_IMAGE_TOO_LARGE', 413, 'Remote image exceeded the size limit.');\n }\n\n chunks.push(Buffer.from(value));\n }\n\n return Buffer.concat(chunks);\n};\n\nexport const fetchRemoteImage = async (\n src: string,\n options: RemoteImageFetchOptions = {}\n): Promise<RemoteImage> => {\n const fetchImpl = options.fetchImpl ?? fetch;\n const lookupHostname = options.lookupHostname ?? ((hostname: string) => {\n return dnsLookup(hostname, {\n all: true,\n verbatim: true\n });\n });\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const remoteUrl = await assertSafeRemoteUrl(src, lookupHostname);\n\n let response: Response;\n\n try {\n response = await fetchImpl(remoteUrl, {\n method: 'GET',\n redirect: 'error',\n signal: AbortSignal.timeout(timeoutMs)\n });\n } catch (error) {\n if (error instanceof RemoteImageFetchError) {\n throw error;\n }\n\n if (\n error instanceof Error\n && (error.name === 'TimeoutError' || error.name === 'AbortError')\n ) {\n throw createRemoteImageFetchError('REMOTE_FETCH_TIMEOUT', 504, 'Remote image fetch timed out.');\n }\n\n throw createRemoteImageFetchError('REMOTE_FETCH_FAILED', 502, 'Remote image could not be fetched.');\n }\n\n if (!response.ok) {\n throw createRemoteImageFetchError('REMOTE_FETCH_FAILED', 502, 'Remote image could not be fetched.');\n }\n\n const contentTypeHeader = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() ?? '';\n const extension = SUPPORTED_IMAGE_TYPES.get(contentTypeHeader);\n\n if (!extension) {\n throw createRemoteImageFetchError(\n 'REMOTE_IMAGE_UNSUPPORTED_CONTENT_TYPE',\n 415,\n 'Remote image content type is not supported.'\n );\n }\n\n return {\n buffer: await readImageBody(response, maxBytes),\n contentType: contentTypeHeader,\n extension\n };\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import models from "../models.js";
|
|
2
|
+
class InvalidTagNameError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "InvalidTagNameError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const normalizeTagName = (name) => {
|
|
9
|
+
const trimmedName = name.trim();
|
|
10
|
+
if (!trimmedName) {
|
|
11
|
+
throw new InvalidTagNameError("A tag name is required.");
|
|
12
|
+
}
|
|
13
|
+
const normalizedName = trimmedName.startsWith("@") ? trimmedName : `@${trimmedName}`;
|
|
14
|
+
if (normalizedName === "@" || /\s/.test(normalizedName.slice(1))) {
|
|
15
|
+
throw new InvalidTagNameError("Tag names must be a single token like @project.");
|
|
16
|
+
}
|
|
17
|
+
return normalizedName;
|
|
18
|
+
};
|
|
19
|
+
const serializeTag = (tag) => ({
|
|
20
|
+
id: String(tag.id),
|
|
21
|
+
name: tag.name,
|
|
22
|
+
createdAt: tag.createdAt.toISOString(),
|
|
23
|
+
updatedAt: tag.updatedAt.toISOString()
|
|
24
|
+
});
|
|
25
|
+
const createTagOrganizationService = (deps) => {
|
|
26
|
+
return {
|
|
27
|
+
ensureTag: async (name) => {
|
|
28
|
+
const normalizedName = normalizeTagName(name);
|
|
29
|
+
const existingTags = await deps.findTagsByName(normalizedName);
|
|
30
|
+
if (existingTags.length > 0) {
|
|
31
|
+
return {
|
|
32
|
+
created: false,
|
|
33
|
+
normalizedName,
|
|
34
|
+
tag: serializeTag(existingTags[0])
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const createdTag = await deps.createTag(normalizedName);
|
|
38
|
+
return {
|
|
39
|
+
created: true,
|
|
40
|
+
normalizedName,
|
|
41
|
+
tag: serializeTag(createdTag)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const defaultTagOrganizationService = createTagOrganizationService({
|
|
47
|
+
createTag: async (name) => {
|
|
48
|
+
return models.tag.create({ data: { name } });
|
|
49
|
+
},
|
|
50
|
+
findTagsByName: async (name) => {
|
|
51
|
+
return models.tag.findMany({
|
|
52
|
+
where: { name },
|
|
53
|
+
orderBy: { createdAt: "asc" }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const ensureTagByName = async (name) => {
|
|
58
|
+
return defaultTagOrganizationService.ensureTag(name);
|
|
59
|
+
};
|
|
60
|
+
export {
|
|
61
|
+
InvalidTagNameError,
|
|
62
|
+
createTagOrganizationService,
|
|
63
|
+
ensureTagByName,
|
|
64
|
+
normalizeTagName
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=tag-organization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/tag-organization.ts"],"sourcesContent":["import models from '~/models.js';\n\ninterface TagRecord {\n id: number;\n name: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\ninterface TagOrganizationDeps {\n createTag: (name: string) => Promise<TagRecord>;\n findTagsByName: (name: string) => Promise<TagRecord[]>;\n}\n\nexport interface TagOrganizationResult {\n created: boolean;\n normalizedName: string;\n tag: {\n id: string;\n name: string;\n createdAt: string;\n updatedAt: string;\n };\n}\n\nexport class InvalidTagNameError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidTagNameError';\n }\n}\n\nexport const normalizeTagName = (name: string) => {\n const trimmedName = name.trim();\n\n if (!trimmedName) {\n throw new InvalidTagNameError('A tag name is required.');\n }\n\n const normalizedName = trimmedName.startsWith('@')\n ? trimmedName\n : `@${trimmedName}`;\n\n if (normalizedName === '@' || /\\s/.test(normalizedName.slice(1))) {\n throw new InvalidTagNameError('Tag names must be a single token like @project.');\n }\n\n return normalizedName;\n};\n\nconst serializeTag = (tag: TagRecord) => ({\n id: String(tag.id),\n name: tag.name,\n createdAt: tag.createdAt.toISOString(),\n updatedAt: tag.updatedAt.toISOString()\n});\n\nexport const createTagOrganizationService = (deps: TagOrganizationDeps) => {\n return {\n ensureTag: async (name: string): Promise<TagOrganizationResult> => {\n const normalizedName = normalizeTagName(name);\n const existingTags = await deps.findTagsByName(normalizedName);\n\n if (existingTags.length > 0) {\n return {\n created: false,\n normalizedName,\n tag: serializeTag(existingTags[0])\n };\n }\n\n const createdTag = await deps.createTag(normalizedName);\n return {\n created: true,\n normalizedName,\n tag: serializeTag(createdTag)\n };\n }\n };\n};\n\nconst defaultTagOrganizationService = createTagOrganizationService({\n createTag: async (name) => {\n return models.tag.create({ data: { name } });\n },\n findTagsByName: async (name) => {\n return models.tag.findMany({\n where: { name },\n orderBy: { createdAt: 'asc' }\n });\n }\n});\n\nexport const ensureTagByName = async (name: string) => {\n return defaultTagOrganizationService.ensureTag(name);\n};\n"],"mappings":"AAAA,OAAO,YAAY;AAyBZ,MAAM,4BAA4B,MAAM;AAAA,EAC3C,YAAY,SAAiB;AACzB,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EAChB;AACJ;AAEO,MAAM,mBAAmB,CAAC,SAAiB;AAC9C,QAAM,cAAc,KAAK,KAAK;AAE9B,MAAI,CAAC,aAAa;AACd,UAAM,IAAI,oBAAoB,yBAAyB;AAAA,EAC3D;AAEA,QAAM,iBAAiB,YAAY,WAAW,GAAG,IAC3C,cACA,IAAI,WAAW;AAErB,MAAI,mBAAmB,OAAO,KAAK,KAAK,eAAe,MAAM,CAAC,CAAC,GAAG;AAC9D,UAAM,IAAI,oBAAoB,iDAAiD;AAAA,EACnF;AAEA,SAAO;AACX;AAEA,MAAM,eAAe,CAAC,SAAoB;AAAA,EACtC,IAAI,OAAO,IAAI,EAAE;AAAA,EACjB,MAAM,IAAI;AAAA,EACV,WAAW,IAAI,UAAU,YAAY;AAAA,EACrC,WAAW,IAAI,UAAU,YAAY;AACzC;AAEO,MAAM,+BAA+B,CAAC,SAA8B;AACvE,SAAO;AAAA,IACH,WAAW,OAAO,SAAiD;AAC/D,YAAM,iBAAiB,iBAAiB,IAAI;AAC5C,YAAM,eAAe,MAAM,KAAK,eAAe,cAAc;AAE7D,UAAI,aAAa,SAAS,GAAG;AACzB,eAAO;AAAA,UACH,SAAS;AAAA,UACT;AAAA,UACA,KAAK,aAAa,aAAa,CAAC,CAAC;AAAA,QACrC;AAAA,MACJ;AAEA,YAAM,aAAa,MAAM,KAAK,UAAU,cAAc;AACtD,aAAO;AAAA,QACH,SAAS;AAAA,QACT;AAAA,QACA,KAAK,aAAa,UAAU;AAAA,MAChC;AAAA,IACJ;AAAA,EACJ;AACJ;AAEA,MAAM,gCAAgC,6BAA6B;AAAA,EAC/D,WAAW,OAAO,SAAS;AACvB,WAAO,OAAO,IAAI,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,EAC/C;AAAA,EACA,gBAAgB,OAAO,SAAS;AAC5B,WAAO,OAAO,IAAI,SAAS;AAAA,MACvB,OAAO,EAAE,KAAK;AAAA,MACd,SAAS,EAAE,WAAW,MAAM;AAAA,IAChC,CAAC;AAAA,EACL;AACJ,CAAC;AAEM,MAAM,kBAAkB,OAAO,SAAiB;AACnD,SAAO,8BAA8B,UAAU,IAAI;AACvD;","names":[]}
|