feat(polling-contracts): add generic polling interface with TypeScript types and Zod schemas

- Add @timesafari/polling-contracts package with comprehensive type definitions
- Implement GenericPollingRequest, PollingResult, and PollingScheduleConfig interfaces
- Add Zod schemas for StarredProjectsRequest/Response and DeepLinkParams validation
- Include calculateBackoffDelay utility with unified retry policy (exponential, linear, fixed)
- Add OutboxPressureManager for storage pressure controls and back-pressure signals
- Implement TelemetryManager with cardinality budgets and PII redaction
- Add ClockSyncManager for JWT timestamp validation and skew tolerance
- Include comprehensive unit tests with Jest snapshots and race condition testing
- Add JWT_ID_PATTERN regex for canonical JWT ID format validation
- Support idempotency with X-Idempotency-Key enforcement
- Implement watermark CAS (Compare-and-Swap) for race condition prevention

This establishes the foundation for the new generic polling system where host apps
define request/response schemas and the plugin provides robust polling logic.
This commit is contained in:
Matthew Raymer
2025-10-07 04:44:01 +00:00
parent 5b7bd95bdd
commit a5831b3c9f
18 changed files with 2599 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
/**
* Validation utilities and helpers
*/
import { z } from 'zod';
import { JWT_ID_PATTERN, ERROR_CODES } from './constants';
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.groups?.ts) {
throw new Error('Invalid JWT ID format');
}
return parseInt(match.groups.ts, 10);
}
/**
* Validate starred projects response
*/
export function validateStarredProjectsResponse(data: any): boolean {
return StarredProjectsResponseSchema.safeParse(data).success;
}
/**
* Validate deep link parameters
*/
export function validateDeepLinkParams(params: any): boolean {
return DeepLinkParamsSchema.safeParse(params).success;
}
/**
* Validate error response
*/
export function validateErrorResponse(data: any): boolean {
return ErrorResponseSchema.safeParse(data).success;
}
/**
* Validate rate limit response
*/
export function validateRateLimitResponse(data: any): boolean {
return RateLimitResponseSchema.safeParse(data).success;
}
/**
* Create response schema validator
*/
export function createResponseValidator<T>(schema: z.ZodSchema<T>) {
return {
validate: (data: any): data is T => schema.safeParse(data).success,
transformError: (error: any) => ({
code: ERROR_CODES.VALIDATION_ERROR,
message: error.message || 'Validation failed',
retryable: false
})
};
}
/**
* Safe parse with error transformation
*/
export function safeParseWithError<T>(
schema: z.ZodSchema<T>,
data: any
): { 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: any): any {
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;
}