@olane/o-server 0.8.0 → 0.8.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.
Files changed (40) hide show
  1. package/dist/src/interfaces/server-config.interface.d.ts +47 -1
  2. package/dist/src/interfaces/server-config.interface.d.ts.map +1 -1
  3. package/dist/src/middleware/error-handler.d.ts +9 -0
  4. package/dist/src/middleware/error-handler.d.ts.map +1 -1
  5. package/dist/src/middleware/error-handler.js +49 -3
  6. package/dist/src/middleware/jwt-auth.d.ts +54 -0
  7. package/dist/src/middleware/jwt-auth.d.ts.map +1 -0
  8. package/dist/src/middleware/jwt-auth.js +145 -0
  9. package/dist/src/o-server.d.ts.map +1 -1
  10. package/dist/src/o-server.js +53 -23
  11. package/dist/src/validation/address-validator.d.ts +13 -0
  12. package/dist/src/validation/address-validator.d.ts.map +1 -0
  13. package/dist/src/validation/address-validator.js +40 -0
  14. package/dist/src/validation/index.d.ts +17 -0
  15. package/dist/src/validation/index.d.ts.map +1 -0
  16. package/dist/src/validation/index.js +16 -0
  17. package/dist/src/validation/method-validator.d.ts +13 -0
  18. package/dist/src/validation/method-validator.d.ts.map +1 -0
  19. package/dist/src/validation/method-validator.js +36 -0
  20. package/dist/src/validation/params-sanitizer.d.ts +15 -0
  21. package/dist/src/validation/params-sanitizer.d.ts.map +1 -0
  22. package/dist/src/validation/params-sanitizer.js +51 -0
  23. package/dist/src/validation/request-validator.d.ts +53 -0
  24. package/dist/src/validation/request-validator.d.ts.map +1 -0
  25. package/dist/src/validation/request-validator.js +53 -0
  26. package/dist/src/validation/validation-error.d.ts +11 -0
  27. package/dist/src/validation/validation-error.d.ts.map +1 -0
  28. package/dist/src/validation/validation-error.js +12 -0
  29. package/dist/test/ai.spec.d.ts +0 -1
  30. package/dist/test/ai.spec.js +20 -13
  31. package/dist/test/error-security.spec.d.ts +2 -0
  32. package/dist/test/error-security.spec.d.ts.map +1 -0
  33. package/dist/test/error-security.spec.js +134 -0
  34. package/dist/test/input-validation.spec.d.ts +14 -0
  35. package/dist/test/input-validation.spec.d.ts.map +1 -0
  36. package/dist/test/input-validation.spec.js +487 -0
  37. package/dist/test/jwt-auth.spec.d.ts +2 -0
  38. package/dist/test/jwt-auth.spec.d.ts.map +1 -0
  39. package/dist/test/jwt-auth.spec.js +397 -0
  40. package/package.json +10 -4
@@ -1,11 +1,31 @@
1
1
  import { oCore } from '@olane/o-core';
2
2
  import { Request } from 'express';
3
3
  import { CorsOptions } from 'cors';
4
+ import { Algorithm } from 'jsonwebtoken';
4
5
  export interface AuthUser {
5
6
  userId?: string;
6
7
  [key: string]: any;
7
8
  }
8
9
  export type AuthenticateFunction = (req: Request) => Promise<AuthUser>;
