ocean-brain 0.4.0 → 0.4.2

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 (71) hide show
  1. package/dist/index.js +5 -0
  2. package/package.json +4 -1
  3. package/server/client/dist/assets/{Calendar-CSXEuWmQ.js → Calendar-DI0PhQTq.js} +1 -1
  4. package/server/client/dist/assets/{Callout-Ap1OcxWN.js → Callout-BAqbXI0L.js} +1 -1
  5. package/server/client/dist/assets/{Graph-C0Ax9qjI.js → Graph-Dhuog4tc.js} +1 -1
  6. package/server/client/dist/assets/{Image-CSbcG-5a.js → Image-DlJ4MDo3.js} +1 -1
  7. package/server/client/dist/assets/{Image.es-Fbe7aDDo.js → Image.es-COwqkQuh.js} +1 -1
  8. package/server/client/dist/assets/Note-ChUWU3Su.js +21 -0
  9. package/server/client/dist/assets/Plus.es-KeV96R_e.js +1 -0
  10. package/server/client/dist/assets/Reminders-D7MCTNxb.js +1 -0
  11. package/server/client/dist/assets/Search-B-fwTr5y.js +1 -0
  12. package/server/client/dist/assets/{SurfaceCard-Br6TjX5W.js → SurfaceCard-CHjcCuqZ.js} +1 -1
  13. package/server/client/dist/assets/Tag-BbV2z-Db.js +1 -0
  14. package/server/client/dist/assets/TagNotes-2HKF09Jm.js +1 -0
  15. package/server/client/dist/assets/{Trash.es-BfCEiL3-.js → Trash.es-BqRTZPrI.js} +1 -1
  16. package/server/client/dist/assets/ViewNotes-TfM3Uzbp.js +1 -0
  17. package/server/client/dist/assets/Views-CUwtnfa8.js +1 -0
  18. package/server/client/dist/assets/{image.api-DGXlQ_Ca.js → image.api-CMdQbCyN.js} +1 -1
  19. package/server/client/dist/assets/index-B4l2UvuC.js +232 -0
  20. package/server/client/dist/assets/{index-Bm9l_L20.css → index-D2gRB1ig.css} +1 -1
  21. package/server/client/dist/assets/{index-B28TO4FC.js → index-zhxZjg7V.js} +1 -1
  22. package/server/client/dist/assets/manage-image-DdXb5Bkj.js +1 -0
  23. package/server/client/dist/assets/{manage-image-detail-hk6jWyiB.js → manage-image-detail-CrrXCQIX.js} +1 -1
  24. package/server/client/dist/assets/{mcp-CSjmBjFR.js → mcp-DqGLZv90.js} +1 -1
  25. package/server/client/dist/assets/{note-vendor-5N5mRkIT.js → note-vendor-B0t5F04o.js} +3 -3
  26. package/server/client/dist/assets/{placeholder-Bmiowo28.js → placeholder-BXLtvdtN.js} +4 -4
  27. package/server/client/dist/assets/trash-3eUAjFRh.js +1 -0
  28. package/server/client/dist/assets/{useReminderMutate-DsIvVpCQ.js → useReminderMutate-v1EXqM90.js} +2 -2
  29. package/server/client/dist/assets/{view-dashboard-CP8nhfxQ.js → view-dashboard-T6gmohr-.js} +1 -1
  30. package/server/client/dist/index.html +3 -3
  31. package/server/dist/app.js +2 -2
  32. package/server/dist/app.js.map +1 -1
  33. package/server/dist/features/auth/http/api.js +2 -5
  34. package/server/dist/features/auth/http/api.js.map +1 -1
  35. package/server/dist/features/auth/http/login-page.js +11 -4
  36. package/server/dist/features/auth/http/login-page.js.map +1 -1
  37. package/server/dist/features/auth/http/pages.js +4 -3
  38. package/server/dist/features/auth/http/pages.js.map +1 -1
  39. package/server/dist/features/auth/service.js +11 -42
  40. package/server/dist/features/auth/service.js.map +1 -1
  41. package/server/dist/features/note/graphql/note.query.resolver.js +4 -1
  42. package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -1
  43. package/server/dist/features/note/graphql/note.type-defs.js +3 -0
  44. package/server/dist/features/note/graphql/note.type-defs.js.map +1 -1
  45. package/server/dist/features/note/services/trash.js +37 -12
  46. package/server/dist/features/note/services/trash.js.map +1 -1
  47. package/server/dist/modules/auth-guard.js +31 -19
  48. package/server/dist/modules/auth-guard.js.map +1 -1
  49. package/server/dist/modules/auth-mode.js +16 -39
  50. package/server/dist/modules/auth-mode.js.map +1 -1
  51. package/server/dist/modules/error-handler.js +7 -0
  52. package/server/dist/modules/error-handler.js.map +1 -1
  53. package/server/dist/modules/rate-limit.js +18 -0
  54. package/server/dist/modules/rate-limit.js.map +1 -0
  55. package/server/dist/routes/api.js +2 -1
  56. package/server/dist/routes/api.js.map +1 -1
  57. package/server/dist/routes/auth-pages.js +2 -1
  58. package/server/dist/routes/auth-pages.js.map +1 -1
  59. package/server/dist/routes/client.js +2 -2
  60. package/server/dist/routes/client.js.map +1 -1
  61. package/server/client/dist/assets/ModalActionRow-DjI5b4bl.js +0 -1
  62. package/server/client/dist/assets/Note-BY6WgYLq.js +0 -1
  63. package/server/client/dist/assets/Reminders-vRksvSQE.js +0 -1
  64. package/server/client/dist/assets/Search-Cq4tuiJO.js +0 -1
  65. package/server/client/dist/assets/Tag-ARDLA-Pb.js +0 -1
  66. package/server/client/dist/assets/TagNotes-CDGVrCJU.js +0 -1
  67. package/server/client/dist/assets/ViewNotes-ZBOQ1adH.js +0 -1
  68. package/server/client/dist/assets/Views-DQHPfdwM.js +0 -1
  69. package/server/client/dist/assets/index-Bsx4P7EA.js +0 -210
  70. package/server/client/dist/assets/manage-image-Dg297A_h.js +0 -1
  71. package/server/client/dist/assets/trash-CM7SbVtW.js +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/features/note/graphql/note.type-defs.ts"],"sourcesContent":["import { gql } from '~/modules/graphql.js';\n\nexport const noteType = gql`\n input PaginationInput {\n limit: Int!\n offset: Int!\n }\n\n input SearchFilterInput {\n query: String!\n sortBy: String\n sortOrder: String\n pinnedFirst: Boolean\n }\n\n input DateRangeInput {\n start: String!\n end: String!\n }\n\n enum NoteLayout {\n narrow\n wide\n full\n }\n\n enum TagMatchMode {\n and\n or\n }\n\n input NoteInput {\n title: String\n content: String\n layout: NoteLayout\n }\n\n input NoteOrderInput {\n id: ID!\n order: Int!\n }\n\n type Tag {\n id: ID!\n name: String!\n createdAt: String!\n updatedAt: String!\n }\n\n type Note {\n id: ID!\n title: String!\n content: String!\n contentAsMarkdown: String!\n createdAt: String!\n updatedAt: String!\n pinned: Boolean!\n order: Int!\n layout: NoteLayout!\n tags: [Tag!]!\n }\n\n type Notes {\n totalCount: Int!\n notes: [Note!]!\n }\n\n type NoteCleanupBackReference {\n id: ID!\n title: String!\n }\n\n type NoteCleanupCandidate {\n id: ID!\n title: String!\n updatedAt: String!\n pinned: Boolean!\n tagNames: [String!]!\n reminderCount: Int!\n backReferenceCount: Int!\n matchedTerms: [String!]!\n reasons: [String!]!\n requiresForce: Boolean!\n forceReasons: [String!]!\n }\n\n type NoteCleanupPreview {\n id: ID!\n title: String!\n updatedAt: String!\n pinned: Boolean!\n tagNames: [String!]!\n reminderCount: Int!\n backReferences: [NoteCleanupBackReference!]!\n orphanedTagNames: [String!]!\n requiresForce: Boolean!\n forceReasons: [String!]!\n }\n\n type NoteSnapshotMeta {\n entrypoint: String\n label: String\n }\n\n type NoteSnapshot {\n id: ID!\n title: String!\n createdAt: String!\n meta: NoteSnapshotMeta!\n }\n\n type DeletedNote {\n id: ID!\n title: String!\n createdAt: String!\n updatedAt: String!\n deletedAt: String!\n pinned: Boolean!\n order: Int!\n layout: NoteLayout!\n tagNames: [String!]!\n }\n\n type DeletedNotes {\n totalCount: Int!\n notes: [DeletedNote!]!\n }\n`;\n\nexport const noteQuery = gql`\n type GraphNode {\n id: ID!\n title: String!\n connections: Int!\n }\n\n type GraphLink {\n source: ID!\n target: ID!\n }\n\n type NoteGraph {\n nodes: [GraphNode!]!\n links: [GraphLink!]!\n }\n\n type Query {\n allNotes(searchFilter: SearchFilterInput, pagination: PaginationInput): Notes!\n tagNotes(searchFilter: SearchFilterInput, pagination: PaginationInput): Notes!\n notesByTagNames(tagNames: [String!]!, mode: TagMatchMode!, pagination: PaginationInput): Notes!\n notesInDateRange(dateRange: DateRangeInput): [Note!]!\n pinnedNotes: [Note!]!\n imageNotes(src: String!): [Note!]!\n backReferences(id: ID!): [Note]!\n note(id: ID!): Note!\n noteCleanupCandidates(query: String, pagination: PaginationInput): [NoteCleanupCandidate!]!\n noteCleanupPreview(id: ID!): NoteCleanupPreview\n noteSnapshots(id: ID!, limit: Int): [NoteSnapshot!]!\n trashedNotes(pagination: PaginationInput): DeletedNotes!\n noteGraph: NoteGraph!\n }\n`;\n\nexport const noteMutation = gql`\n type Mutation {\n createNote(note: NoteInput!): Note!\n updateNote(id: ID!, note: NoteInput!, editSessionId: String): Note!\n deleteNote(id: ID!): Boolean!\n restoreNoteSnapshot(id: ID!): Note!\n restoreTrashedNote(id: ID!): Note!\n purgeTrashedNote(id: ID!): Boolean!\n pinNote(id: ID!, pinned: Boolean!): Note!\n reorderNotes(notes: [NoteOrderInput!]!): [Note!]!\n }\n`;\n\nexport const noteTypeDefs = `\n ${noteType}\n ${noteQuery}\n ${noteMutation}\n`;\n"],"mappings":"AAAA,SAAS,WAAW;AAEb,MAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+HjB,MAAM,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkClB,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAarB,MAAM,eAAe;AAAA,MACtB,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA;","names":[]}
