ocean-brain 0.7.4 → 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.
- package/dist/index.js +3 -3
- package/dist/mcp.js +180 -736
- package/package.json +4 -1
- package/server/client/dist/assets/ArrowRight.es-C4zlc54Z.js +1 -0
- package/server/client/dist/assets/{Calendar-Bz7jheg6.js → Calendar-COrN6Hjv.js} +2 -2
- package/server/client/dist/assets/Callout-BK0K9E74.js +1 -0
- package/server/client/dist/assets/Copy.es-zm8f_XXX.js +1 -0
- package/server/client/dist/assets/Graph-BH1fzhue.js +1 -0
- package/server/client/dist/assets/{Image-DxWhlIDG.js → Image-KXuHOG4b.js} +1 -1
- package/server/client/dist/assets/Image.es-BKahRbIm.js +1 -0
- package/server/client/dist/assets/Note-C143cKKw.js +12 -0
- package/server/client/dist/assets/Plus.es-BqeC8T8h.js +1 -0
- package/server/client/dist/assets/Reminders-BYG5rz1M.js +1 -0
- package/server/client/dist/assets/Search-DBWsIMDP.js +1 -0
- package/server/client/dist/assets/{SurfaceCard-CfL1jD-O.js → SurfaceCard-CNSQfYfM.js} +1 -1
- package/server/client/dist/assets/Tag-5aCfSO8Y.js +1 -0
- package/server/client/dist/assets/TagNotes-BPZjN-6l.js +1 -0
- package/server/client/dist/assets/Trash.es-CRQkTnXZ.js +1 -0
- package/server/client/dist/assets/ViewNotes-DGLNF96x.js +1 -0
- package/server/client/dist/assets/ViewSectionDialog-CzskgmMF.js +102 -0
- package/server/client/dist/assets/Views-DR5jT23G.js +1 -0
- package/server/client/dist/assets/{graph-vendor-CUxe67Lr.js → graph-vendor-UTvF2NS_.js} +2 -2
- package/server/client/dist/assets/image.api-8dlMwzMd.js +17 -0
- package/server/client/dist/assets/{index-C5BGzDgt.css → index-BEUqDuAZ.css} +1 -1
- package/server/client/dist/assets/{index-DiMt3Dz4.js → index-DGIAh3ZY.js} +1 -1
- package/server/client/dist/assets/index-VHp17GQX.js +342 -0
- package/server/client/dist/assets/manage-image-RqtWKas2.js +1 -0
- package/server/client/dist/assets/{manage-image-detail-Bqw8NURO.js → manage-image-detail-BTSIbLlQ.js} +1 -1
- package/server/client/dist/assets/mcp-EDurbggR.js +4 -0
- package/server/client/dist/assets/mcp-admin-adapter-DmGUo5G8.js +1 -0
- package/server/client/dist/assets/{note-core-Z6kqulGB.js → note-core-BklwyZHk.js} +1 -1
- package/server/client/dist/assets/{note-vendor-i1M2FSI2.js → note-vendor-DkYKUmLo.js} +1 -1
- package/server/client/dist/assets/{placeholder-Ro00byT7.js → placeholder-DeKgSBnC.js} +4 -4
- package/server/client/dist/assets/{properties-Tid6-ghT.js → properties-BjrZvOYn.js} +1 -1
- package/server/client/dist/assets/tag.api-Dl8DQd0m.js +21 -0
- package/server/client/dist/assets/trash-C5ntddLu.js +1 -0
- package/server/client/dist/assets/{useReminderMutate-BWymIujb.js → useReminderMutate-CoXXqJY2.js} +1 -1
- package/server/client/dist/index.html +5 -5
- package/server/dist/app.js +2 -2
- package/server/dist/app.js.map +1 -1
- package/server/dist/features/auth/http/api.js +5 -1
- package/server/dist/features/auth/http/api.js.map +1 -1
- package/server/dist/features/auth/http/login-page.js +1 -1
- package/server/dist/features/auth/http/pages.js +8 -1
- package/server/dist/features/auth/http/pages.js.map +1 -1
- package/server/dist/features/auth/service.js +16 -7
- package/server/dist/features/auth/service.js.map +1 -1
- package/server/dist/features/image/http/upload.js +34 -23
- package/server/dist/features/image/http/upload.js.map +1 -1
- package/server/dist/features/image/services/validation.js +111 -0
- package/server/dist/features/image/services/validation.js.map +1 -0
- package/server/dist/features/mcp-admin/http/handlers.js +11 -3
- package/server/dist/features/mcp-admin/http/handlers.js.map +1 -1
- package/server/dist/features/note/http/mcp.js +8 -43
- package/server/dist/features/note/http/mcp.js.map +1 -1
- package/server/dist/features/note/services/cleanup.js +2 -2
- package/server/dist/features/note/services/cleanup.js.map +1 -1
- package/server/dist/features/note/services/markdown-intent-write.js +11 -43
- package/server/dist/features/note/services/markdown-intent-write.js.map +1 -1
- package/server/dist/features/note/services/markdown-patch.js +4 -123
- package/server/dist/features/note/services/markdown-patch.js.map +1 -1
- package/server/dist/features/note/services/write.js +13 -13
- package/server/dist/features/note/services/write.js.map +1 -1
- package/server/dist/features/tag/graphql/tag.query.resolver.js +16 -1
- package/server/dist/features/tag/graphql/tag.query.resolver.js.map +1 -1
- package/server/dist/features/tag/graphql/tag.type-defs.js +2 -0
- package/server/dist/features/tag/graphql/tag.type-defs.js.map +1 -1
- package/server/dist/modules/app-version.js +105 -0
- package/server/dist/modules/app-version.js.map +1 -0
- package/server/dist/modules/auth-guard.js +21 -2
- package/server/dist/modules/auth-guard.js.map +1 -1
- package/server/dist/modules/auth-redirect.js +10 -0
- package/server/dist/modules/auth-redirect.js.map +1 -0
- package/server/dist/modules/mcp-auth.js +62 -1
- package/server/dist/modules/mcp-auth.js.map +1 -1
- package/server/dist/modules/rate-limit.js +15 -1
- package/server/dist/modules/rate-limit.js.map +1 -1
- package/server/dist/modules/server-events-handler.js +17 -0
- package/server/dist/modules/server-events-handler.js.map +1 -1
- package/server/dist/modules/session-store.js +149 -0
- package/server/dist/modules/session-store.js.map +1 -0
- package/server/dist/paths.js +1 -0
- package/server/dist/paths.js.map +1 -1
- package/server/dist/routes/api.js +39 -20
- package/server/dist/routes/api.js.map +1 -1
- package/server/dist/routes/auth-pages.js +13 -2
- package/server/dist/routes/auth-pages.js.map +1 -1
- package/server/dist/routes/client.js +63 -3
- package/server/dist/routes/client.js.map +1 -1
- package/server/dist/routes/graphql.js +23 -19
- package/server/dist/routes/graphql.js.map +1 -1
- package/server/client/dist/assets/Callout-DlFLkTCG.js +0 -1
- package/server/client/dist/assets/Graph-BdxtJvog.js +0 -1
- package/server/client/dist/assets/Image.es-BFodqKS5.js +0 -1
- package/server/client/dist/assets/Note-BgMxsWZT.js +0 -22
- package/server/client/dist/assets/Plus.es-Yq7J1Ur4.js +0 -1
- package/server/client/dist/assets/Reminders-CUsR56ff.js +0 -1
- package/server/client/dist/assets/Search-DncrGgOZ.js +0 -1
- package/server/client/dist/assets/Tag-CKDLs2Y6.js +0 -1
- package/server/client/dist/assets/TagNotes-ByX_Qias.js +0 -1
- package/server/client/dist/assets/Trash.es-BPrF6TPV.js +0 -1
- package/server/client/dist/assets/ViewNotes-r1kxKZ8O.js +0 -1
- package/server/client/dist/assets/ViewSectionTableRenderer-DQ4UeCnm.js +0 -102
- package/server/client/dist/assets/Views-FzFBtNT2.js +0 -1
- package/server/client/dist/assets/image.api-Bcg3TFCG.js +0 -17
- package/server/client/dist/assets/index-BFfQKAUY.js +0 -361
- package/server/client/dist/assets/manage-image-DzDlncCM.js +0 -1
- package/server/client/dist/assets/mcp-BqasqTWC.js +0 -1
- package/server/client/dist/assets/trash-bHK02-Up.js +0 -1
- package/server/dist/features/image/services/remote-fetch.js +0 -176
- 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};
|
package/server/client/dist/assets/{useReminderMutate-BWymIujb.js → useReminderMutate-CoXXqJY2.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{j as y}from"./note-vendor-
|
|
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-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/graph-vendor-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/note-core-
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/note-vendor-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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>
|
package/server/dist/app.js
CHANGED
|
@@ -2,7 +2,7 @@ import express from "express";
|
|
|
2
2
|
import { createMcpAdminService } from "./features/mcp-admin/service.js";
|
|
3
3
|
import { purgeExpiredNoteSnapshots } from "./features/note/services/snapshot.js";
|
|
4
4
|
import { purgeExpiredTrashedNotes } from "./features/note/services/trash.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createSessionMiddleware } from "./modules/auth-guard.js";
|
|
6
6
|
import { createErrorHandler } from "./modules/error-handler.js";
|
|
7
7
|
import logger from "./modules/logger.js";
|
|
8
8
|
import { createApiRouter, createAuthPagesRouter, createClientRouter, createGraphqlRouter } from "./routes/index.js";
|
|
@@ -18,7 +18,7 @@ const createAppWithMcpAuth = (authConfig, mcpAdminService) => {
|
|
|
18
18
|
process.stderr.write(`[recovery] Startup cleanup failed: ${message}
|
|
19
19
|
`);
|
|
20
20
|
});
|
|
21
|
-
app.use(logger).use(createSessionMiddleware(authConfig)).use(express.urlencoded({ extended: false })).use(express.json({ limit: "50mb" })).use(
|
|
21
|
+
app.use(logger).use(createSessionMiddleware(authConfig)).use(express.urlencoded({ extended: false })).use(express.json({ limit: "50mb" })).use("/api", createApiRouter(authConfig, mcpAdminService)).use(createAuthPagesRouter(authConfig)).use("/graphql", createGraphqlRouter(authConfig, mcpAdminService)).use(createClientRouter(authConfig)).use(createErrorHandler());
|
|
22
22
|
return app;
|
|
23
23
|
};
|
|
24
24
|
var app_default = createApp;
|
package/server/dist/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/app.ts"],"sourcesContent":["import express from 'express';\nimport { createMcpAdminService, type McpAdminService } from './features/mcp-admin/service.js';\nimport { purgeExpiredNoteSnapshots } from './features/note/services/snapshot.js';\nimport { purgeExpiredTrashedNotes } from './features/note/services/trash.js';\nimport {
|
|
1
|
+
{"version":3,"sources":["../src/app.ts"],"sourcesContent":["import express from 'express';\nimport { createMcpAdminService, type McpAdminService } from './features/mcp-admin/service.js';\nimport { purgeExpiredNoteSnapshots } from './features/note/services/snapshot.js';\nimport { purgeExpiredTrashedNotes } from './features/note/services/trash.js';\nimport { createSessionMiddleware } from './modules/auth-guard.js';\nimport type { AuthConfig } from './modules/auth-mode.js';\nimport { createErrorHandler } from './modules/error-handler.js';\nimport logger from './modules/logger.js';\nimport { createApiRouter, createAuthPagesRouter, createClientRouter, createGraphqlRouter } from './routes/index.js';\n\nexport const createApp = (authConfig: AuthConfig) => {\n const mcpAdminService = createMcpAdminService();\n return createAppWithMcpAuth(authConfig, mcpAdminService);\n};\n\nexport const createAppWithMcpAuth = (authConfig: AuthConfig, mcpAdminService: McpAdminService) => {\n const app = express();\n app.locals.authConfig = authConfig;\n\n void Promise.all([purgeExpiredNoteSnapshots(), purgeExpiredTrashedNotes()]).catch((error) => {\n const message = error instanceof Error ? error.message : 'Unknown recovery cleanup error';\n process.stderr.write(`[recovery] Startup cleanup failed: ${message}\\n`);\n });\n\n app.use(logger)\n .use(createSessionMiddleware(authConfig))\n .use(express.urlencoded({ extended: false }))\n .use(express.json({ limit: '50mb' }))\n .use('/api', createApiRouter(authConfig, mcpAdminService))\n .use(createAuthPagesRouter(authConfig))\n .use('/graphql', createGraphqlRouter(authConfig, mcpAdminService))\n .use(createClientRouter(authConfig))\n .use(createErrorHandler());\n\n return app;\n};\n\nexport default createApp;\n"],"mappings":"AAAA,OAAO,aAAa;AACpB,SAAS,6BAAmD;AAC5D,SAAS,iCAAiC;AAC1C,SAAS,gCAAgC;AACzC,SAAS,+BAA+B;AAExC,SAAS,0BAA0B;AACnC,OAAO,YAAY;AACnB,SAAS,iBAAiB,uBAAuB,oBAAoB,2BAA2B;AAEzF,MAAM,YAAY,CAAC,eAA2B;AACjD,QAAM,kBAAkB,sBAAsB;AAC9C,SAAO,qBAAqB,YAAY,eAAe;AAC3D;AAEO,MAAM,uBAAuB,CAAC,YAAwB,oBAAqC;AAC9F,QAAM,MAAM,QAAQ;AACpB,MAAI,OAAO,aAAa;AAExB,OAAK,QAAQ,IAAI,CAAC,0BAA0B,GAAG,yBAAyB,CAAC,CAAC,EAAE,MAAM,CAAC,UAAU;AACzF,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,YAAQ,OAAO,MAAM,sCAAsC,OAAO;AAAA,CAAI;AAAA,EAC1E,CAAC;AAED,MAAI,IAAI,MAAM,EACT,IAAI,wBAAwB,UAAU,CAAC,EACvC,IAAI,QAAQ,WAAW,EAAE,UAAU,MAAM,CAAC,CAAC,EAC3C,IAAI,QAAQ,KAAK,EAAE,OAAO,OAAO,CAAC,CAAC,EACnC,IAAI,QAAQ,gBAAgB,YAAY,eAAe,CAAC,EACxD,IAAI,sBAAsB,UAAU,CAAC,EACrC,IAAI,YAAY,oBAAoB,YAAY,eAAe,CAAC,EAChE,IAAI,mBAAmB,UAAU,CAAC,EAClC,IAAI,mBAAmB,CAAC;AAE7B,SAAO;AACX;AAEA,IAAO,cAAQ;","names":[]}
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
buildSessionResponse,
|
|
6
6
|
compareSharedSecret,
|
|
7
7
|
destroySession,
|
|
8
|
-
|
|
8
|
+
refreshCsrfToken,
|
|
9
|
+
regenerateSession,
|
|
10
|
+
setSessionStatusHeaders
|
|
9
11
|
} from "../service.js";
|
|
10
12
|
const createLoginHandler = (authConfig) => {
|
|
11
13
|
return async (req, res) => {
|
|
@@ -16,6 +18,7 @@ const createLoginHandler = (authConfig) => {
|
|
|
16
18
|
}
|
|
17
19
|
await regenerateSession(req);
|
|
18
20
|
req.session.authenticated = true;
|
|
21
|
+
refreshCsrfToken(req);
|
|
19
22
|
res.status(200).json(buildSessionResponse(authConfig, req)).end();
|
|
20
23
|
};
|
|
21
24
|
};
|
|
@@ -29,6 +32,7 @@ const createLogoutHandler = (authConfig) => {
|
|
|
29
32
|
};
|
|
30
33
|
const createSessionStatusHandler = (authConfig) => {
|
|
31
34
|
return async (req, res) => {
|
|
35
|
+
setSessionStatusHeaders(res);
|
|
32
36
|
res.status(200).json(buildSessionResponse(authConfig, req)).end();
|
|
33
37
|
};
|
|
34
38
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/features/auth/http/api.ts"],"sourcesContent":["import { buildAuthSessionResponse } from '@baejino/auth';\nimport type { AuthConfig } from '~/modules/auth-mode.js';\nimport { createAppError } from '~/modules/error-handler.js';\nimport type { Controller } from '~/types/index.js';\nimport {\n assertPasswordLoginAvailable,\n buildSessionResponse,\n compareSharedSecret,\n destroySession,\n regenerateSession,\n} from '../service.js';\n\nexport const createLoginHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n const expectedPassword = assertPasswordLoginAvailable(authConfig);\n const password = typeof req.body?.password === 'string' ? req.body.password : '';\n\n if (!password || !compareSharedSecret(expectedPassword, password)) {\n throw createAppError(401, 'UNAUTHORIZED', 'Invalid password');\n }\n\n await regenerateSession(req);\n req.session.authenticated = true;\n\n res.status(200).json(buildSessionResponse(authConfig, req)).end();\n };\n};\n\nexport const createLogoutHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n if (authConfig.mode === 'password') {\n await destroySession(req);\n }\n\n res.status(200).json(buildAuthSessionResponse(authConfig, false)).end();\n };\n};\n\nexport const createSessionStatusHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n res.status(200).json(buildSessionResponse(authConfig, req)).end();\n };\n};\n"],"mappings":"AAAA,SAAS,gCAAgC;AAEzC,SAAS,sBAAsB;AAE/B;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AAEA,MAAM,qBAAqB,CAAC,eAAuC;AACtE,SAAO,OAAO,KAAK,QAAQ;AACvB,UAAM,mBAAmB,6BAA6B,UAAU;AAChE,UAAM,WAAW,OAAO,IAAI,MAAM,aAAa,WAAW,IAAI,KAAK,WAAW;AAE9E,QAAI,CAAC,YAAY,CAAC,oBAAoB,kBAAkB,QAAQ,GAAG;AAC/D,YAAM,eAAe,KAAK,gBAAgB,kBAAkB;AAAA,IAChE;AAEA,UAAM,kBAAkB,GAAG;AAC3B,QAAI,QAAQ,gBAAgB;
|
|
1
|
+
{"version":3,"sources":["../../../../src/features/auth/http/api.ts"],"sourcesContent":["import { buildAuthSessionResponse } from '@baejino/auth';\nimport type { AuthConfig } from '~/modules/auth-mode.js';\nimport { createAppError } from '~/modules/error-handler.js';\nimport type { Controller } from '~/types/index.js';\nimport {\n assertPasswordLoginAvailable,\n buildSessionResponse,\n compareSharedSecret,\n destroySession,\n refreshCsrfToken,\n regenerateSession,\n setSessionStatusHeaders,\n} from '../service.js';\n\nexport const createLoginHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n const expectedPassword = assertPasswordLoginAvailable(authConfig);\n const password = typeof req.body?.password === 'string' ? req.body.password : '';\n\n if (!password || !compareSharedSecret(expectedPassword, password)) {\n throw createAppError(401, 'UNAUTHORIZED', 'Invalid password');\n }\n\n await regenerateSession(req);\n req.session.authenticated = true;\n refreshCsrfToken(req);\n\n res.status(200).json(buildSessionResponse(authConfig, req)).end();\n };\n};\n\nexport const createLogoutHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n if (authConfig.mode === 'password') {\n await destroySession(req);\n }\n\n res.status(200).json(buildAuthSessionResponse(authConfig, false)).end();\n };\n};\n\nexport const createSessionStatusHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n setSessionStatusHeaders(res);\n res.status(200).json(buildSessionResponse(authConfig, req)).end();\n };\n};\n"],"mappings":"AAAA,SAAS,gCAAgC;AAEzC,SAAS,sBAAsB;AAE/B;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AAEA,MAAM,qBAAqB,CAAC,eAAuC;AACtE,SAAO,OAAO,KAAK,QAAQ;AACvB,UAAM,mBAAmB,6BAA6B,UAAU;AAChE,UAAM,WAAW,OAAO,IAAI,MAAM,aAAa,WAAW,IAAI,KAAK,WAAW;AAE9E,QAAI,CAAC,YAAY,CAAC,oBAAoB,kBAAkB,QAAQ,GAAG;AAC/D,YAAM,eAAe,KAAK,gBAAgB,kBAAkB;AAAA,IAChE;AAEA,UAAM,kBAAkB,GAAG;AAC3B,QAAI,QAAQ,gBAAgB;AAC5B,qBAAiB,GAAG;AAEpB,QAAI,OAAO,GAAG,EAAE,KAAK,qBAAqB,YAAY,GAAG,CAAC,EAAE,IAAI;AAAA,EACpE;AACJ;AAEO,MAAM,sBAAsB,CAAC,eAAuC;AACvE,SAAO,OAAO,KAAK,QAAQ;AACvB,QAAI,WAAW,SAAS,YAAY;AAChC,YAAM,eAAe,GAAG;AAAA,IAC5B;AAEA,QAAI,OAAO,GAAG,EAAE,KAAK,yBAAyB,YAAY,KAAK,CAAC,EAAE,IAAI;AAAA,EAC1E;AACJ;AAEO,MAAM,6BAA6B,CAAC,eAAuC;AAC9E,SAAO,OAAO,KAAK,QAAQ;AACvB,4BAAwB,GAAG;AAC3B,QAAI,OAAO,GAAG,EAAE,KAAK,qBAAqB,YAAY,GAAG,CAAC,EAAE,IAAI;AAAA,EACpE;AACJ;","names":[]}
|
|
@@ -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
|
|
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,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
compareSharedSecret,
|
|
3
|
+
destroySession,
|
|
4
|
+
refreshCsrfToken,
|
|
5
|
+
regenerateSession,
|
|
6
|
+
sanitizeRedirectPath
|
|
7
|
+
} from "../service.js";
|
|
2
8
|
import { renderLoginPage } from "./login-page.js";
|
|
3
9
|
const createLoginPageHandler = (authConfig) => {
|
|
4
10
|
return async (req, res) => {
|
|
@@ -30,6 +36,7 @@ const createLoginPageSubmitHandler = (authConfig) => {
|
|
|
30
36
|
}
|
|
31
37
|
await regenerateSession(req);
|
|
32
38
|
req.session.authenticated = true;
|
|
39
|
+
refreshCsrfToken(req);
|
|
33
40
|
res.redirect(303, nextPath);
|
|
34
41
|
};
|
|
35
42
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/features/auth/http/pages.ts"],"sourcesContent":["import type { AuthConfig } from '~/modules/auth-mode.js';\nimport type { Controller } from '~/types/index.js';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../../src/features/auth/http/pages.ts"],"sourcesContent":["import type { AuthConfig } from '~/modules/auth-mode.js';\nimport type { Controller } from '~/types/index.js';\nimport {\n compareSharedSecret,\n destroySession,\n refreshCsrfToken,\n regenerateSession,\n sanitizeRedirectPath,\n} from '../service.js';\nimport { renderLoginPage } from './login-page.js';\n\nexport const createLoginPageHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n if (authConfig.mode !== 'password' || req.session?.authenticated) {\n res.redirect(303, sanitizeRedirectPath(req.query.next));\n return;\n }\n\n const nextPath = sanitizeRedirectPath(req.query.next);\n res.status(200)\n .type('html')\n .send(renderLoginPage({ nextPath, csrfToken: res.locals._csrf }))\n .end();\n };\n};\n\nexport const createLoginPageSubmitHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n const nextPath = sanitizeRedirectPath(req.body?.next);\n\n if (authConfig.mode !== 'password' || !authConfig.password) {\n res.redirect(303, nextPath);\n return;\n }\n\n const password = typeof req.body?.password === 'string' ? req.body.password : '';\n\n if (!password || !compareSharedSecret(authConfig.password, password)) {\n res.status(401)\n .type('html')\n .send(\n renderLoginPage({\n nextPath,\n errorMessage: 'Invalid password',\n csrfToken: res.locals._csrf,\n }),\n )\n .end();\n return;\n }\n\n await regenerateSession(req);\n req.session.authenticated = true;\n refreshCsrfToken(req);\n\n res.redirect(303, nextPath);\n };\n};\n\nexport const createLogoutPageHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n if (authConfig.mode === 'password') {\n await destroySession(req);\n res.redirect(303, '/login');\n return;\n }\n\n res.redirect(303, '/');\n };\n};\n"],"mappings":"AAEA;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP,SAAS,uBAAuB;AAEzB,MAAM,yBAAyB,CAAC,eAAuC;AAC1E,SAAO,OAAO,KAAK,QAAQ;AACvB,QAAI,WAAW,SAAS,cAAc,IAAI,SAAS,eAAe;AAC9D,UAAI,SAAS,KAAK,qBAAqB,IAAI,MAAM,IAAI,CAAC;AACtD;AAAA,IACJ;AAEA,UAAM,WAAW,qBAAqB,IAAI,MAAM,IAAI;AACpD,QAAI,OAAO,GAAG,EACT,KAAK,MAAM,EACX,KAAK,gBAAgB,EAAE,UAAU,WAAW,IAAI,OAAO,MAAM,CAAC,CAAC,EAC/D,IAAI;AAAA,EACb;AACJ;AAEO,MAAM,+BAA+B,CAAC,eAAuC;AAChF,SAAO,OAAO,KAAK,QAAQ;AACvB,UAAM,WAAW,qBAAqB,IAAI,MAAM,IAAI;AAEpD,QAAI,WAAW,SAAS,cAAc,CAAC,WAAW,UAAU;AACxD,UAAI,SAAS,KAAK,QAAQ;AAC1B;AAAA,IACJ;AAEA,UAAM,WAAW,OAAO,IAAI,MAAM,aAAa,WAAW,IAAI,KAAK,WAAW;AAE9E,QAAI,CAAC,YAAY,CAAC,oBAAoB,WAAW,UAAU,QAAQ,GAAG;AAClE,UAAI,OAAO,GAAG,EACT,KAAK,MAAM,EACX;AAAA,QACG,gBAAgB;AAAA,UACZ;AAAA,UACA,cAAc;AAAA,UACd,WAAW,IAAI,OAAO;AAAA,QAC1B,CAAC;AAAA,MACL,EACC,IAAI;AACT;AAAA,IACJ;AAEA,UAAM,kBAAkB,GAAG;AAC3B,QAAI,QAAQ,gBAAgB;AAC5B,qBAAiB,GAAG;AAEpB,QAAI,SAAS,KAAK,QAAQ;AAAA,EAC9B;AACJ;AAEO,MAAM,0BAA0B,CAAC,eAAuC;AAC3E,SAAO,OAAO,KAAK,QAAQ;AACvB,QAAI,WAAW,SAAS,YAAY;AAChC,YAAM,eAAe,GAAG;AACxB,UAAI,SAAS,KAAK,QAAQ;AAC1B;AAAA,IACJ;AAEA,QAAI,SAAS,KAAK,GAAG;AAAA,EACzB;AACJ;","names":[]}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { buildAuthSessionResponse
|
|
1
|
+
import { buildAuthSessionResponse } from "@baejino/auth";
|
|
2
2
|
import { compareSharedSecret as compareCommonSharedSecret } from "@baejino/auth/crypto";
|
|
3
3
|
import crypto from "crypto";
|
|
4
|
+
import { sanitizeRedirectPath } from "../../modules/auth-redirect.js";
|
|
4
5
|
import { createAppError } from "../../modules/error-handler.js";
|
|
6
|
+
const AUTH_SESSION_GENERATION_HEADER = "X-Ocean-Brain-Session-Generation";
|
|
7
|
+
const AUTH_SESSION_GENERATION = crypto.randomUUID();
|
|
5
8
|
const createPasswordHash = async (password) => {
|
|
6
9
|
const salt = crypto.randomBytes(16).toString("hex");
|
|
7
10
|
const hash = crypto.pbkdf2Sync(password, salt, 1e3, 64, "sha512").toString("hex");
|
|
@@ -14,6 +17,14 @@ const comparePassword = async (password, storedHash) => {
|
|
|
14
17
|
};
|
|
15
18
|
const compareSharedSecret = compareCommonSharedSecret;
|
|
16
19
|
const buildSessionResponse = (authConfig, req) => buildAuthSessionResponse(authConfig, Boolean(req.session?.authenticated));
|
|
20
|
+
const setSessionStatusHeaders = (res) => {
|
|
21
|
+
res.set(AUTH_SESSION_GENERATION_HEADER, AUTH_SESSION_GENERATION);
|
|
22
|
+
res.set("Cache-Control", "no-store");
|
|
23
|
+
};
|
|
24
|
+
const refreshCsrfToken = (req) => {
|
|
25
|
+
const requestWithCsrf = req;
|
|
26
|
+
requestWithCsrf.csrfToken?.();
|
|
27
|
+
};
|
|
17
28
|
const assertPasswordLoginAvailable = (authConfig) => {
|
|
18
29
|
if (authConfig.mode !== "password") {
|
|
19
30
|
throw createAppError(409, "AUTH_OPEN_MODE", "Login is unavailable while auth mode is open.");
|
|
@@ -45,19 +56,17 @@ const destroySession = async (req) => {
|
|
|
45
56
|
});
|
|
46
57
|
});
|
|
47
58
|
};
|
|
48
|
-
const sanitizeRedirectPath = (value) => sanitizeCommonRedirectPath(value, {
|
|
49
|
-
fallbackPath: "/",
|
|
50
|
-
loginPath: "/login",
|
|
51
|
-
allowedAbsoluteHosts: ["localhost", "127.0.0.1", "::1"]
|
|
52
|
-
});
|
|
53
59
|
export {
|
|
60
|
+
AUTH_SESSION_GENERATION_HEADER,
|
|
54
61
|
assertPasswordLoginAvailable,
|
|
55
62
|
buildSessionResponse,
|
|
56
63
|
comparePassword,
|
|
57
64
|
compareSharedSecret,
|
|
58
65
|
createPasswordHash,
|
|
59
66
|
destroySession,
|
|
67
|
+
refreshCsrfToken,
|
|
60
68
|
regenerateSession,
|
|
61
|
-
sanitizeRedirectPath
|
|
69
|
+
sanitizeRedirectPath,
|
|
70
|
+
setSessionStatusHeaders
|
|
62
71
|
};
|
|
63
72
|
//# sourceMappingURL=service.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/features/auth/service.ts"],"sourcesContent":["import { buildAuthSessionResponse
|
|
1
|
+
{"version":3,"sources":["../../../src/features/auth/service.ts"],"sourcesContent":["import { buildAuthSessionResponse } from '@baejino/auth';\nimport { compareSharedSecret as compareCommonSharedSecret } from '@baejino/auth/crypto';\nimport crypto from 'crypto';\nimport type { Request, Response } from 'express';\nimport type { AuthConfig } from '~/modules/auth-mode.js';\nimport { sanitizeRedirectPath } from '~/modules/auth-redirect.js';\nimport { createAppError } from '~/modules/error-handler.js';\n\nexport const AUTH_SESSION_GENERATION_HEADER = 'X-Ocean-Brain-Session-Generation';\nconst AUTH_SESSION_GENERATION = crypto.randomUUID();\n\nexport const createPasswordHash = async (password: string) => {\n const salt = crypto.randomBytes(16).toString('hex');\n const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');\n return `${salt}:${hash}`;\n};\n\nexport const comparePassword = async (password: string, storedHash: string) => {\n const [salt, hash] = storedHash.split(':');\n const newHash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');\n return hash === newHash;\n};\n\nexport const compareSharedSecret = compareCommonSharedSecret;\n\nexport const buildSessionResponse = (authConfig: AuthConfig, req: Request) =>\n buildAuthSessionResponse(authConfig, Boolean(req.session?.authenticated));\n\nexport const setSessionStatusHeaders = (res: Response) => {\n res.set(AUTH_SESSION_GENERATION_HEADER, AUTH_SESSION_GENERATION);\n res.set('Cache-Control', 'no-store');\n};\n\nexport const refreshCsrfToken = (req: Request) => {\n const requestWithCsrf = req as Request & { csrfToken?: () => string };\n requestWithCsrf.csrfToken?.();\n};\n\nexport const assertPasswordLoginAvailable = (authConfig: AuthConfig) => {\n if (authConfig.mode !== 'password') {\n throw createAppError(409, 'AUTH_OPEN_MODE', 'Login is unavailable while auth mode is open.');\n }\n\n return authConfig.password;\n};\n\nexport const regenerateSession = async (req: Request) => {\n await new Promise<void>((resolve, reject) => {\n req.session.regenerate((error) => {\n if (error) {\n reject(error);\n return;\n }\n\n resolve();\n });\n });\n};\n\nexport const destroySession = async (req: Request) => {\n if (!req.session) {\n return;\n }\n\n await new Promise<void>((resolve, reject) => {\n req.session.destroy((error) => {\n if (error) {\n reject(error);\n return;\n }\n\n resolve();\n });\n });\n};\n\nexport { sanitizeRedirectPath };\n"],"mappings":"AAAA,SAAS,gCAAgC;AACzC,SAAS,uBAAuB,iCAAiC;AACjE,OAAO,YAAY;AAGnB,SAAS,4BAA4B;AACrC,SAAS,sBAAsB;AAExB,MAAM,iCAAiC;AAC9C,MAAM,0BAA0B,OAAO,WAAW;AAE3C,MAAM,qBAAqB,OAAO,aAAqB;AAC1D,QAAM,OAAO,OAAO,YAAY,EAAE,EAAE,SAAS,KAAK;AAClD,QAAM,OAAO,OAAO,WAAW,UAAU,MAAM,KAAM,IAAI,QAAQ,EAAE,SAAS,KAAK;AACjF,SAAO,GAAG,IAAI,IAAI,IAAI;AAC1B;AAEO,MAAM,kBAAkB,OAAO,UAAkB,eAAuB;AAC3E,QAAM,CAAC,MAAM,IAAI,IAAI,WAAW,MAAM,GAAG;AACzC,QAAM,UAAU,OAAO,WAAW,UAAU,MAAM,KAAM,IAAI,QAAQ,EAAE,SAAS,KAAK;AACpF,SAAO,SAAS;AACpB;AAEO,MAAM,sBAAsB;AAE5B,MAAM,uBAAuB,CAAC,YAAwB,QACzD,yBAAyB,YAAY,QAAQ,IAAI,SAAS,aAAa,CAAC;AAErE,MAAM,0BAA0B,CAAC,QAAkB;AACtD,MAAI,IAAI,gCAAgC,uBAAuB;AAC/D,MAAI,IAAI,iBAAiB,UAAU;AACvC;AAEO,MAAM,mBAAmB,CAAC,QAAiB;AAC9C,QAAM,kBAAkB;AACxB,kBAAgB,YAAY;AAChC;AAEO,MAAM,+BAA+B,CAAC,eAA2B;AACpE,MAAI,WAAW,SAAS,YAAY;AAChC,UAAM,eAAe,KAAK,kBAAkB,+CAA+C;AAAA,EAC/F;AAEA,SAAO,WAAW;AACtB;AAEO,MAAM,oBAAoB,OAAO,QAAiB;AACrD,QAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AACzC,QAAI,QAAQ,WAAW,CAAC,UAAU;AAC9B,UAAI,OAAO;AACP,eAAO,KAAK;AACZ;AAAA,MACJ;AAEA,cAAQ;AAAA,IACZ,CAAC;AAAA,EACL,CAAC;AACL;AAEO,MAAM,iBAAiB,OAAO,QAAiB;AAClD,MAAI,CAAC,IAAI,SAAS;AACd;AAAA,EACJ;AAEA,QAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AACzC,QAAI,QAAQ,QAAQ,CAAC,UAAU;AAC3B,UAAI,OAAO;AACP,eAAO,KAAK;AACZ;AAAA,MACJ;AAEA,cAAQ;AAAA,IACZ,CAAC;AAAA,EACL,CAAC;AACL;","names":[]}
|
|
@@ -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"
|
|
30
|
+
if (typeof image !== "string") {
|
|
14
31
|
throw createAppError(400, "INVALID_IMAGE_UPLOAD", "No image uploaded");
|
|
15
32
|
}
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
38
|
-
throw
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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":[]}
|