@olane/o-server 0.7.62 → 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.
- package/dist/src/interfaces/server-config.interface.d.ts +47 -1
- package/dist/src/interfaces/server-config.interface.d.ts.map +1 -1
- package/dist/src/middleware/error-handler.d.ts +9 -0
- package/dist/src/middleware/error-handler.d.ts.map +1 -1
- package/dist/src/middleware/error-handler.js +49 -3
- package/dist/src/middleware/jwt-auth.d.ts +54 -0
- package/dist/src/middleware/jwt-auth.d.ts.map +1 -0
- package/dist/src/middleware/jwt-auth.js +145 -0
- package/dist/src/o-server.d.ts.map +1 -1
- package/dist/src/o-server.js +53 -23
- package/dist/src/validation/address-validator.d.ts +13 -0
- package/dist/src/validation/address-validator.d.ts.map +1 -0
- package/dist/src/validation/address-validator.js +40 -0
- package/dist/src/validation/index.d.ts +17 -0
- package/dist/src/validation/index.d.ts.map +1 -0
- package/dist/src/validation/index.js +16 -0
- package/dist/src/validation/method-validator.d.ts +13 -0
- package/dist/src/validation/method-validator.d.ts.map +1 -0
- package/dist/src/validation/method-validator.js +36 -0
- package/dist/src/validation/params-sanitizer.d.ts +15 -0
- package/dist/src/validation/params-sanitizer.d.ts.map +1 -0
- package/dist/src/validation/params-sanitizer.js +51 -0
- package/dist/src/validation/request-validator.d.ts +53 -0
- package/dist/src/validation/request-validator.d.ts.map +1 -0
- package/dist/src/validation/request-validator.js +53 -0
- package/dist/src/validation/validation-error.d.ts +11 -0
- package/dist/src/validation/validation-error.d.ts.map +1 -0
- package/dist/src/validation/validation-error.js +12 -0
- package/dist/test/ai.spec.d.ts +0 -1
- package/dist/test/ai.spec.js +20 -13
- package/dist/test/error-security.spec.d.ts +2 -0
- package/dist/test/error-security.spec.d.ts.map +1 -0
- package/dist/test/error-security.spec.js +134 -0
- package/dist/test/input-validation.spec.d.ts +14 -0
- package/dist/test/input-validation.spec.d.ts.map +1 -0
- package/dist/test/input-validation.spec.js +487 -0
- package/dist/test/jwt-auth.spec.d.ts +2 -0
- package/dist/test/jwt-auth.spec.d.ts.map +1 -0
- package/dist/test/jwt-auth.spec.js +397 -0
- 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
|
-
/**
|
|
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;
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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;
|
|
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"}
|
package/dist/src/o-server.js
CHANGED
|
@@ -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
|
-
//
|
|
18
|
-
if (
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
66
|
-
const
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
+
}
|