10
+ /**
11
+ * JWT authentication configuration
12
+ */
13
+ export interface JwtAuthConfig {
14
+ /** JWT verification method: 'publicKey' (RS256) or 'secret' (HS256) */
15
+ method: 'publicKey' | 'secret';
16
+ /** Secret key for HS256 verification (required if method='secret') */
17
+ secret?: string;
18
+ /** Path to public key PEM file for RS256 verification (required if method='publicKey') */
19
+ publicKeyPath?: string;
20
+ /** Expected issuer (iss claim) - optional */
21
+ issuer?: string;
22
+ /** Expected audience (aud claim) - optional */
23
+ audience?: string;
24
+ /** Allowed algorithms - defaults to ['RS256'] for publicKey, ['HS256'] for secret */
25
+ algorithms?: Algorithm[];
26
+ /** Clock tolerance in seconds for exp/nbf validation - default 0 */
27
+ clockTolerance?: number;
28
+ }
9
29
  export interface ServerConfig {
10
30
  /** Node instance (any oCore-based node with 'use' method) */
11
31
  node: oCore;
@@ -15,8 +35,34 @@ export interface ServerConfig {
15
35
  basePath?: string;
16
36
  /** CORS configuration */
17
37
  cors?: CorsOptions;
18
- /** Authentication middleware */
38
+ /**
39
+ * @deprecated Use jwtAuth instead. Authentication middleware will be removed in future versions.
40
+ * JWT authentication is now mandatory (except for /health endpoint).
41
+ */
19
42
  authenticate?: AuthenticateFunction;
43
+ /**
44
+ * JWT authentication configuration.
45
+ * When provided, JWT verification will be enforced on all routes except /health.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * // RS256 with public key
50
+ * jwtAuth: {
51
+ * method: 'publicKey',
52
+ * publicKeyPath: '/path/to/public-key.pem',
53
+ * issuer: 'https://auth.example.com',
54
+ * audience: 'https://api.example.com'
55
+ * }
56
+ *
57
+ * // HS256 with secret
58
+ * jwtAuth: {
59
+ * method: 'secret',
60
+ * secret: 'your-secret-key',
61
+ * clockTolerance: 5
62
+ * }
63
+ * ```
64
+ */
65
+ jwtAuth?: JwtAuthConfig;
20
66
  /** Enable debug logging */
21
67
  debug?: boolean;
22
68
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.interface.d.ts","sourceRoot":"","sources":["../../../src/interfaces/server-config.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AAEnC,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvE,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,IAAI,EAAE,KAAK,CAAC;IAEZ,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,yBAAyB;IACzB,IAAI,CAAC,EAAE,WAAW,CAAC;IAEnB,gCAAgC;IAChC,YAAY,CAAC,EAAE,oBAAoB,CAAC;IAEpC,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,GAAG,EAAE,GAAG,CAAC;IAET,uBAAuB;IACvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB,sBAAsB;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB"}
1
+ {"version":3,"file":"server-config.interface.d.ts","sourceRoot":"","sources":["../../../src/interfaces/server-config.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,MAAM,EAAE,WAAW,GAAG,QAAQ,CAAC;IAE/B,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,0FAA0F;IAC1F,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,qFAAqF;IACrF,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IAEzB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,IAAI,EAAE,KAAK,CAAC;IAEZ,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,yBAAyB;IACzB,IAAI,CAAC,EAAE,WAAW,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,oBAAoB,CAAC;IAEpC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,GAAG,EAAE,GAAG,CAAC;IAET,uBAAuB;IACvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB,sBAAsB;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB"}
@@ -4,5 +4,14 @@ export interface OlaneError extends Error {
4
4
  status?: number;
5
5
  details?: any;
6
6
  }
7
+ /**
8
+ * Sanitizes error message for production use
9
+ * Removes sensitive information and returns a generic message
10
+ */
11
+ export declare function sanitizeErrorMessage(code: string, originalMessage: string): string;
12
+ /**
13
+ * Global error handler middleware
14
+ * Sanitizes errors for production and provides detailed errors in development
15
+ */
7
16
  export declare function errorHandler(err: OlaneError, req: Request, res: Response, next: NextFunction): void;
8
17
  //# sourceMappingURL=error-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../../../src/middleware/error-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,MAAM,WAAW,UAAW,SAAQ,KAAK;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,GAAG,CAAC;CACf;AAED,wBAAgB,YAAY,CAC1B,GAAG,EAAE,UAAU,EACf,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,QAenB"}
1
+ {"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../../../src/middleware/error-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,MAAM,WAAW,UAAW,SAAQ,KAAK;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,GAAG,CAAC;CACf;AA0BD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAQlF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,UAAU,EACf,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,QAuBnB"}
@@ -1,11 +1,57 @@
1
+ /**
2
+ * Map of error codes to production-safe error messages
3
+ * These messages are generic and don't leak sensitive information
4
+ */
5
+ const PRODUCTION_ERROR_MESSAGES = {
6
+ NODE_NOT_FOUND: 'The requested resource was not found',
7
+ TOOL_NOT_FOUND: 'The requested tool was not found',
8
+ INVALID_PARAMS: 'Invalid parameters provided',
9
+ TIMEOUT: 'The request timed out',
10
+ EXECUTION_ERROR: 'An error occurred while processing your request',
11
+ INTERNAL_ERROR: 'An internal error occurred',
12
+ UNAUTHORIZED: 'Unauthorized access',
13
+ FORBIDDEN: 'Access forbidden',
14
+ // JWT-specific error codes
15
+ MISSING_TOKEN: 'Authentication required',
16
+ INVALID_TOKEN_FORMAT: 'Invalid authentication format',
17
+ TOKEN_EXPIRED: 'Authentication token has expired',
18
+ TOKEN_NOT_ACTIVE: 'Authentication token not yet valid',
19
+ INVALID_TOKEN: 'Invalid authentication token',
20
+ // Input validation error codes
21
+ INVALID_ADDRESS: 'Invalid address format',
22
+ INVALID_METHOD: 'Invalid method name',
23
+ };
24
+ /**
25
+ * Sanitizes error message for production use
26
+ * Removes sensitive information and returns a generic message
27
+ */
28
+ export function sanitizeErrorMessage(code, originalMessage) {
29
+ // In production, use generic messages from the map
30
+ if (process.env.NODE_ENV !== 'development') {
31
+ return PRODUCTION_ERROR_MESSAGES[code] || PRODUCTION_ERROR_MESSAGES.INTERNAL_ERROR;
32
+ }
33
+ // In development, return the original message
34
+ return originalMessage;
35
+ }
36
+ /**
37
+ * Global error handler middleware
38
+ * Sanitizes errors for production and provides detailed errors in development
39
+ */
1
40
  export function errorHandler(err, req, res, next) {
2
- console.error('[o-server] Error:', err);
41
+ // Log the full error server-side (never sent to client)
42
+ console.error('[o-server] Error:', {
43
+ message: err.message,
44
+ code: err.code,
45
+ stack: err.stack,
46
+ details: err.details,
47
+ });
3
48
  const status = err.status || 500;
49
+ const errorCode = err.code || 'INTERNAL_ERROR';
4
50
  const errorResponse = {
5
51
  success: false,
6
52
  error: {
7
- code: err.code || 'INTERNAL_ERROR',
8
- message: err.message || 'An internal error occurred',
53
+ code: errorCode,
54
+ message: sanitizeErrorMessage(errorCode, err.message || 'An internal error occurred'),
9
55
  details: process.env.NODE_ENV === 'development' ? err.details : undefined,
10
56
  },
11
57
  };
@@ -0,0 +1,54 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import jwt from 'jsonwebtoken';
3
+ /**
4
+ * JWT verification configuration
5
+ */
6
+ export interface JwtConfig {
7
+ /** JWT verification method: 'publicKey' (RS256) or 'secret' (HS256) */
8
+ method: 'publicKey' | 'secret';
9
+ /** Secret key for HS256 verification (required if method='secret') */
10
+ secret?: string;
11
+ /** Path to public key PEM file for RS256 verification (required if method='publicKey') */
12
+ publicKeyPath?: string;
13
+ /** Expected issuer (iss claim) - optional */
14
+ issuer?: string;
15
+ /** Expected audience (aud claim) - optional */
16
+ audience?: string;
17
+ /** Allowed algorithms - defaults to ['RS256'] for publicKey, ['HS256'] for secret */
18
+ algorithms?: jwt.Algorithm[];
19
+ /** Clock tolerance in seconds for exp/nbf validation - default 0 */
20
+ clockTolerance?: number;
21
+ }
22
+ /**
23
+ * JWT token payload added to Express request
24
+ */
25
+ export interface JwtPayload {
26
+ sub?: string;
27
+ iss?: string;
28
+ aud?: string | string[];
29
+ exp?: number;
30
+ nbf?: number;
31
+ iat?: number;
32
+ [key: string]: any;
33
+ }
34
+ declare global {
35
+ namespace Express {
36
+ interface Request {
37
+ jwt?: JwtPayload;
38
+ }
39
+ }
40
+ }
41
+ /**
42
+ * Creates JWT authentication middleware
43
+ *
44
+ * @param config - JWT configuration
45
+ * @returns Express middleware function
46
+ *
47
+ * @throws {OlaneError} MISSING_TOKEN - No Authorization header present
48
+ * @throws {OlaneError} INVALID_TOKEN_FORMAT - Malformed token (not "Bearer <token>")
49
+ * @throws {OlaneError} TOKEN_EXPIRED - Token exp claim in the past
50
+ * @throws {OlaneError} TOKEN_NOT_ACTIVE - Token nbf claim in the future
51
+ * @throws {OlaneError} INVALID_TOKEN - Signature verification failed or invalid structure
52
+ */
53
+ export declare function createJwtMiddleware(config: JwtConfig): (req: Request, res: Response, next: NextFunction) => Promise<void>;
54
+ //# sourceMappingURL=jwt-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt-auth.d.ts","sourceRoot":"","sources":["../../../src/middleware/jwt-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,GAAG,MAAM,cAAc,CAAC;AAI/B;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,uEAAuE;IACvE,MAAM,EAAE,WAAW,GAAG,QAAQ,CAAC;IAE/B,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,0FAA0F;IAC1F,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,qFAAqF;IACrF,UAAU,CAAC,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC;IAE7B,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAGD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,GAAG,CAAC,EAAE,UAAU,CAAC;SAClB;KACF;CACF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,SA4ChC,OAAO,OAAO,QAAQ,QAAQ,YAAY,mBAyG9D"}
@@ -0,0 +1,145 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import { readFileSync } from 'fs';
3
+ /**
4
+ * Creates JWT authentication middleware
5
+ *
6
+ * @param config - JWT configuration
7
+ * @returns Express middleware function
8
+ *
9
+ * @throws {OlaneError} MISSING_TOKEN - No Authorization header present
10
+ * @throws {OlaneError} INVALID_TOKEN_FORMAT - Malformed token (not "Bearer <token>")
11
+ * @throws {OlaneError} TOKEN_EXPIRED - Token exp claim in the past
12
+ * @throws {OlaneError} TOKEN_NOT_ACTIVE - Token nbf claim in the future
13
+ * @throws {OlaneError} INVALID_TOKEN - Signature verification failed or invalid structure
14
+ */
15
+ export function createJwtMiddleware(config) {
16
+ // Validate configuration
17
+ if (!config.method) {
18
+ throw new Error('JWT method is required (publicKey or secret)');
19
+ }
20
+ if (config.method === 'secret' && !config.secret) {
21
+ throw new Error('JWT secret is required when method is "secret"');
22
+ }
23
+ if (config.method === 'publicKey' && !config.publicKeyPath) {
24
+ throw new Error('JWT publicKeyPath is required when method is "publicKey"');
25
+ }
26
+ // Load verification key
27
+ let verificationKey;
28
+ if (config.method === 'publicKey') {
29
+ try {
30
+ verificationKey = readFileSync(config.publicKeyPath, 'utf8');
31
+ }
32
+ catch (error) {
33
+ throw new Error(`Failed to read public key file: ${error.message}`);
34
+ }
35
+ }
36
+ else {
37
+ verificationKey = config.secret;
38
+ }
39
+ // Set default algorithms based on method
40
+ const algorithms = config.algorithms || (config.method === 'publicKey' ? ['RS256'] : ['HS256']);
41
+ // Build verify options
42
+ const verifyOptions = {
43
+ algorithms,
44
+ clockTolerance: config.clockTolerance || 0,
45
+ };
46
+ if (config.issuer) {
47
+ verifyOptions.issuer = config.issuer;
48
+ }
49
+ if (config.audience) {
50
+ verifyOptions.audience = config.audience;
51
+ }
52
+ // Return middleware function
53
+ return async (req, res, next) => {
54
+ try {
55
+ // Extract Authorization header
56
+ const authHeader = req.headers.authorization;
57
+ if (!authHeader) {
58
+ const error = new Error('No authorization token provided');
59
+ error.code = 'MISSING_TOKEN';
60
+ error.status = 401;
61
+ throw error;
62
+ }
63
+ // Validate Bearer format
64
+ const parts = authHeader.split(' ');
65
+ if (parts.length !== 2 || parts[0] !== 'Bearer') {
66
+ const error = new Error('Invalid authorization format. Expected: Bearer <token>');
67
+ error.code = 'INVALID_TOKEN_FORMAT';
68
+ error.status = 401;
69
+ throw error;
70
+ }
71
+ const token = parts[1];
72
+ // Verify token
73
+ try {
74
+ const decoded = jwt.verify(token, verificationKey, verifyOptions);
75
+ // Additional validation for standard claims
76
+ const now = Math.floor(Date.now() / 1000);
77
+ // Check expiration (if not already checked by jwt.verify)
78
+ if (decoded.exp && decoded.exp <= now - (config.clockTolerance || 0)) {
79
+ const error = new Error('Token has expired');
80
+ error.code = 'TOKEN_EXPIRED';
81
+ error.status = 401;
82
+ throw error;
83
+ }
84
+ // Check not before
85
+ if (decoded.nbf && decoded.nbf > now + (config.clockTolerance || 0)) {
86
+ const error = new Error('Token is not yet valid');
87
+ error.code = 'TOKEN_NOT_ACTIVE';
88
+ error.status = 401;
89
+ throw error;
90
+ }
91
+ // Check subject exists
92
+ if (!decoded.sub) {
93
+ const error = new Error('Token missing required claim: sub');
94
+ error.code = 'INVALID_TOKEN';
95
+ error.status = 401;
96
+ throw error;
97
+ }
98
+ // Attach decoded payload to request
99
+ req.jwt = decoded;
100
+ // Also attach to req.user for backward compatibility
101
+ if (!req.user) {
102
+ req.user = {
103
+ userId: decoded.sub,
104
+ ...decoded,
105
+ };
106
+ }
107
+ next();
108
+ }
109
+ catch (error) {
110
+ // Handle jwt.verify errors
111
+ if (error.name === 'TokenExpiredError') {
112
+ const err = new Error('Token has expired');
113
+ err.code = 'TOKEN_EXPIRED';
114
+ err.status = 401;
115
+ throw err;
116
+ }
117
+ if (error.name === 'NotBeforeError') {
118
+ const err = new Error('Token is not yet valid');
119
+ err.code = 'TOKEN_NOT_ACTIVE';
120
+ err.status = 401;
121
+ throw err;
122
+ }
123
+ if (error.name === 'JsonWebTokenError') {
124
+ const err = new Error(`Invalid token: ${error.message}`);
125
+ err.code = 'INVALID_TOKEN';
126
+ err.status = 401;
127
+ throw err;
128
+ }
129
+ // Re-throw if already an OlaneError
130
+ if (error.code) {
131
+ throw error;
132
+ }
133
+ // Generic invalid token error
134
+ const err = new Error('Token verification failed');
135
+ err.code = 'INVALID_TOKEN';
136
+ err.status = 401;
137
+ throw err;
138
+ }
139
+ }
140
+ catch (error) {
141
+ // Pass error to error handler
142
+ next(error);
143
+ }
144
+ };
145
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"o-server.d.ts","sourceRoot":"","sources":["../../src/o-server.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,YAAY,EACZ,cAAc,EACf,MAAM,yCAAyC,CAAC;AAQjD,wBAAgB,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,cAAc,CA2O5D"}
1
+ {"version":3,"file":"o-server.d.ts","sourceRoot":"","sources":["../../src/o-server.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,YAAY,EACZ,cAAc,EACf,MAAM,yCAAyC,CAAC;AAiBjD,wBAAgB,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,cAAc,CAgR5D"}
@@ -2,10 +2,12 @@ import express from 'express';
2
2
  import cors from 'cors';
3
3
  import { errorHandler } from './middleware/error-handler.js';
4
4
  import { authMiddleware } from './middleware/auth.js';
5
+ import { createJwtMiddleware } from './middleware/jwt-auth.js';
5
6
  import { ServerLogger } from './utils/logger.js';
6
7
  import { oAddress } from '@olane/o-core';
8
+ import { validateAddress, validateMethod, sanitizeParams, validateRequest, useRequestSchema, streamRequestSchema, } from './validation/index.js';
7
9
  export function oServer(config) {
8
- const { node, port = 3000, basePath = '/api/v1', cors: corsConfig, authenticate, debug = false, } = config;
10
+ const { node, port = 3000, basePath = '/api/v1', cors: corsConfig, authenticate, jwtAuth, debug = false, } = config;
9
11
  const app = express();
10
12
  const logger = new ServerLogger(debug);
11
13
  let server = null;
@@ -14,8 +16,20 @@ export function oServer(config) {
14
16
  if (corsConfig) {
15
17
  app.use(cors(corsConfig));
16
18
  }
17
- // Optional authentication
18
- if (authenticate) {
19
+ // JWT authentication (mandatory, except for health endpoint)
20
+ if (jwtAuth) {
21
+ const jwtMiddleware = createJwtMiddleware(jwtAuth);
22
+ // Apply JWT middleware to all routes except health
23
+ app.use((req, res, next) => {
24
+ if (req.path === `${basePath}/health`) {
25
+ return next(); // Skip JWT for health check
26
+ }
27
+ return jwtMiddleware(req, res, next);
28
+ });
29
+ }
30
+ else if (authenticate) {
31
+ // Deprecated: Legacy authentication support
32
+ logger.log('⚠️ WARNING: The "authenticate" parameter is deprecated. Please migrate to "jwtAuth".');
19
33
  app.use(basePath, authMiddleware(authenticate));
20
34
  }
21
35
  // Health check endpoint
@@ -32,18 +46,20 @@ export function oServer(config) {
32
46
  // This is the main entrypoint that wraps the node's 'use' method
33
47
  app.post(`${basePath}/use`, async (req, res, next) => {
34
48
  try {
35
- const { address: addressStr, method, params, id } = req.body;
36
- if (!addressStr) {
37
- const error = new Error('Address is required');
38
- error.code = 'INVALID_PARAMS';
39
- error.status = 400;
40
- throw error;
41
- }
49
+ // Validate request schema
50
+ const validated = validateRequest(req.body, useRequestSchema);
51
+ const { address: addressStr, method, params, id } = validated;
52
+ // Validate address
53
+ validateAddress(addressStr);
54
+ // Validate method
55
+ validateMethod(method);
56
+ // Sanitize params
57
+ const sanitizedParams = sanitizeParams(params);
42
58
  logger.debugLog(`Calling use with address ${addressStr}, method: ${method}`);
43
59
  const address = new oAddress(addressStr);
44
60
  const result = await node.use(address, {
45
61
  method,
46
- params,
62
+ params: sanitizedParams,
47
63
  id,
48
64
  });
49
65
  const response = {
@@ -62,11 +78,19 @@ export function oServer(config) {
62
78
  try {
63
79
  const { address: addressParam, method } = req.params;
64
80
  const params = req.body;
65
- logger.debugLog(`Calling method ${method} on ${addressParam} with params:`, params);
66
- const address = new oAddress(`o://${addressParam}`);
81
+ // Construct full address
82
+ const addressStr = `o://${addressParam}`;
83
+ // Validate address
84
+ validateAddress(addressStr);
85
+ // Validate method
86
+ validateMethod(method);
87
+ // Sanitize params
88
+ const sanitizedParams = sanitizeParams(params);
89
+ logger.debugLog(`Calling method ${method} on ${addressParam} with params:`, sanitizedParams);
90
+ const address = new oAddress(addressStr);
67
91
  const result = await node.use(address, {
68
92
  method,
69
- params,
93
+ params: sanitizedParams,
70
94
  });
71
95
  const response = {
72
96
  success: true,
@@ -81,13 +105,15 @@ export function oServer(config) {
81
105
  // Streaming endpoint - POST /api/v1/use/stream
82
106
  app.post(`${basePath}/use/stream`, async (req, res, next) => {
83
107
  try {
84
- const { address: addressStr, method, params } = req.body;
85
- if (!addressStr) {
86
- const error = new Error('Address is required');
87
- error.code = 'INVALID_PARAMS';
88
- error.status = 400;
89
- throw error;
90
- }
108
+ // Validate request schema
109
+ const validated = validateRequest(req.body, streamRequestSchema);
110
+ const { address: addressStr, method, params } = validated;
111
+ // Validate address
112
+ validateAddress(addressStr);
113
+ // Validate method
114
+ validateMethod(method);
115
+ // Sanitize params
116
+ const sanitizedParams = sanitizeParams(params);
91
117
  logger.debugLog(`Streaming use call to ${addressStr}, method: ${method}`);
92
118
  // Set headers for Server-Sent Events
93
119
  res.setHeader('Content-Type', 'text/event-stream');
@@ -99,7 +125,7 @@ export function oServer(config) {
99
125
  // For now, execute and return result
100
126
  const result = await node.use(address, {
101
127
  method,
102
- params,
128
+ params: sanitizedParams,
103
129
  });
104
130
  res.write(`data: ${JSON.stringify({
105
131
  type: 'complete',
@@ -152,7 +178,11 @@ export function oServer(config) {
152
178
  olaneError.code = 'EXECUTION_ERROR';
153
179
  olaneError.status = 500;
154
180
  }
155
- olaneError.details = error.details || error.stack;
181
+ // Only include stack traces and details in development mode
182
+ olaneError.details =
183
+ process.env.NODE_ENV === 'development'
184
+ ? error.details || error.stack
185
+ : undefined;
156
186
  next(olaneError);
157
187
  }
158
188
  // Server instance
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Validates address strings to prevent path traversal and injection attacks
3
+ *
4
+ * Security checks:
5
+ * - Path traversal prevention (../, ..\\, URL-encoded variants)
6
+ * - o:// protocol format validation
7
+ * - Control character blocking (null bytes, newlines, etc.)
8
+ *
9
+ * @param address - The address string to validate
10
+ * @throws {ValidationError} If validation fails
11
+ */
12
+ export declare function validateAddress(address: string): void;
13
+ //# sourceMappingURL=address-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address-validator.d.ts","sourceRoot":"","sources":["../../../src/validation/address-validator.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAkDrD"}
@@ -0,0 +1,40 @@
1
+ import { ValidationError } from './validation-error.js';
2
+ /**
3
+ * Validates address strings to prevent path traversal and injection attacks
4
+ *
5
+ * Security checks:
6
+ * - Path traversal prevention (../, ..\\, URL-encoded variants)
7
+ * - o:// protocol format validation
8
+ * - Control character blocking (null bytes, newlines, etc.)
9
+ *
10
+ * @param address - The address string to validate
11
+ * @throws {ValidationError} If validation fails
12
+ */
13
+ export function validateAddress(address) {
14
+ if (!address || typeof address !== 'string') {
15
+ throw new ValidationError('Invalid address: must be a non-empty string', 'INVALID_ADDRESS');
16
+ }
17
+ // Block path traversal - literal patterns
18
+ if (address.includes('../') || address.includes('..\\')) {
19
+ throw new ValidationError('Invalid address: path traversal detected', 'INVALID_ADDRESS');
20
+ }
21
+ // Block path traversal - URL-encoded variants
22
+ // %2e = '.', so %2e%2e = '..'
23
+ const lowerAddress = address.toLowerCase();
24
+ if (lowerAddress.includes('%2e%2e') || lowerAddress.includes('%252e')) {
25
+ throw new ValidationError('Invalid address: path traversal detected', 'INVALID_ADDRESS');
26
+ }
27
+ // Validate o:// format
28
+ if (!address.startsWith('o://')) {
29
+ throw new ValidationError('Invalid address: must start with o://', 'INVALID_ADDRESS');
30
+ }
31
+ // Block control characters (0x00-0x1F and 0x7F)
32
+ // These include null bytes, newlines, tabs, etc.
33
+ if (/[\x00-\x1F\x7F]/.test(address)) {
34
+ throw new ValidationError('Invalid address: control characters not allowed', 'INVALID_ADDRESS');
35
+ }
36
+ // Block other dangerous patterns
37
+ if (address.includes('\\')) {
38
+ throw new ValidationError('Invalid address: backslashes not allowed', 'INVALID_ADDRESS');
39
+ }
40
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Input Validation & Sanitization Module
3
+ *
4
+ * Provides comprehensive security validation for o-server endpoints:
5
+ * - Address validation (path traversal, format, control chars)
6
+ * - Method validation (private methods, prototype pollution)
7
+ * - Parameter sanitization (recursive dangerous property removal)
8
+ * - Request schema validation (Zod-based type safety)
9
+ *
10
+ * Part of Phase 1 Security - Wave 2 (Input Validation)
11
+ */
12
+ export { ValidationError } from './validation-error.js';
13
+ export { validateAddress } from './address-validator.js';
14
+ export { validateMethod } from './method-validator.js';
15
+ export { sanitizeParams } from './params-sanitizer.js';
16
+ export { validateRequest, useRequestSchema, convenienceRequestSchema, streamRequestSchema, } from './request-validator.js';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/validation/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Input Validation & Sanitization Module
3
+ *
4
+ * Provides comprehensive security validation for o-server endpoints:
5
+ * - Address validation (path traversal, format, control chars)
6
+ * - Method validation (private methods, prototype pollution)
7
+ * - Parameter sanitization (recursive dangerous property removal)
8
+ * - Request schema validation (Zod-based type safety)
9
+ *
10
+ * Part of Phase 1 Security - Wave 2 (Input Validation)
11
+ */
12
+ export { ValidationError } from './validation-error.js';
13
+ export { validateAddress } from './address-validator.js';
14
+ export { validateMethod } from './method-validator.js';
15
+ export { sanitizeParams } from './params-sanitizer.js';
16
+ export { validateRequest, useRequestSchema, convenienceRequestSchema, streamRequestSchema, } from './request-validator.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Validates method names to prevent prototype pollution and private method access
3
+ *
4
+ * Security checks:
5
+ * - Private method blocking (methods starting with '_')
6
+ * - Prototype pollution prevention (__proto__, constructor, prototype)
7
+ * - Case-insensitive blocking
8
+ *
9
+ * @param method - The method name to validate
10
+ * @throws {ValidationError} If validation fails
11
+ */
12
+ export declare function validateMethod(method: string): void;
13
+ //# sourceMappingURL=method-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"method-validator.d.ts","sourceRoot":"","sources":["../../../src/validation/method-validator.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAoCnD"}
@@ -0,0 +1,36 @@
1
+ import { ValidationError } from './validation-error.js';
2
+ /**
3
+ * List of method names that are blocked due to prototype pollution risks
4
+ * These are checked case-insensitively
5
+ */
6
+ const BLOCKED_METHODS = ['__proto__', 'constructor', 'prototype'];
7
+ /**
8
+ * Validates method names to prevent prototype pollution and private method access
9
+ *
10
+ * Security checks:
11
+ * - Private method blocking (methods starting with '_')
12
+ * - Prototype pollution prevention (__proto__, constructor, prototype)
13
+ * - Case-insensitive blocking
14
+ *
15
+ * @param method - The method name to validate
16
+ * @throws {ValidationError} If validation fails
17
+ */
18
+ export function validateMethod(method) {
19
+ if (!method || typeof method !== 'string') {
20
+ throw new ValidationError('Invalid method: must be a non-empty string', 'INVALID_METHOD');
21
+ }
22
+ // Block private methods (starting with underscore)
23
+ if (method.startsWith('_')) {
24
+ throw new ValidationError('Invalid method: private methods not allowed', 'INVALID_METHOD');
25
+ }
26
+ // Block prototype pollution methods (case-insensitive)
27
+ const lowerMethod = method.toLowerCase();
28
+ const isBlocked = BLOCKED_METHODS.some((blocked) => lowerMethod === blocked.toLowerCase());
29
+ if (isBlocked) {
30
+ throw new ValidationError('Invalid method: blocked method name', 'INVALID_METHOD');
31
+ }
32
+ // Block methods with control characters
33
+ if (/[\x00-\x1F\x7F]/.test(method)) {
34
+ throw new ValidationError('Invalid method: control characters not allowed', 'INVALID_METHOD');
35
+ }
36
+ }