You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
161 lines
4.0 KiB
161 lines
4.0 KiB
/**
|
|
* Validation utilities and helpers
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import { JWT_ID_PATTERN, ERROR_CODES } from './constants';
|
|
import { PollingError } from './types';
|
|
import {
|
|
StarredProjectsResponseSchema,
|
|
DeepLinkParamsSchema,
|
|
ErrorResponseSchema,
|
|
RateLimitResponseSchema
|
|
} from './schemas';
|
|
|
|
/**
|
|
* Validate JWT ID format
|
|
*/
|
|
export function validateJwtId(jwtId: string): boolean {
|
|
return JWT_ID_PATTERN.test(jwtId);
|
|
}
|
|
|
|
/**
|
|
* Compare JWT IDs lexicographically
|
|
*/
|
|
export function compareJwtIds(a: string, b: string): number {
|
|
if (!validateJwtId(a) || !validateJwtId(b)) {
|
|
throw new Error('Invalid JWT ID format');
|
|
}
|
|
return a.localeCompare(b);
|
|
}
|
|
|
|
/**
|
|
* Extract timestamp from JWT ID
|
|
*/
|
|
export function extractJwtTimestamp(jwtId: string): number {
|
|
const match = jwtId.match(JWT_ID_PATTERN);
|
|
if (!match || !match[1]) {
|
|
throw new Error('Invalid JWT ID format');
|
|
}
|
|
return parseInt(match[1], 10);
|
|
}
|
|
|
|
/**
|
|
* Validate starred projects response
|
|
*/
|
|
export function validateStarredProjectsResponse(data: unknown): boolean {
|
|
return StarredProjectsResponseSchema.safeParse(data).success;
|
|
}
|
|
|
|
/**
|
|
* Validate deep link parameters
|
|
*/
|
|
export function validateDeepLinkParams(params: unknown): boolean {
|
|
return DeepLinkParamsSchema.safeParse(params).success;
|
|
}
|
|
|
|
/**
|
|
* Validate error response
|
|
*/
|
|
export function validateErrorResponse(data: unknown): boolean {
|
|
return ErrorResponseSchema.safeParse(data).success;
|
|
}
|
|
|
|
/**
|
|
* Validate rate limit response
|
|
*/
|
|
export function validateRateLimitResponse(data: unknown): boolean {
|
|
return RateLimitResponseSchema.safeParse(data).success;
|
|
}
|
|
|
|
/**
|
|
* Create response schema validator
|
|
*/
|
|
export function createResponseValidator<T>(schema: z.ZodSchema<T>): {
|
|
validate: (data: unknown) => data is T;
|
|
transformError: (error: unknown) => PollingError;
|
|
} {
|
|
return {
|
|
validate: (data: unknown): data is T => schema.safeParse(data).success,
|
|
transformError: (error: unknown): PollingError => ({
|
|
code: ERROR_CODES.VALIDATION_ERROR,
|
|
message: (error as Error).message || 'Validation failed',
|
|
retryable: false
|
|
})
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Safe parse with error transformation
|
|
*/
|
|
export function safeParseWithError<T>(
|
|
schema: z.ZodSchema<T>,
|
|
data: unknown
|
|
): { success: true; data: T } | { success: false; error: string } {
|
|
const result = schema.safeParse(data);
|
|
|
|
if (result.success) {
|
|
return { success: true, data: result.data };
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate idempotency key format
|
|
*/
|
|
export function validateIdempotencyKey(key: string): boolean {
|
|
// UUID v4 format
|
|
const uuidV4Pattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
return uuidV4Pattern.test(key);
|
|
}
|
|
|
|
/**
|
|
* Generate idempotency key
|
|
*/
|
|
export function generateIdempotencyKey(): string {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
const r = Math.random() * 16 | 0;
|
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hash DID for privacy in logs
|
|
*/
|
|
export function hashDid(did: string): string {
|
|
// Simple hash for privacy (use crypto in production)
|
|
const hash = did.split('').reduce((a, b) => {
|
|
a = ((a << 5) - a) + b.charCodeAt(0);
|
|
return a & a;
|
|
}, 0);
|
|
return `did:hash:${Math.abs(hash).toString(16)}`;
|
|
}
|
|
|
|
/**
|
|
* Redact PII from logs
|
|
*/
|
|
export function redactPii(data: unknown): unknown {
|
|
const redacted = JSON.parse(JSON.stringify(data));
|
|
|
|
// Redact DID patterns
|
|
if (typeof redacted === 'string') {
|
|
return redacted.replace(/did:key:[a-zA-Z0-9]+/g, (match) => hashDid(match));
|
|
}
|
|
|
|
if (typeof redacted === 'object' && redacted !== null) {
|
|
for (const key in redacted) {
|
|
if (typeof redacted[key] === 'string') {
|
|
redacted[key] = redacted[key].replace(/did:key:[a-zA-Z0-9]+/g, (match: string) => hashDid(match));
|
|
} else if (typeof redacted[key] === 'object') {
|
|
redacted[key] = redactPii(redacted[key]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return redacted;
|
|
}
|
|
|