Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions example/http-example.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// module.mjs

// Schema: only these functions are callable via SendScript
export const schema = [
'login',
'addTodo',
'listTodos',
'removeTodo'
];

// In-memory storage for all users
const allUsers = new Map(); // username -> user object

// Factory: creates a per-user module object
export default function perRequestWrapper(req) {
const auth = req.headers['authorization'];
if (!auth || !auth.startsWith('Bearer ')) {
throw new Error('Missing or invalid Authorization header');
}
const username = auth.slice('Bearer '.length).trim();
if (!username) throw new Error('Username required in token');

// Reuse existing user or create new
let user = allUsers.get(username);
if (!user) {
user = { username, todos: [] };
allUsers.set(username, user);
}

return {
login: async () => ({ username: user.username }),

addTodo: async (text) => {
if (!text) throw new Error('Todo text required');
const todo = { id: user.todos.length + 1, text };
user.todos.push(todo);
return todo;
},

listTodos: async () => [...user.todos],

removeTodo: async (id) => {
const index = user.todos.findIndex(t => t.id === id);
if (index === -1) throw new Error('Todo not found');
const [removed] = user.todos.splice(index, 1);
return removed;
}
};
}
85 changes: 85 additions & 0 deletions sendscript-http.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env node

import repl from './repl.mjs';
import http from 'http';
import Debug from './debug.mjs'
import fs from 'fs';
import path from 'path';
import url from 'url';
import Parse from './parse.mjs';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

const debug = Debug.extend('http-server')

const argv = yargs(hideBin(process.argv))
.option('module', {
alias: 'm',
type: 'string',
description: 'Path to the JS module to serve',
default: './module.mjs',
})
.option('port', {
alias: 'p',
type: 'number',
description: 'Port to listen on',
default: 3000,
})
.option('per-request', {
alias: 'r',
type: 'boolean',
description: 'Path to a JS file exporting a function: (req) => module',
})
.help('h')
.alias('h', 'help')
.parse();

debug('argv', argv)

const moduleFullPath = path.resolve(process.cwd(), argv.module);
if (!fs.existsSync(moduleFullPath)) {
console.error(`Module not found: ${moduleFullPath}`);
process.exit(1);
}

async function loadModule() {
const m = await import(url.pathToFileURL(moduleFullPath).href + `?t=${Date.now()}`);
if (!m.schema) {
throw new Error(`Module must export a "schema" property`);
}
return m;
}

// Load initial module
let mod = await loadModule();

repl(send, mod.repl)

const identity = x => x

const server = http.createServer(async (req, res) => {
if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'POST only' }));
}


let body = '';
req.on('data', chunk => body += chunk);
req.on('end', async () => {
try {
const module = argv.perRequest ? mod.default(req) : mod.default
const parse = Parse(mod.schema, module)
const result = await parse(body);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ result }));
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
}
});
});

server.listen(argv.port, () => {
console.log(`Serving module via SendScript on http://localhost:${argv.port}`);
});