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,126 @@
/**
* Zod schemas for strong structural validation
*/
import { z } from 'zod';
import { JWT_ID_PATTERN } from './constants';
// Core schemas
export const PlanSummarySchema = z.object({
jwtId: z.string().regex(JWT_ID_PATTERN, 'Invalid JWT ID format'),
handleId: z.string().min(1),
name: z.string().min(1),
description: z.string(),
issuerDid: z.string().startsWith('did:key:'),
agentDid: z.string().startsWith('did:key:'),
startTime: z.string().datetime(),
endTime: z.string().datetime(),
locLat: z.number().nullable().optional(),
locLon: z.number().nullable().optional(),
url: z.string().url().nullable().optional(),
version: z.string()
});
export const PreviousClaimSchema = z.object({
jwtId: z.string().regex(JWT_ID_PATTERN),
claimType: z.string(),
claimData: z.record(z.any()),
metadata: z.object({
createdAt: z.string().datetime(),
updatedAt: z.string().datetime()
})
});
export const PlanSummaryAndPreviousClaimSchema = z.object({
planSummary: PlanSummarySchema,
previousClaim: PreviousClaimSchema.optional()
});
export const StarredProjectsResponseSchema = z.object({
data: z.array(PlanSummaryAndPreviousClaimSchema),
hitLimit: z.boolean(),
pagination: z.object({
hasMore: z.boolean(),
nextAfterId: z.string().regex(JWT_ID_PATTERN).nullable()
})
});
export const StarredProjectsRequestSchema = z.object({
planIds: z.array(z.string()).min(1),
afterId: z.string().regex(JWT_ID_PATTERN).optional(),
beforeId: z.string().regex(JWT_ID_PATTERN).optional(),
limit: z.number().min(1).max(100).default(100)
});
// Deep link parameter validation
export const DeepLinkParamsSchema = z.object({
jwtIds: z.array(z.string().regex(JWT_ID_PATTERN)).max(10).optional(),
projectId: z.string().regex(/^[a-zA-Z0-9_-]+$/).optional(),
jwtId: z.string().regex(JWT_ID_PATTERN).optional(),
shortlink: z.string().min(1).optional()
}).refine(
(data) => data.jwtIds || data.projectId || data.shortlink,
'At least one of jwtIds, projectId, or shortlink must be provided'
);
// Error response schema
export const ErrorResponseSchema = z.object({
error: z.string(),
code: z.string().optional(),
message: z.string(),
details: z.record(z.any()).optional(),
retryAfter: z.number().optional(),
requestId: z.string().optional()
});
// Rate limit response schema
export const RateLimitResponseSchema = z.object({
error: z.literal('Rate limit exceeded'),
code: z.literal('RATE_LIMIT_EXCEEDED'),
message: z.string(),
details: z.object({
limit: z.number(),
window: z.string(),
resetAt: z.string().datetime(),
remaining: z.number().optional()
}),
retryAfter: z.number(),
requestId: z.string()
});
// Acknowledgment request schema
export const AcknowledgmentRequestSchema = z.object({
acknowledgedJwtIds: z.array(z.string().regex(JWT_ID_PATTERN)).min(1),
acknowledgedAt: z.string().datetime(),
clientVersion: z.string()
});
// Acknowledgment response schema
export const AcknowledgmentResponseSchema = z.object({
acknowledged: z.number(),
failed: z.number(),
alreadyAcknowledged: z.number(),
acknowledgmentId: z.string(),
timestamp: z.string().datetime()
});
// Clock sync response schema
export const ClockSyncResponseSchema = z.object({
serverTime: z.number(),
clientTime: z.number(),
offset: z.number(),
ntpServers: z.array(z.string()).optional()
});
// Type exports for use in host apps
export type StarredProjectsRequest = z.infer<typeof StarredProjectsRequestSchema>;
export type StarredProjectsResponse = z.infer<typeof StarredProjectsResponseSchema>;
export type PlanSummary = z.infer<typeof PlanSummarySchema>;
export type PreviousClaim = z.infer<typeof PreviousClaimSchema>;
export type PlanSummaryAndPreviousClaim = z.infer<typeof PlanSummaryAndPreviousClaimSchema>;
export type DeepLinkParams = z.infer<typeof DeepLinkParamsSchema>;
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
export type RateLimitResponse = z.infer<typeof RateLimitResponseSchema>;
export type AcknowledgmentRequest = z.infer<typeof AcknowledgmentRequestSchema>;
export type AcknowledgmentResponse = z.infer<typeof AcknowledgmentResponseSchema>;
export type ClockSyncResponse = z.infer<typeof ClockSyncResponseSchema>;