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

/**
* 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;
}