1
+ {"version":3,"sources":["../../../../src/features/note/graphql/note.type-defs.ts"],"sourcesContent":["import { gql } from '~/modules/graphql.js';\n\nexport const noteType = gql`\n input PaginationInput {\n limit: Int!\n offset: Int!\n }\n\n input SearchFilterInput {\n query: String!\n sortBy: String\n sortOrder: String\n pinnedFirst: Boolean\n }\n\n input DateRangeInput {\n start: String!\n end: String!\n }\n\n enum NoteLayout {\n narrow\n wide\n full\n }\n\n enum TagMatchMode {\n and\n or\n }\n\n input NoteInput {\n title: String\n content: String\n layout: NoteLayout\n }\n\n input NoteOrderInput {\n id: ID!\n order: Int!\n }\n\n type Tag {\n id: ID!\n name: String!\n createdAt: String!\n updatedAt: String!\n }\n\n type Note {\n id: ID!\n title: String!\n content: String!\n contentAsMarkdown: String!\n createdAt: String!\n updatedAt: String!\n pinned: Boolean!\n order: Int!\n layout: NoteLayout!\n tags: [Tag!]!\n }\n\n type Notes {\n totalCount: Int!\n notes: [Note!]!\n }\n\n type NoteCleanupBackReference {\n id: ID!\n title: String!\n }\n\n type NoteCleanupCandidate {\n id: ID!\n title: String!\n updatedAt: String!\n pinned: Boolean!\n tagNames: [String!]!\n reminderCount: Int!\n backReferenceCount: Int!\n matchedTerms: [String!]!\n reasons: [String!]!\n requiresForce: Boolean!\n forceReasons: [String!]!\n }\n\n type NoteCleanupPreview {\n id: ID!\n title: String!\n updatedAt: String!\n pinned: Boolean!\n tagNames: [String!]!\n reminderCount: Int!\n backReferences: [NoteCleanupBackReference!]!\n orphanedTagNames: [String!]!\n requiresForce: Boolean!\n forceReasons: [String!]!\n }\n\n type NoteSnapshotMeta {\n entrypoint: String\n label: String\n }\n\n type NoteSnapshot {\n id: ID!\n title: String!\n createdAt: String!\n meta: NoteSnapshotMeta!\n }\n\n type DeletedNote {\n id: ID!\n title: String!\n createdAt: String!\n updatedAt: String!\n deletedAt: String!\n contentPreview: String!\n contentAsMarkdown: String\n pinned: Boolean!\n order: Int!\n layout: NoteLayout!\n tagNames: [String!]!\n }\n\n type DeletedNotes {\n totalCount: Int!\n notes: [DeletedNote!]!\n }\n`;\n\nexport const noteQuery = gql`\n type GraphNode {\n id: ID!\n title: String!\n connections: Int!\n }\n\n type GraphLink {\n source: ID!\n target: ID!\n }\n\n type NoteGraph {\n nodes: [GraphNode!]!\n links: [GraphLink!]!\n }\n\n type Query {\n allNotes(searchFilter: SearchFilterInput, pagination: PaginationInput): Notes!\n tagNotes(searchFilter: SearchFilterInput, pagination: PaginationInput): Notes!\n notesByTagNames(tagNames: [String!]!, mode: TagMatchMode!, pagination: PaginationInput): Notes!\n notesInDateRange(dateRange: DateRangeInput): [Note!]!\n pinnedNotes: [Note!]!\n imageNotes(src: String!): [Note!]!\n backReferences(id: ID!): [Note]!\n note(id: ID!): Note!\n noteCleanupCandidates(query: String, pagination: PaginationInput): [NoteCleanupCandidate!]!\n noteCleanupPreview(id: ID!): NoteCleanupPreview\n noteSnapshots(id: ID!, limit: Int): [NoteSnapshot!]!\n trashedNote(id: ID!): DeletedNote\n trashedNotes(pagination: PaginationInput): DeletedNotes!\n noteGraph: NoteGraph!\n }\n`;\n\nexport const noteMutation = gql`\n type Mutation {\n createNote(note: NoteInput!): Note!\n updateNote(id: ID!, note: NoteInput!, editSessionId: String): Note!\n deleteNote(id: ID!): Boolean!\n restoreNoteSnapshot(id: ID!): Note!\n restoreTrashedNote(id: ID!): Note!\n purgeTrashedNote(id: ID!): Boolean!\n pinNote(id: ID!, pinned: Boolean!): Note!\n reorderNotes(notes: [NoteOrderInput!]!): [Note!]!\n }\n`;\n\nexport const noteTypeDefs = `\n ${noteType}\n ${noteQuery}\n ${noteMutation}\n`;\n"],"mappings":"AAAA,SAAS,WAAW;AAEb,MAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiIjB,MAAM,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmClB,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAarB,MAAM,eAAe;AAAA,MACtB,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA;","names":[]}
@@ -4,24 +4,35 @@ import {
4
4
  RECOVERY_CLEANUP_BATCH_LIMIT,
5
5
  TRASH_RETENTION_DAYS
6
6
  } from "../../../modules/recovery-retention.js";
7
- import { buildNoteSearchProjection } from "./search.js";
7
+ import { buildNoteSearchProjection, extractVisibleSearchTextFromContent } from "./search.js";
8
+ const TRASHED_NOTE_CONTENT_PREVIEW_MAX_LENGTH = 240;
8
9
  class NoteRestoreConflictError extends Error {
9
10
  constructor(message = "A live note with the same id already exists.") {
10
11
  super(message);
11
12
  this.name = "NoteRestoreConflictError";
12
13
  }
13
14
  }
