-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathhandler.ts
More file actions
135 lines (111 loc) · 3.69 KB
/
Copy pathhandler.ts
File metadata and controls
135 lines (111 loc) · 3.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
* @peac/api/handler - PEAC verify API handler
* /peac/verify endpoint with RFC 9457 Problem Details
*/
import { handleVerifyError, validationError } from './errors.js';
import type { VerifyRequest, VerifyResponse, HttpStatus } from './types.js';
export interface VerifyContext {
verifyFn: (jws: string, keys: Record<string, any>) => Promise<any>;
defaultKeys: Record<string, any>;
generateTraceId?: () => string;
}
export class VerifyApiHandler {
constructor(private ctx: VerifyContext) {}
async handle(
request: VerifyRequest,
instance?: string
): Promise<{ status: HttpStatus; body: VerifyResponse | any }> {
try {
// Validate request format
const validation = this.validateRequest(request);
if (!validation.valid) {
const error = validationError(validation.errors!, instance);
return error;
}
// Use provided keys or defaults
const keys = request.keys || this.ctx.defaultKeys;
// Verify the receipt
const result = await this.ctx.verifyFn(request.receipt, keys);
// Build successful response
const response: VerifyResponse = {
valid: true,
receipt: {
header: result.hdr,
payload: result.obj,
},
verification: {
signature: 'valid',
schema: 'valid',
timestamp: new Date().toISOString(),
key_id: result.hdr.kid,
},
};
return {
status: 200,
body: response,
};
} catch (error) {
return handleVerifyError(error, instance);
}
}
private validateRequest(request: VerifyRequest): { valid: boolean; errors?: string[] } {
const errors: string[] = [];
if (!request) {
errors.push('Request body is required');
} else {
if (!request.receipt) {
errors.push('receipt field is required');
} else if (typeof request.receipt !== 'string') {
errors.push('receipt must be a string');
} else if (!this.isValidJwsFormat(request.receipt)) {
errors.push('receipt must be a valid JWS compact serialization (header.payload.signature)');
}
if (request.keys && typeof request.keys !== 'object') {
errors.push('keys must be an object');
}
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
};
}
private isValidJwsFormat(jws: string): boolean {
const parts = jws.split('.');
if (parts.length !== 3) return false;
// Basic base64url format check
const base64urlPattern = /^[A-Za-z0-9_-]+$/;
return parts.every((part) => base64urlPattern.test(part));
}
}
// Express.js adapter
export function createExpressHandler(ctx: VerifyContext) {
const handler = new VerifyApiHandler(ctx);
return async (req: any, res: any) => {
const instance = req.originalUrl || req.url;
const result = await handler.handle(req.body, instance);
res.status(result.status);
if (result.status !== 200) {
res.set('Content-Type', 'application/problem+json');
} else {
res.set('Content-Type', 'application/json');
}
res.json(result.body);
};
}
// Hono adapter
export function createHonoHandler(ctx: VerifyContext) {
const handler = new VerifyApiHandler(ctx);
return async (c: any) => {
const body = await c.req.json();
const instance = c.req.url;
const result = await handler.handle(body, instance);
const headers = {
'Content-Type': result.status === 200 ? 'application/json' : 'application/problem+json',
};
return c.json(result.body, result.status, headers);
};
}
// Generic handler for any framework
export function createGenericHandler(ctx: VerifyContext) {
return new VerifyApiHandler(ctx);
}