@mostajs/net 1.0.0-alpha.1

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.
@@ -0,0 +1,12 @@
1
+ import type { TransportMiddleware } from '../core/types.js';
2
+ /**
3
+ * API key authentication middleware.
4
+ *
5
+ * Extracts the API key from:
6
+ * - Header: Authorization: Bearer msk_live_...
7
+ * - Query: ?apikey=msk_live_...
8
+ *
9
+ * If .mosta/apikeys.json has no subscriptions, all requests are allowed (open mode).
10
+ * If subscriptions exist, a valid API key is required.
11
+ */
12
+ export declare const apiKeyMiddleware: TransportMiddleware;
@@ -0,0 +1,62 @@
1
+ // @mostajs/net — API Key authentication middleware
2
+ // Validates Bearer token against .mosta/apikeys.json
3
+ // Checks permissions against the 3D matrix (API key × SGBD × transport)
4
+ // Author: Dr Hamid MADANI drmdh@msn.com
5
+ import { validateApiKey, checkPermission, opToPermission } from './apikeys.js';
6
+ import { getCurrentDialectType } from '@mostajs/orm';
7
+ /**
8
+ * API key authentication middleware.
9
+ *
10
+ * Extracts the API key from:
11
+ * - Header: Authorization: Bearer msk_live_...
12
+ * - Query: ?apikey=msk_live_...
13
+ *
14
+ * If .mosta/apikeys.json has no subscriptions, all requests are allowed (open mode).
15
+ * If subscriptions exist, a valid API key is required.
16
+ */
17
+ export const apiKeyMiddleware = async (req, ctx, next) => {
18
+ const key = ctx.apiKey;
19
+ // If no key provided, check if we're in open mode (no subscriptions configured)
20
+ if (!key) {
21
+ const { readApiKeys } = await import('./apikeys.js');
22
+ const data = readApiKeys();
23
+ if (data.subscriptions.length === 0) {
24
+ // Open mode: no API keys configured, allow everything
25
+ return next();
26
+ }
27
+ return {
28
+ status: 'error',
29
+ error: {
30
+ code: 'UNAUTHORIZED',
31
+ message: 'API key required. Pass it via Authorization: Bearer <key> header',
32
+ },
33
+ };
34
+ }
35
+ // Validate key
36
+ const sub = validateApiKey(key);
37
+ if (!sub) {
38
+ return {
39
+ status: 'error',
40
+ error: {
41
+ code: 'UNAUTHORIZED',
42
+ message: 'Invalid or revoked API key',
43
+ },
44
+ };
45
+ }
46
+ // Check permission: dialect × transport × operation
47
+ const dialect = getCurrentDialectType();
48
+ const perm = opToPermission(req.op);
49
+ if (!checkPermission(sub, dialect, ctx.transport, perm)) {
50
+ return {
51
+ status: 'error',
52
+ error: {
53
+ code: 'FORBIDDEN',
54
+ message: `Subscription "${sub.name}" does not have ${perm} permission for ${dialect}/${ctx.transport}`,
55
+ },
56
+ };
57
+ }
58
+ // Enrich context
59
+ ctx.subscription = sub.name;
60
+ ctx.permissions = sub.permissions[dialect] || sub.permissions['*'] || {};
61
+ return next();
62
+ };
@@ -0,0 +1,34 @@
1
+ export interface Subscription {
2
+ name: string;
3
+ key: string | null;
4
+ hash: string;
5
+ created: string;
6
+ status: 'active' | 'revoked';
7
+ permissions: Record<string, Record<string, string>>;
8
+ }
9
+ export interface ApiKeysFile {
10
+ subscriptions: Subscription[];
11
+ }
12
+ /** Read .mosta/apikeys.json — returns empty if not found */
13
+ export declare function readApiKeys(): ApiKeysFile;
14
+ /** Write .mosta/apikeys.json */
15
+ export declare function writeApiKeys(data: ApiKeysFile): void;
16
+ /** Generate a new API key with prefix msk_live_ or msk_test_ */
17
+ export declare function generateApiKey(mode?: 'live' | 'test'): string;
18
+ /** Hash a key with SHA-256 */
19
+ export declare function hashApiKey(key: string): string;
20
+ /** Create a new subscription, returns the clear-text key (shown once) */
21
+ export declare function createSubscription(name: string, permissions?: Record<string, Record<string, string>>, mode?: 'live' | 'test'): {
22
+ subscription: Subscription;
23
+ clearKey: string;
24
+ };
25
+ /** Revoke a subscription by name */
26
+ export declare function revokeSubscription(name: string): boolean;
27
+ /** Validate an API key and return the matching subscription (or null) */
28
+ export declare function validateApiKey(key: string): Subscription | null;
29
+ /** Check if a subscription has permission for a given dialect + transport + operation */
30
+ export declare function checkPermission(sub: Subscription, dialect: string, transport: string, operation: 'r' | 'c' | 'u' | 'd'): boolean;
31
+ /**
32
+ * Map ORM operation to permission check letter
33
+ */
34
+ export declare function opToPermission(op: string): 'r' | 'c' | 'u' | 'd';
@@ -0,0 +1,130 @@
1
+ // @mostajs/net — API Key management
2
+ // Reads .mosta/apikeys.json, validates keys, checks permissions
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { createHash, randomBytes } from 'crypto';
7
+ // ============================================================
8
+ // File I/O
9
+ // ============================================================
10
+ const APIKEYS_DIR = '.mosta';
11
+ const APIKEYS_FILE = 'apikeys.json';
12
+ function getApiKeysPath() {
13
+ return join(process.cwd(), APIKEYS_DIR, APIKEYS_FILE);
14
+ }
15
+ /** Read .mosta/apikeys.json — returns empty if not found */
16
+ export function readApiKeys() {
17
+ const path = getApiKeysPath();
18
+ if (!existsSync(path)) {
19
+ return { subscriptions: [] };
20
+ }
21
+ const raw = readFileSync(path, 'utf-8');
22
+ return JSON.parse(raw);
23
+ }
24
+ /** Write .mosta/apikeys.json */
25
+ export function writeApiKeys(data) {
26
+ const dir = join(process.cwd(), APIKEYS_DIR);
27
+ if (!existsSync(dir)) {
28
+ mkdirSync(dir, { recursive: true });
29
+ }
30
+ writeFileSync(getApiKeysPath(), JSON.stringify(data, null, 2) + '\n', 'utf-8');
31
+ }
32
+ // ============================================================
33
+ // Key generation
34
+ // ============================================================
35
+ /** Generate a new API key with prefix msk_live_ or msk_test_ */
36
+ export function generateApiKey(mode = 'live') {
37
+ const prefix = mode === 'live' ? 'msk_live_' : 'msk_test_';
38
+ const random = randomBytes(24).toString('base64url');
39
+ return prefix + random;
40
+ }
41
+ /** Hash a key with SHA-256 */
42
+ export function hashApiKey(key) {
43
+ return createHash('sha256').update(key).digest('hex');
44
+ }
45
+ // ============================================================
46
+ // Subscription CRUD
47
+ // ============================================================
48
+ /** Create a new subscription, returns the clear-text key (shown once) */
49
+ export function createSubscription(name, permissions = { '*': { '*': 'crud' } }, mode = 'live') {
50
+ const data = readApiKeys();
51
+ // Check duplicate name
52
+ if (data.subscriptions.some(s => s.name === name)) {
53
+ throw new Error(`Subscription "${name}" already exists`);
54
+ }
55
+ const clearKey = generateApiKey(mode);
56
+ const subscription = {
57
+ name,
58
+ key: clearKey,
59
+ hash: hashApiKey(clearKey),
60
+ created: new Date().toISOString().slice(0, 10),
61
+ status: 'active',
62
+ permissions,
63
+ };
64
+ data.subscriptions.push(subscription);
65
+ writeApiKeys(data);
66
+ return { subscription, clearKey };
67
+ }
68
+ /** Revoke a subscription by name */
69
+ export function revokeSubscription(name) {
70
+ const data = readApiKeys();
71
+ const sub = data.subscriptions.find(s => s.name === name);
72
+ if (!sub)
73
+ return false;
74
+ sub.status = 'revoked';
75
+ sub.permissions = {};
76
+ writeApiKeys(data);
77
+ return true;
78
+ }
79
+ // ============================================================
80
+ // Validation
81
+ // ============================================================
82
+ /** Validate an API key and return the matching subscription (or null) */
83
+ export function validateApiKey(key) {
84
+ const hash = hashApiKey(key);
85
+ const data = readApiKeys();
86
+ return data.subscriptions.find(s => s.hash === hash && s.status === 'active') || null;
87
+ }
88
+ /** Check if a subscription has permission for a given dialect + transport + operation */
89
+ export function checkPermission(sub, dialect, transport, operation) {
90
+ // Find matching permission entry (exact match or wildcard)
91
+ const dialectPerms = sub.permissions[dialect] || sub.permissions['*'];
92
+ if (!dialectPerms)
93
+ return false;
94
+ const rights = dialectPerms[transport] || dialectPerms['*'];
95
+ if (!rights)
96
+ return false;
97
+ // Check operation against rights string
98
+ switch (operation) {
99
+ case 'r': return rights.includes('r');
100
+ case 'c': return rights.includes('c') && rights.includes('r'); // 'cr' or 'crud'
101
+ case 'u': return rights.includes('crud');
102
+ case 'd': return rights.includes('crud');
103
+ default: return false;
104
+ }
105
+ }
106
+ /**
107
+ * Map ORM operation to permission check letter
108
+ */
109
+ export function opToPermission(op) {
110
+ switch (op) {
111
+ case 'findAll':
112
+ case 'findOne':
113
+ case 'findById':
114
+ case 'count':
115
+ case 'search':
116
+ case 'aggregate':
117
+ case 'stream':
118
+ return 'r';
119
+ case 'create':
120
+ case 'upsert':
121
+ return 'c';
122
+ case 'update':
123
+ return 'u';
124
+ case 'delete':
125
+ case 'deleteMany':
126
+ return 'd';
127
+ default:
128
+ return 'r';
129
+ }
130
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ // @mostajs/net CLI — `npx @mostajs/net serve`
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ import { loadNetConfig, getEnabledTransports, TRANSPORT_NAMES } from './core/config.js';
5
+ const args = process.argv.slice(2);
6
+ const command = args[0];
7
+ const arg1 = args[1];
8
+ const arg2 = args[2];
9
+ async function main() {
10
+ switch (command) {
11
+ case 'generate-apikey': {
12
+ const { createSubscription } = await import('./auth/apikeys.js');
13
+ const name = arg1 || 'default';
14
+ const mode = (arg2 === 'test' ? 'test' : 'live');
15
+ const { subscription, clearKey } = createSubscription(name, { '*': { '*': 'crud' } }, mode);
16
+ console.log(`\n API Key generated for "${subscription.name}":`);
17
+ console.log(` Key: ${clearKey}`);
18
+ console.log(` Hash: ${subscription.hash}`);
19
+ console.log(` Mode: ${mode}`);
20
+ console.log(`\n Saved in .mosta/apikeys.json\n`);
21
+ break;
22
+ }
23
+ case 'hash-password': {
24
+ if (!arg1) {
25
+ console.log('Usage: mostajs-net hash-password <password>');
26
+ process.exit(1);
27
+ }
28
+ // Dynamic import to avoid requiring bcrypt if not used
29
+ // Use SHA-256 for password hashing (no bcrypt dependency required)
30
+ const { createHash } = await import('crypto');
31
+ const hash = createHash('sha256').update(arg1).digest('hex');
32
+ console.log(`\n Password hash (SHA-256):`);
33
+ console.log(` ${hash}\n`);
34
+ console.log(` Add to .env.local:`);
35
+ console.log(` MOSTA_ADMIN_PASS_HASH=${hash}\n`);
36
+ break;
37
+ }
38
+ case 'serve':
39
+ case undefined: {
40
+ console.log(`
41
+ \x1b[1m@mostajs/net\x1b[0m v1.0.0-alpha.1
42
+ ${'─'.repeat(35)}
43
+ `);
44
+ const config = loadNetConfig();
45
+ const enabled = getEnabledTransports(config);
46
+ if (enabled.length === 0) {
47
+ console.log(' \x1b[33m⚠\x1b[0m No transports enabled.');
48
+ console.log(` Set MOSTA_NET_{TRANSPORT}_ENABLED=true in .env.local`);
49
+ console.log(` Available: ${TRANSPORT_NAMES.join(', ')}\n`);
50
+ process.exit(1);
51
+ }
52
+ console.log(` Transports enabled: ${enabled.join(', ')}`);
53
+ console.log(` Port: ${config.port}\n`);
54
+ // Dynamic import to avoid loading ORM at parse time
55
+ const { startServer } = await import('./server.js');
56
+ const server = await startServer();
57
+ // Graceful shutdown
58
+ const shutdown = async () => {
59
+ console.log('\n Shutting down...');
60
+ await server.stop();
61
+ process.exit(0);
62
+ };
63
+ process.on('SIGINT', shutdown);
64
+ process.on('SIGTERM', shutdown);
65
+ break;
66
+ }
67
+ case 'info': {
68
+ const config = loadNetConfig();
69
+ const enabled = getEnabledTransports(config);
70
+ const { readApiKeys } = await import('./auth/apikeys.js');
71
+ const keys = readApiKeys();
72
+ console.log(JSON.stringify({
73
+ port: config.port,
74
+ transports: { enabled, all: [...TRANSPORT_NAMES] },
75
+ apikeys: keys.subscriptions.map(s => ({ name: s.name, status: s.status, created: s.created })),
76
+ }, null, 2));
77
+ break;
78
+ }
79
+ case 'list-keys': {
80
+ const { readApiKeys } = await import('./auth/apikeys.js');
81
+ const keys = readApiKeys();
82
+ if (keys.subscriptions.length === 0) {
83
+ console.log('\n No API keys configured. Use: mostajs-net generate-apikey <name>\n');
84
+ }
85
+ else {
86
+ console.log(`\n API Keys (${keys.subscriptions.length}):`);
87
+ for (const s of keys.subscriptions) {
88
+ const icon = s.status === 'active' ? '\x1b[32m●\x1b[0m' : '\x1b[31m○\x1b[0m';
89
+ console.log(` ${icon} ${s.name} (${s.status}, ${s.created})`);
90
+ }
91
+ console.log('');
92
+ }
93
+ break;
94
+ }
95
+ case 'revoke-key': {
96
+ if (!arg1) {
97
+ console.log('Usage: mostajs-net revoke-key <name>');
98
+ process.exit(1);
99
+ }
100
+ const { revokeSubscription } = await import('./auth/apikeys.js');
101
+ if (revokeSubscription(arg1)) {
102
+ console.log(`\n Subscription "${arg1}" revoked.\n`);
103
+ }
104
+ else {
105
+ console.log(`\n Subscription "${arg1}" not found.\n`);
106
+ process.exit(1);
107
+ }
108
+ break;
109
+ }
110
+ default:
111
+ console.log(`Unknown command: ${command}`);
112
+ console.log('Usage: mostajs-net <command>');
113
+ console.log('');
114
+ console.log('Commands:');
115
+ console.log(' serve Start the server');
116
+ console.log(' info Show config and API keys');
117
+ console.log(' generate-apikey Generate a new API key');
118
+ console.log(' list-keys List all API keys');
119
+ console.log(' revoke-key <name> Revoke an API key');
120
+ console.log(' hash-password <pw> Hash a password');
121
+ process.exit(1);
122
+ }
123
+ }
124
+ main().catch((err) => {
125
+ console.error('\x1b[31mFatal:\x1b[0m', err.message || err);
126
+ process.exit(1);
127
+ });
@@ -0,0 +1,15 @@
1
+ import type { NetServerConfig } from './types.js';
2
+ /** All known transport names */
3
+ export declare const TRANSPORT_NAMES: readonly ["rest", "grpc", "graphql", "ws", "sse", "trpc", "mcp", "odata", "arrow", "jsonrpc", "nats"];
4
+ export type TransportName = typeof TRANSPORT_NAMES[number];
5
+ /**
6
+ * Load net configuration from environment variables.
7
+ *
8
+ * Convention: MOSTA_NET_{TRANSPORT}_ENABLED, MOSTA_NET_{TRANSPORT}_PORT, MOSTA_NET_{TRANSPORT}_PATH
9
+ * Example: MOSTA_NET_REST_ENABLED=true, MOSTA_NET_GRPC_PORT=50051
10
+ */
11
+ export declare function loadNetConfig(): NetServerConfig;
12
+ /**
13
+ * Get the list of enabled transport names from config.
14
+ */
15
+ export declare function getEnabledTransports(config: NetServerConfig): TransportName[];
@@ -0,0 +1,60 @@
1
+ // @mostajs/net — Configuration loader
2
+ // Reads MOSTA_NET_* from process.env (populated by .env.local)
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ /** All known transport names */
5
+ export const TRANSPORT_NAMES = [
6
+ 'rest', 'grpc', 'graphql', 'ws', 'sse',
7
+ 'trpc', 'mcp', 'odata', 'arrow', 'jsonrpc', 'nats',
8
+ ];
9
+ /** Default port for the main HTTP server */
10
+ const DEFAULT_PORT = 4488;
11
+ /** Default ports for transports that need dedicated ports */
12
+ const DEFAULT_TRANSPORT_PORTS = {
13
+ grpc: 50051,
14
+ arrow: 50052,
15
+ };
16
+ /** Default HTTP paths for transports mounted on the main server */
17
+ const DEFAULT_TRANSPORT_PATHS = {
18
+ rest: '/api/v1',
19
+ graphql: '/graphql',
20
+ ws: '/ws',
21
+ sse: '/events',
22
+ trpc: '/trpc',
23
+ mcp: '/mcp',
24
+ odata: '/odata',
25
+ jsonrpc: '/rpc',
26
+ };
27
+ /**
28
+ * Load net configuration from environment variables.
29
+ *
30
+ * Convention: MOSTA_NET_{TRANSPORT}_ENABLED, MOSTA_NET_{TRANSPORT}_PORT, MOSTA_NET_{TRANSPORT}_PATH
31
+ * Example: MOSTA_NET_REST_ENABLED=true, MOSTA_NET_GRPC_PORT=50051
32
+ */
33
+ export function loadNetConfig() {
34
+ const port = parseInt(process.env.MOSTA_NET_PORT || '', 10) || DEFAULT_PORT;
35
+ const transports = {};
36
+ for (const name of TRANSPORT_NAMES) {
37
+ const upper = name.toUpperCase();
38
+ const enabled = process.env[`MOSTA_NET_${upper}_ENABLED`] === 'true';
39
+ transports[name] = {
40
+ enabled,
41
+ port: parseInt(process.env[`MOSTA_NET_${upper}_PORT`] || '', 10) || DEFAULT_TRANSPORT_PORTS[name],
42
+ path: process.env[`MOSTA_NET_${upper}_PATH`] || DEFAULT_TRANSPORT_PATHS[name],
43
+ options: {},
44
+ };
45
+ // Transport-specific options
46
+ if (name === 'mcp') {
47
+ transports[name].options.mode = process.env.MOSTA_NET_MCP_MODE || 'http';
48
+ }
49
+ if (name === 'nats') {
50
+ transports[name].options.url = process.env.MOSTA_NET_NATS_URL || 'nats://localhost:4222';
51
+ }
52
+ }
53
+ return { port, transports };
54
+ }
55
+ /**
56
+ * Get the list of enabled transport names from config.
57
+ */
58
+ export function getEnabledTransports(config) {
59
+ return TRANSPORT_NAMES.filter(name => config.transports[name]?.enabled);
60
+ }
@@ -0,0 +1,15 @@
1
+ import type { ITransport, TransportConfig } from './types.js';
2
+ import type { TransportName } from './config.js';
3
+ /**
4
+ * Get or create a transport by name.
5
+ * Returns null if the transport is not enabled in config.
6
+ */
7
+ export declare function getTransport(name: TransportName, config: TransportConfig): Promise<ITransport | null>;
8
+ /**
9
+ * Get all active (started) transports.
10
+ */
11
+ export declare function getActiveTransports(): ITransport[];
12
+ /**
13
+ * Stop all active transports.
14
+ */
15
+ export declare function stopAllTransports(): Promise<void>;
@@ -0,0 +1,60 @@
1
+ // @mostajs/net — Transport Factory
2
+ // Lazy-loads transport adapters (like getDialect() in @mostajs/orm)
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ /**
5
+ * Dynamically load a transport module.
6
+ * Only the selected transport is loaded — no unused dependencies in memory.
7
+ * Mirror of loadDialectModule() in @mostajs/orm.
8
+ */
9
+ async function loadTransportModule(name) {
10
+ switch (name) {
11
+ case 'rest': return import('../transports/rest.transport.js');
12
+ case 'sse': return import('../transports/sse.transport.js');
13
+ case 'graphql': return import('../transports/graphql.transport.js');
14
+ case 'ws': return import('../transports/ws.transport.js');
15
+ case 'jsonrpc': return import('../transports/jsonrpc.transport.js');
16
+ case 'mcp': return import('../transports/mcp.transport.js');
17
+ // Future transports:
18
+ // case 'sse': return import('../transports/sse.transport.js');
19
+ // case 'trpc': return import('../transports/trpc.transport.js');
20
+ // case 'mcp': return import('../transports/mcp.transport.js');
21
+ // case 'odata': return import('../transports/odata.transport.js');
22
+ // case 'arrow': return import('../transports/arrow.transport.js');
23
+ // case 'jsonrpc': return import('../transports/jsonrpc.transport.js');
24
+ // case 'nats': return import('../transports/nats.transport.js');
25
+ default:
26
+ throw new Error(`Transport "${name}" is not yet implemented`);
27
+ }
28
+ }
29
+ /** Registry of active transport instances */
30
+ const activeTransports = new Map();
31
+ /**
32
+ * Get or create a transport by name.
33
+ * Returns null if the transport is not enabled in config.
34
+ */
35
+ export async function getTransport(name, config) {
36
+ if (!config.enabled)
37
+ return null;
38
+ if (activeTransports.has(name)) {
39
+ return activeTransports.get(name);
40
+ }
41
+ const mod = await loadTransportModule(name);
42
+ const transport = mod.createTransport();
43
+ activeTransports.set(name, transport);
44
+ return transport;
45
+ }
46
+ /**
47
+ * Get all active (started) transports.
48
+ */
49
+ export function getActiveTransports() {
50
+ return Array.from(activeTransports.values());
51
+ }
52
+ /**
53
+ * Stop all active transports.
54
+ */
55
+ export async function stopAllTransports() {
56
+ for (const transport of activeTransports.values()) {
57
+ await transport.stop();
58
+ }
59
+ activeTransports.clear();
60
+ }
@@ -0,0 +1,12 @@
1
+ import type { OrmRequest, OrmResponse } from '@mostajs/orm';
2
+ import type { TransportMiddleware, TransportContext } from './types.js';
3
+ /**
4
+ * Compose an array of middlewares into a single execution chain.
5
+ * Each middleware calls next() to pass to the next one.
6
+ * The last function in the chain executes the actual ORM operation.
7
+ */
8
+ export declare function composeMiddleware(middlewares: TransportMiddleware[], handler: (req: OrmRequest, ctx: TransportContext) => Promise<OrmResponse>): (req: OrmRequest, ctx: TransportContext) => Promise<OrmResponse>;
9
+ /**
10
+ * Logging middleware — logs every request/response.
11
+ */
12
+ export declare const loggingMiddleware: TransportMiddleware;
@@ -0,0 +1,32 @@
1
+ // @mostajs/net — Middleware pipeline
2
+ // Composes middleware functions (auth, rate-limit, logging) into a chain
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ /**
5
+ * Compose an array of middlewares into a single execution chain.
6
+ * Each middleware calls next() to pass to the next one.
7
+ * The last function in the chain executes the actual ORM operation.
8
+ */
9
+ export function composeMiddleware(middlewares, handler) {
10
+ return async (req, ctx) => {
11
+ let index = 0;
12
+ const next = async () => {
13
+ if (index < middlewares.length) {
14
+ const mw = middlewares[index++];
15
+ return mw(req, ctx, next);
16
+ }
17
+ return handler(req, ctx);
18
+ };
19
+ return next();
20
+ };
21
+ }
22
+ /**
23
+ * Logging middleware — logs every request/response.
24
+ */
25
+ export const loggingMiddleware = async (req, ctx, next) => {
26
+ const start = Date.now();
27
+ const res = await next();
28
+ const ms = Date.now() - start;
29
+ const status = res.status === 'ok' ? '\x1b[32mOK\x1b[0m' : '\x1b[31mERR\x1b[0m';
30
+ console.log(`[${ctx.transport}] ${req.op} ${req.entity}${req.id ? '/' + req.id : ''} → ${status} (${ms}ms)`);
31
+ return res;
32
+ };
@@ -0,0 +1,62 @@
1
+ import type { EntitySchema, OrmRequest, OrmResponse } from '@mostajs/orm';
2
+ export interface TransportConfig {
3
+ /** Is this transport enabled? */
4
+ enabled: boolean;
5
+ /** Dedicated port (for transports that need their own port, e.g. gRPC) */
6
+ port?: number;
7
+ /** HTTP path prefix (for transports mounted on the main HTTP server) */
8
+ path?: string;
9
+ /** Transport-specific options */
10
+ options?: Record<string, unknown>;
11
+ }
12
+ export interface TransportInfo {
13
+ /** Transport identifier (e.g. 'rest', 'grpc', 'graphql') */
14
+ name: string;
15
+ /** Current status */
16
+ status: 'running' | 'stopped' | 'error';
17
+ /** Listening URL (e.g. "http://localhost:4488/api/v1") */
18
+ url?: string;
19
+ /** Dedicated port (e.g. 50051 for gRPC) */
20
+ port?: number;
21
+ /** Registered entity names */
22
+ entities: string[];
23
+ /** Runtime stats */
24
+ stats: {
25
+ requests: number;
26
+ errors: number;
27
+ startedAt: number;
28
+ };
29
+ }
30
+ export interface TransportContext {
31
+ /** Transport name that received the request */
32
+ transport: string;
33
+ /** API key (if present in the request) */
34
+ apiKey?: string;
35
+ /** Resolved subscription name (after API key validation) */
36
+ subscription?: string;
37
+ /** Resolved permissions for this request */
38
+ permissions?: Record<string, string>;
39
+ /** Extra metadata per transport */
40
+ meta?: Record<string, unknown>;
41
+ }
42
+ export type TransportMiddleware = (req: OrmRequest, ctx: TransportContext, next: () => Promise<OrmResponse>) => Promise<OrmResponse>;
43
+ export interface ITransport {
44
+ /** Transport identifier (e.g. 'rest', 'grpc', 'graphql') */
45
+ readonly name: string;
46
+ /** Initialize and start the transport */
47
+ start(config: TransportConfig): Promise<void>;
48
+ /** Stop the transport gracefully */
49
+ stop(): Promise<void>;
50
+ /** Register an entity schema — generates endpoints for this entity */
51
+ registerEntity(schema: EntitySchema): void;
52
+ /** Add a middleware to the transport pipeline */
53
+ use(middleware: TransportMiddleware): void;
54
+ /** Get runtime info (for admin dashboard) */
55
+ getInfo(): TransportInfo;
56
+ }
57
+ export interface NetServerConfig {
58
+ /** Main HTTP port (shared by REST, GraphQL, SSE, etc.) */
59
+ port: number;
60
+ /** Transport configurations keyed by transport name */
61
+ transports: Record<string, TransportConfig>;
62
+ }
@@ -0,0 +1,4 @@
1
+ // @mostajs/net — Core Types
2
+ // ITransport interface (mirror of IDialect in @mostajs/orm)
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ export {};
@@ -0,0 +1,16 @@
1
+ export type { ITransport, TransportConfig, TransportInfo, TransportContext, TransportMiddleware, NetServerConfig, } from './core/types.js';
2
+ export { loadNetConfig, getEnabledTransports, TRANSPORT_NAMES } from './core/config.js';
3
+ export type { TransportName } from './core/config.js';
4
+ export { getTransport, getActiveTransports, stopAllTransports } from './core/factory.js';
5
+ export { composeMiddleware, loggingMiddleware } from './core/middleware.js';
6
+ export { RestTransport } from './transports/rest.transport.js';
7
+ export { SSETransport } from './transports/sse.transport.js';
8
+ export { GraphQLTransport } from './transports/graphql.transport.js';
9
+ export { WebSocketTransport } from './transports/ws.transport.js';
10
+ export { JsonRpcTransport } from './transports/jsonrpc.transport.js';
11
+ export { McpTransport } from './transports/mcp.transport.js';
12
+ export { readApiKeys, writeApiKeys, generateApiKey, hashApiKey, createSubscription, revokeSubscription, validateApiKey, checkPermission, } from './auth/apikeys.js';
13
+ export type { Subscription, ApiKeysFile } from './auth/apikeys.js';
14
+ export { apiKeyMiddleware } from './auth/apikey-middleware.js';
15
+ export { startServer } from './server.js';
16
+ export type { NetServer } from './server.js';