ocean-brain 0.7.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/index.js +3 -3
  2. package/dist/mcp.js +180 -736
  3. package/package.json +4 -1
  4. package/server/client/dist/assets/ArrowRight.es-C4zlc54Z.js +1 -0
  5. package/server/client/dist/assets/{Calendar-Bc2zJueR.js → Calendar-COrN6Hjv.js} +2 -2
  6. package/server/client/dist/assets/Callout-BK0K9E74.js +1 -0
  7. package/server/client/dist/assets/Copy.es-zm8f_XXX.js +1 -0
  8. package/server/client/dist/assets/Graph-BH1fzhue.js +1 -0
  9. package/server/client/dist/assets/{Image-DxWhlIDG.js → Image-KXuHOG4b.js} +1 -1
  10. package/server/client/dist/assets/Image.es-BKahRbIm.js +1 -0
  11. package/server/client/dist/assets/Note-C143cKKw.js +12 -0
  12. package/server/client/dist/assets/Plus.es-BqeC8T8h.js +1 -0
  13. package/server/client/dist/assets/Reminders-BYG5rz1M.js +1 -0
  14. package/server/client/dist/assets/Search-DBWsIMDP.js +1 -0
  15. package/server/client/dist/assets/{SurfaceCard-CkAVMLPs.js → SurfaceCard-CNSQfYfM.js} +1 -1
  16. package/server/client/dist/assets/Tag-5aCfSO8Y.js +1 -0
  17. package/server/client/dist/assets/TagNotes-BPZjN-6l.js +1 -0
  18. package/server/client/dist/assets/Trash.es-CRQkTnXZ.js +1 -0
  19. package/server/client/dist/assets/ViewNotes-DGLNF96x.js +1 -0
  20. package/server/client/dist/assets/ViewSectionDialog-CzskgmMF.js +102 -0
  21. package/server/client/dist/assets/Views-DR5jT23G.js +1 -0
  22. package/server/client/dist/assets/{graph-vendor-CUxe67Lr.js → graph-vendor-UTvF2NS_.js} +2 -2
  23. package/server/client/dist/assets/image.api-8dlMwzMd.js +17 -0
  24. package/server/client/dist/assets/{index-C5BGzDgt.css → index-BEUqDuAZ.css} +1 -1
  25. package/server/client/dist/assets/{index-D3BfroIT.js → index-DGIAh3ZY.js} +1 -1
  26. package/server/client/dist/assets/index-VHp17GQX.js +342 -0
  27. package/server/client/dist/assets/manage-image-RqtWKas2.js +1 -0
  28. package/server/client/dist/assets/{manage-image-detail-C8Q7K7I3.js → manage-image-detail-BTSIbLlQ.js} +1 -1
  29. package/server/client/dist/assets/mcp-EDurbggR.js +4 -0
  30. package/server/client/dist/assets/mcp-admin-adapter-DmGUo5G8.js +1 -0
  31. package/server/client/dist/assets/{note-core-Z6kqulGB.js → note-core-BklwyZHk.js} +1 -1
  32. package/server/client/dist/assets/{note-vendor-i1M2FSI2.js → note-vendor-DkYKUmLo.js} +1 -1
  33. package/server/client/dist/assets/{placeholder-Cr_nI8UI.js → placeholder-DeKgSBnC.js} +4 -4
  34. package/server/client/dist/assets/{properties-CvYWJWUM.js → properties-BjrZvOYn.js} +1 -1
  35. package/server/client/dist/assets/tag.api-Dl8DQd0m.js +21 -0
  36. package/server/client/dist/assets/trash-C5ntddLu.js +1 -0
  37. package/server/client/dist/assets/{useReminderMutate-CDmIEwsw.js → useReminderMutate-CoXXqJY2.js} +1 -1
  38. package/server/client/dist/index.html +5 -5
  39. package/server/dist/features/auth/http/login-page.js +1 -1
  40. package/server/dist/features/image/http/upload.js +34 -23
  41. package/server/dist/features/image/http/upload.js.map +1 -1
  42. package/server/dist/features/image/services/validation.js +111 -0
  43. package/server/dist/features/image/services/validation.js.map +1 -0
  44. package/server/dist/features/mcp-admin/http/handlers.js +11 -3
  45. package/server/dist/features/mcp-admin/http/handlers.js.map +1 -1
  46. package/server/dist/features/note/http/mcp.js +8 -43
  47. package/server/dist/features/note/http/mcp.js.map +1 -1
  48. package/server/dist/features/note/services/cleanup.js +2 -2
  49. package/server/dist/features/note/services/cleanup.js.map +1 -1
  50. package/server/dist/features/note/services/markdown-intent-write.js +11 -43
  51. package/server/dist/features/note/services/markdown-intent-write.js.map +1 -1
  52. package/server/dist/features/note/services/markdown-patch.js +4 -123
  53. package/server/dist/features/note/services/markdown-patch.js.map +1 -1
  54. package/server/dist/features/note/services/write.js +13 -13
  55. package/server/dist/features/note/services/write.js.map +1 -1
  56. package/server/dist/features/tag/graphql/tag.query.resolver.js +16 -1
  57. package/server/dist/features/tag/graphql/tag.query.resolver.js.map +1 -1
  58. package/server/dist/features/tag/graphql/tag.type-defs.js +2 -0
  59. package/server/dist/features/tag/graphql/tag.type-defs.js.map +1 -1
  60. package/server/dist/modules/app-version.js +105 -0
  61. package/server/dist/modules/app-version.js.map +1 -0
  62. package/server/dist/modules/mcp-auth.js +62 -1
  63. package/server/dist/modules/mcp-auth.js.map +1 -1
  64. package/server/dist/paths.js +1 -0
  65. package/server/dist/paths.js.map +1 -1
  66. package/server/dist/routes/api.js +2 -8
  67. package/server/dist/routes/api.js.map +1 -1
  68. package/server/dist/routes/client.js +51 -1
  69. package/server/dist/routes/client.js.map +1 -1
  70. package/server/client/dist/assets/Callout-B4kA_Cvr.js +0 -1
  71. package/server/client/dist/assets/Graph-uq5jfJgt.js +0 -1
  72. package/server/client/dist/assets/Image.es-BpAOJjJT.js +0 -1
  73. package/server/client/dist/assets/Note-Bo8ZYRP-.js +0 -22
  74. package/server/client/dist/assets/Plus.es-Dc_js8AC.js +0 -1
  75. package/server/client/dist/assets/Reminders-lj7Equld.js +0 -1
  76. package/server/client/dist/assets/Search-1wcRO5--.js +0 -1
  77. package/server/client/dist/assets/Tag-CeygwFfv.js +0 -1
  78. package/server/client/dist/assets/TagNotes-B8DqItxw.js +0 -1
  79. package/server/client/dist/assets/Trash.es-OuQVb_SX.js +0 -1
  80. package/server/client/dist/assets/ViewNotes-BH7PbdOD.js +0 -1
  81. package/server/client/dist/assets/ViewSectionTableRenderer-BJwhd5be.js +0 -102
  82. package/server/client/dist/assets/Views-eYFtsdFj.js +0 -1
  83. package/server/client/dist/assets/image.api-C0eYGCeH.js +0 -17
  84. package/server/client/dist/assets/index-jIfkRdY_.js +0 -361
  85. package/server/client/dist/assets/manage-image-C0YwkLIP.js +0 -1
  86. package/server/client/dist/assets/mcp-DHHhTLC8.js +0 -1
  87. package/server/client/dist/assets/trash-DA27aW7L.js +0 -1
  88. package/server/dist/features/image/services/remote-fetch.js +0 -176
  89. package/server/dist/features/image/services/remote-fetch.js.map +0 -1