14
- const serializeTrashedNote = (note) => ({
15
- id: String(note.id),
16
- title: note.title,
17
- createdAt: note.createdAt.toISOString(),
18
- updatedAt: note.updatedAt.toISOString(),
19
- deletedAt: note.deletedAt.toISOString(),
20
- pinned: note.pinned,
21
- order: note.order,
22
- layout: note.layout,
23
- tagNames: note.tags.map((tag) => tag.name)
24
- });
15
+ const buildTrashedNoteContentPreview = (content) => {
16
+ const text = extractVisibleSearchTextFromContent(content);
17
+ if (text.length <= TRASHED_NOTE_CONTENT_PREVIEW_MAX_LENGTH) {
18
+ return text;
19
+ }
20
+ return `${text.slice(0, TRASHED_NOTE_CONTENT_PREVIEW_MAX_LENGTH).trimEnd()}\u2026`;
21
+ };
22
+ const serializeTrashedNote = (note) => {
23
+ return {
24
+ id: String(note.id),
25
+ title: note.title,
26
+ createdAt: note.createdAt.toISOString(),
27
+ updatedAt: note.updatedAt.toISOString(),
28
+ deletedAt: note.deletedAt.toISOString(),
29
+ contentPreview: buildTrashedNoteContentPreview(note.content),
30
+ pinned: note.pinned,
31
+ order: note.order,
32
+ layout: note.layout,
33
+ tagNames: note.tags.map((tag) => tag.name)
34
+ };
35
+ };
25
36
  const restoreTagIdsInContent = (content, tagIdByName) => {
26
37
  const rewriteNodes = (nodes) => {
27
38
  return nodes.map((node) => ({
@@ -73,6 +84,18 @@ const createNoteTrashService = (deps) => ({
73
84
  const trashedNote = await deps.moveNoteToTrash(note);
74
85
  return serializeTrashedNote(trashedNote);
75
86
  },
87
+ getNoteById: async (id) => {
88
+ await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
89
+ const deletedNote = await deps.findDeletedNote(id);
90
+ if (!deletedNote) {
91
+ return null;
92
+ }
93
+ const { blocksToMarkdown } = await import("../../../modules/blocknote.js");
94
+ return {
95
+ ...serializeTrashedNote(deletedNote),
96
+ contentAsMarkdown: await blocksToMarkdown(deletedNote.content)
97
+ };
98
+ },
76
99
  restoreNoteById: async (id) => {
77
100
  await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
78
101
  const deletedNote = await deps.findDeletedNote(id);
@@ -249,6 +272,7 @@ const noteTrashService = createNoteTrashService({
249
272
  }
250
273
  });
251
274
  const listTrashedNotes = noteTrashService.listTrashedNotes;
275
+ const getTrashedNoteById = noteTrashService.getNoteById;
252
276
  const trashNoteById = noteTrashService.trashNoteById;
253
277
  const restoreTrashedNoteById = noteTrashService.restoreNoteById;
254
278
  const purgeTrashedNoteById = noteTrashService.purgeNoteById;
@@ -258,6 +282,7 @@ const purgeExpiredTrashedNotes = async () => {
258
282
  export {
259
283
  NoteRestoreConflictError,
260
284
  createNoteTrashService,
285
+ getTrashedNoteById,
261
286
  listTrashedNotes,
262
287
  purgeExpiredTrashedNotes,
263
288
  purgeTrashedNoteById,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/features/note/services/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 '~/modules/recovery-retention.js';\nimport { buildNoteSearchProjection } from './search.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 purgeDeletedNote: (note: DeletedNoteRecord) => Promise<void>;\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' ? contentNode.props.tag : 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?: { limit?: number; offset?: number }): Promise<TrashedNotesResult> => {\n await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\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([deps.countDeletedNotes(), deps.listDeletedNotes(offset, limit)]);\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(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\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(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\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 purgeNoteById: async (id: number): Promise<TrashedNoteSummary | null> => {\n await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n\n const deletedNote = await deps.findDeletedNote(id);\n\n if (!deletedNote) {\n return null;\n }\n\n const summary = serializeTrashedNote(deletedNote);\n await deps.purgeDeletedNote(deletedNote);\n return summary;\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 purgeDeletedNote: async (deletedNote) => {\n await models.deletedNote.delete({ where: { id: deletedNote.id } });\n },\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 =\n 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 =\n 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 ...buildNoteSearchProjection({\n title: deletedNote.title,\n content: restoredContent,\n }),\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 purgeTrashedNoteById = noteTrashService.purgeNoteById;\nexport const purgeExpiredTrashedNotes = async () => {\n return defaultPurgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/features/note/services/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 '~/modules/recovery-retention.js';\nimport { buildNoteSearchProjection, extractVisibleSearchTextFromContent } from './search.js';\n\nconst TRASHED_NOTE_CONTENT_PREVIEW_MAX_LENGTH = 240;\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 contentPreview: 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 interface TrashedNoteDetail extends TrashedNoteSummary {\n contentAsMarkdown: string;\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 purgeDeletedNote: (note: DeletedNoteRecord) => Promise<void>;\n purgeExpiredDeletedNotes: (before: Date, limit: number) => Promise<number>;\n restoreDeletedNote: (note: DeletedNoteRecord) => Promise<RestoredNoteRecord>;\n}\n\nconst buildTrashedNoteContentPreview = (content: string) => {\n const text = extractVisibleSearchTextFromContent(content);\n\n if (text.length <= TRASHED_NOTE_CONTENT_PREVIEW_MAX_LENGTH) {\n return text;\n }\n\n return `${text.slice(0, TRASHED_NOTE_CONTENT_PREVIEW_MAX_LENGTH).trimEnd()}…`;\n};\n\nconst serializeTrashedNote = (note: DeletedNoteRecord): TrashedNoteSummary => {\n return {\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 contentPreview: buildTrashedNoteContentPreview(note.content),\n pinned: note.pinned,\n order: note.order,\n layout: note.layout,\n tagNames: note.tags.map((tag) => tag.name),\n };\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' ? contentNode.props.tag : 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?: { limit?: number; offset?: number }): Promise<TrashedNotesResult> => {\n await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\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([deps.countDeletedNotes(), deps.listDeletedNotes(offset, limit)]);\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(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\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 getNoteById: async (id: number): Promise<TrashedNoteDetail | null> => {\n await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n\n const deletedNote = await deps.findDeletedNote(id);\n\n if (!deletedNote) {\n return null;\n }\n\n const { blocksToMarkdown } = await import('~/modules/blocknote.js');\n\n return {\n ...serializeTrashedNote(deletedNote),\n contentAsMarkdown: await blocksToMarkdown(deletedNote.content),\n };\n },\n\n restoreNoteById: async (id: number) => {\n await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\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 purgeNoteById: async (id: number): Promise<TrashedNoteSummary | null> => {\n await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n\n const deletedNote = await deps.findDeletedNote(id);\n\n if (!deletedNote) {\n return null;\n }\n\n const summary = serializeTrashedNote(deletedNote);\n await deps.purgeDeletedNote(deletedNote);\n return summary;\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 purgeDeletedNote: async (deletedNote) => {\n await models.deletedNote.delete({ where: { id: deletedNote.id } });\n },\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 =\n 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 =\n 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 ...buildNoteSearchProjection({\n title: deletedNote.title,\n content: restoredContent,\n }),\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 getTrashedNoteById = noteTrashService.getNoteById;\nexport const trashNoteById = noteTrashService.trashNoteById;\nexport const restoreTrashedNoteById = noteTrashService.restoreNoteById;\nexport const purgeTrashedNoteById = noteTrashService.purgeNoteById;\nexport const purgeExpiredTrashedNotes = async () => {\n return defaultPurgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n};\n"],"mappings":"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","names":[]}
@@ -1,37 +1,45 @@
1
+ import { buildUnauthorizedGraphqlPayload, buildUnauthorizedPayload } from "@baejino/auth";
1
2
  import session from "express-session";
2
3
  import { GraphQLError } from "graphql";
4
+ import lusca from "lusca";
3
5
  const JSON_HEADERS = { "Content-Type": "application/json" };
4
- const buildUnauthorizedPayload = () => ({
5
- code: "UNAUTHORIZED",
6
- message: "Authentication required"
7
- });
8
- const buildUnauthorizedGraphqlPayload = () => ({
9
- errors: [
10
- {
11
- message: "Authentication required",
12
- extensions: { code: "UNAUTHORIZED" }
13
- }
14
- ]
15
- });
16
6
  const isAuthenticatedRequest = (req) => Boolean(req.session?.authenticated);
17
7
  const createSessionMiddleware = (authConfig) => {
18
- if (authConfig.mode !== "password" || !authConfig.sessionSecret) {
8
+ if (authConfig.mode !== "password") {
19
9
  return (_req, _res, next) => next();
20
10
  }
21
11
  return session({
22
12
  secret: authConfig.sessionSecret,
23
- name: "ocean-brain.sid",
13
+ name: authConfig.cookieName,
24
14
  resave: false,
25
15
  saveUninitialized: false,
26
16
  cookie: {
27
17
  httpOnly: true,
28
- sameSite: "lax"
18
+ sameSite: "lax",
19
+ secure: process.env.NODE_ENV === "production",
20
+ path: "/"
29
21
  }
30
22
  });
31
23
  };
24
+ const createCsrfProtection = (authConfig) => {
25
+ if (authConfig.mode !== "password") {
26
+ return (_req, _res, next) => next();
27
+ }
28
+ return lusca.csrf({
29
+ angular: true,
30
+ cookie: {
31
+ options: {
32
+ path: "/",
33
+ sameSite: "lax",
34
+ secure: process.env.NODE_ENV === "production"
35
+ }
36
+ },
37
+ blocklist: ["/api/mcp", "/graphql/mcp"]
38
+ });
39
+ };
32
40
  const requireSessionForWrite = (authConfig) => {
33
41
  return (req, res, next) => {
34
- if (authConfig.mode === "disabled" || isAuthenticatedRequest(req)) {
42
+ if (authConfig.mode === "open" || isAuthenticatedRequest(req)) {
35
43
  next();
36
44
  return;
37
45
  }
@@ -40,7 +48,7 @@ const requireSessionForWrite = (authConfig) => {
40
48
  };
41
49
  const requireSessionForGraphql = (authConfig) => {
42
50
  return (req, res, next) => {
43
- if (authConfig.mode === "disabled" || isAuthenticatedRequest(req)) {
51
+ if (authConfig.mode === "open" || isAuthenticatedRequest(req)) {
44
52
  next();
45
53
  return;
46
54
  }
@@ -54,10 +62,13 @@ const createMutationAuthValidationRule = () => {
54
62
  if (node.operation !== "mutation") {
55
63
  return;
56
64
  }
65
+ const unauthorizedError = buildUnauthorizedGraphqlPayload().errors[0];
57
66
  context.reportError(
58
- new GraphQLError("Authentication required", {
67
+ new GraphQLError(unauthorizedError.message, {
59
68
  nodes: [node],
60
- extensions: { code: "UNAUTHORIZED" }
69
+ extensions: {
70
+ code: unauthorizedError.extensions.code
71
+ }
61
72
  })
62
73
  );
63
74
  }
@@ -65,6 +76,7 @@ const createMutationAuthValidationRule = () => {
65
76
  };
66
77
  };
67
78
  export {
79
+ createCsrfProtection,
68
80
  createMutationAuthValidationRule,
69
81
  createSessionMiddleware,
70
82
  isAuthenticatedRequest,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/modules/auth-guard.ts"],"sourcesContent":["import type { NextFunction, Request, RequestHandler, Response } from 'express';\nimport session from 'express-session';\nimport type { ValidationRule } from 'graphql';\nimport { GraphQLError } from 'graphql';\n\nimport type { AuthConfig } from './auth-mode.js';\n\nconst JSON_HEADERS = { 'Content-Type': 'application/json' };\n\nconst buildUnauthorizedPayload = () => ({\n code: 'UNAUTHORIZED',\n message: 'Authentication required',\n});\n\nconst buildUnauthorizedGraphqlPayload = () => ({\n errors: [\n {\n message: 'Authentication required',\n extensions: { code: 'UNAUTHORIZED' },\n },\n ],\n});\n\nexport const isAuthenticatedRequest = (req: Request) => Boolean(req.session?.authenticated);\n\nexport const createSessionMiddleware = (authConfig: AuthConfig): RequestHandler => {\n if (authConfig.mode !== 'password' || !authConfig.sessionSecret) {\n return (_req, _res, next) => next();\n }\n\n return session({\n secret: authConfig.sessionSecret,\n name: 'ocean-brain.sid',\n resave: false,\n saveUninitialized: false,\n cookie: {\n httpOnly: true,\n sameSite: 'lax',\n },\n });\n};\n\nexport const requireSessionForWrite = (authConfig: AuthConfig): RequestHandler => {\n return (req: Request, res: Response, next: NextFunction) => {\n if (authConfig.mode === 'disabled' || isAuthenticatedRequest(req)) {\n next();\n return;\n }\n\n res.status(401).set(JSON_HEADERS).json(buildUnauthorizedPayload()).end();\n };\n};\n\nexport const requireSessionForGraphql = (authConfig: AuthConfig): RequestHandler => {\n return (req: Request, res: Response, next: NextFunction) => {\n if (authConfig.mode === 'disabled' || isAuthenticatedRequest(req)) {\n next();\n return;\n }\n\n res.status(401).set(JSON_HEADERS).json(buildUnauthorizedGraphqlPayload()).end();\n };\n};\n\nexport const createMutationAuthValidationRule = (): ValidationRule => {\n return (context) => {\n return {\n OperationDefinition(node) {\n if (node.operation !== 'mutation') {\n return;\n }\n\n context.reportError(\n new GraphQLError('Authentication required', {\n nodes: [node],\n extensions: { code: 'UNAUTHORIZED' },\n }),\n );\n },\n };\n };\n};\n"],"mappings":"AACA,OAAO,aAAa;AAEpB,SAAS,oBAAoB;AAI7B,MAAM,eAAe,EAAE,gBAAgB,mBAAmB;AAE1D,MAAM,2BAA2B,OAAO;AAAA,EACpC,MAAM;AAAA,EACN,SAAS;AACb;AAEA,MAAM,kCAAkC,OAAO;AAAA,EAC3C,QAAQ;AAAA,IACJ;AAAA,MACI,SAAS;AAAA,MACT,YAAY,EAAE,MAAM,eAAe;AAAA,IACvC;AAAA,EACJ;AACJ;AAEO,MAAM,yBAAyB,CAAC,QAAiB,QAAQ,IAAI,SAAS,aAAa;AAEnF,MAAM,0BAA0B,CAAC,eAA2C;AAC/E,MAAI,WAAW,SAAS,cAAc,CAAC,WAAW,eAAe;AAC7D,WAAO,CAAC,MAAM,MAAM,SAAS,KAAK;AAAA,EACtC;AAEA,SAAO,QAAQ;AAAA,IACX,QAAQ,WAAW;AAAA,IACnB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,mBAAmB;AAAA,IACnB,QAAQ;AAAA,MACJ,UAAU;AAAA,MACV,UAAU;AAAA,IACd;AAAA,EACJ,CAAC;AACL;AAEO,MAAM,yBAAyB,CAAC,eAA2C;AAC9E,SAAO,CAAC,KAAc,KAAe,SAAuB;AACxD,QAAI,WAAW,SAAS,cAAc,uBAAuB,GAAG,GAAG;AAC/D,WAAK;AACL;AAAA,IACJ;AAEA,QAAI,OAAO,GAAG,EAAE,IAAI,YAAY,EAAE,KAAK,yBAAyB,CAAC,EAAE,IAAI;AAAA,EAC3E;AACJ;AAEO,MAAM,2BAA2B,CAAC,eAA2C;AAChF,SAAO,CAAC,KAAc,KAAe,SAAuB;AACxD,QAAI,WAAW,SAAS,cAAc,uBAAuB,GAAG,GAAG;AAC/D,WAAK;AACL;AAAA,IACJ;AAEA,QAAI,OAAO,GAAG,EAAE,IAAI,YAAY,EAAE,KAAK,gCAAgC,CAAC,EAAE,IAAI;AAAA,EAClF;AACJ;AAEO,MAAM,mCAAmC,MAAsB;AAClE,SAAO,CAAC,YAAY;AAChB,WAAO;AAAA,MACH,oBAAoB,MAAM;AACtB,YAAI,KAAK,cAAc,YAAY;AAC/B;AAAA,QACJ;AAEA,gBAAQ;AAAA,UACJ,IAAI,aAAa,2BAA2B;AAAA,YACxC,OAAO,CAAC,IAAI;AAAA,YACZ,YAAY,EAAE,MAAM,eAAe;AAAA,UACvC,CAAC;AAAA,QACL;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/modules/auth-guard.ts"],"sourcesContent":["import { buildUnauthorizedGraphqlPayload, buildUnauthorizedPayload } from '@baejino/auth';\nimport type { NextFunction, Request, RequestHandler, Response } from 'express';\nimport session from 'express-session';\nimport type { ValidationRule } from 'graphql';\nimport { GraphQLError } from 'graphql';\nimport lusca from 'lusca';\n\nimport type { AuthConfig } from './auth-mode.js';\n\nconst JSON_HEADERS = { 'Content-Type': 'application/json' };\n\nexport const isAuthenticatedRequest = (req: Request) => Boolean(req.session?.authenticated);\n\nexport const createSessionMiddleware = (authConfig: AuthConfig): RequestHandler => {\n if (authConfig.mode !== 'password') {\n return (_req, _res, next) => next();\n }\n\n return session({\n secret: authConfig.sessionSecret,\n name: authConfig.cookieName,\n resave: false,\n saveUninitialized: false,\n cookie: {\n httpOnly: true,\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n },\n });\n};\n\nexport const createCsrfProtection = (authConfig: AuthConfig): RequestHandler => {\n if (authConfig.mode !== 'password') {\n return (_req, _res, next) => next();\n }\n\n return lusca.csrf({\n angular: true,\n cookie: {\n options: {\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n },\n },\n blocklist: ['/api/mcp', '/graphql/mcp'],\n });\n};\n\nexport const requireSessionForWrite = (authConfig: AuthConfig): RequestHandler => {\n return (req: Request, res: Response, next: NextFunction) => {\n if (authConfig.mode === 'open' || isAuthenticatedRequest(req)) {\n next();\n return;\n }\n\n res.status(401).set(JSON_HEADERS).json(buildUnauthorizedPayload()).end();\n };\n};\n\nexport const requireSessionForGraphql = (authConfig: AuthConfig): RequestHandler => {\n return (req: Request, res: Response, next: NextFunction) => {\n if (authConfig.mode === 'open' || isAuthenticatedRequest(req)) {\n next();\n return;\n }\n\n res.status(401).set(JSON_HEADERS).json(buildUnauthorizedGraphqlPayload()).end();\n };\n};\n\nexport const createMutationAuthValidationRule = (): ValidationRule => {\n return (context) => {\n return {\n OperationDefinition(node) {\n if (node.operation !== 'mutation') {\n return;\n }\n\n const unauthorizedError = buildUnauthorizedGraphqlPayload().errors[0];\n\n context.reportError(\n new GraphQLError(unauthorizedError.message, {\n nodes: [node],\n extensions: {\n code: unauthorizedError.extensions.code,\n },\n }),\n );\n },\n };\n };\n};\n"],"mappings":"AAAA,SAAS,iCAAiC,gCAAgC;AAE1E,OAAO,aAAa;AAEpB,SAAS,oBAAoB;AAC7B,OAAO,WAAW;AAIlB,MAAM,eAAe,EAAE,gBAAgB,mBAAmB;AAEnD,MAAM,yBAAyB,CAAC,QAAiB,QAAQ,IAAI,SAAS,aAAa;AAEnF,MAAM,0BAA0B,CAAC,eAA2C;AAC/E,MAAI,WAAW,SAAS,YAAY;AAChC,WAAO,CAAC,MAAM,MAAM,SAAS,KAAK;AAAA,EACtC;AAEA,SAAO,QAAQ;AAAA,IACX,QAAQ,WAAW;AAAA,IACnB,MAAM,WAAW;AAAA,IACjB,QAAQ;AAAA,IACR,mBAAmB;AAAA,IACnB,QAAQ;AAAA,MACJ,UAAU;AAAA,MACV,UAAU;AAAA,MACV,QAAQ,QAAQ,IAAI,aAAa;AAAA,MACjC,MAAM;AAAA,IACV;AAAA,EACJ,CAAC;AACL;AAEO,MAAM,uBAAuB,CAAC,eAA2C;AAC5E,MAAI,WAAW,SAAS,YAAY;AAChC,WAAO,CAAC,MAAM,MAAM,SAAS,KAAK;AAAA,EACtC;AAEA,SAAO,MAAM,KAAK;AAAA,IACd,SAAS;AAAA,IACT,QAAQ;AAAA,MACJ,SAAS;AAAA,QACL,MAAM;AAAA,QACN,UAAU;AAAA,QACV,QAAQ,QAAQ,IAAI,aAAa;AAAA,MACrC;AAAA,IACJ;AAAA,IACA,WAAW,CAAC,YAAY,cAAc;AAAA,EAC1C,CAAC;AACL;AAEO,MAAM,yBAAyB,CAAC,eAA2C;AAC9E,SAAO,CAAC,KAAc,KAAe,SAAuB;AACxD,QAAI,WAAW,SAAS,UAAU,uBAAuB,GAAG,GAAG;AAC3D,WAAK;AACL;AAAA,IACJ;AAEA,QAAI,OAAO,GAAG,EAAE,IAAI,YAAY,EAAE,KAAK,yBAAyB,CAAC,EAAE,IAAI;AAAA,EAC3E;AACJ;AAEO,MAAM,2BAA2B,CAAC,eAA2C;AAChF,SAAO,CAAC,KAAc,KAAe,SAAuB;AACxD,QAAI,WAAW,SAAS,UAAU,uBAAuB,GAAG,GAAG;AAC3D,WAAK;AACL;AAAA,IACJ;AAEA,QAAI,OAAO,GAAG,EAAE,IAAI,YAAY,EAAE,KAAK,gCAAgC,CAAC,EAAE,IAAI;AAAA,EAClF;AACJ;AAEO,MAAM,mCAAmC,MAAsB;AAClE,SAAO,CAAC,YAAY;AAChB,WAAO;AAAA,MACH,oBAAoB,MAAM;AACtB,YAAI,KAAK,cAAc,YAAY;AAC/B;AAAA,QACJ;AAEA,cAAM,oBAAoB,gCAAgC,EAAE,OAAO,CAAC;AAEpE,gBAAQ;AAAA,UACJ,IAAI,aAAa,kBAAkB,SAAS;AAAA,YACxC,OAAO,CAAC,IAAI;AAAA,YACZ,YAAY;AAAA,cACR,MAAM,kBAAkB,WAAW;AAAA,YACvC;AAAA,UACJ,CAAC;AAAA,QACL;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AACJ;","names":[]}
@@ -1,48 +1,25 @@
1
- const isTruthy = (value) => {
2
- if (!value) {
3
- return false;
4
- }
5
- return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
6
- };
7
- const ensurePasswordModeRequirements = (env) => {
8
- if (!env.OCEAN_BRAIN_PASSWORD) {
9
- throw new Error("Missing OCEAN_BRAIN_PASSWORD. Set OCEAN_BRAIN_PASSWORD to enable password auth mode.");
10
- }
11
- if (!env.OCEAN_BRAIN_SESSION_SECRET) {
12
- throw new Error(
13
- "Missing OCEAN_BRAIN_SESSION_SECRET. Set OCEAN_BRAIN_SESSION_SECRET when password auth mode is enabled."
14
- );
15
- }
16
- };
17
- const resolveAuthConfig = (env) => {
18
- const allowInsecureNoAuth = isTruthy(env.OCEAN_BRAIN_ALLOW_INSECURE_NO_AUTH);
19
- if (allowInsecureNoAuth) {
20
- return {
21
- mode: "disabled",
22
- source: "auto"
23
- };
24
- }
25
- if (env.OCEAN_BRAIN_PASSWORD) {
26
- ensurePasswordModeRequirements(env);
27
- return {
28
- mode: "password",
29
- password: env.OCEAN_BRAIN_PASSWORD,
30
- sessionSecret: env.OCEAN_BRAIN_SESSION_SECRET,
31
- source: "auto"
32
- };
33
- }
34
- throw new Error(
35
- "Unable to resolve auth mode. Set OCEAN_BRAIN_PASSWORD and OCEAN_BRAIN_SESSION_SECRET for password mode, or set OCEAN_BRAIN_ALLOW_INSECURE_NO_AUTH=true for disabled mode."
36
- );
37
- };
1
+ import { resolvePasswordAuthConfig } from "@baejino/auth";
2
+ const AUTH_SESSION_COOKIE_NAME = "ocean-brain.sid";
3
+ const resolveAuthConfig = (env) => resolvePasswordAuthConfig({
4
+ env,
5
+ passwordEnv: "OCEAN_BRAIN_PASSWORD",
6
+ sessionSecretEnv: "OCEAN_BRAIN_SESSION_SECRET",
7
+ allowOpenEnv: "OCEAN_BRAIN_ALLOW_INSECURE_NO_AUTH",
8
+ requireExplicitOpen: true,
9
+ allowPasswordAsSessionSecret: false,
10
+ cookieName: AUTH_SESSION_COOKIE_NAME
11
+ });
38
12
  const logAuthConfig = (authConfig) => {
39
- if (authConfig.mode === "disabled") {
40
- process.stderr.write("[auth] Running in disabled mode. Authentication is not required for write access.\n");
13
+ if (authConfig.mode === "open") {
14
+ process.stderr.write(
15
+ "[auth] Running in explicit open mode. Authentication is not required for write access.\n"
16
+ );
41
17
  return;
42
18
  }
43
19
  process.stdout.write("[auth] Running in password mode. Write access requires an authenticated session.\n");
44
20
  };
45
21
  export {
22
+ AUTH_SESSION_COOKIE_NAME,
46
23
  logAuthConfig,
47
24
  resolveAuthConfig
48
25
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/modules/auth-mode.ts"],"sourcesContent":["export type AuthMode = 'password' | 'disabled';\n\nexport interface AuthConfig {\n mode: AuthMode;\n password?: string;\n sessionSecret?: string;\n source: 'auto' | 'override';\n}\n\nexport interface AuthModeEnvironment {\n [key: string]: string | undefined;\n OCEAN_BRAIN_ALLOW_INSECURE_NO_AUTH?: string;\n OCEAN_BRAIN_PASSWORD?: string;\n OCEAN_BRAIN_SESSION_SECRET?: string;\n}\n\nconst isTruthy = (value?: string) => {\n if (!value) {\n return false;\n }\n\n return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());\n};\n\nconst ensurePasswordModeRequirements = (env: AuthModeEnvironment) => {\n if (!env.OCEAN_BRAIN_PASSWORD) {\n throw new Error('Missing OCEAN_BRAIN_PASSWORD. Set OCEAN_BRAIN_PASSWORD to enable password auth mode.');\n }\n\n if (!env.OCEAN_BRAIN_SESSION_SECRET) {\n throw new Error(\n 'Missing OCEAN_BRAIN_SESSION_SECRET. Set OCEAN_BRAIN_SESSION_SECRET when password auth mode is enabled.',\n );\n }\n};\n\nexport const resolveAuthConfig = (env: AuthModeEnvironment): AuthConfig => {\n const allowInsecureNoAuth = isTruthy(env.OCEAN_BRAIN_ALLOW_INSECURE_NO_AUTH);\n\n if (allowInsecureNoAuth) {\n return {\n mode: 'disabled',\n source: 'auto',\n };\n }\n\n if (env.OCEAN_BRAIN_PASSWORD) {\n ensurePasswordModeRequirements(env);\n\n return {\n mode: 'password',\n password: env.OCEAN_BRAIN_PASSWORD,\n sessionSecret: env.OCEAN_BRAIN_SESSION_SECRET,\n source: 'auto',\n };\n }\n\n throw new Error(\n 'Unable to resolve auth mode. Set OCEAN_BRAIN_PASSWORD and OCEAN_BRAIN_SESSION_SECRET for password mode, or set OCEAN_BRAIN_ALLOW_INSECURE_NO_AUTH=true for disabled mode.',\n );\n};\n\nexport const logAuthConfig = (authConfig: AuthConfig) => {\n if (authConfig.mode === 'disabled') {\n process.stderr.write('[auth] Running in disabled mode. Authentication is not required for write access.\\n');\n return;\n }\n\n process.stdout.write('[auth] Running in password mode. Write access requires an authenticated session.\\n');\n};\n"],"mappings":"AAgBA,MAAM,WAAW,CAAC,UAAmB;AACjC,MAAI,CAAC,OAAO;AACR,WAAO;AAAA,EACX;AAEA,SAAO,CAAC,KAAK,QAAQ,OAAO,IAAI,EAAE,SAAS,MAAM,KAAK,EAAE,YAAY,CAAC;AACzE;AAEA,MAAM,iCAAiC,CAAC,QAA6B;AACjE,MAAI,CAAC,IAAI,sBAAsB;AAC3B,UAAM,IAAI,MAAM,sFAAsF;AAAA,EAC1G;AAEA,MAAI,CAAC,IAAI,4BAA4B;AACjC,UAAM,IAAI;AAAA,MACN;AAAA,IACJ;AAAA,EACJ;AACJ;AAEO,MAAM,oBAAoB,CAAC,QAAyC;AACvE,QAAM,sBAAsB,SAAS,IAAI,kCAAkC;AAE3E,MAAI,qBAAqB;AACrB,WAAO;AAAA,MACH,MAAM;AAAA,MACN,QAAQ;AAAA,IACZ;AAAA,EACJ;AAEA,MAAI,IAAI,sBAAsB;AAC1B,mCAA+B,GAAG;AAElC,WAAO;AAAA,MACH,MAAM;AAAA,MACN,UAAU,IAAI;AAAA,MACd,eAAe,IAAI;AAAA,MACnB,QAAQ;AAAA,IACZ;AAAA,EACJ;AAEA,QAAM,IAAI;AAAA,IACN;AAAA,EACJ;AACJ;AAEO,MAAM,gBAAgB,CAAC,eAA2B;AACrD,MAAI,WAAW,SAAS,YAAY;AAChC,YAAQ,OAAO,MAAM,qFAAqF;AAC1G;AAAA,EACJ;AAEA,UAAQ,OAAO,MAAM,oFAAoF;AAC7G;","names":[]}
1
+ {"version":3,"sources":["../../src/modules/auth-mode.ts"],"sourcesContent":["import { type AuthConfig, type AuthEnvironment, resolvePasswordAuthConfig } from '@baejino/auth';\n\nexport type { AuthConfig, AuthMode } from '@baejino/auth';\n\nexport const AUTH_SESSION_COOKIE_NAME = 'ocean-brain.sid';\n\nexport interface AuthModeEnvironment extends AuthEnvironment {\n [key: string]: string | undefined;\n OCEAN_BRAIN_ALLOW_INSECURE_NO_AUTH?: string;\n OCEAN_BRAIN_PASSWORD?: string;\n OCEAN_BRAIN_SESSION_SECRET?: string;\n}\n\nexport const resolveAuthConfig = (env: AuthModeEnvironment): AuthConfig =>\n resolvePasswordAuthConfig({\n env,\n passwordEnv: 'OCEAN_BRAIN_PASSWORD',\n sessionSecretEnv: 'OCEAN_BRAIN_SESSION_SECRET',\n allowOpenEnv: 'OCEAN_BRAIN_ALLOW_INSECURE_NO_AUTH',\n requireExplicitOpen: true,\n allowPasswordAsSessionSecret: false,\n cookieName: AUTH_SESSION_COOKIE_NAME,\n });\n\nexport const logAuthConfig = (authConfig: AuthConfig) => {\n if (authConfig.mode === 'open') {\n process.stderr.write(\n '[auth] Running in explicit open mode. Authentication is not required for write access.\\n',\n );\n return;\n }\n\n process.stdout.write('[auth] Running in password mode. Write access requires an authenticated session.\\n');\n};\n"],"mappings":"AAAA,SAAgD,iCAAiC;AAI1E,MAAM,2BAA2B;AASjC,MAAM,oBAAoB,CAAC,QAC9B,0BAA0B;AAAA,EACtB;AAAA,EACA,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,8BAA8B;AAAA,EAC9B,YAAY;AAChB,CAAC;AAEE,MAAM,gBAAgB,CAAC,eAA2B;AACrD,MAAI,WAAW,SAAS,QAAQ;AAC5B,YAAQ,OAAO;AAAA,MACX;AAAA,IACJ;AACA;AAAA,EACJ;AAEA,UAAQ,OAAO,MAAM,oFAAoF;AAC7G;","names":[]}
@@ -24,6 +24,13 @@ const createErrorHandler = () => {
24
24
  }).end();
25
25
  return;
26
26
  }
27
+ if (error instanceof Error && error.message.startsWith("CSRF token ")) {
28
+ res.status(403).json({
29
+ code: "CSRF_TOKEN_INVALID",
30
+ message: error.message
31
+ }).end();
32
+ return;
33
+ }
27
34
  const message = error instanceof Error ? error.stack || error.message : String(error);
28
35
  process.stderr.write(`[error] ${message}
29
36
  `);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/modules/error-handler.ts"],"sourcesContent":["import type { ErrorRequestHandler } from 'express';\n\nexport class AppError extends Error {\n code: string;\n status: number;\n\n constructor(status: number, code: string, message: string) {\n super(message);\n this.name = 'AppError';\n this.status = status;\n this.code = code;\n }\n}\n\nexport const createAppError = (status: number, code: string, message: string) => {\n return new AppError(status, code, message);\n};\n\nexport const createErrorHandler = (): ErrorRequestHandler => {\n return (error, _req, res, next) => {\n if (res.headersSent) {\n next(error);\n return;\n }\n\n if (error instanceof AppError) {\n res.status(error.status)\n .json({\n code: error.code,\n message: error.message,\n })\n .end();\n return;\n }\n\n const message = error instanceof Error ? error.stack || error.message : String(error);\n process.stderr.write(`[error] ${message}\\n`);\n\n res.status(500)\n .json({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Internal Server Error',\n })\n .end();\n };\n};\n"],"mappings":"AAEO,MAAM,iBAAiB,MAAM;AAAA,EAChC;AAAA,EACA;AAAA,EAEA,YAAY,QAAgB,MAAc,SAAiB;AACvD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EAChB;AACJ;AAEO,MAAM,iBAAiB,CAAC,QAAgB,MAAc,YAAoB;AAC7E,SAAO,IAAI,SAAS,QAAQ,MAAM,OAAO;AAC7C;AAEO,MAAM,qBAAqB,MAA2B;AACzD,SAAO,CAAC,OAAO,MAAM,KAAK,SAAS;AAC/B,QAAI,IAAI,aAAa;AACjB,WAAK,KAAK;AACV;AAAA,IACJ;AAEA,QAAI,iBAAiB,UAAU;AAC3B,UAAI,OAAO,MAAM,MAAM,EAClB,KAAK;AAAA,QACF,MAAM,MAAM;AAAA,QACZ,SAAS,MAAM;AAAA,MACnB,CAAC,EACA,IAAI;AACT;AAAA,IACJ;AAEA,UAAM,UAAU,iBAAiB,QAAQ,MAAM,SAAS,MAAM,UAAU,OAAO,KAAK;AACpF,YAAQ,OAAO,MAAM,WAAW,OAAO;AAAA,CAAI;AAE3C,QAAI,OAAO,GAAG,EACT,KAAK;AAAA,MACF,MAAM;AAAA,MACN,SAAS;AAAA,IACb,CAAC,EACA,IAAI;AAAA,EACb;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/modules/error-handler.ts"],"sourcesContent":["import type { ErrorRequestHandler } from 'express';\n\nexport class AppError extends Error {\n code: string;\n status: number;\n\n constructor(status: number, code: string, message: string) {\n super(message);\n this.name = 'AppError';\n this.status = status;\n this.code = code;\n }\n}\n\nexport const createAppError = (status: number, code: string, message: string) => {\n return new AppError(status, code, message);\n};\n\nexport const createErrorHandler = (): ErrorRequestHandler => {\n return (error, _req, res, next) => {\n if (res.headersSent) {\n next(error);\n return;\n }\n\n if (error instanceof AppError) {\n res.status(error.status)\n .json({\n code: error.code,\n message: error.message,\n })\n .end();\n return;\n }\n\n if (error instanceof Error && error.message.startsWith('CSRF token ')) {\n res.status(403)\n .json({\n code: 'CSRF_TOKEN_INVALID',\n message: error.message,\n })\n .end();\n return;\n }\n\n const message = error instanceof Error ? error.stack || error.message : String(error);\n process.stderr.write(`[error] ${message}\\n`);\n\n res.status(500)\n .json({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Internal Server Error',\n })\n .end();\n };\n};\n"],"mappings":"AAEO,MAAM,iBAAiB,MAAM;AAAA,EAChC;AAAA,EACA;AAAA,EAEA,YAAY,QAAgB,MAAc,SAAiB;AACvD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EAChB;AACJ;AAEO,MAAM,iBAAiB,CAAC,QAAgB,MAAc,YAAoB;AAC7E,SAAO,IAAI,SAAS,QAAQ,MAAM,OAAO;AAC7C;AAEO,MAAM,qBAAqB,MAA2B;AACzD,SAAO,CAAC,OAAO,MAAM,KAAK,SAAS;AAC/B,QAAI,IAAI,aAAa;AACjB,WAAK,KAAK;AACV;AAAA,IACJ;AAEA,QAAI,iBAAiB,UAAU;AAC3B,UAAI,OAAO,MAAM,MAAM,EAClB,KAAK;AAAA,QACF,MAAM,MAAM;AAAA,QACZ,SAAS,MAAM;AAAA,MACnB,CAAC,EACA,IAAI;AACT;AAAA,IACJ;AAEA,QAAI,iBAAiB,SAAS,MAAM,QAAQ,WAAW,aAAa,GAAG;AACnE,UAAI,OAAO,GAAG,EACT,KAAK;AAAA,QACF,MAAM;AAAA,QACN,SAAS,MAAM;AAAA,MACnB,CAAC,EACA,IAAI;AACT;AAAA,IACJ;AAEA,UAAM,UAAU,iBAAiB,QAAQ,MAAM,SAAS,MAAM,UAAU,OAAO,KAAK;AACpF,YAAQ,OAAO,MAAM,WAAW,OAAO;AAAA,CAAI;AAE3C,QAAI,OAAO,GAAG,EACT,KAAK;AAAA,MACF,MAAM;AAAA,MACN,SAAS;AAAA,IACb,CAAC,EACA,IAAI;AAAA,EACb;AACJ;","names":[]}
@@ -0,0 +1,18 @@
1
+ import { rateLimit } from "express-rate-limit";
2
+ const AUTH_RATE_LIMIT_MESSAGE = "Too many authentication attempts. Please try again later.";
3
+ const createAuthAttemptRateLimit = () => rateLimit({
4
+ windowMs: 15 * 60 * 1e3,
5
+ limit: 10,
6
+ standardHeaders: true,
7
+ legacyHeaders: false,
8
+ handler: (_req, res) => {
9
+ res.status(429).json({
10
+ code: "AUTH_RATE_LIMITED",
11
+ message: AUTH_RATE_LIMIT_MESSAGE
12
+ });
13
+ }
14
+ });
15
+ export {
16
+ createAuthAttemptRateLimit
17
+ };
18
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/modules/rate-limit.ts"],"sourcesContent":["import { rateLimit } from 'express-rate-limit';\n\nconst AUTH_RATE_LIMIT_MESSAGE = 'Too many authentication attempts. Please try again later.';\n\nexport const createAuthAttemptRateLimit = () =>\n rateLimit({\n windowMs: 15 * 60 * 1000,\n limit: 10,\n standardHeaders: true,\n legacyHeaders: false,\n handler: (_req, res) => {\n res.status(429).json({\n code: 'AUTH_RATE_LIMITED',\n message: AUTH_RATE_LIMIT_MESSAGE,\n });\n },\n });\n"],"mappings":"AAAA,SAAS,iBAAiB;AAE1B,MAAM,0BAA0B;AAEzB,MAAM,6BAA6B,MACtC,UAAU;AAAA,EACN,UAAU,KAAK,KAAK;AAAA,EACpB,OAAO;AAAA,EACP,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,SAAS,CAAC,MAAM,QAAQ;AACpB,QAAI,OAAO,GAAG,EAAE,KAAK;AAAA,MACjB,MAAM;AAAA,MACN,SAAS;AAAA,IACb,CAAC;AAAA,EACL;AACJ,CAAC;","names":[]}
@@ -8,10 +8,11 @@ import {
8
8
  createMcpAdminStatusHandler
9
9
  } from "../features/mcp-admin/http/handlers.js";
10
10
  import { requireSessionForWrite } from "../modules/auth-guard.js";
11
+ import { createAuthAttemptRateLimit } from "../modules/rate-limit.js";
11
12
  import { createServerEventsHandler } from "../modules/server-events-handler.js";
12
13
  import useAsync from "../modules/use-async.js";
13
14
  import { createMcpRouter } from "./mcp.js";
14
- const createApiRouter = (authConfig, mcpAdminService) => Router().use("/mcp", createMcpRouter(authConfig, mcpAdminService)).post("/auth/login", useAsync(createLoginHandler(authConfig))).post("/auth/logout", useAsync(createLogoutHandler(authConfig))).get("/auth/session", useAsync(createSessionStatusHandler(authConfig))).get(
15
+ const createApiRouter = (authConfig, mcpAdminService) => Router().use("/mcp", createMcpRouter(authConfig, mcpAdminService)).post("/auth/login", createAuthAttemptRateLimit(), useAsync(createLoginHandler(authConfig))).post("/auth/logout", useAsync(createLogoutHandler(authConfig))).get("/auth/session", useAsync(createSessionStatusHandler(authConfig))).get(
15
16
  "/mcp-admin/status",
16
17
  requireSessionForWrite(authConfig),
17
18
  useAsync(createMcpAdminStatusHandler(mcpAdminService))
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routes/api.ts"],"sourcesContent":["import { Router } from 'express';\nimport { createLoginHandler, createLogoutHandler, createSessionStatusHandler } from '../features/auth/http/api.js';\nimport { createUploadImageFromSrcHandler, createUploadImageHandler } from '../features/image/http/upload.js';\nimport {\n createMcpAdminRevokeTokenHandler,\n createMcpAdminRotateTokenHandler,\n createMcpAdminSetEnabledHandler,\n createMcpAdminStatusHandler,\n} from '../features/mcp-admin/http/handlers.js';\nimport type { McpAdminService } from '../features/mcp-admin/service.js';\nimport { requireSessionForWrite } from '../modules/auth-guard.js';\nimport type { AuthConfig } from '../modules/auth-mode.js';\nimport { createServerEventsHandler } from '../modules/server-events-handler.js';\nimport useAsync from '../modules/use-async.js';\nimport { createMcpRouter } from './mcp.js';\n\ntype McpAdminApiService = Pick<\n McpAdminService,\n 'getStatus' | 'setEnabled' | 'rotateToken' | 'revokeActiveToken' | 'validatePresentedToken'\n>;\n\nexport const createApiRouter = (authConfig: AuthConfig, mcpAdminService: McpAdminApiService) =>\n Router()\n .use('/mcp', createMcpRouter(authConfig, mcpAdminService))\n .post('/auth/login', useAsync(createLoginHandler(authConfig)))\n .post('/auth/logout', useAsync(createLogoutHandler(authConfig)))\n .get('/auth/session', useAsync(createSessionStatusHandler(authConfig)))\n .get(\n '/mcp-admin/status',\n requireSessionForWrite(authConfig),\n useAsync(createMcpAdminStatusHandler(mcpAdminService)),\n )\n .post(\n '/mcp-admin/enabled',\n requireSessionForWrite(authConfig),\n useAsync(createMcpAdminSetEnabledHandler(mcpAdminService)),\n )\n .post(\n '/mcp-admin/token/rotate',\n requireSessionForWrite(authConfig),\n useAsync(createMcpAdminRotateTokenHandler(mcpAdminService)),\n )\n .post(\n '/mcp-admin/token/revoke',\n requireSessionForWrite(authConfig),\n useAsync(createMcpAdminRevokeTokenHandler(mcpAdminService)),\n )\n .post('/image', requireSessionForWrite(authConfig), useAsync(createUploadImageHandler()))\n .post('/image-from-src', requireSessionForWrite(authConfig), useAsync(createUploadImageFromSrcHandler()))\n .get('/events', requireSessionForWrite(authConfig), createServerEventsHandler());\n"],"mappings":"AAAA,SAAS,cAAc;AACvB,SAAS,oBAAoB,qBAAqB,kCAAkC;AACpF,SAAS,iCAAiC,gCAAgC;AAC1E;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AAEP,SAAS,8BAA8B;AAEvC,SAAS,iCAAiC;AAC1C,OAAO,cAAc;AACrB,SAAS,uBAAuB;AAOzB,MAAM,kBAAkB,CAAC,YAAwB,oBACpD,OAAO,EACF,IAAI,QAAQ,gBAAgB,YAAY,eAAe,CAAC,EACxD,KAAK,eAAe,SAAS,mBAAmB,UAAU,CAAC,CAAC,EAC5D,KAAK,gBAAgB,SAAS,oBAAoB,UAAU,CAAC,CAAC,EAC9D,IAAI,iBAAiB,SAAS,2BAA2B,UAAU,CAAC,CAAC,EACrE;AAAA,EACG;AAAA,EACA,uBAAuB,UAAU;AAAA,EACjC,SAAS,4BAA4B,eAAe,CAAC;AACzD,EACC;AAAA,EACG;AAAA,EACA,uBAAuB,UAAU;AAAA,EACjC,SAAS,gCAAgC,eAAe,CAAC;AAC7D,EACC;AAAA,EACG;AAAA,EACA,uBAAuB,UAAU;AAAA,EACjC,SAAS,iCAAiC,eAAe,CAAC;AAC9D,EACC;AAAA,EACG;AAAA,EACA,uBAAuB,UAAU;AAAA,EACjC,SAAS,iCAAiC,eAAe,CAAC;AAC9D,EACC,KAAK,UAAU,uBAAuB,UAAU,GAAG,SAAS,yBAAyB,CAAC,CAAC,EACvF,KAAK,mBAAmB,uBAAuB,UAAU,GAAG,SAAS,gCAAgC,CAAC,CAAC,EACvG,IAAI,WAAW,uBAAuB,UAAU,GAAG,0BAA0B,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/routes/api.ts"],"sourcesContent":["import { Router } from 'express';\nimport { createLoginHandler, createLogoutHandler, createSessionStatusHandler } from '../features/auth/http/api.js';\nimport { createUploadImageFromSrcHandler, createUploadImageHandler } from '../features/image/http/upload.js';\nimport {\n createMcpAdminRevokeTokenHandler,\n createMcpAdminRotateTokenHandler,\n createMcpAdminSetEnabledHandler,\n createMcpAdminStatusHandler,\n} from '../features/mcp-admin/http/handlers.js';\nimport type { McpAdminService } from '../features/mcp-admin/service.js';\nimport { requireSessionForWrite } from '../modules/auth-guard.js';\nimport type { AuthConfig } from '../modules/auth-mode.js';\nimport { createAuthAttemptRateLimit } from '../modules/rate-limit.js';\nimport { createServerEventsHandler } from '../modules/server-events-handler.js';\nimport useAsync from '../modules/use-async.js';\nimport { createMcpRouter } from './mcp.js';\n\ntype McpAdminApiService = Pick<\n McpAdminService,\n 'getStatus' | 'setEnabled' | 'rotateToken' | 'revokeActiveToken' | 'validatePresentedToken'\n>;\n\nexport const createApiRouter = (authConfig: AuthConfig, mcpAdminService: McpAdminApiService) =>\n Router()\n .use('/mcp', createMcpRouter(authConfig, mcpAdminService))\n .post('/auth/login', createAuthAttemptRateLimit(), useAsync(createLoginHandler(authConfig)))\n .post('/auth/logout', useAsync(createLogoutHandler(authConfig)))\n .get('/auth/session', useAsync(createSessionStatusHandler(authConfig)))\n .get(\n '/mcp-admin/status',\n requireSessionForWrite(authConfig),\n useAsync(createMcpAdminStatusHandler(mcpAdminService)),\n )\n .post(\n '/mcp-admin/enabled',\n requireSessionForWrite(authConfig),\n useAsync(createMcpAdminSetEnabledHandler(mcpAdminService)),\n )\n .post(\n '/mcp-admin/token/rotate',\n requireSessionForWrite(authConfig),\n useAsync(createMcpAdminRotateTokenHandler(mcpAdminService)),\n )\n .post(\n '/mcp-admin/token/revoke',\n requireSessionForWrite(authConfig),\n useAsync(createMcpAdminRevokeTokenHandler(mcpAdminService)),\n )\n .post('/image', requireSessionForWrite(authConfig), useAsync(createUploadImageHandler()))\n .post('/image-from-src', requireSessionForWrite(authConfig), useAsync(createUploadImageFromSrcHandler()))\n .get('/events', requireSessionForWrite(authConfig), createServerEventsHandler());\n"],"mappings":"AAAA,SAAS,cAAc;AACvB,SAAS,oBAAoB,qBAAqB,kCAAkC;AACpF,SAAS,iCAAiC,gCAAgC;AAC1E;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AAEP,SAAS,8BAA8B;AAEvC,SAAS,kCAAkC;AAC3C,SAAS,iCAAiC;AAC1C,OAAO,cAAc;AACrB,SAAS,uBAAuB;AAOzB,MAAM,kBAAkB,CAAC,YAAwB,oBACpD,OAAO,EACF,IAAI,QAAQ,gBAAgB,YAAY,eAAe,CAAC,EACxD,KAAK,eAAe,2BAA2B,GAAG,SAAS,mBAAmB,UAAU,CAAC,CAAC,EAC1F,KAAK,gBAAgB,SAAS,oBAAoB,UAAU,CAAC,CAAC,EAC9D,IAAI,iBAAiB,SAAS,2BAA2B,UAAU,CAAC,CAAC,EACrE;AAAA,EACG;AAAA,EACA,uBAAuB,UAAU;AAAA,EACjC,SAAS,4BAA4B,eAAe,CAAC;AACzD,EACC;AAAA,EACG;AAAA,EACA,uBAAuB,UAAU;AAAA,EACjC,SAAS,gCAAgC,eAAe,CAAC;AAC7D,EACC;AAAA,EACG;AAAA,EACA,uBAAuB,UAAU;AAAA,EACjC,SAAS,iCAAiC,eAAe,CAAC;AAC9D,EACC;AAAA,EACG;AAAA,EACA,uBAAuB,UAAU;AAAA,EACjC,SAAS,iCAAiC,eAAe,CAAC;AAC9D,EACC,KAAK,UAAU,uBAAuB,UAAU,GAAG,SAAS,yBAAyB,CAAC,CAAC,EACvF,KAAK,mBAAmB,uBAAuB,UAAU,GAAG,SAAS,gCAAgC,CAAC,CAAC,EACvG,IAAI,WAAW,uBAAuB,UAAU,GAAG,0BAA0B,CAAC;","names":[]}
@@ -4,7 +4,8 @@ import {
4
4
  createLoginPageSubmitHandler,
5
5
  createLogoutPageHandler
6
6
  } from "../features/auth/http/pages.js";
7
- const createAuthPagesRouter = (authConfig) => Router().get("/login", createLoginPageHandler(authConfig)).post("/login", createLoginPageSubmitHandler(authConfig)).post("/logout", createLogoutPageHandler(authConfig));
7
+ import { createAuthAttemptRateLimit } from "../modules/rate-limit.js";
8
+ const createAuthPagesRouter = (authConfig) => Router().get("/login", createLoginPageHandler(authConfig)).post("/login", createAuthAttemptRateLimit(), createLoginPageSubmitHandler(authConfig)).post("/logout", createLogoutPageHandler(authConfig));
8
9
  export {
9
10
  createAuthPagesRouter
10
11
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routes/auth-pages.ts"],"sourcesContent":["import { Router } from 'express';\nimport {\n createLoginPageHandler,\n createLoginPageSubmitHandler,\n createLogoutPageHandler,\n} from '../features/auth/http/pages.js';\nimport type { AuthConfig } from '../modules/auth-mode.js';\n\nexport const createAuthPagesRouter = (authConfig: AuthConfig) =>\n Router()\n .get('/login', createLoginPageHandler(authConfig))\n .post('/login', createLoginPageSubmitHandler(authConfig))\n .post('/logout', createLogoutPageHandler(authConfig));\n"],"mappings":"AAAA,SAAS,cAAc;AACvB;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,OACG;AAGA,MAAM,wBAAwB,CAAC,eAClC,OAAO,EACF,IAAI,UAAU,uBAAuB,UAAU,CAAC,EAChD,KAAK,UAAU,6BAA6B,UAAU,CAAC,EACvD,KAAK,WAAW,wBAAwB,UAAU,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/routes/auth-pages.ts"],"sourcesContent":["import { Router } from 'express';\nimport {\n createLoginPageHandler,\n createLoginPageSubmitHandler,\n createLogoutPageHandler,\n} from '../features/auth/http/pages.js';\nimport type { AuthConfig } from '../modules/auth-mode.js';\nimport { createAuthAttemptRateLimit } from '../modules/rate-limit.js';\n\nexport const createAuthPagesRouter = (authConfig: AuthConfig) =>\n Router()\n .get('/login', createLoginPageHandler(authConfig))\n .post('/login', createAuthAttemptRateLimit(), createLoginPageSubmitHandler(authConfig))\n .post('/logout', createLogoutPageHandler(authConfig));\n"],"mappings":"AAAA,SAAS,cAAc;AACvB;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,OACG;AAEP,SAAS,kCAAkC;AAEpC,MAAM,wBAAwB,CAAC,eAClC,OAAO,EACF,IAAI,UAAU,uBAAuB,UAAU,CAAC,EAChD,KAAK,UAAU,2BAA2B,GAAG,6BAA6B,UAAU,CAAC,EACrF,KAAK,WAAW,wBAAwB,UAAU,CAAC;","names":[]}
@@ -6,7 +6,7 @@ const shouldBlockClientRoute = (authConfig, requestPath, authenticated) => {
6
6
  if (authConfig.mode !== "password" || authenticated) {
7
7
  return false;
8
8
  }
9
- if (requestPath.startsWith("/api") || requestPath.startsWith("/graphql") || requestPath.startsWith("/auth")) {
9
+ if (requestPath.startsWith("/api") || requestPath.startsWith("/graphql") || requestPath === "/login" || requestPath === "/logout") {
10
10
  return false;
11
11
  }
12
12
  return path.extname(requestPath) === "";
@@ -14,7 +14,7 @@ const shouldBlockClientRoute = (authConfig, requestPath, authenticated) => {
14
14
  const createClientRouter = (authConfig) => Router().use("/assets/images", express.static(paths.imageDir)).use((req, res, next) => {
15
15
  if (shouldBlockClientRoute(authConfig, req.path, isAuthenticatedRequest(req))) {
16
16
  const redirectPath = encodeURIComponent(req.originalUrl || "/");
17
- res.redirect(303, `/auth/login?next=${redirectPath}`);
17
+ res.redirect(303, `/login?next=${redirectPath}`);
18
18
  return;
19
19
  }
20
20
  next();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routes/client.ts"],"sourcesContent":["import express, { Router } from 'express';\nimport path from 'path';\nimport { isAuthenticatedRequest } from '../modules/auth-guard.js';\nimport type { AuthConfig } from '../modules/auth-mode.js';\nimport { paths } from '../paths.js';\n\nconst shouldBlockClientRoute = (authConfig: AuthConfig, requestPath: string, authenticated: boolean) => {\n if (authConfig.mode !== 'password' || authenticated) {\n return false;\n }\n\n if (requestPath.startsWith('/api') || requestPath.startsWith('/graphql') || requestPath.startsWith('/auth')) {\n return false;\n }\n\n return path.extname(requestPath) === '';\n};\n\nexport const createClientRouter = (authConfig: AuthConfig) =>\n Router()\n .use('/assets/images', express.static(paths.imageDir))\n .use((req, res, next) => {\n if (shouldBlockClientRoute(authConfig, req.path, isAuthenticatedRequest(req))) {\n const redirectPath = encodeURIComponent(req.originalUrl || '/');\n res.redirect(303, `/auth/login?next=${redirectPath}`);\n return;\n }\n\n next();\n })\n .use(express.static(paths.clientDist, { extensions: ['html'] }))\n .get(/.*/, (_req, res) => {\n res.sendFile(paths.clientIndex);\n });\n"],"mappings":"AAAA,OAAO,WAAW,cAAc;AAChC,OAAO,UAAU;AACjB,SAAS,8BAA8B;AAEvC,SAAS,aAAa;AAEtB,MAAM,yBAAyB,CAAC,YAAwB,aAAqB,kBAA2B;AACpG,MAAI,WAAW,SAAS,cAAc,eAAe;AACjD,WAAO;AAAA,EACX;AAEA,MAAI,YAAY,WAAW,MAAM,KAAK,YAAY,WAAW,UAAU,KAAK,YAAY,WAAW,OAAO,GAAG;AACzG,WAAO;AAAA,EACX;AAEA,SAAO,KAAK,QAAQ,WAAW,MAAM;AACzC;AAEO,MAAM,qBAAqB,CAAC,eAC/B,OAAO,EACF,IAAI,kBAAkB,QAAQ,OAAO,MAAM,QAAQ,CAAC,EACpD,IAAI,CAAC,KAAK,KAAK,SAAS;AACrB,MAAI,uBAAuB,YAAY,IAAI,MAAM,uBAAuB,GAAG,CAAC,GAAG;AAC3E,UAAM,eAAe,mBAAmB,IAAI,eAAe,GAAG;AAC9D,QAAI,SAAS,KAAK,oBAAoB,YAAY,EAAE;AACpD;AAAA,EACJ;AAEA,OAAK;AACT,CAAC,EACA,IAAI,QAAQ,OAAO,MAAM,YAAY,EAAE,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,EAC9D,IAAI,MAAM,CAAC,MAAM,QAAQ;AACtB,MAAI,SAAS,MAAM,WAAW;AAClC,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/routes/client.ts"],"sourcesContent":["import express, { Router } from 'express';\nimport path from 'path';\nimport { isAuthenticatedRequest } from '../modules/auth-guard.js';\nimport type { AuthConfig } from '../modules/auth-mode.js';\nimport { paths } from '../paths.js';\n\nconst shouldBlockClientRoute = (authConfig: AuthConfig, requestPath: string, authenticated: boolean) => {\n if (authConfig.mode !== 'password' || authenticated) {\n return false;\n }\n\n if (\n requestPath.startsWith('/api') ||\n requestPath.startsWith('/graphql') ||\n requestPath === '/login' ||\n requestPath === '/logout'\n ) {\n return false;\n }\n\n return path.extname(requestPath) === '';\n};\n\nexport const createClientRouter = (authConfig: AuthConfig) =>\n Router()\n .use('/assets/images', express.static(paths.imageDir))\n .use((req, res, next) => {\n if (shouldBlockClientRoute(authConfig, req.path, isAuthenticatedRequest(req))) {\n const redirectPath = encodeURIComponent(req.originalUrl || '/');\n res.redirect(303, `/login?next=${redirectPath}`);\n return;\n }\n\n next();\n })\n .use(express.static(paths.clientDist, { extensions: ['html'] }))\n .get(/.*/, (_req, res) => {\n res.sendFile(paths.clientIndex);\n });\n"],"mappings":"AAAA,OAAO,WAAW,cAAc;AAChC,OAAO,UAAU;AACjB,SAAS,8BAA8B;AAEvC,SAAS,aAAa;AAEtB,MAAM,yBAAyB,CAAC,YAAwB,aAAqB,kBAA2B;AACpG,MAAI,WAAW,SAAS,cAAc,eAAe;AACjD,WAAO;AAAA,EACX;AAEA,MACI,YAAY,WAAW,MAAM,KAC7B,YAAY,WAAW,UAAU,KACjC,gBAAgB,YAChB,gBAAgB,WAClB;AACE,WAAO;AAAA,EACX;AAEA,SAAO,KAAK,QAAQ,WAAW,MAAM;AACzC;AAEO,MAAM,qBAAqB,CAAC,eAC/B,OAAO,EACF,IAAI,kBAAkB,QAAQ,OAAO,MAAM,QAAQ,CAAC,EACpD,IAAI,CAAC,KAAK,KAAK,SAAS;AACrB,MAAI,uBAAuB,YAAY,IAAI,MAAM,uBAAuB,GAAG,CAAC,GAAG;AAC3E,UAAM,eAAe,mBAAmB,IAAI,eAAe,GAAG;AAC9D,QAAI,SAAS,KAAK,eAAe,YAAY,EAAE;AAC/C;AAAA,EACJ;AAEA,OAAK;AACT,CAAC,EACA,IAAI,QAAQ,OAAO,MAAM,YAAY,EAAE,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,EAC9D,IAAI,MAAM,CAAC,MAAM,QAAQ;AACtB,MAAI,SAAS,MAAM,WAAW;AAClC,CAAC;","names":[]}
@@ -1 +0,0 @@
1
- import{a as e}from"./graph-vendor-CUxe67Lr.js";import{p as n}from"./index-Bsx4P7EA.js";import{j as r}from"./note-vendor-5N5mRkIT.js";const m=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M228,128a12,12,0,0,1-12,12H140v76a12,12,0,0,1-24,0V140H40a12,12,0,0,1,0-24h76V40a12,12,0,0,1,24,0v76h76A12,12,0,0,1,228,128Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,56V200a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V56A16,16,0,0,1,56,40H200A16,16,0,0,1,216,56Z",opacity:"0.2"}),e.createElement("path",{d:"M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM184,136H136v48a8,8,0,0,1-16,0V136H72a8,8,0,0,1,0-16h48V72a8,8,0,0,1,16,0v48h48a8,8,0,0,1,0,16Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M222,128a6,6,0,0,1-6,6H134v82a6,6,0,0,1-12,0V134H40a6,6,0,0,1,0-12h82V40a6,6,0,0,1,12,0v82h82A6,6,0,0,1,222,128Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M220,128a4,4,0,0,1-4,4H132v84a4,4,0,0,1-8,0V132H40a4,4,0,0,1,0-8h84V40a4,4,0,0,1,8,0v84h84A4,4,0,0,1,220,128Z"}))]]),t=e.forwardRef((a,l)=>e.createElement(n,{ref:l,...a,weights:m}));t.displayName="PlusIcon";const s=t;function p({children:a}){return r.jsx("div",{className:"flex w-full flex-col-reverse gap-2 sm:flex-row sm:justify-end",children:a})}export{p as M,s as n};