@uvrn/api 1.0.0
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/TESTING_GUIDE.md +495 -0
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +52 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/types.d.ts +17 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +6 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +10 -0
- package/dist/middleware/errorHandler.d.ts.map +1 -0
- package/dist/middleware/errorHandler.js +67 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/routes/delta.d.ts +10 -0
- package/dist/routes/delta.d.ts.map +1 -0
- package/dist/routes/delta.js +97 -0
- package/dist/routes/delta.js.map +1 -0
- package/dist/routes/health.d.ts +10 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +56 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +130 -0
- package/dist/server.js.map +1 -0
- package/dist/types/api.d.ts +32 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/api.js +6 -0
- package/dist/types/api.js.map +1 -0
- package/jest.config.js +13 -0
- package/package.json +44 -0
- package/src/config/loader.ts +60 -0
- package/src/config/types.ts +18 -0
- package/src/index.ts +13 -0
- package/src/middleware/errorHandler.ts +69 -0
- package/src/routes/delta.ts +120 -0
- package/src/routes/health.ts +65 -0
- package/src/server.ts +139 -0
- package/src/types/api.ts +35 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Error Handler
|
|
3
|
+
* Handles uncaught errors and formats error responses
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FastifyInstance, FastifyError, FastifyRequest, FastifyReply } from 'fastify';
|
|
7
|
+
import { ErrorResponse } from '../types/api';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Register global error handler
|
|
11
|
+
*/
|
|
12
|
+
export function registerErrorHandler(server: FastifyInstance): void {
|
|
13
|
+
server.setErrorHandler((error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
|
|
14
|
+
// Log the error
|
|
15
|
+
request.log.error({
|
|
16
|
+
err: error,
|
|
17
|
+
url: request.url,
|
|
18
|
+
method: request.method
|
|
19
|
+
}, 'Request error');
|
|
20
|
+
|
|
21
|
+
// Determine status code
|
|
22
|
+
const statusCode = error.statusCode || 500;
|
|
23
|
+
|
|
24
|
+
// Map error to code
|
|
25
|
+
let errorCode = 'INTERNAL_ERROR';
|
|
26
|
+
if (statusCode === 400) errorCode = 'BAD_REQUEST';
|
|
27
|
+
else if (statusCode === 404) errorCode = 'NOT_FOUND';
|
|
28
|
+
else if (statusCode === 415) errorCode = 'UNSUPPORTED_MEDIA_TYPE';
|
|
29
|
+
else if (statusCode === 429) errorCode = 'RATE_LIMIT_EXCEEDED';
|
|
30
|
+
else if (statusCode === 503) errorCode = 'SERVICE_UNAVAILABLE';
|
|
31
|
+
|
|
32
|
+
// Build error response
|
|
33
|
+
const errorResponse: ErrorResponse = {
|
|
34
|
+
error: {
|
|
35
|
+
code: errorCode,
|
|
36
|
+
message: error.message || 'An unexpected error occurred',
|
|
37
|
+
details: {
|
|
38
|
+
statusCode
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Don't expose internal error details in production
|
|
44
|
+
if (process.env.NODE_ENV !== 'production' && error.stack) {
|
|
45
|
+
errorResponse.error.details = {
|
|
46
|
+
...errorResponse.error.details,
|
|
47
|
+
stack: error.stack
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
reply.code(statusCode).send(errorResponse);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Handle 404 errors
|
|
55
|
+
server.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => {
|
|
56
|
+
const errorResponse: ErrorResponse = {
|
|
57
|
+
error: {
|
|
58
|
+
code: 'NOT_FOUND',
|
|
59
|
+
message: `Route ${request.method} ${request.url} not found`,
|
|
60
|
+
details: {
|
|
61
|
+
method: request.method,
|
|
62
|
+
url: request.url
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
reply.code(404).send(errorResponse);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delta Engine API Routes
|
|
3
|
+
* Endpoints for bundle processing, validation, and verification
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
7
|
+
import {
|
|
8
|
+
DeltaBundle,
|
|
9
|
+
DeltaReceipt,
|
|
10
|
+
runDeltaEngine,
|
|
11
|
+
validateBundle,
|
|
12
|
+
verifyReceipt
|
|
13
|
+
} from '@uvrn/core';
|
|
14
|
+
import { ErrorResponse, ValidationResponse } from '../types/api';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register delta engine routes
|
|
18
|
+
*/
|
|
19
|
+
export async function registerDeltaRoutes(server: FastifyInstance): Promise<void> {
|
|
20
|
+
// POST /api/v1/delta/run - Execute engine on bundle
|
|
21
|
+
server.post<{
|
|
22
|
+
Body: DeltaBundle;
|
|
23
|
+
Reply: DeltaReceipt | ErrorResponse;
|
|
24
|
+
}>('/api/v1/delta/run', async (request: FastifyRequest<{ Body: DeltaBundle }>, reply: FastifyReply) => {
|
|
25
|
+
try {
|
|
26
|
+
const bundle = request.body;
|
|
27
|
+
|
|
28
|
+
// Validate bundle schema
|
|
29
|
+
const validation = validateBundle(bundle);
|
|
30
|
+
if (!validation.valid) {
|
|
31
|
+
return reply.code(400).send({
|
|
32
|
+
error: {
|
|
33
|
+
code: 'INVALID_BUNDLE',
|
|
34
|
+
message: 'Bundle validation failed',
|
|
35
|
+
details: { validationError: validation.error }
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Execute engine
|
|
41
|
+
const receipt = runDeltaEngine(bundle);
|
|
42
|
+
|
|
43
|
+
return reply.code(200).send(receipt);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
46
|
+
request.log.error({ error: errorMessage }, 'Engine execution failed');
|
|
47
|
+
|
|
48
|
+
return reply.code(500).send({
|
|
49
|
+
error: {
|
|
50
|
+
code: 'ENGINE_ERROR',
|
|
51
|
+
message: 'Failed to process bundle',
|
|
52
|
+
details: { error: errorMessage }
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// POST /api/v1/delta/validate - Validate bundle schema
|
|
59
|
+
server.post<{
|
|
60
|
+
Body: DeltaBundle;
|
|
61
|
+
Reply: ValidationResponse | ErrorResponse;
|
|
62
|
+
}>('/api/v1/delta/validate', async (request: FastifyRequest<{ Body: DeltaBundle }>, reply: FastifyReply) => {
|
|
63
|
+
try {
|
|
64
|
+
const bundle = request.body;
|
|
65
|
+
const validation = validateBundle(bundle);
|
|
66
|
+
|
|
67
|
+
if (!validation.valid) {
|
|
68
|
+
return reply.code(200).send({
|
|
69
|
+
valid: false,
|
|
70
|
+
errors: [
|
|
71
|
+
{
|
|
72
|
+
field: 'bundle',
|
|
73
|
+
message: validation.error || 'Validation failed'
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return reply.code(200).send({ valid: true });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
82
|
+
request.log.error({ error: errorMessage }, 'Validation failed');
|
|
83
|
+
|
|
84
|
+
return reply.code(500).send({
|
|
85
|
+
error: {
|
|
86
|
+
code: 'VALIDATION_ERROR',
|
|
87
|
+
message: 'Failed to validate bundle',
|
|
88
|
+
details: { error: errorMessage }
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// POST /api/v1/delta/verify - Verify receipt replay
|
|
95
|
+
server.post<{
|
|
96
|
+
Body: DeltaReceipt;
|
|
97
|
+
Reply: { verified: boolean; recomputedHash?: string } | ErrorResponse;
|
|
98
|
+
}>('/api/v1/delta/verify', async (request: FastifyRequest<{ Body: DeltaReceipt }>, reply: FastifyReply) => {
|
|
99
|
+
try {
|
|
100
|
+
const receipt = request.body;
|
|
101
|
+
const verifyResult = verifyReceipt(receipt);
|
|
102
|
+
|
|
103
|
+
return reply.code(200).send({
|
|
104
|
+
verified: verifyResult.verified,
|
|
105
|
+
recomputedHash: verifyResult.recomputedHash
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
109
|
+
request.log.error({ error: errorMessage }, 'Verification failed');
|
|
110
|
+
|
|
111
|
+
return reply.code(500).send({
|
|
112
|
+
error: {
|
|
113
|
+
code: 'VERIFICATION_ERROR',
|
|
114
|
+
message: 'Failed to verify receipt',
|
|
115
|
+
details: { error: errorMessage }
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health and Version Routes
|
|
3
|
+
* System status and version information endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
7
|
+
import { HealthResponse, VersionResponse } from '../types/api';
|
|
8
|
+
|
|
9
|
+
const SERVER_START_TIME = Date.now();
|
|
10
|
+
const API_VERSION = '1.0.0';
|
|
11
|
+
const PROTOCOL_VERSION = '1.0';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register health and version routes
|
|
15
|
+
*/
|
|
16
|
+
export async function registerHealthRoutes(server: FastifyInstance): Promise<void> {
|
|
17
|
+
// GET /api/v1/health - Health check endpoint
|
|
18
|
+
server.get<{
|
|
19
|
+
Reply: HealthResponse;
|
|
20
|
+
}>('/api/v1/health', async (_request: FastifyRequest, reply: FastifyReply) => {
|
|
21
|
+
const uptime = Date.now() - SERVER_START_TIME;
|
|
22
|
+
|
|
23
|
+
// Check if engine is available by attempting to import it
|
|
24
|
+
let engineAvailable = true;
|
|
25
|
+
try {
|
|
26
|
+
require('@uvrn/core');
|
|
27
|
+
} catch {
|
|
28
|
+
engineAvailable = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const health: HealthResponse = {
|
|
32
|
+
status: engineAvailable ? 'healthy' : 'unhealthy',
|
|
33
|
+
uptime,
|
|
34
|
+
version: API_VERSION,
|
|
35
|
+
engine: {
|
|
36
|
+
available: engineAvailable
|
|
37
|
+
},
|
|
38
|
+
timestamp: new Date().toISOString()
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const statusCode = engineAvailable ? 200 : 503;
|
|
42
|
+
return reply.code(statusCode).send(health);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// GET /api/v1/version - Version information
|
|
46
|
+
server.get<{
|
|
47
|
+
Reply: VersionResponse;
|
|
48
|
+
}>('/api/v1/version', async (_request: FastifyRequest, reply: FastifyReply) => {
|
|
49
|
+
let engineVersion = 'unknown';
|
|
50
|
+
try {
|
|
51
|
+
const enginePkg = require('@uvrn/core/package.json');
|
|
52
|
+
engineVersion = enginePkg.version;
|
|
53
|
+
} catch {
|
|
54
|
+
// Engine version not available
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const version: VersionResponse = {
|
|
58
|
+
apiVersion: API_VERSION,
|
|
59
|
+
engineVersion,
|
|
60
|
+
protocolVersion: PROTOCOL_VERSION
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return reply.code(200).send(version);
|
|
64
|
+
});
|
|
65
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delta Engine API Server
|
|
3
|
+
* Fastify-based REST API for bundle processing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Fastify, { FastifyInstance } from 'fastify';
|
|
7
|
+
import cors from '@fastify/cors';
|
|
8
|
+
import helmet from '@fastify/helmet';
|
|
9
|
+
import rateLimit from '@fastify/rate-limit';
|
|
10
|
+
import { loadConfig } from './config/loader';
|
|
11
|
+
import { ServerConfig } from './config/types';
|
|
12
|
+
import { registerDeltaRoutes } from './routes/delta';
|
|
13
|
+
import { registerHealthRoutes } from './routes/health';
|
|
14
|
+
import { registerErrorHandler } from './middleware/errorHandler';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create and configure Fastify server instance
|
|
18
|
+
*/
|
|
19
|
+
export async function createServer(config?: ServerConfig): Promise<FastifyInstance> {
|
|
20
|
+
// Load configuration
|
|
21
|
+
const serverConfig = config || loadConfig();
|
|
22
|
+
|
|
23
|
+
// Create Fastify instance with logging
|
|
24
|
+
const server = Fastify({
|
|
25
|
+
logger: {
|
|
26
|
+
level: serverConfig.logLevel,
|
|
27
|
+
transport: serverConfig.nodeEnv === 'development' ? {
|
|
28
|
+
target: 'pino-pretty',
|
|
29
|
+
options: {
|
|
30
|
+
translateTime: 'HH:MM:ss Z',
|
|
31
|
+
ignore: 'pid,hostname'
|
|
32
|
+
}
|
|
33
|
+
} : undefined
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Register plugins
|
|
38
|
+
await server.register(helmet, {
|
|
39
|
+
contentSecurityPolicy: serverConfig.nodeEnv === 'production' ? undefined : false
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await server.register(cors, {
|
|
43
|
+
origin: serverConfig.corsOrigins,
|
|
44
|
+
credentials: true
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await server.register(rateLimit, {
|
|
48
|
+
max: serverConfig.rateLimitMax,
|
|
49
|
+
timeWindow: serverConfig.rateLimitTimeWindow,
|
|
50
|
+
errorResponseBuilder: () => ({
|
|
51
|
+
error: {
|
|
52
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
53
|
+
message: 'Too many requests, please try again later',
|
|
54
|
+
details: {
|
|
55
|
+
rateLimitMax: serverConfig.rateLimitMax,
|
|
56
|
+
timeWindow: serverConfig.rateLimitTimeWindow
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Add request logging hook
|
|
63
|
+
server.addHook('onRequest', async (request, _reply) => {
|
|
64
|
+
request.log.info({
|
|
65
|
+
url: request.url,
|
|
66
|
+
method: request.method,
|
|
67
|
+
ip: request.ip
|
|
68
|
+
}, 'Incoming request');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
server.addHook('onResponse', async (request, reply) => {
|
|
72
|
+
request.log.info({
|
|
73
|
+
url: request.url,
|
|
74
|
+
method: request.method,
|
|
75
|
+
statusCode: reply.statusCode,
|
|
76
|
+
responseTime: reply.elapsedTime
|
|
77
|
+
}, 'Request completed');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Validate content-type for POST requests
|
|
81
|
+
server.addHook('preHandler', async (request, reply) => {
|
|
82
|
+
if (request.method === 'POST' && request.url.startsWith('/api/v1/delta/')) {
|
|
83
|
+
const contentType = request.headers['content-type'];
|
|
84
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
85
|
+
return reply.code(415).send({
|
|
86
|
+
error: {
|
|
87
|
+
code: 'UNSUPPORTED_MEDIA_TYPE',
|
|
88
|
+
message: 'Content-Type must be application/json',
|
|
89
|
+
details: {
|
|
90
|
+
receivedContentType: contentType || 'none'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Register routes
|
|
99
|
+
await registerHealthRoutes(server);
|
|
100
|
+
await registerDeltaRoutes(server);
|
|
101
|
+
|
|
102
|
+
// Register error handler (must be last)
|
|
103
|
+
registerErrorHandler(server);
|
|
104
|
+
|
|
105
|
+
return server;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Start the server
|
|
110
|
+
*/
|
|
111
|
+
export async function startServer(config?: ServerConfig): Promise<FastifyInstance> {
|
|
112
|
+
const serverConfig = config || loadConfig();
|
|
113
|
+
const server = await createServer(serverConfig);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await server.listen({
|
|
117
|
+
port: serverConfig.port,
|
|
118
|
+
host: serverConfig.host
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
server.log.info(`🚀 Delta Engine API server running at http://${serverConfig.host}:${serverConfig.port}`);
|
|
122
|
+
server.log.info(`📊 Health check: http://${serverConfig.host}:${serverConfig.port}/api/v1/health`);
|
|
123
|
+
server.log.info(`📦 Environment: ${serverConfig.nodeEnv}`);
|
|
124
|
+
server.log.info(`🔒 Rate limit: ${serverConfig.rateLimitMax} requests per ${serverConfig.rateLimitTimeWindow}`);
|
|
125
|
+
|
|
126
|
+
return server;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
server.log.error(error);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Start server if run directly
|
|
134
|
+
if (require.main === module) {
|
|
135
|
+
startServer().catch((error) => {
|
|
136
|
+
console.error('Failed to start server:', error);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
});
|
|
139
|
+
}
|
package/src/types/api.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API-specific type definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ErrorResponse {
|
|
6
|
+
error: {
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
details?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface HealthResponse {
|
|
14
|
+
status: 'healthy' | 'unhealthy';
|
|
15
|
+
uptime: number;
|
|
16
|
+
version: string;
|
|
17
|
+
engine: {
|
|
18
|
+
available: boolean;
|
|
19
|
+
};
|
|
20
|
+
timestamp: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface VersionResponse {
|
|
24
|
+
apiVersion: string;
|
|
25
|
+
engineVersion: string;
|
|
26
|
+
protocolVersion: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ValidationResponse {
|
|
30
|
+
valid: boolean;
|
|
31
|
+
errors?: Array<{
|
|
32
|
+
field: string;
|
|
33
|
+
message: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|