@@ -0,0 +1 @@
1
+ import{j as e}from"./note-vendor-DkYKUmLo.js";import{Y as Q,j as F,aF as M,k as L,G as _,a5 as I,b1 as K,q as i,ao as E,b2 as B,N as O,b3 as G,P as w,Q as Y,T as d,aG as H,B as b,Z as W,b4 as $,i as l,m as z,d as U,ak as V,u as Z,b5 as J,M as y}from"./index-VHp17GQX.js";import{a as k}from"./graph-vendor-UTvF2NS_.js";import{S as C}from"./SurfaceCard-CNSQfYfM.js";import"./note-core-BklwyZHk.js";const m=25,v=F(M),S=30,R=`Deleted notes stay here for ${S} days before permanent removal`,g={title:"Permanently delete note?",description:"This note is referenced by the notes below. If you permanently delete it, those links will stay broken. Other notes will not be edited automatically.",confirmLabel:"Delete now",confirmVariant:"danger"},P=s=>U(s).format("YYYY-MM-DD HH:mm"),X=({note:s,onClose:o})=>{const a=s?.id??"",c=s?.title||"Untitled note",r=Z({queryKey:i.notes.trashDetail(a),enabled:!!s,queryFn:async()=>{const h=await J(a);if(h.type==="error")throw h;if(!h.trashedNote)throw new Error("Trashed note not found");return h.trashedNote}}),f=r.data?.contentAsMarkdown.trim()??"";return e.jsxs(y,{isOpen:!!s,onClose:o,variant:"inspect",children:[e.jsx(y.Header,{title:c,onClose:o}),e.jsx(y.Body,{children:e.jsxs("div",{className:"flex flex-col gap-3",children:[s&&e.jsxs(d,{as:"p",variant:"meta",tone:"secondary",children:["Deleted ",P(s.deletedAt)]}),r.isLoading&&e.jsx(d,{as:"p",variant:"meta",tone:"secondary",children:"Loading deleted note content..."}),r.isError&&e.jsx(d,{as:"p",variant:"meta",tone:"error",children:"Failed to load deleted note content."}),!r.isLoading&&!r.isError&&f&&e.jsx("pre",{className:"max-h-[60vh] overflow-auto whitespace-pre-wrap break-words rounded-[14px] border border-border-subtle bg-hover-subtle/50 px-4 py-3 text-sm leading-6 text-fg-secondary",children:f}),!r.isLoading&&!r.isError&&!f&&e.jsx(d,{as:"p",variant:"meta",tone:"secondary",className:"rounded-[14px] border border-border-subtle bg-hover-subtle/50 px-4 py-3",children:"No readable content was found for this deleted note."})]})}),e.jsx(y.Footer,{children:e.jsx(b,{variant:"ghost",size:"sm",onClick:o,children:"Close"})})]})},ee=()=>e.jsx(w,{title:"Trash",variant:"default",heading:e.jsx(l,{width:118,height:24,className:"rounded-full"}),description:e.jsx(l,{width:352,height:16,className:"rounded-full"}),children:e.jsx("div",{className:"grid gap-3",children:Array.from({length:3},(s,o)=>e.jsx(C,{padding:"compact",children:e.jsxs("div",{className:"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between",children:[e.jsxs("div",{className:"min-w-0 flex-1 space-y-2",children:[e.jsx(l,{width:"40%",height:18,className:"rounded-full"}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[e.jsx(l,{width:168,height:14,className:"rounded-full"}),e.jsx(l,{width:176,height:14,className:"rounded-full"})]}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[e.jsx(l,{width:56,height:24,className:"rounded-full"}),e.jsx(l,{width:72,height:24,className:"rounded-full"})]})]}),e.jsx(l,{width:92,height:32,className:"rounded-[12px]"})]})},o))})}),te=()=>{const{page:s}=v.useSearch(),o=v.useNavigate(),a=L(),c=z(),r=_(),[f,h]=k.useState(null),[x,j]=k.useState(null),{data:u}=I({queryKey:i.notes.trash({limit:m,offset:(s-1)*m}),queryFn:async()=>{const t=await K({limit:m,offset:(s-1)*m});if(t.type==="error")throw t;return t.trashedNotes}}),N=E({mutationFn:B,onSuccess:async t=>{if(t.type==="error")throw t;await Promise.all([a.invalidateQueries({queryKey:i.notes.all(),exact:!1}),a.invalidateQueries({queryKey:i.tags.all(),exact:!1}),a.invalidateQueries({queryKey:i.reminders.all(),exact:!1}),a.invalidateQueries({queryKey:i.images.all(),exact:!1}),a.invalidateQueries({queryKey:i.calendar.all(),exact:!1})]),c("The note has been restored."),o({to:O,params:{id:t.restoreTrashedNote.id}})},onError:()=>{c("Failed to restore the note.")}}),p=E({mutationFn:G,onSuccess:async t=>{if(t.type==="error")throw t;await a.invalidateQueries({queryKey:i.notes.trashAll(),exact:!1}),c("The note has been permanently deleted.")},onError:()=>{c("Failed to permanently delete the note.")}}),D=()=>{if(!x)return;const{id:t}=x;j(null),p.mutate(t)},q=async t=>{const n=await V(t);if(n.type==="error"){c("Failed to check linked notes before permanently deleting this note.");return}if(a.setQueryData(i.notes.backReferences(t),n.backReferences),n.backReferences.length>0){j({id:t,backReferences:n.backReferences});return}await r("Permanently delete this note? This cannot be undone.")&&p.mutate(t)},T=u.totalCount>0?`Trash (${u.totalCount})`:void 0,A=u.notes.find(t=>t.id===f)??null;return u.notes.length===0?e.jsx(w,{title:"Trash",variant:"default",heading:T,description:R,children:e.jsx(Y,{title:"Trash is empty",description:`Delete a note and you can restore it here for ${S} days before permanent removal`})}):e.jsxs(w,{title:"Trash",variant:"default",heading:T,description:R,children:[e.jsxs("div",{className:"flex flex-col gap-4",children:[e.jsx("div",{className:"grid gap-3",children:u.notes.map(t=>e.jsx(C,{padding:"compact",children:e.jsxs("div",{className:"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between",children:[e.jsxs("div",{className:"min-w-0 flex-1",children:[e.jsxs("div",{className:"flex min-w-0 items-start gap-1.5",children:[e.jsx(d,{as:"h2",variant:"body",weight:"medium",className:"min-w-0 flex-1 break-words leading-[1.25]",children:t.title||"Untitled note"}),t.pinned&&e.jsx("span",{title:"Pinned",className:"inline-flex h-5 w-5 shrink-0 items-center justify-center text-fg-tertiary",children:e.jsx(H,{className:"h-3.5 w-3.5"})})]}),e.jsx("div",{className:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-1",children:e.jsxs(d,{as:"div",variant:"meta",weight:"medium",tone:"secondary",children:["Deleted ",P(t.deletedAt)]})}),t.tagNames.length>0&&e.jsx("div",{className:"mt-2 flex flex-wrap gap-1.5",children:t.tagNames.map(n=>e.jsx(d,{as:"span",variant:"label",weight:"medium",tone:"tertiary",className:"rounded-full border border-border-subtle bg-hover-subtle px-2 py-0.5",children:n},`${t.id}-${n}`))}),t.contentPreview&&e.jsx("div",{className:"mt-3 rounded-[14px] border border-border-subtle bg-hover-subtle/50 px-3 py-2",children:e.jsx(d,{as:"p",variant:"meta",tone:"secondary",className:"line-clamp-3 whitespace-pre-wrap break-words",children:t.contentPreview})})]}),e.jsxs("div",{className:"flex shrink-0 flex-wrap gap-2 sm:justify-end",children:[e.jsx(b,{variant:"subtle",size:"sm",onClick:()=>h(t.id),children:"View content"}),e.jsx(b,{variant:"subtle",size:"sm",isLoading:N.isPending&&N.variables===t.id,onClick:()=>N.mutate(t.id),children:"Restore"}),e.jsx(b,{variant:"soft-danger",size:"sm",isLoading:p.isPending&&p.variables===t.id,onClick:()=>q(t.id),children:"Delete now"})]})]})},t.id))}),u.totalCount>m&&e.jsx(W,{page:s,last:Math.ceil(u.totalCount/m),onChange:t=>{o({search:n=>({...n,page:t})})}})]}),e.jsx(X,{note:A,onClose:()=>h(null)}),e.jsx($,{isOpen:!!x,title:g.title,description:g.description,references:x?.backReferences??[],confirmLabel:g.confirmLabel,confirmVariant:g.confirmVariant,onClose:()=>j(null),onConfirm:D})]})},oe=()=>{const{page:s}=v.useSearch();return e.jsx(Q,{fallback:e.jsx(ee,{}),errorTitle:"Failed to load trash",errorDescription:"Retry loading deleted notes",resetKeys:[s],children:e.jsx(te,{})})};export{oe as default};
@@ -1,4 +1,4 @@
1
- import{j as y}from"./note-vendor-i1M2FSI2.js";import{h as u,a6 as f,q as n,k as R,m as $}from"./index-jIfkRdY_.js";import{a as l}from"./graph-vendor-CUxe67Lr.js";const g=(e={})=>({limit:e.limit??10,offset:e.offset??0}),D=async(e,t)=>u(`
1
+ import{j as y}from"./note-vendor-DkYKUmLo.js";import{h as u,a5 as f,q as n,k as R,m as $}from"./index-VHp17GQX.js";import{a as l}from"./graph-vendor-UTvF2NS_.js";const g=(e={})=>({limit:e.limit??10,offset:e.offset??0}),D=async(e,t)=>u(`
2
2
  query FetchNoteReminders($noteId: ID!, $pagination: PaginationInput) {
3
3
  noteReminders(noteId: $noteId, pagination: $pagination) {
4
4
  totalCount
@@ -6,11 +6,11 @@
6
6
  <link rel="icon" type="image/png" href="/icon.png" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>Ocean Brain</title>
9
- <script type="module" crossorigin src="/assets/index-jIfkRdY_.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/graph-vendor-CUxe67Lr.js">
11
- <link rel="modulepreload" crossorigin href="/assets/note-core-Z6kqulGB.js">
12
- <link rel="modulepreload" crossorigin href="/assets/note-vendor-i1M2FSI2.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-C5BGzDgt.css">
9
+ <script type="module" crossorigin src="/assets/index-VHp17GQX.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/graph-vendor-UTvF2NS_.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/note-core-BklwyZHk.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/note-vendor-DkYKUmLo.js">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-BEUqDuAZ.css">
14
14
  </head>
15
15
 
16
16
  <body>
@@ -3,7 +3,7 @@ const LOGIN_ERROR_TOKEN = "<!-- OCEAN_BRAIN_LOGIN_ERROR -->";
3
3
  const NEXT_PATH_TOKEN = "OCEAN_BRAIN_NEXT_PATH";
4
4
  const readLoginPageTemplate = () => {
5
5
  if (true) {
6
- return '<!doctype html>\n<html lang="en">\n<head>\n <meta charset="utf-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1" />\n <title>Ocean Brain Sign In</title>\n <style>\n :root {\n color-scheme: light dark;\n --surface: #f7f8fa;\n --elevated: #fbfcfd;\n --fg-default: #202631;\n --fg-secondary: #626d7c;\n --fg-tertiary: #8993a2;\n --fg-placeholder: #98a2b0;\n --fg-error: #a24545;\n --fg-on-filled: #f6f8fb;\n --border-subtle: #e5e9ee;\n --border-focus: #757f8d;\n --border-error: #d47777;\n --accent-soft-danger: rgba(214, 141, 135, 0.18);\n --accent-soft-primary: rgba(93, 102, 114, 0.12);\n --cta: #23272d;\n --cta-hover: #171a1f;\n --page-bg: #f2f5f8;\n font-family: "Pretendard Variable", "Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;\n background: var(--page-bg);\n color: var(--fg-default);\n }\n @media (prefers-color-scheme: dark) {\n :root {\n --surface: #16191e;\n --elevated: #1d2229;\n --fg-default: #e6ebf2;\n --fg-secondary: #a9b3c2;\n --fg-tertiary: #7f8998;\n --fg-placeholder: #717b88;\n --fg-error: #efaaaa;\n --fg-on-filled: #12161c;\n --border-subtle: #29303a;\n --border-focus: #b4c0cf;\n --border-error: #efaaaa;\n --accent-soft-danger: rgba(215, 146, 139, 0.14);\n --accent-soft-primary: rgba(184, 192, 202, 0.12);\n --cta: #edf1f5;\n --cta-hover: #f8fafc;\n --page-bg: #101318;\n }\n }\n * {\n box-sizing: border-box;\n }\n body {\n margin: 0;\n min-height: 100vh;\n display: grid;\n place-items: center;\n background: var(--page-bg);\n padding: 24px;\n }\n .shell {\n width: min(392px, 100%);\n }\n .brand {\n margin-bottom: 14px;\n color: var(--fg-tertiary);\n font-size: 12px;\n font-weight: 700;\n line-height: 1.4;\n }\n .panel {\n padding: 24px;\n border: 1px solid var(--border-subtle);\n border-radius: 16px;\n background: var(--surface);\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.1),\n 0 1px 4px -2px rgba(23, 29, 38, 0.04);\n }\n h1 {\n margin: 0;\n color: var(--fg-default);\n font-size: 1.375rem;\n line-height: 1.3;\n font-weight: 700;\n }\n .lead {\n margin: 6px 0 22px;\n color: var(--fg-tertiary);\n font-size: 0.875rem;\n line-height: 1.5;\n font-weight: 500;\n }\n label {\n display: block;\n margin-bottom: 8px;\n color: var(--fg-secondary);\n font-size: 0.75rem;\n line-height: 1.4;\n font-weight: 600;\n }\n form {\n display: grid;\n gap: 16px;\n }\n .field {\n display: grid;\n }\n input {\n width: 100%;\n box-sizing: border-box;\n padding: 14px 16px;\n border: 1px solid var(--border-subtle);\n border-radius: 16px;\n background: var(--elevated);\n font-size: 16px;\n color: var(--fg-default);\n transition: border-color 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;\n }\n input::placeholder {\n color: var(--fg-placeholder);\n }\n input:focus {\n outline: none;\n border-color: var(--border-focus);\n box-shadow: 0 0 0 4px var(--accent-soft-primary);\n }\n .hint {\n margin-top: 10px;\n color: var(--fg-tertiary);\n font-size: 0.75rem;\n line-height: 1.4;\n }\n button {\n width: 100%;\n border: 1px solid transparent;\n border-radius: 14px;\n background: var(--cta);\n color: var(--fg-on-filled);\n padding: 14px 18px;\n font-size: 16px;\n font-weight: 600;\n cursor: pointer;\n transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;\n }\n button:hover {\n background: var(--cta-hover);\n }\n button:active {\n transform: translateY(1px);\n }\n button:focus-visible {\n outline: none;\n box-shadow: 0 0 0 4px var(--accent-soft-primary);\n }\n .error {\n margin-bottom: 18px;\n padding: 13px 14px;\n border: 1px solid var(--border-error);\n border-radius: 14px;\n background: var(--accent-soft-danger);\n color: var(--fg-error);\n font-size: 14px;\n font-weight: 600;\n }\n @media (prefers-color-scheme: dark) {\n .panel {\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.03),\n 0 1px 4px -2px rgba(0, 0, 0, 0.18);\n }\n }\n @media (max-width: 640px) {\n body {\n place-items: stretch;\n padding: 18px;\n }\n .shell {\n width: 100%;\n margin: auto 0;\n }\n .panel {\n padding: 22px;\n }\n }\n </style>\n</head>\n<body>\n <main class="shell">\n <div class="brand">Ocean Brain</div>\n <section class="panel" aria-labelledby="login-title">\n <h1 id="login-title">Sign in</h1>\n <p class="lead">Enter the workspace password to continue.</p>\n <!-- OCEAN_BRAIN_LOGIN_ERROR -->\n <form method="post" action="/login">\n <!-- OCEAN_BRAIN_CSRF_INPUT -->\n <input type="hidden" name="next" value="OCEAN_BRAIN_NEXT_PATH" />\n <div class="field">\n <label for="password">Password</label>\n <input id="password" name="password" type="password" autocomplete="current-password" required autofocus />\n </div>\n <button type="submit">Sign in</button>\n </form>\n <div class="hint">This session must be authenticated before the workspace loads.</div>\n </section>\n </main>\n</body>\n</html>\n';
6
+ return '<!doctype html>\n<html lang="en">\n<head>\n <meta charset="utf-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1" />\n <title>Ocean Brain Sign In</title>\n <style>\n :root {\n color-scheme: light dark;\n --surface: #f7f8fa;\n --elevated: #fbfcfd;\n --fg-default: #202631;\n --fg-secondary: #626d7c;\n --fg-tertiary: #8993a2;\n --fg-placeholder: #98a2b0;\n --fg-error: #a24545;\n --fg-on-filled: #f6f8fb;\n --border-subtle: #e5e9ee;\n --border-focus: #757f8d;\n --border-error: #d47777;\n --accent-soft-danger: rgba(214, 141, 135, 0.18);\n --accent-soft-primary: rgba(93, 102, 114, 0.12);\n --cta: #23272d;\n --cta-hover: #171a1f;\n --page-bg: #f2f5f8;\n font-family: "Pretendard Variable", "Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;\n background: var(--page-bg);\n color: var(--fg-default);\n }\n @media (prefers-color-scheme: dark) {\n :root {\n --surface: #16191e;\n --elevated: #1d2229;\n --fg-default: #e6ebf2;\n --fg-secondary: #a9b3c2;\n --fg-tertiary: #7f8998;\n --fg-placeholder: #717b88;\n --fg-error: #efaaaa;\n --fg-on-filled: #12161c;\n --border-subtle: #29303a;\n --border-focus: #b4c0cf;\n --border-error: #efaaaa;\n --accent-soft-danger: rgba(215, 146, 139, 0.14);\n --accent-soft-primary: rgba(184, 192, 202, 0.12);\n --cta: #edf1f5;\n --cta-hover: #f8fafc;\n --page-bg: #101318;\n }\n }\n * {\n box-sizing: border-box;\n }\n body {\n margin: 0;\n min-height: 100vh;\n display: grid;\n place-items: center;\n background: var(--page-bg);\n padding: 24px;\n }\n .shell {\n width: min(392px, 100%);\n }\n .brand {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 14px;\n color: var(--fg-tertiary);\n font-size: 12px;\n font-weight: 700;\n line-height: 1.4;\n }\n .brand img {\n width: 28px;\n height: 28px;\n border-radius: 8px;\n }\n .panel {\n padding: 24px;\n border: 1px solid var(--border-subtle);\n border-radius: 16px;\n background: var(--surface);\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.1),\n 0 1px 4px -2px rgba(23, 29, 38, 0.04);\n }\n .sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n }\n label {\n display: block;\n margin-bottom: 8px;\n color: var(--fg-secondary);\n font-size: 0.75rem;\n line-height: 1.4;\n font-weight: 600;\n }\n form {\n display: grid;\n gap: 16px;\n }\n .field {\n display: grid;\n }\n input {\n width: 100%;\n box-sizing: border-box;\n padding: 14px 16px;\n border: 1px solid var(--border-subtle);\n border-radius: 16px;\n background: var(--elevated);\n font-size: 16px;\n color: var(--fg-default);\n transition: border-color 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;\n }\n input::placeholder {\n color: var(--fg-placeholder);\n }\n input:focus {\n outline: none;\n border-color: var(--border-focus);\n box-shadow: 0 0 0 4px var(--accent-soft-primary);\n }\n button {\n width: 100%;\n border: 1px solid transparent;\n border-radius: 14px;\n background: var(--cta);\n color: var(--fg-on-filled);\n padding: 14px 18px;\n font-size: 16px;\n font-weight: 600;\n cursor: pointer;\n transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;\n }\n button:hover {\n background: var(--cta-hover);\n }\n button:active {\n transform: translateY(1px);\n }\n button:focus-visible {\n outline: none;\n box-shadow: 0 0 0 4px var(--accent-soft-primary);\n }\n .error {\n margin-bottom: 18px;\n padding: 13px 14px;\n border: 1px solid var(--border-error);\n border-radius: 14px;\n background: var(--accent-soft-danger);\n color: var(--fg-error);\n font-size: 14px;\n font-weight: 600;\n }\n @media (prefers-color-scheme: dark) {\n .panel {\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.03),\n 0 1px 4px -2px rgba(0, 0, 0, 0.18);\n }\n }\n @media (max-width: 640px) {\n body {\n place-items: stretch;\n padding: 18px;\n }\n .shell {\n width: 100%;\n margin: auto 0;\n }\n .panel {\n padding: 22px;\n }\n }\n </style>\n</head>\n<body>\n <main class="shell">\n <div class="brand"><img src="/icon.png" alt="" aria-hidden="true" />Ocean Brain</div>\n <section class="panel" aria-labelledby="login-title">\n <h1 id="login-title" class="sr-only">Sign in</h1>\n <!-- OCEAN_BRAIN_LOGIN_ERROR -->\n <form method="post" action="/login">\n <!-- OCEAN_BRAIN_CSRF_INPUT -->\n <input type="hidden" name="next" value="OCEAN_BRAIN_NEXT_PATH" />\n <div class="field">\n <label for="password">Password</label>\n <input id="password" name="password" type="password" autocomplete="current-password" required autofocus />\n </div>\n <button type="submit">Sign in</button>\n </form>\n </section>\n </main>\n</body>\n</html>\n';
7
7
  }
8
8
  return readFileSync(new URL("./login-page.html", import.meta.url), "utf8");
9
9
  };
@@ -1,48 +1,59 @@
1
1
  import { createAppError } from "../../../modules/error-handler.js";
2
- import { fetchRemoteImage, RemoteImageFetchError } from "../services/remote-fetch.js";
3
2
  import { persistUploadedImage } from "../services/upload.js";
3
+ import { ImageValidationError, normalizeImageContentType, validateImagePayload } from "../services/validation.js";
4
+ const IMAGE_DATA_URL_PATTERN = /^data:(image\/[a-z0-9.+-]+);base64,([A-Za-z0-9+/]+={0,2})$/i;
4
5
  const createUploadResponse = (image) => {
5
6
  return {
6
7
  id: image.id,
7
8
  url: image.url
8
9
  };
9
10
  };
11
+ const parseImageDataUrl = (image) => {
12
+ const match = IMAGE_DATA_URL_PATTERN.exec(image);
13
+ if (!match || !match[2] || match[2].length % 4 !== 0) {
14
+ return null;
15
+ }
16
+ return {
17
+ base64: match[2],
18
+ contentType: normalizeImageContentType(match[1])
19
+ };
20
+ };
21
+ const createUploadValidationError = (error) => {
22
+ if (error.code === "IMAGE_UNSUPPORTED_CONTENT_TYPE") {
23
+ return createAppError(415, "IMAGE_UPLOAD_UNSUPPORTED_TYPE", "Uploaded image content type is not supported.");
24
+ }
25
+ return createAppError(400, "INVALID_IMAGE_UPLOAD", "Uploaded image content is invalid.");
26
+ };
10
27
  const createUploadImageHandler = (persistImage = persistUploadedImage) => {
11
28
  return async (req, res) => {
12
29
  const { image } = req.body ?? {};
13
- if (typeof image !== "string" || !image.match(/data:image\/\s*(\w+);base64,/)) {
30
+ if (typeof image !== "string") {
14
31
  throw createAppError(400, "INVALID_IMAGE_UPLOAD", "No image uploaded");
15
32
  }
16
- const [info, data] = image.split(",");
17
- const buffer = Buffer.from(data, "base64");
18
- const extension = info.split(";")[0]?.split("/")[1] ?? "";
19
- const uploadedImage = await persistImage({
20
- buffer,
21
- extension
22
- });
23
- res.status(200).json(createUploadResponse(uploadedImage)).end();
24
- };
25
- };
26
- const createUploadImageFromSrcHandler = (fetchImage = fetchRemoteImage, persistImage = persistUploadedImage) => {
27
- return async (req, res) => {
28
- const src = String(req.body?.src ?? "");
33
+ const parsedImage = parseImageDataUrl(image);
34
+ if (!parsedImage) {
35
+ throw createAppError(400, "INVALID_IMAGE_UPLOAD", "No image uploaded");
36
+ }
37
+ let validatedImage;
29
38
  try {
30
- const remoteImage = await fetchImage(src);
31
- const uploadedImage = await persistImage({
32
- buffer: remoteImage.buffer,
33
- extension: remoteImage.extension
39
+ validatedImage = validateImagePayload({
40
+ buffer: Buffer.from(parsedImage.base64, "base64"),
41
+ contentType: parsedImage.contentType
34
42
  });
35
- res.status(200).json(createUploadResponse(uploadedImage)).end();
36
43
  } catch (error) {
37
- if (error instanceof RemoteImageFetchError) {
38
- throw createAppError(error.status, error.code, error.message);
44
+ if (error instanceof ImageValidationError) {
45
+ throw createUploadValidationError(error);
39
46
  }
40
47
  throw error;
41
48
  }
49
+ const uploadedImage = await persistImage({
50
+ buffer: validatedImage.buffer,
51
+ extension: validatedImage.extension
52
+ });
53
+ res.status(200).json(createUploadResponse(uploadedImage)).end();
42
54
  };
43
55
  };
44
56
  export {
45
- createUploadImageFromSrcHandler,
46
57
  createUploadImageHandler
47
58
  };
48
59
  //# sourceMappingURL=upload.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/features/image/http/upload.ts"],"sourcesContent":["import { createAppError } from '~/modules/error-handler.js';\nimport type { Controller } from '~/types/index.js';\nimport { fetchRemoteImage, type RemoteImage, RemoteImageFetchError } from '../services/remote-fetch.js';\nimport { type PersistedImageSummary, type PersistImageInput, persistUploadedImage } from '../services/upload.js';\n\ntype PersistImage = (input: PersistImageInput) => Promise<PersistedImageSummary>;\ntype FetchImage = (src: string) => Promise<RemoteImage>;\n\nconst createUploadResponse = (image: PersistedImageSummary) => {\n return {\n id: image.id,\n url: image.url,\n };\n};\n\nexport const createUploadImageHandler = (persistImage: PersistImage = persistUploadedImage): Controller => {\n return async (req, res) => {\n const { image } = req.body ?? {};\n\n if (typeof image !== 'string' || !image.match(/data:image\\/\\s*(\\w+);base64,/)) {\n throw createAppError(400, 'INVALID_IMAGE_UPLOAD', 'No image uploaded');\n }\n\n const [info, data] = image.split(',');\n const buffer = Buffer.from(data, 'base64');\n const extension = info.split(';')[0]?.split('/')[1] ?? '';\n const uploadedImage = await persistImage({\n buffer,\n extension,\n });\n\n res.status(200).json(createUploadResponse(uploadedImage)).end();\n };\n};\n\nexport const createUploadImageFromSrcHandler = (\n fetchImage: FetchImage = fetchRemoteImage,\n persistImage: PersistImage = persistUploadedImage,\n): Controller => {\n return async (req, res) => {\n const src = String(req.body?.src ?? '');\n\n try {\n const remoteImage = await fetchImage(src);\n const uploadedImage = await persistImage({\n buffer: remoteImage.buffer,\n extension: remoteImage.extension,\n });\n\n res.status(200).json(createUploadResponse(uploadedImage)).end();\n } catch (error) {\n if (error instanceof RemoteImageFetchError) {\n throw createAppError(error.status, error.code, error.message);\n }\n\n throw error;\n }\n };\n};\n"],"mappings":"AAAA,SAAS,sBAAsB;AAE/B,SAAS,kBAAoC,6BAA6B;AAC1E,SAA6D,4BAA4B;AAKzF,MAAM,uBAAuB,CAAC,UAAiC;AAC3D,SAAO;AAAA,IACH,IAAI,MAAM;AAAA,IACV,KAAK,MAAM;AAAA,EACf;AACJ;AAEO,MAAM,2BAA2B,CAAC,eAA6B,yBAAqC;AACvG,SAAO,OAAO,KAAK,QAAQ;AACvB,UAAM,EAAE,MAAM,IAAI,IAAI,QAAQ,CAAC;AAE/B,QAAI,OAAO,UAAU,YAAY,CAAC,MAAM,MAAM,8BAA8B,GAAG;AAC3E,YAAM,eAAe,KAAK,wBAAwB,mBAAmB;AAAA,IACzE;AAEA,UAAM,CAAC,MAAM,IAAI,IAAI,MAAM,MAAM,GAAG;AACpC,UAAM,SAAS,OAAO,KAAK,MAAM,QAAQ;AACzC,UAAM,YAAY,KAAK,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,EAAE,CAAC,KAAK;AACvD,UAAM,gBAAgB,MAAM,aAAa;AAAA,MACrC;AAAA,MACA;AAAA,IACJ,CAAC;AAED,QAAI,OAAO,GAAG,EAAE,KAAK,qBAAqB,aAAa,CAAC,EAAE,IAAI;AAAA,EAClE;AACJ;AAEO,MAAM,kCAAkC,CAC3C,aAAyB,kBACzB,eAA6B,yBAChB;AACb,SAAO,OAAO,KAAK,QAAQ;AACvB,UAAM,MAAM,OAAO,IAAI,MAAM,OAAO,EAAE;AAEtC,QAAI;AACA,YAAM,cAAc,MAAM,WAAW,GAAG;AACxC,YAAM,gBAAgB,MAAM,aAAa;AAAA,QACrC,QAAQ,YAAY;AAAA,QACpB,WAAW,YAAY;AAAA,MAC3B,CAAC;AAED,UAAI,OAAO,GAAG,EAAE,KAAK,qBAAqB,aAAa,CAAC,EAAE,IAAI;AAAA,IAClE,SAAS,OAAO;AACZ,UAAI,iBAAiB,uBAAuB;AACxC,cAAM,eAAe,MAAM,QAAQ,MAAM,MAAM,MAAM,OAAO;AAAA,MAChE;AAEA,YAAM;AAAA,IACV;AAAA,EACJ;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../../../src/features/image/http/upload.ts"],"sourcesContent":["import { createAppError } from '~/modules/error-handler.js';\nimport type { Controller } from '~/types/index.js';\nimport { type PersistedImageSummary, type PersistImageInput, persistUploadedImage } from '../services/upload.js';\nimport { ImageValidationError, normalizeImageContentType, validateImagePayload } from '../services/validation.js';\n\ntype PersistImage = (input: PersistImageInput) => Promise<PersistedImageSummary>;\n\nconst IMAGE_DATA_URL_PATTERN = /^data:(image\\/[a-z0-9.+-]+);base64,([A-Za-z0-9+/]+={0,2})$/i;\n\nconst createUploadResponse = (image: PersistedImageSummary) => {\n return {\n id: image.id,\n url: image.url,\n };\n};\n\nconst parseImageDataUrl = (image: string) => {\n const match = IMAGE_DATA_URL_PATTERN.exec(image);\n\n if (!match || !match[2] || match[2].length % 4 !== 0) {\n return null;\n }\n\n return {\n base64: match[2],\n contentType: normalizeImageContentType(match[1]),\n };\n};\n\nconst createUploadValidationError = (error: ImageValidationError) => {\n if (error.code === 'IMAGE_UNSUPPORTED_CONTENT_TYPE') {\n return createAppError(415, 'IMAGE_UPLOAD_UNSUPPORTED_TYPE', 'Uploaded image content type is not supported.');\n }\n\n return createAppError(400, 'INVALID_IMAGE_UPLOAD', 'Uploaded image content is invalid.');\n};\n\nexport const createUploadImageHandler = (persistImage: PersistImage = persistUploadedImage): Controller => {\n return async (req, res) => {\n const { image } = req.body ?? {};\n\n if (typeof image !== 'string') {\n throw createAppError(400, 'INVALID_IMAGE_UPLOAD', 'No image uploaded');\n }\n\n const parsedImage = parseImageDataUrl(image);\n\n if (!parsedImage) {\n throw createAppError(400, 'INVALID_IMAGE_UPLOAD', 'No image uploaded');\n }\n\n let validatedImage;\n\n try {\n validatedImage = validateImagePayload({\n buffer: Buffer.from(parsedImage.base64, 'base64'),\n contentType: parsedImage.contentType,\n });\n } catch (error) {\n if (error instanceof ImageValidationError) {\n throw createUploadValidationError(error);\n }\n\n throw error;\n }\n\n const uploadedImage = await persistImage({\n buffer: validatedImage.buffer,\n extension: validatedImage.extension,\n });\n\n res.status(200).json(createUploadResponse(uploadedImage)).end();\n };\n};\n"],"mappings":"AAAA,SAAS,sBAAsB;AAE/B,SAA6D,4BAA4B;AACzF,SAAS,sBAAsB,2BAA2B,4BAA4B;AAItF,MAAM,yBAAyB;AAE/B,MAAM,uBAAuB,CAAC,UAAiC;AAC3D,SAAO;AAAA,IACH,IAAI,MAAM;AAAA,IACV,KAAK,MAAM;AAAA,EACf;AACJ;AAEA,MAAM,oBAAoB,CAAC,UAAkB;AACzC,QAAM,QAAQ,uBAAuB,KAAK,KAAK;AAE/C,MAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,MAAM,CAAC,EAAE,SAAS,MAAM,GAAG;AAClD,WAAO;AAAA,EACX;AAEA,SAAO;AAAA,IACH,QAAQ,MAAM,CAAC;AAAA,IACf,aAAa,0BAA0B,MAAM,CAAC,CAAC;AAAA,EACnD;AACJ;AAEA,MAAM,8BAA8B,CAAC,UAAgC;AACjE,MAAI,MAAM,SAAS,kCAAkC;AACjD,WAAO,eAAe,KAAK,iCAAiC,+CAA+C;AAAA,EAC/G;AAEA,SAAO,eAAe,KAAK,wBAAwB,oCAAoC;AAC3F;AAEO,MAAM,2BAA2B,CAAC,eAA6B,yBAAqC;AACvG,SAAO,OAAO,KAAK,QAAQ;AACvB,UAAM,EAAE,MAAM,IAAI,IAAI,QAAQ,CAAC;AAE/B,QAAI,OAAO,UAAU,UAAU;AAC3B,YAAM,eAAe,KAAK,wBAAwB,mBAAmB;AAAA,IACzE;AAEA,UAAM,cAAc,kBAAkB,KAAK;AAE3C,QAAI,CAAC,aAAa;AACd,YAAM,eAAe,KAAK,wBAAwB,mBAAmB;AAAA,IACzE;AAEA,QAAI;AAEJ,QAAI;AACA,uBAAiB,qBAAqB;AAAA,QAClC,QAAQ,OAAO,KAAK,YAAY,QAAQ,QAAQ;AAAA,QAChD,aAAa,YAAY;AAAA,MAC7B,CAAC;AAAA,IACL,SAAS,OAAO;AACZ,UAAI,iBAAiB,sBAAsB;AACvC,cAAM,4BAA4B,KAAK;AAAA,MAC3C;AAEA,YAAM;AAAA,IACV;AAEA,UAAM,gBAAgB,MAAM,aAAa;AAAA,MACrC,QAAQ,eAAe;AAAA,MACvB,WAAW,eAAe;AAAA,IAC9B,CAAC;AAED,QAAI,OAAO,GAAG,EAAE,KAAK,qBAAqB,aAAa,CAAC,EAAE,IAAI;AAAA,EAClE;AACJ;","names":[]}
@@ -0,0 +1,111 @@
1
+ class ImageValidationError extends Error {
2
+ code;
3
+ status;
4
+ constructor(code, status, message) {
5
+ super(message);
6
+ this.name = "ImageValidationError";
7
+ this.code = code;
8
+ this.status = status;
9
+ }
10
+ }
11
+ const SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Map([
12
+ ["image/png", "png"],
13
+ ["image/jpeg", "jpg"],
14
+ ["image/gif", "gif"],
15
+ ["image/webp", "webp"],
16
+ ["image/bmp", "bmp"],
17
+ ["image/avif", "avif"]
18
+ ]);
19
+ const startsWithBytes = (buffer, bytes) => {
20
+ return bytes.every((byte, index) => buffer[index] === byte);
21
+ };
22
+ const readAscii = (buffer, start, end) => {
23
+ return buffer.subarray(start, end).toString("ascii");
24
+ };
25
+ const hasPngSignature = (buffer) => {
26
+ return buffer.length >= 8 && startsWithBytes(buffer, [137, 80, 78, 71, 13, 10, 26, 10]);
27
+ };
28
+ const hasJpegSignature = (buffer) => {
29
+ return buffer.length >= 3 && startsWithBytes(buffer, [255, 216, 255]);
30
+ };
31
+ const hasGifSignature = (buffer) => {
32
+ return buffer.length >= 6 && ["GIF87a", "GIF89a"].includes(readAscii(buffer, 0, 6));
33
+ };
34
+ const hasWebpSignature = (buffer) => {
35
+ return buffer.length >= 12 && readAscii(buffer, 0, 4) === "RIFF" && readAscii(buffer, 8, 12) === "WEBP";
36
+ };
37
+ const hasBmpSignature = (buffer) => {
38
+ return buffer.length >= 2 && readAscii(buffer, 0, 2) === "BM";
39
+ };
40
+ const hasAvifSignature = (buffer) => {
41
+ if (buffer.length < 16 || readAscii(buffer, 4, 8) !== "ftyp") {
42
+ return false;
43
+ }
44
+ const ftypBoxSize = buffer.readUInt32BE(0);
45
+ if (ftypBoxSize !== 0 && (ftypBoxSize < 16 || ftypBoxSize > buffer.length)) {
46
+ return false;
47
+ }
48
+ const isAvifBrand = (brand) => brand === "avif" || brand === "avis";
49
+ const majorBrand = readAscii(buffer, 8, 12);
50
+ if (isAvifBrand(majorBrand)) {
51
+ return true;
52
+ }
53
+ const boxEnd = ftypBoxSize === 0 ? buffer.length : ftypBoxSize;
54
+ for (let offset = 16; offset + 4 <= boxEnd; offset += 4) {
55
+ const brand = readAscii(buffer, offset, offset + 4);
56
+ if (isAvifBrand(brand)) {
57
+ return true;
58
+ }
59
+ }
60
+ return false;
61
+ };
62
+ const hasExpectedImageSignature = (contentType, buffer) => {
63
+ switch (contentType) {
64
+ case "image/png":
65
+ return hasPngSignature(buffer);
66
+ case "image/jpeg":
67
+ return hasJpegSignature(buffer);
68
+ case "image/gif":
69
+ return hasGifSignature(buffer);
70
+ case "image/webp":
71
+ return hasWebpSignature(buffer);
72
+ case "image/bmp":
73
+ return hasBmpSignature(buffer);
74
+ case "image/avif":
75
+ return hasAvifSignature(buffer);
76
+ default:
77
+ return false;
78
+ }
79
+ };
80
+ const normalizeImageContentType = (contentType) => {
81
+ return contentType?.split(";")[0]?.trim().toLowerCase() ?? "";
82
+ };
83
+ const getSupportedImageExtension = (contentType) => {
84
+ return SUPPORTED_IMAGE_TYPES.get(normalizeImageContentType(contentType));
85
+ };
86
+ const validateImagePayload = ({
87
+ buffer,
88
+ contentType: rawContentType
89
+ }) => {
90
+ const contentType = normalizeImageContentType(rawContentType);
91
+ const extension = SUPPORTED_IMAGE_TYPES.get(contentType);
92
+ if (!extension) {
93
+ throw new ImageValidationError("IMAGE_UNSUPPORTED_CONTENT_TYPE", 415, "Image content type is not supported.");
94
+ }
95
+ const hasExpectedSignature = hasExpectedImageSignature(contentType, buffer);
96
+ if (!hasExpectedSignature) {
97
+ throw new ImageValidationError("IMAGE_INVALID_CONTENT", 415, "Image content does not match the declared type.");
98
+ }
99
+ return {
100
+ buffer,
101
+ contentType,
102
+ extension
103
+ };
104
+ };
105
+ export {
106
+ ImageValidationError,
107
+ getSupportedImageExtension,
108
+ normalizeImageContentType,
109
+ validateImagePayload
110
+ };
111
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/features/image/services/validation.ts"],"sourcesContent":["export type ImageValidationErrorCode = 'IMAGE_INVALID_CONTENT' | 'IMAGE_UNSUPPORTED_CONTENT_TYPE';\n\nexport class ImageValidationError extends Error {\n public readonly code: ImageValidationErrorCode;\n public readonly status: number;\n\n constructor(code: ImageValidationErrorCode, status: number, message: string) {\n super(message);\n this.name = 'ImageValidationError';\n this.code = code;\n this.status = status;\n }\n}\n\ninterface ValidateImagePayloadInput {\n buffer: Buffer;\n contentType: string;\n}\n\nexport interface ValidatedImagePayload {\n buffer: Buffer;\n contentType: string;\n extension: string;\n}\n\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 startsWithBytes = (buffer: Buffer, bytes: number[]) => {\n return bytes.every((byte, index) => buffer[index] === byte);\n};\n\nconst readAscii = (buffer: Buffer, start: number, end: number) => {\n return buffer.subarray(start, end).toString('ascii');\n};\n\nconst hasPngSignature = (buffer: Buffer) => {\n return buffer.length >= 8 && startsWithBytes(buffer, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);\n};\n\nconst hasJpegSignature = (buffer: Buffer) => {\n return buffer.length >= 3 && startsWithBytes(buffer, [0xff, 0xd8, 0xff]);\n};\n\nconst hasGifSignature = (buffer: Buffer) => {\n return buffer.length >= 6 && ['GIF87a', 'GIF89a'].includes(readAscii(buffer, 0, 6));\n};\n\nconst hasWebpSignature = (buffer: Buffer) => {\n return buffer.length >= 12 && readAscii(buffer, 0, 4) === 'RIFF' && readAscii(buffer, 8, 12) === 'WEBP';\n};\n\nconst hasBmpSignature = (buffer: Buffer) => {\n return buffer.length >= 2 && readAscii(buffer, 0, 2) === 'BM';\n};\n\nconst hasAvifSignature = (buffer: Buffer) => {\n if (buffer.length < 16 || readAscii(buffer, 4, 8) !== 'ftyp') {\n return false;\n }\n\n const ftypBoxSize = buffer.readUInt32BE(0);\n\n if (ftypBoxSize !== 0 && (ftypBoxSize < 16 || ftypBoxSize > buffer.length)) {\n return false;\n }\n\n const isAvifBrand = (brand: string) => brand === 'avif' || brand === 'avis';\n const majorBrand = readAscii(buffer, 8, 12);\n\n if (isAvifBrand(majorBrand)) {\n return true;\n }\n\n const boxEnd = ftypBoxSize === 0 ? buffer.length : ftypBoxSize;\n\n for (let offset = 16; offset + 4 <= boxEnd; offset += 4) {\n const brand = readAscii(buffer, offset, offset + 4);\n\n if (isAvifBrand(brand)) {\n return true;\n }\n }\n\n return false;\n};\n\nconst hasExpectedImageSignature = (contentType: string, buffer: Buffer) => {\n switch (contentType) {\n case 'image/png':\n return hasPngSignature(buffer);\n case 'image/jpeg':\n return hasJpegSignature(buffer);\n case 'image/gif':\n return hasGifSignature(buffer);\n case 'image/webp':\n return hasWebpSignature(buffer);\n case 'image/bmp':\n return hasBmpSignature(buffer);\n case 'image/avif':\n return hasAvifSignature(buffer);\n default:\n return false;\n }\n};\n\nexport const normalizeImageContentType = (contentType: string | null | undefined) => {\n return contentType?.split(';')[0]?.trim().toLowerCase() ?? '';\n};\n\nexport const getSupportedImageExtension = (contentType: string) => {\n return SUPPORTED_IMAGE_TYPES.get(normalizeImageContentType(contentType));\n};\n\nexport const validateImagePayload = ({\n buffer,\n contentType: rawContentType,\n}: ValidateImagePayloadInput): ValidatedImagePayload => {\n const contentType = normalizeImageContentType(rawContentType);\n const extension = SUPPORTED_IMAGE_TYPES.get(contentType);\n\n if (!extension) {\n throw new ImageValidationError('IMAGE_UNSUPPORTED_CONTENT_TYPE', 415, 'Image content type is not supported.');\n }\n\n const hasExpectedSignature = hasExpectedImageSignature(contentType, buffer);\n\n if (!hasExpectedSignature) {\n throw new ImageValidationError('IMAGE_INVALID_CONTENT', 415, 'Image content does not match the declared type.');\n }\n\n return {\n buffer,\n contentType,\n extension,\n };\n};\n"],"mappings":"AAEO,MAAM,6BAA6B,MAAM;AAAA,EAC5B;AAAA,EACA;AAAA,EAEhB,YAAY,MAAgC,QAAgB,SAAiB;AACzE,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAClB;AACJ;AAaA,MAAM,wBAAwB,oBAAI,IAAI;AAAA,EAClC,CAAC,aAAa,KAAK;AAAA,EACnB,CAAC,cAAc,KAAK;AAAA,EACpB,CAAC,aAAa,KAAK;AAAA,EACnB,CAAC,cAAc,MAAM;AAAA,EACrB,CAAC,aAAa,KAAK;AAAA,EACnB,CAAC,cAAc,MAAM;AACzB,CAAC;AAED,MAAM,kBAAkB,CAAC,QAAgB,UAAoB;AACzD,SAAO,MAAM,MAAM,CAAC,MAAM,UAAU,OAAO,KAAK,MAAM,IAAI;AAC9D;AAEA,MAAM,YAAY,CAAC,QAAgB,OAAe,QAAgB;AAC9D,SAAO,OAAO,SAAS,OAAO,GAAG,EAAE,SAAS,OAAO;AACvD;AAEA,MAAM,kBAAkB,CAAC,WAAmB;AACxC,SAAO,OAAO,UAAU,KAAK,gBAAgB,QAAQ,CAAC,KAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,EAAI,CAAC;AACzG;AAEA,MAAM,mBAAmB,CAAC,WAAmB;AACzC,SAAO,OAAO,UAAU,KAAK,gBAAgB,QAAQ,CAAC,KAAM,KAAM,GAAI,CAAC;AAC3E;AAEA,MAAM,kBAAkB,CAAC,WAAmB;AACxC,SAAO,OAAO,UAAU,KAAK,CAAC,UAAU,QAAQ,EAAE,SAAS,UAAU,QAAQ,GAAG,CAAC,CAAC;AACtF;AAEA,MAAM,mBAAmB,CAAC,WAAmB;AACzC,SAAO,OAAO,UAAU,MAAM,UAAU,QAAQ,GAAG,CAAC,MAAM,UAAU,UAAU,QAAQ,GAAG,EAAE,MAAM;AACrG;AAEA,MAAM,kBAAkB,CAAC,WAAmB;AACxC,SAAO,OAAO,UAAU,KAAK,UAAU,QAAQ,GAAG,CAAC,MAAM;AAC7D;AAEA,MAAM,mBAAmB,CAAC,WAAmB;AACzC,MAAI,OAAO,SAAS,MAAM,UAAU,QAAQ,GAAG,CAAC,MAAM,QAAQ;AAC1D,WAAO;AAAA,EACX;AAEA,QAAM,cAAc,OAAO,aAAa,CAAC;AAEzC,MAAI,gBAAgB,MAAM,cAAc,MAAM,cAAc,OAAO,SAAS;AACxE,WAAO;AAAA,EACX;AAEA,QAAM,cAAc,CAAC,UAAkB,UAAU,UAAU,UAAU;AACrE,QAAM,aAAa,UAAU,QAAQ,GAAG,EAAE;AAE1C,MAAI,YAAY,UAAU,GAAG;AACzB,WAAO;AAAA,EACX;AAEA,QAAM,SAAS,gBAAgB,IAAI,OAAO,SAAS;AAEnD,WAAS,SAAS,IAAI,SAAS,KAAK,QAAQ,UAAU,GAAG;AACrD,UAAM,QAAQ,UAAU,QAAQ,QAAQ,SAAS,CAAC;AAElD,QAAI,YAAY,KAAK,GAAG;AACpB,aAAO;AAAA,IACX;AAAA,EACJ;AAEA,SAAO;AACX;AAEA,MAAM,4BAA4B,CAAC,aAAqB,WAAmB;AACvE,UAAQ,aAAa;AAAA,IACjB,KAAK;AACD,aAAO,gBAAgB,MAAM;AAAA,IACjC,KAAK;AACD,aAAO,iBAAiB,MAAM;AAAA,IAClC,KAAK;AACD,aAAO,gBAAgB,MAAM;AAAA,IACjC,KAAK;AACD,aAAO,iBAAiB,MAAM;AAAA,IAClC,KAAK;AACD,aAAO,gBAAgB,MAAM;AAAA,IACjC,KAAK;AACD,aAAO,iBAAiB,MAAM;AAAA,IAClC;AACI,aAAO;AAAA,EACf;AACJ;AAEO,MAAM,4BAA4B,CAAC,gBAA2C;AACjF,SAAO,aAAa,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,YAAY,KAAK;AAC/D;AAEO,MAAM,6BAA6B,CAAC,gBAAwB;AAC/D,SAAO,sBAAsB,IAAI,0BAA0B,WAAW,CAAC;AAC3E;AAEO,MAAM,uBAAuB,CAAC;AAAA,EACjC;AAAA,EACA,aAAa;AACjB,MAAwD;AACpD,QAAM,cAAc,0BAA0B,cAAc;AAC5D,QAAM,YAAY,sBAAsB,IAAI,WAAW;AAEvD,MAAI,CAAC,WAAW;AACZ,UAAM,IAAI,qBAAqB,kCAAkC,KAAK,sCAAsC;AAAA,EAChH;AAEA,QAAM,uBAAuB,0BAA0B,aAAa,MAAM;AAE1E,MAAI,CAAC,sBAAsB;AACvB,UAAM,IAAI,qBAAqB,yBAAyB,KAAK,iDAAiD;AAAA,EAClH;AAEA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,EACJ;AACJ;","names":[]}
@@ -1,8 +1,16 @@
1
+ import { getOceanBrainVersionInfo } from "../../../modules/app-version.js";
1
2
  import { createAppError } from "../../../modules/error-handler.js";
2
3
  import { createMcpAdminService } from "../service.js";
4
+ const createMcpAdminStatusResponse = async (service) => {
5
+ const status = await service.getStatus();
6
+ return {
7
+ ...status,
8
+ server: getOceanBrainVersionInfo()
9
+ };
10
+ };
3
11
  const createMcpAdminStatusHandler = (service = createMcpAdminService()) => {
4
12
  return async (_req, res) => {
5
- const status = await service.getStatus();
13
+ const status = await createMcpAdminStatusResponse(service);
6
14
  res.status(200).json(status).end();
7
15
  };
8
16
  };
@@ -13,7 +21,7 @@ const createMcpAdminSetEnabledHandler = (service = createMcpAdminService()) => {
13
21
  throw createAppError(400, "INVALID_MCP_ENABLED", "enabled must be a boolean.");
14
22
  }
15
23
  await service.setEnabled(enabled);
16
- const status = await service.getStatus();
24
+ const status = await createMcpAdminStatusResponse(service);
17
25
  res.status(200).json(status).end();
18
26
  };
19
27
  };
@@ -29,7 +37,7 @@ const createMcpAdminRotateTokenHandler = (service = createMcpAdminService()) =>
29
37
  const createMcpAdminRevokeTokenHandler = (service = createMcpAdminService()) => {
30
38
  return async (_req, res) => {
31
39
  await service.revokeActiveToken();
32
- const status = await service.getStatus();
40
+ const status = await createMcpAdminStatusResponse(service);
33
41
  res.status(200).json(status).end();
34
42
  };
35
43
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/features/mcp-admin/http/handlers.ts"],"sourcesContent":["import { createAppError } from '~/modules/error-handler.js';\nimport type { Controller } from '~/types/index.js';\nimport { createMcpAdminService, type McpAdminService } from '../service.js';\n\ntype McpAdminControllerService = Pick<\n McpAdminService,\n 'getStatus' | 'setEnabled' | 'rotateToken' | 'revokeActiveToken'\n>;\n\nexport const createMcpAdminStatusHandler = (\n service: McpAdminControllerService = createMcpAdminService(),\n): Controller => {\n return async (_req, res) => {\n const status = await service.getStatus();\n res.status(200).json(status).end();\n };\n};\n\nexport const createMcpAdminSetEnabledHandler = (\n service: McpAdminControllerService = createMcpAdminService(),\n): Controller => {\n return async (req, res) => {\n const enabled = req.body?.enabled;\n if (typeof enabled !== 'boolean') {\n throw createAppError(400, 'INVALID_MCP_ENABLED', 'enabled must be a boolean.');\n }\n\n await service.setEnabled(enabled);\n const status = await service.getStatus();\n res.status(200).json(status).end();\n };\n};\n\nexport const createMcpAdminRotateTokenHandler = (\n service: McpAdminControllerService = createMcpAdminService(),\n): Controller => {\n return async (_req, res) => {\n const result = await service.rotateToken();\n res.status(200)\n .json({\n token: result.token,\n message: 'Save this token now. It is shown only once.',\n })\n .end();\n };\n};\n\nexport const createMcpAdminRevokeTokenHandler = (\n service: McpAdminControllerService = createMcpAdminService(),\n): Controller => {\n return async (_req, res) => {\n await service.revokeActiveToken();\n const status = await service.getStatus();\n res.status(200).json(status).end();\n };\n};\n"],"mappings":"AAAA,SAAS,sBAAsB;AAE/B,SAAS,6BAAmD;AAOrD,MAAM,8BAA8B,CACvC,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,MAAM,QAAQ;AACxB,UAAM,SAAS,MAAM,QAAQ,UAAU;AACvC,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,EAAE,IAAI;AAAA,EACrC;AACJ;AAEO,MAAM,kCAAkC,CAC3C,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,KAAK,QAAQ;AACvB,UAAM,UAAU,IAAI,MAAM;AAC1B,QAAI,OAAO,YAAY,WAAW;AAC9B,YAAM,eAAe,KAAK,uBAAuB,4BAA4B;AAAA,IACjF;AAEA,UAAM,QAAQ,WAAW,OAAO;AAChC,UAAM,SAAS,MAAM,QAAQ,UAAU;AACvC,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,EAAE,IAAI;AAAA,EACrC;AACJ;AAEO,MAAM,mCAAmC,CAC5C,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,MAAM,QAAQ;AACxB,UAAM,SAAS,MAAM,QAAQ,YAAY;AACzC,QAAI,OAAO,GAAG,EACT,KAAK;AAAA,MACF,OAAO,OAAO;AAAA,MACd,SAAS;AAAA,IACb,CAAC,EACA,IAAI;AAAA,EACb;AACJ;AAEO,MAAM,mCAAmC,CAC5C,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,MAAM,QAAQ;AACxB,UAAM,QAAQ,kBAAkB;AAChC,UAAM,SAAS,MAAM,QAAQ,UAAU;AACvC,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,EAAE,IAAI;AAAA,EACrC;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../../../src/features/mcp-admin/http/handlers.ts"],"sourcesContent":["import { getOceanBrainVersionInfo } from '~/modules/app-version.js';\nimport { createAppError } from '~/modules/error-handler.js';\nimport type { Controller } from '~/types/index.js';\nimport { createMcpAdminService, type McpAdminService } from '../service.js';\n\ntype McpAdminControllerService = Pick<\n McpAdminService,\n 'getStatus' | 'setEnabled' | 'rotateToken' | 'revokeActiveToken'\n>;\n\nconst createMcpAdminStatusResponse = async (service: McpAdminControllerService) => {\n const status = await service.getStatus();\n\n return {\n ...status,\n server: getOceanBrainVersionInfo(),\n };\n};\n\nexport const createMcpAdminStatusHandler = (\n service: McpAdminControllerService = createMcpAdminService(),\n): Controller => {\n return async (_req, res) => {\n const status = await createMcpAdminStatusResponse(service);\n res.status(200).json(status).end();\n };\n};\n\nexport const createMcpAdminSetEnabledHandler = (\n service: McpAdminControllerService = createMcpAdminService(),\n): Controller => {\n return async (req, res) => {\n const enabled = req.body?.enabled;\n if (typeof enabled !== 'boolean') {\n throw createAppError(400, 'INVALID_MCP_ENABLED', 'enabled must be a boolean.');\n }\n\n await service.setEnabled(enabled);\n const status = await createMcpAdminStatusResponse(service);\n res.status(200).json(status).end();\n };\n};\n\nexport const createMcpAdminRotateTokenHandler = (\n service: McpAdminControllerService = createMcpAdminService(),\n): Controller => {\n return async (_req, res) => {\n const result = await service.rotateToken();\n res.status(200)\n .json({\n token: result.token,\n message: 'Save this token now. It is shown only once.',\n })\n .end();\n };\n};\n\nexport const createMcpAdminRevokeTokenHandler = (\n service: McpAdminControllerService = createMcpAdminService(),\n): Controller => {\n return async (_req, res) => {\n await service.revokeActiveToken();\n const status = await createMcpAdminStatusResponse(service);\n res.status(200).json(status).end();\n };\n};\n"],"mappings":"AAAA,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;AAE/B,SAAS,6BAAmD;AAO5D,MAAM,+BAA+B,OAAO,YAAuC;AAC/E,QAAM,SAAS,MAAM,QAAQ,UAAU;AAEvC,SAAO;AAAA,IACH,GAAG;AAAA,IACH,QAAQ,yBAAyB;AAAA,EACrC;AACJ;AAEO,MAAM,8BAA8B,CACvC,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,MAAM,QAAQ;AACxB,UAAM,SAAS,MAAM,6BAA6B,OAAO;AACzD,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,EAAE,IAAI;AAAA,EACrC;AACJ;AAEO,MAAM,kCAAkC,CAC3C,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,KAAK,QAAQ;AACvB,UAAM,UAAU,IAAI,MAAM;AAC1B,QAAI,OAAO,YAAY,WAAW;AAC9B,YAAM,eAAe,KAAK,uBAAuB,4BAA4B;AAAA,IACjF;AAEA,UAAM,QAAQ,WAAW,OAAO;AAChC,UAAM,SAAS,MAAM,6BAA6B,OAAO;AACzD,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,EAAE,IAAI;AAAA,EACrC;AACJ;AAEO,MAAM,mCAAmC,CAC5C,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,MAAM,QAAQ;AACxB,UAAM,SAAS,MAAM,QAAQ,YAAY;AACzC,QAAI,OAAO,GAAG,EACT,KAAK;AAAA,MACF,OAAO,OAAO;AAAA,MACd,SAAS;AAAA,IACb,CAAC,EACA,IAAI;AAAA,EACb;AACJ;AAEO,MAAM,mCAAmC,CAC5C,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,MAAM,QAAQ;AACxB,UAAM,QAAQ,kBAAkB;AAChC,UAAM,SAAS,MAAM,6BAA6B,OAAO;AACzD,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,EAAE,IAAI;AAAA,EACrC;AACJ;","names":[]}
@@ -38,15 +38,6 @@ const resolveOptionalString = (value, code, message) => {
38
38
  }
39
39
  throw createAppError(400, code, message);
40
40
  };
41
- const resolveOptionalBoolean = (value, code, message) => {
42
- if (value === void 0) {
43
- return void 0;
44
- }
45
- if (typeof value === "boolean") {
46
- return value;
47
- }
48
- throw createAppError(400, code, message);
49
- };
50
41
  const resolveMetadataPropertyPatch = (value) => {
51
42
  if (value === void 0) {
52
43
  return void 0;
@@ -117,24 +108,6 @@ const resolveMarkdownWritePolicy = (value) => {
117
108
  }
118
109
  policy.allowNoop = value.allowNoop;
119
110
  }
120
- if (value.maxChangedChars !== void 0) {
121
- if (!isNonNegativeInteger(value.maxChangedChars)) {
122
- throw createAppError(400, "INVALID_MARKDOWN_POLICY", "maxChangedChars must be a non-negative integer.");
123
- }
124
- policy.maxChangedChars = value.maxChangedChars;
125
- }
126
- if (value.maxChangedLines !== void 0) {
127
- if (!isNonNegativeInteger(value.maxChangedLines)) {
128
- throw createAppError(400, "INVALID_MARKDOWN_POLICY", "maxChangedLines must be a non-negative integer.");
129
- }
130
- policy.maxChangedLines = value.maxChangedLines;
131
- }
132
- if (value.diffPreviewMaxChars !== void 0) {
133
- if (!isNonNegativeInteger(value.diffPreviewMaxChars)) {
134
- throw createAppError(400, "INVALID_MARKDOWN_POLICY", "diffPreviewMaxChars must be a non-negative integer.");
135
- }
136
- policy.diffPreviewMaxChars = value.diffPreviewMaxChars;
137
- }
138
111
  if (value.preserveTags !== void 0) {
139
112
  if (!PRESERVATION_POLICIES.has(value.preserveTags)) {
140
113
  throw createAppError(400, "INVALID_MARKDOWN_POLICY", "preserveTags must be boolean or warn.");
@@ -368,7 +341,7 @@ const createMcpNoteWriteBaselineHandler = (findNoteBaseline = async (id) => mode
368
341
  };
369
342
  const createMcpPatchNoteMarkdownHandler = (patchMarkdown = patchNoteMarkdown, emitEvent = emitServerEvent) => {
370
343
  return async (req, res) => {
371
- const { id, expectedUpdatedAt, baseMarkdownSha256, intent, selector, operation, policy, dryRun } = req.body ?? {};
344
+ const { id, expectedUpdatedAt, baseMarkdownSha256, intent, selector, operation, policy } = req.body ?? {};
372
345
  const noteId = resolvePositiveNoteId(id);
373
346
  const resolvedExpectedUpdatedAt = resolveOptionalString(
374
347
  expectedUpdatedAt,
@@ -380,7 +353,6 @@ const createMcpPatchNoteMarkdownHandler = (patchMarkdown = patchNoteMarkdown, em
380
353
  "INVALID_MARKDOWN_HASH",
381
354
  "baseMarkdownSha256 must be a string."
382
355
  );
383
- const resolvedDryRun = resolveOptionalBoolean(dryRun, "INVALID_DRY_RUN", "dryRun must be a boolean.");
384
356
  if (typeof intent !== "string") {
385
357
  throw createAppError(400, "INVALID_PATCH_INTENT", "Patch intent must be a string.");
386
358
  }
@@ -391,8 +363,7 @@ const createMcpPatchNoteMarkdownHandler = (patchMarkdown = patchNoteMarkdown, em
391
363
  intent,
392
364
  selector: resolvePatchSelector(selector),
393
365
  operation: resolvePatchOperation(operation),
394
- policy: resolveMarkdownWritePolicy(policy),
395
- dryRun: resolvedDryRun
366
+ policy: resolveMarkdownWritePolicy(policy)
396
367
  });
397
368
  if (result.status === "applied") {
398
369
  emitEvent({
@@ -407,7 +378,7 @@ const createMcpPatchNoteMarkdownHandler = (patchMarkdown = patchNoteMarkdown, em
407
378
  };
408
379
  const createMcpAppendNoteMarkdownHandler = (appendMarkdown = appendNoteMarkdown, emitEvent = emitServerEvent) => {
409
380
  return async (req, res) => {
410
- const { id, expectedUpdatedAt, baseMarkdownSha256, intent, insertion, placement, separator, policy, dryRun } = req.body ?? {};
381
+ const { id, expectedUpdatedAt, baseMarkdownSha256, intent, insertion, placement, separator, policy } = req.body ?? {};
411
382
  const noteId = resolvePositiveNoteId(id);
412
383
  const resolvedExpectedUpdatedAt = resolveOptionalString(
413
384
  expectedUpdatedAt,
@@ -419,7 +390,6 @@ const createMcpAppendNoteMarkdownHandler = (appendMarkdown = appendNoteMarkdown,
419
390
  "INVALID_MARKDOWN_HASH",
420
391
  "baseMarkdownSha256 must be a string."
421
392
  );
422
- const resolvedDryRun = resolveOptionalBoolean(dryRun, "INVALID_DRY_RUN", "dryRun must be a boolean.");
423
393
  if (typeof intent !== "string") {
424
394
  throw createAppError(400, "INVALID_APPEND_INTENT", "Append intent must be a string.");
425
395
  }
@@ -434,8 +404,7 @@ const createMcpAppendNoteMarkdownHandler = (appendMarkdown = appendNoteMarkdown,
434
404
  insertion,
435
405
  placement: resolveAppendPlacement(placement),
436
406
  separator: resolveSeparator(separator),
437
- policy: resolveMarkdownWritePolicy(policy),
438
- dryRun: resolvedDryRun
407
+ policy: resolveMarkdownWritePolicy(policy)
439
408
  });
440
409
  if (result.status === "applied") {
441
410
  emitEvent({
@@ -450,7 +419,7 @@ const createMcpAppendNoteMarkdownHandler = (appendMarkdown = appendNoteMarkdown,
450
419
  };
451
420
  const createMcpReplaceNoteMarkdownHandler = (replaceMarkdown = replaceNoteMarkdown, emitEvent = emitServerEvent) => {
452
421
  return async (req, res) => {
453
- const { id, expectedUpdatedAt, baseMarkdownSha256, intent, replacement, policy, dryRun } = req.body ?? {};
422
+ const { id, expectedUpdatedAt, baseMarkdownSha256, intent, replacement, policy } = req.body ?? {};
454
423
  const noteId = resolvePositiveNoteId(id);
455
424
  const resolvedExpectedUpdatedAt = resolveOptionalString(
456
425
  expectedUpdatedAt,
@@ -462,7 +431,6 @@ const createMcpReplaceNoteMarkdownHandler = (replaceMarkdown = replaceNoteMarkdo
462
431
  "INVALID_MARKDOWN_HASH",
463
432
  "baseMarkdownSha256 must be a string."
464
433
  );
465
- const resolvedDryRun = resolveOptionalBoolean(dryRun, "INVALID_DRY_RUN", "dryRun must be a boolean.");
466
434
  if (typeof intent !== "string") {
467
435
  throw createAppError(400, "INVALID_REPLACE_INTENT", "Replace intent must be a string.");
468
436
  }
@@ -475,8 +443,7 @@ const createMcpReplaceNoteMarkdownHandler = (replaceMarkdown = replaceNoteMarkdo
475
443
  baseMarkdownSha256: resolvedBaseMarkdownSha256,
476
444
  intent,
477
445
  replacement,
478
- policy: resolveMarkdownWritePolicy(policy),
479
- dryRun: resolvedDryRun
446
+ policy: resolveMarkdownWritePolicy(policy)
480
447
  });
481
448
  if (result.status === "applied") {
482
449
  emitEvent({
@@ -491,11 +458,10 @@ const createMcpReplaceNoteMarkdownHandler = (replaceMarkdown = replaceNoteMarkdo
491
458
  };
492
459
  const createMcpUpdateNoteMetadataHandler = (updateMetadata = updateNoteMetadata, emitEvent = emitServerEvent) => {
493
460
  return async (req, res) => {
494
- const { id, expectedUpdatedAt, title, layout, properties, dryRun } = req.body ?? {};
461
+ const { id, expectedUpdatedAt, title, layout, properties } = req.body ?? {};
495
462
  const noteId = resolvePositiveNoteId(id);
496
463
  const resolvedLayout = resolveNoteLayout(layout);
497
464
  const resolvedProperties = resolveMetadataPropertyPatch(properties);
498
- const resolvedDryRun = resolveOptionalBoolean(dryRun, "INVALID_DRY_RUN", "dryRun must be a boolean.");
499
465
  if (typeof expectedUpdatedAt !== "string") {
500
466
  throw createAppError(400, "INVALID_NOTE_VERSION", "expectedUpdatedAt must be a string.");
501
467
  }
@@ -513,8 +479,7 @@ const createMcpUpdateNoteMetadataHandler = (updateMetadata = updateNoteMetadata,
513
479
  expectedUpdatedAt,
514
480
  ...title !== void 0 ? { title } : {},
515
481
  ...resolvedLayout ? { layout: resolvedLayout } : {},
516
- ...resolvedProperties !== void 0 ? { properties: resolvedProperties } : {},
517
- dryRun: resolvedDryRun
482
+ ...resolvedProperties !== void 0 ? { properties: resolvedProperties } : {}
518
483
  });
519
484
  if (result.status === "applied") {
520
485
  emitEvent({