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:
126
packages/polling-contracts/src/schemas.ts
Normal file
126
packages/polling-contracts/src/schemas.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user