@@ -63,8 +63,16 @@ app.use(helmet({
6363
6464app . use ( ( req , res , next ) => {
6565 if ( req . path === '/upload' ) return next ( ) ;
66+
67+ const ct = req . headers [ 'content-type' ] || '' ;
68+
69+ if ( ct . startsWith ( 'text/plain' ) ) {
70+ return express . text ( { limit : '10mb' } ) ( req , res , next ) ;
71+ }
72+
6673 express . json ( ) ( req , res , next ) ;
6774} ) ;
75+
6876app . use ( cookieParser ( ) ) ;
6977app . use ( requireAuth ) ;
7078app . use ( "/uploads" , ( req , res , next ) => {
@@ -225,19 +233,28 @@ const COOKIE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
225233const sessions = new Map ( ) ;
226234
227235function requireAuth ( req , res , next ) {
228- if ( ! config . auth . passphrase ) return next ( ) ; // no passphrase set, skip
236+ if ( ! config . auth . passphrase ) return next ( ) ;
229237
230- // Allow the login route itself through
231238 if ( req . path === "/login" || req . path === "/info" || req . path === "/health" ) return next ( ) ;
232239
240+ // Terminal / API clients — accept passphrase via header
241+ const headerPass = req . headers [ "x-passphrase" ] ;
242+ if ( headerPass && headerPass === config . auth . passphrase ) return next ( ) ;
233243
234- // Check cookie holds a valid session token
244+ // Browser clients — check session cookie
235245 const cookie = req . cookies [ COOKIE_NAME ] ;
236246 if ( cookie && sessions . has ( cookie ) ) return next ( ) ;
237247
238- // Not authenticated
239248 if ( req . path . startsWith ( "/socket.io" ) ) return next ( ) ;
240- if ( req . headers [ "content-type" ] === "application/json" || req . xhr ) {
249+
250+ // Return JSON 401 for any non-browser request
251+ const ct = req . headers [ "content-type" ] || "" ;
252+ const isApiRequest = ct . startsWith ( "application/json" ) ||
253+ ct . startsWith ( "text/plain" ) ||
254+ req . xhr ||
255+ req . headers [ "x-passphrase" ] !== undefined ;
256+
257+ if ( isApiRequest ) {
241258 return res . status ( 401 ) . json ( { error : "Unauthorized" } ) ;
242259 }
243260
@@ -453,9 +470,27 @@ app.use((err, req, res, next) => {
453470 next ( err ) ;
454471} ) ;
455472
456- /* TEXT/LINK */
457- app . post ( "/text" , dropLimiter , ( req , res ) => {
458- const { content, channel, uploader } = req . body ;
473+ /* TEXT/LINK — accepts application/json and text/plain */
474+ function handleTextPost ( req , res ) {
475+ let content , channel , uploader ;
476+
477+ if ( req . is ( "text/plain" ) ) {
478+ // Terminal path — body is raw string, metadata comes from headers
479+ content = typeof req . body === "string" ? req . body : String ( req . body ) ;
480+ channel = ( req . headers [ "x-channel" ] || "general" ) . trim ( ) ;
481+ uploader = ( req . headers [ "x-uploader" ] || "terminal" ) . trim ( ) ;
482+ } else {
483+ // Browser / JSON path — existing behaviour unchanged
484+ ( { content, channel, uploader } = req . body || { } ) ;
485+ }
486+
487+ if ( ! content || ! content . trim ( ) ) {
488+ return res . status ( 400 ) . json ( { error : "Content is required" } ) ;
489+ }
490+
491+ if ( ! channel ) {
492+ return res . status ( 400 ) . json ( { error : "Channel is required" } ) ;
493+ }
459494
460495 const item = {
461496 type : "text" ,
@@ -476,7 +511,11 @@ app.post("/text", dropLimiter, (req, res) => {
476511 res . json ( item ) ;
477512 }
478513 ) ;
479- } ) ;
514+ }
515+
516+ app . post ( "/text" , dropLimiter , handleTextPost ) ;
517+ app . post ( "/push" , dropLimiter , handleTextPost ) ; // terminal-friendly alias
518+
480519
481520/* DELETE ITEM */
482521app . delete ( "/item/:id" , ( req , res ) => {
0 commit comments