@greatwait/contracts 0.1.0-alpha.1 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Bounds a receiver applies to the deliveries this service sends it.
3
+ *
4
+ * **A receiver's bounds are not the sender's, and the shipped SDK used the
5
+ * sender's.** `createNodeCallbackMiddleware` and the receiver conformance suite
6
+ * both defaulted their body limit to `TEST_SINK_BOUNDS.maxRequestBodyBytes` —
7
+ * a number whose every neighbour is justified by the test sink running *inside
8
+ * the API Worker's request budget, on the product's own account*. None of that
9
+ * reasoning is about a customer's own server, and the value it produced was
10
+ * wrong for one.
11
+ */
12
+ /**
13
+ * Headroom for the envelope wrapped around a payload.
14
+ *
15
+ * A receiver is sent an envelope, not a payload: version, type, schedule and
16
+ * occurrence identifiers, the planned time, the attempt ordinal, the trigger
17
+ * that says why the delivery exists, and in `callback` mode a completion URL,
18
+ * bearer token and deadline. All of that is additive to `payloadBytes`.
19
+ *
20
+ * **Measured, not chosen.** Serializing every published delivery test vector
21
+ * around a payload at exactly `SCHEDULING_BOUNDS.payloadBytes`, with a 180-
22
+ * character completion host and a 512-character token — both far past anything
23
+ * the service mints — the largest envelope exceeded the payload maximum by
24
+ * 1,097 bytes. `callback-receiver.test.ts` re-derives that rather than trusting
25
+ * this sentence, so an envelope that grows past the headroom fails a test
26
+ * instead of silently costing a customer their largest deliveries.
27
+ *
28
+ * Four kibibytes is roughly four times the measured worst case, which is slack
29
+ * enough to absorb a new envelope field and tight enough that this is still a
30
+ * bound rather than a shrug.
31
+ */
32
+ export declare const CALLBACK_ENVELOPE_HEADROOM_BYTES = 4096;
33
+ /**
34
+ * The largest callback body a conforming receiver reads before refusing.
35
+ *
36
+ * Derived rather than written down, because the thing that must stay true is
37
+ * the *relationship*: a receiver has to accept the largest payload this service
38
+ * will accept, plus whatever the envelope adds. A literal here would be a
39
+ * second statement of `payloadBytes` and would go stale the first time that
40
+ * moved.
41
+ *
42
+ * **The defect this replaces was live in `0.1.0-alpha.1`.** The old default was
43
+ * 65,536 — exactly `payloadBytes`, and therefore below every envelope carrying
44
+ * a maximum payload. A customer scheduling their largest legal payload against
45
+ * a receiver built with the SDK's defaults got `413 body_too_large`, which the
46
+ * sender classifies as terminal: the occurrence fails and is not retried. The
47
+ * bound was reachable by anyone using both the documented maximum and the
48
+ * documented defaults.
49
+ */
50
+ export declare const CALLBACK_RECEIVER_MAX_BODY_BYTES: number;
@@ -0,0 +1,51 @@
1
+ import { SCHEDULING_BOUNDS } from "./scheduling.js";
2
+ /**
3
+ * Bounds a receiver applies to the deliveries this service sends it.
4
+ *
5
+ * **A receiver's bounds are not the sender's, and the shipped SDK used the
6
+ * sender's.** `createNodeCallbackMiddleware` and the receiver conformance suite
7
+ * both defaulted their body limit to `TEST_SINK_BOUNDS.maxRequestBodyBytes` —
8
+ * a number whose every neighbour is justified by the test sink running *inside
9
+ * the API Worker's request budget, on the product's own account*. None of that
10
+ * reasoning is about a customer's own server, and the value it produced was
11
+ * wrong for one.
12
+ */
13
+ /**
14
+ * Headroom for the envelope wrapped around a payload.
15
+ *
16
+ * A receiver is sent an envelope, not a payload: version, type, schedule and
17
+ * occurrence identifiers, the planned time, the attempt ordinal, the trigger
18
+ * that says why the delivery exists, and in `callback` mode a completion URL,
19
+ * bearer token and deadline. All of that is additive to `payloadBytes`.
20
+ *
21
+ * **Measured, not chosen.** Serializing every published delivery test vector
22
+ * around a payload at exactly `SCHEDULING_BOUNDS.payloadBytes`, with a 180-
23
+ * character completion host and a 512-character token — both far past anything
24
+ * the service mints — the largest envelope exceeded the payload maximum by
25
+ * 1,097 bytes. `callback-receiver.test.ts` re-derives that rather than trusting
26
+ * this sentence, so an envelope that grows past the headroom fails a test
27
+ * instead of silently costing a customer their largest deliveries.
28
+ *
29
+ * Four kibibytes is roughly four times the measured worst case, which is slack
30
+ * enough to absorb a new envelope field and tight enough that this is still a
31
+ * bound rather than a shrug.
32
+ */
33
+ export const CALLBACK_ENVELOPE_HEADROOM_BYTES = 4_096;
34
+ /**
35
+ * The largest callback body a conforming receiver reads before refusing.
36
+ *
37
+ * Derived rather than written down, because the thing that must stay true is
38
+ * the *relationship*: a receiver has to accept the largest payload this service
39
+ * will accept, plus whatever the envelope adds. A literal here would be a
40
+ * second statement of `payloadBytes` and would go stale the first time that
41
+ * moved.
42
+ *
43
+ * **The defect this replaces was live in `0.1.0-alpha.1`.** The old default was
44
+ * 65,536 — exactly `payloadBytes`, and therefore below every envelope carrying
45
+ * a maximum payload. A customer scheduling their largest legal payload against
46
+ * a receiver built with the SDK's defaults got `413 body_too_large`, which the
47
+ * sender classifies as terminal: the occurrence fails and is not retried. The
48
+ * bound was reachable by anyone using both the documented maximum and the
49
+ * documented defaults.
50
+ */
51
+ export const CALLBACK_RECEIVER_MAX_BODY_BYTES = SCHEDULING_BOUNDS.payloadBytes + CALLBACK_ENVELOPE_HEADROOM_BYTES;
package/dist/index.d.ts CHANGED
@@ -8,7 +8,6 @@ export { CREDENTIAL_PREFIX_LENGTH, CREDENTIAL_PREFIX_PATTERN, CREDENTIAL_ROTATIO
8
8
  export { MAX_PARTITION_SHARDS, PROJECTION_DRAIN_LIMIT, SINGLE_SHARD, partitionInstanceName, partitionShardCount, partitionShardFor, partitionShardForIdempotencyKey, type CreateScheduleCommand, type CreateScheduleOutcome, type AttemptPagePosition, type DrainProjectionsOutcome, type DrainProjectionsQuery, type ForecastOutcome, type ForecastPartitionBucket, type ForecastQuery, type IdempotencyDisposition, type ListSchedulesOutcome, type ListSchedulesQuery, type ListAttemptsOutcome, type ListAttemptsQuery, type ListOccurrencesOutcome, type ListOccurrencesQuery, type OccurrencePagePosition, type PartitionOutcome, type PartitionRefusal, type PartitionRefused, type PartitionRpc, type ProjectionIntent, type ReadScheduleQuery, type ReadAttemptOutcome, type ReadAttemptQuery, type ReadOccurrenceOutcome, type ReadOccurrenceQuery, type RedeliverOccurrenceCommand, type RedeliverOccurrenceOutcome, type ReplaceScheduleCommand, type RetainedProjections, type ScheduleDefinition, type ScheduleWriteCommand, type ScheduleWriteOutcome, type SettleProjectionsCommand, type SettleProjectionsOutcome, } from "./partition.js";
9
9
  export { EVENT_TYPES, eventEnvelopeSchema, eventTypeSchema, genericEventSchema, type EventType, type GenericEvent, } from "./events.js";
10
10
  export { MEMBERSHIP_ROLES, MEMBERSHIP_STATUSES, externalMembershipIdSchema, externalOrganizationIdSchema, externalUserIdSchema, grantsAccess, leastRole, leastStatus, membershipChangeSchema, membershipRoleSchema, membershipSchema, membershipStatusSchema, organizationTombstoneSchema, sessionMembershipSchema, type ExternalMembershipId, type ExternalOrganizationId, type ExternalUserId, type Membership, type MembershipChange, type MembershipRole, type MembershipStatus, type OrganizationTombstone, type SessionMembership, } from "./identity.js";
11
- export { CLERK_WEBHOOK_PATH, SVIX_HEADERS, WEBHOOK_SIGNATURE_SCHEME, WEBHOOK_TOLERANCE_SECONDS, isWebhookTimestampFresh, svixHeadersSchema, svixIdSchema, svixSignatureHeaderSchema, svixTimestampSchema, tryWebhookSigningInput, webhookSignatureCandidates, webhookSigningInput, } from "./webhooks.js";
12
11
  export { CHANGE_KINDS, CHANGE_OPERATIONS, CHANGE_SUBJECTS, CONTRACT_VERSION, assess, changeKindSchema, classifyChange, contractVersionSchema, type ChangeKind, type ChangeOperation, type ChangeSubject, type CompatibilityVerdict, type ContractChange, } from "./compatibility.js";
13
12
  export { WORKSPACE_LAYERS, scaffoldProblemSchema, scaffoldVersionSchema, toolDescriptorSchema, toolListSchema, workspaceIdentitySchema, workspaceLayerSchema, workspaceScaffoldSchema, type ScaffoldProblem, type ScaffoldVersion, type ToolDescriptor, type ToolList, type WorkspaceIdentity, type WorkspaceLayer, type WorkspaceScaffold, } from "./workspace.js";
14
13
  export { MOCK_BEHAVIORS, MOCK_HEADERS, OPERATIONS, UNIVERSAL_PROBLEMS, AUTHENTICATED_PROBLEMS, findOperation, fixtureListSchema, mockBehaviorSchema, mockScenarioRequestSchema, mockScenarioResponseSchema, operationsFor, problemsFor, requestSchemaFor, responseSchemaFor, successStatusFor, type FixtureList, type McpDisposition, type MockBehavior, type MockScenarioRequest, type MockScenarioResponse, type OperationDefinition, type OperationId, type OperationSurface, } from "./operations.js";
@@ -17,7 +16,7 @@ export { ATTEMPT_DURATION_MAX_MS, ATTEMPT_FAILURE_SNIPPET_MAX_LENGTH, ATTEMPT_RE
17
16
  export { portableAttemptRecordSchema, portableEncryptedPayloadRecordSchema, portableOccurrenceRecordSchema, portableReceiptRecordSchema, portableScheduleRecordSchema, portableSchedulingRecordSetSchema, type PortableAttemptRecord, type PortableEncryptedPayloadRecord, type PortableOccurrenceRecord, type PortableReceiptRecord, type PortableScheduleRecord, type PortableSchedulingRecordSet, } from "./scheduling-records.js";
18
17
  export { ROLE_SCOPE_CEILINGS, capScopesToRole, ceilingForRole, credentialScopes, grantableScopes, policyMatrix, roleAllows, roleCanPerform, type PolicyMatrixRow, } from "./policy.js";
19
18
  export { ACCESS_TOKEN_PATTERN, ACCESS_TOKEN_TTL_SECONDS, AUTHORIZATION_CODE_PATTERN, AUTHORIZATION_CODE_TTL_SECONDS, AUTHORIZATION_REQUEST_TTL_SECONDS, MAX_REDIRECT_URIS, OAUTH_ERRORS, OAUTH_ERROR_STATUS, OAUTH_GRANT_STATUSES, OAUTH_GRANT_TYPES, OAUTH_RESPONSE_TYPES, OAUTH_REVOCATION_REASONS, OAUTH_ROUTES, OAUTH_SECRET_PATTERN, OAUTH_TOKEN_NAMESPACE_LENGTH, OAUTH_TOKEN_SECRET_LENGTH, PKCE_CHALLENGE_PATTERN, PKCE_METHODS, PKCE_VERIFIER_PATTERN, REFRESH_TOKEN_PATTERN, REFRESH_TOKEN_TTL_SECONDS, TOKEN_ENDPOINT_AUTH_METHODS, authorizationParametersSchema, authorizationServerMetadataSchema, bearerChallenge, clientRegistrationRequestSchema, clientRegistrationSchema, consentDecisionSchema, consentOutcomeSchema, consentPromptSchema, formatScopeString, isLoopbackRedirect, oauthError, oauthErrorCodeSchema, oauthErrorSchema, oauthGrantListSchema, oauthGrantRevokedSchema, oauthGrantSchema, oauthGrantStatusSchema, oauthGrantTypeSchema, oauthRevocationReasonSchema, parseScopeString, pkceMethodSchema, protectedResourceMetadataSchema, redirectUriSchema, revocationRequestSchema, tokenRequestSchema, tokenResponseSchema, type AuthorizationParameters, type AuthorizationServerMetadata, type ClientRegistration, type ClientRegistrationRequest, type ConsentDecision, type ConsentOutcome, type ConsentPrompt, type OAuthErrorBody, type OAuthErrorCode, type OAuthGrant, type OAuthGrantList, type OAuthGrantRevoked, type OAuthGrantStatus, type OAuthGrantType, type OAuthRevocationReason, type ProtectedResourceMetadata, type RevocationRequest, type TokenRequest, type TokenResponse, } from "./oauth.js";
20
- export { MCP_INSTRUCTIONS, MCP_ROUTES } from "./mcp.js";
19
+ export { CALLBACK_ENVELOPE_HEADROOM_BYTES, CALLBACK_RECEIVER_MAX_BODY_BYTES, } from "./callback-receiver.js";
21
20
  export { CALLBACK_OWNERSHIP_POLICIES, DEFAULT_CALLBACK_OWNERSHIP_POLICY, readCallbackOwnershipPolicy, sendableStatuses, type CallbackOwnershipPolicy, } from "./callback-ownership.js";
22
21
  export { CALLBACK_ENDPOINT_SECRET_PATTERN, CALLBACK_URL_REJECTION_REASONS, evaluateCallbackUrl, isPublicCallbackAddress, type CallbackUrlRejectionReason, } from "./callback-safety.js";
23
22
  export { CALLBACK_ENDPOINT_LIMIT_PER_ENVIRONMENT, CALLBACK_ENDPOINT_STATUSES, CALLBACK_SIGNING_KEY_ROTATION_OVERLAP_SECONDS, CALLBACK_VERIFICATION_FAILURES, CALLBACK_VERIFICATION_HEADERS, CALLBACK_VERIFICATION_PROTOCOL, CALLBACK_VERIFICATION_PROTOCOL_VERSION, CALLBACK_VERIFICATION_RATE_LIMITS, CALLBACK_VERIFICATION_RESPONSE_BYTES, CALLBACK_VERIFICATION_TIMEOUT_MILLISECONDS, CALLBACK_VERIFICATION_TYPE, callbackEndpointCreatedSchema, callbackEndpointPageSchema, callbackEndpointRequestSchema, callbackEndpointRotatedSchema, callbackEndpointSchema, callbackEndpointStatusSchema, callbackSigningKeyRotationRequestSchema, callbackSigningKeysSchema, callbackVerificationChallengeSchema, callbackVerificationFailureSchema, type CallbackEndpoint, type CallbackEndpointCreated, type CallbackEndpointPage, type CallbackEndpointRequest, type CallbackEndpointRotated, type CallbackEndpointStatus, type CallbackSigningKeyRotationRequest, type CallbackSigningKeys, type CallbackVerificationChallenge, type CallbackVerificationFailure, } from "./endpoints.js";
package/dist/index.js CHANGED
@@ -8,7 +8,6 @@ export { CREDENTIAL_PREFIX_LENGTH, CREDENTIAL_PREFIX_PATTERN, CREDENTIAL_ROTATIO
8
8
  export { MAX_PARTITION_SHARDS, PROJECTION_DRAIN_LIMIT, SINGLE_SHARD, partitionInstanceName, partitionShardCount, partitionShardFor, partitionShardForIdempotencyKey, } from "./partition.js";
9
9
  export { EVENT_TYPES, eventEnvelopeSchema, eventTypeSchema, genericEventSchema, } from "./events.js";
10
10
  export { MEMBERSHIP_ROLES, MEMBERSHIP_STATUSES, externalMembershipIdSchema, externalOrganizationIdSchema, externalUserIdSchema, grantsAccess, leastRole, leastStatus, membershipChangeSchema, membershipRoleSchema, membershipSchema, membershipStatusSchema, organizationTombstoneSchema, sessionMembershipSchema, } from "./identity.js";
11
- export { CLERK_WEBHOOK_PATH, SVIX_HEADERS, WEBHOOK_SIGNATURE_SCHEME, WEBHOOK_TOLERANCE_SECONDS, isWebhookTimestampFresh, svixHeadersSchema, svixIdSchema, svixSignatureHeaderSchema, svixTimestampSchema, tryWebhookSigningInput, webhookSignatureCandidates, webhookSigningInput, } from "./webhooks.js";
12
11
  export { CHANGE_KINDS, CHANGE_OPERATIONS, CHANGE_SUBJECTS, CONTRACT_VERSION, assess, changeKindSchema, classifyChange, contractVersionSchema, } from "./compatibility.js";
13
12
  export { WORKSPACE_LAYERS, scaffoldProblemSchema, scaffoldVersionSchema, toolDescriptorSchema, toolListSchema, workspaceIdentitySchema, workspaceLayerSchema, workspaceScaffoldSchema, } from "./workspace.js";
14
13
  export { MOCK_BEHAVIORS, MOCK_HEADERS, OPERATIONS, UNIVERSAL_PROBLEMS, AUTHENTICATED_PROBLEMS, findOperation, fixtureListSchema, mockBehaviorSchema, mockScenarioRequestSchema, mockScenarioResponseSchema, operationsFor, problemsFor, requestSchemaFor, responseSchemaFor, successStatusFor, } from "./operations.js";
@@ -17,7 +16,7 @@ export { ATTEMPT_DURATION_MAX_MS, ATTEMPT_FAILURE_SNIPPET_MAX_LENGTH, ATTEMPT_RE
17
16
  export { portableAttemptRecordSchema, portableEncryptedPayloadRecordSchema, portableOccurrenceRecordSchema, portableReceiptRecordSchema, portableScheduleRecordSchema, portableSchedulingRecordSetSchema, } from "./scheduling-records.js";
18
17
  export { ROLE_SCOPE_CEILINGS, capScopesToRole, ceilingForRole, credentialScopes, grantableScopes, policyMatrix, roleAllows, roleCanPerform, } from "./policy.js";
19
18
  export { ACCESS_TOKEN_PATTERN, ACCESS_TOKEN_TTL_SECONDS, AUTHORIZATION_CODE_PATTERN, AUTHORIZATION_CODE_TTL_SECONDS, AUTHORIZATION_REQUEST_TTL_SECONDS, MAX_REDIRECT_URIS, OAUTH_ERRORS, OAUTH_ERROR_STATUS, OAUTH_GRANT_STATUSES, OAUTH_GRANT_TYPES, OAUTH_RESPONSE_TYPES, OAUTH_REVOCATION_REASONS, OAUTH_ROUTES, OAUTH_SECRET_PATTERN, OAUTH_TOKEN_NAMESPACE_LENGTH, OAUTH_TOKEN_SECRET_LENGTH, PKCE_CHALLENGE_PATTERN, PKCE_METHODS, PKCE_VERIFIER_PATTERN, REFRESH_TOKEN_PATTERN, REFRESH_TOKEN_TTL_SECONDS, TOKEN_ENDPOINT_AUTH_METHODS, authorizationParametersSchema, authorizationServerMetadataSchema, bearerChallenge, clientRegistrationRequestSchema, clientRegistrationSchema, consentDecisionSchema, consentOutcomeSchema, consentPromptSchema, formatScopeString, isLoopbackRedirect, oauthError, oauthErrorCodeSchema, oauthErrorSchema, oauthGrantListSchema, oauthGrantRevokedSchema, oauthGrantSchema, oauthGrantStatusSchema, oauthGrantTypeSchema, oauthRevocationReasonSchema, parseScopeString, pkceMethodSchema, protectedResourceMetadataSchema, redirectUriSchema, revocationRequestSchema, tokenRequestSchema, tokenResponseSchema, } from "./oauth.js";
20
- export { MCP_INSTRUCTIONS, MCP_ROUTES } from "./mcp.js";
19
+ export { CALLBACK_ENVELOPE_HEADROOM_BYTES, CALLBACK_RECEIVER_MAX_BODY_BYTES, } from "./callback-receiver.js";
21
20
  export { CALLBACK_OWNERSHIP_POLICIES, DEFAULT_CALLBACK_OWNERSHIP_POLICY, readCallbackOwnershipPolicy, sendableStatuses, } from "./callback-ownership.js";
22
21
  export { CALLBACK_ENDPOINT_SECRET_PATTERN, CALLBACK_URL_REJECTION_REASONS, evaluateCallbackUrl, isPublicCallbackAddress, } from "./callback-safety.js";
23
22
  export { CALLBACK_ENDPOINT_LIMIT_PER_ENVIRONMENT, CALLBACK_ENDPOINT_STATUSES, CALLBACK_SIGNING_KEY_ROTATION_OVERLAP_SECONDS, CALLBACK_VERIFICATION_FAILURES, CALLBACK_VERIFICATION_HEADERS, CALLBACK_VERIFICATION_PROTOCOL, CALLBACK_VERIFICATION_PROTOCOL_VERSION, CALLBACK_VERIFICATION_RATE_LIMITS, CALLBACK_VERIFICATION_RESPONSE_BYTES, CALLBACK_VERIFICATION_TIMEOUT_MILLISECONDS, CALLBACK_VERIFICATION_TYPE, callbackEndpointCreatedSchema, callbackEndpointPageSchema, callbackEndpointRequestSchema, callbackEndpointRotatedSchema, callbackEndpointSchema, callbackEndpointStatusSchema, callbackSigningKeyRotationRequestSchema, callbackSigningKeysSchema, callbackVerificationChallengeSchema, callbackVerificationFailureSchema, } from "./endpoints.js";
@@ -2379,7 +2379,7 @@ export declare const OPERATIONS: readonly [{
2379
2379
  response: "response";
2380
2380
  callback: "callback";
2381
2381
  }>>;
2382
- maxDurationSeconds: z.ZodOptional<z.ZodLiteral<5 | 300 | 86400 | 30 | 900 | 1800 | 3600 | 43200>>;
2382
+ maxDurationSeconds: z.ZodOptional<z.ZodLiteral<300 | 5 | 30 | 900 | 1800 | 3600 | 43200 | 86400>>;
2383
2383
  }, z.core.$strict>>;
2384
2384
  everySeconds: z.ZodOptional<z.ZodNumber>;
2385
2385
  endpoint: z.ZodOptional<z.ZodString>;
@@ -3360,7 +3360,7 @@ export declare const OPERATIONS: readonly [{
3360
3360
  response: "response";
3361
3361
  callback: "callback";
3362
3362
  }>>;
3363
- maxDurationSeconds: z.ZodOptional<z.ZodLiteral<5 | 300 | 86400 | 30 | 900 | 1800 | 3600 | 43200>>;
3363
+ maxDurationSeconds: z.ZodOptional<z.ZodLiteral<300 | 5 | 30 | 900 | 1800 | 3600 | 43200 | 86400>>;
3364
3364
  }, z.core.$strict>>;
3365
3365
  everySeconds: z.ZodOptional<z.ZodNumber>;
3366
3366
  endpoint: z.ZodOptional<z.ZodString>;
@@ -127,7 +127,7 @@ export declare const FREE_DELIVERY_DURATION_SECONDS = 30;
127
127
  * than thirty minutes has to do is answer immediately and call us back.
128
128
  */
129
129
  export declare const RESPONSE_DELIVERY_MAX_DURATION_SECONDS = 1800;
130
- export declare const deliveryDurationSecondsSchema: z.ZodLiteral<5 | 300 | 86400 | 30 | 900 | 1800 | 3600 | 43200>;
130
+ export declare const deliveryDurationSecondsSchema: z.ZodLiteral<300 | 5 | 30 | 900 | 1800 | 3600 | 43200 | 86400>;
131
131
  export type DeliveryDurationSeconds = z.infer<typeof deliveryDurationSecondsSchema>;
132
132
  /** Whether a duration is past the point where it needs a plan that allows it. */
133
133
  export declare function isLongRunningDuration(seconds: number): boolean;
@@ -1041,7 +1041,7 @@ export declare const scheduleRequestSchema: z.ZodObject<{
1041
1041
  response: "response";
1042
1042
  callback: "callback";
1043
1043
  }>>;
1044
- maxDurationSeconds: z.ZodOptional<z.ZodLiteral<5 | 300 | 86400 | 30 | 900 | 1800 | 3600 | 43200>>;
1044
+ maxDurationSeconds: z.ZodOptional<z.ZodLiteral<300 | 5 | 30 | 900 | 1800 | 3600 | 43200 | 86400>>;
1045
1045
  }, z.core.$strict>>;
1046
1046
  everySeconds: z.ZodOptional<z.ZodNumber>;
1047
1047
  endpoint: z.ZodOptional<z.ZodString>;
@@ -100,8 +100,18 @@ export declare const TEST_SINK_BOUNDS: {
100
100
  readonly maxRetainedDeliveries: 50;
101
101
  /** Sinks per project/environment. */
102
102
  readonly maxSinksPerEnvironment: 10;
103
- /** Largest request body a sink will read. Larger is refused unread. */
104
- readonly maxRequestBodyBytes: 65536;
103
+ /**
104
+ * Largest request body a sink will read. Larger is refused unread.
105
+ *
106
+ * **A sink is a receiver, so this is the receiver's bound rather than one of
107
+ * its own.** It used to be a literal 65,536, which is exactly
108
+ * `SCHEDULING_BOUNDS.payloadBytes` — and therefore below every envelope
109
+ * carrying a maximum payload, so a customer rehearsing their largest legal
110
+ * delivery against a sink got the 413 they were rehearsing to avoid. The
111
+ * neighbours here are all justified by the sink running inside the API
112
+ * Worker's budget; this one never was.
113
+ */
114
+ readonly maxRequestBodyBytes: number;
105
115
  /** Captured body bytes retained on a receipt; beyond this it is truncated. */
106
116
  readonly maxRetainedBodyBytes: 4096;
107
117
  };
@@ -967,8 +977,18 @@ export declare const CALLBACK_RECEIVER_PROTOCOL: {
967
977
  readonly maxRetainedDeliveries: 50;
968
978
  /** Sinks per project/environment. */
969
979
  readonly maxSinksPerEnvironment: 10;
970
- /** Largest request body a sink will read. Larger is refused unread. */
971
- readonly maxRequestBodyBytes: 65536;
980
+ /**
981
+ * Largest request body a sink will read. Larger is refused unread.
982
+ *
983
+ * **A sink is a receiver, so this is the receiver's bound rather than one of
984
+ * its own.** It used to be a literal 65,536, which is exactly
985
+ * `SCHEDULING_BOUNDS.payloadBytes` — and therefore below every envelope
986
+ * carrying a maximum payload, so a customer rehearsing their largest legal
987
+ * delivery against a sink got the 413 they were rehearsing to avoid. The
988
+ * neighbours here are all justified by the sink running inside the API
989
+ * Worker's budget; this one never was.
990
+ */
991
+ readonly maxRequestBodyBytes: number;
972
992
  /** Captured body bytes retained on a receipt; beyond this it is truncated. */
973
993
  readonly maxRetainedBodyBytes: 4096;
974
994
  };
package/dist/test-sink.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { CALLBACK_DEFERRAL_STATUS, CALLBACK_PENDING_DISCRIMINATOR, } from "./delivery.js";
3
3
  import { callbackSigningKeyIdSchema, deliveryIdSchema, endpointIdSchema, occurrenceIdSchema, projectIdSchema, scheduleIdSchema, testSinkDeliveryIdSchema, testSinkIdSchema, } from "./identifiers.js";
4
+ import { CALLBACK_RECEIVER_MAX_BODY_BYTES } from "./callback-receiver.js";
4
5
  import { pageSchema } from "./protocol.js";
5
6
  import { CALLBACK_HEADERS, signatureHeaderSchema } from "./signatures.js";
6
7
  import { timestampSchema } from "./time.js";
@@ -106,8 +107,18 @@ export const TEST_SINK_BOUNDS = {
106
107
  maxRetainedDeliveries: 50,
107
108
  /** Sinks per project/environment. */
108
109
  maxSinksPerEnvironment: 10,
109
- /** Largest request body a sink will read. Larger is refused unread. */
110
- maxRequestBodyBytes: 65_536,
110
+ /**
111
+ * Largest request body a sink will read. Larger is refused unread.
112
+ *
113
+ * **A sink is a receiver, so this is the receiver's bound rather than one of
114
+ * its own.** It used to be a literal 65,536, which is exactly
115
+ * `SCHEDULING_BOUNDS.payloadBytes` — and therefore below every envelope
116
+ * carrying a maximum payload, so a customer rehearsing their largest legal
117
+ * delivery against a sink got the 413 they were rehearsing to avoid. The
118
+ * neighbours here are all justified by the sink running inside the API
119
+ * Worker's budget; this one never was.
120
+ */
121
+ maxRequestBodyBytes: CALLBACK_RECEIVER_MAX_BODY_BYTES,
111
122
  /** Captured body bytes retained on a receipt; beyond this it is truncated. */
112
123
  maxRetainedBodyBytes: 4_096,
113
124
  };
@@ -132,7 +132,7 @@
132
132
  "maxHoldMs": 45000,
133
133
  "maxRetainedDeliveries": 50,
134
134
  "maxSinksPerEnvironment": 10,
135
- "maxRequestBodyBytes": 65536,
135
+ "maxRequestBodyBytes": 69632,
136
136
  "maxRetainedBodyBytes": 4096
137
137
  },
138
138
  "clientErrorStatuses": [400, 401, 403, 404, 409, 410, 422],
@@ -23555,7 +23555,7 @@
23555
23555
  "maxHoldMs": 45000,
23556
23556
  "maxRetainedDeliveries": 50,
23557
23557
  "maxSinksPerEnvironment": 10,
23558
- "maxRequestBodyBytes": 65536,
23558
+ "maxRequestBodyBytes": 69632,
23559
23559
  "maxRetainedBodyBytes": 4096
23560
23560
  },
23561
23561
  "clientErrorStatuses": [400, 401, 403, 404, 409, 410, 422],
package/package.json CHANGED
@@ -25,6 +25,11 @@
25
25
  "import": "./dist/callback-ownership.js",
26
26
  "default": "./dist/callback-ownership.js"
27
27
  },
28
+ "./callback-receiver": {
29
+ "types": "./dist/callback-receiver.d.ts",
30
+ "import": "./dist/callback-receiver.js",
31
+ "default": "./dist/callback-receiver.js"
32
+ },
28
33
  "./callback-safety": {
29
34
  "types": "./dist/callback-safety.d.ts",
30
35
  "import": "./dist/callback-safety.js",
@@ -149,5 +154,5 @@
149
154
  },
150
155
  "sideEffects": false,
151
156
  "type": "module",
152
- "version": "0.1.0-alpha.1"
157
+ "version": "0.1.0-alpha.2"
153
158
  }
package/dist/mcp.d.ts DELETED
@@ -1,51 +0,0 @@
1
- /**
2
- * Where the remote MCP surface lives, and what it tells an agent about itself.
3
- *
4
- * Stated once for the same reason `OAUTH_ROUTES` is: two readers must agree.
5
- * The router serves these paths and the protected-resource metadata document
6
- * advertises them, and a path that drifts from its advertisement is not a 404 —
7
- * it is a client that cannot authenticate and cannot say why.
8
- *
9
- * Outside `/v1`, following the convention `CLERK_WEBHOOK_PATH` and
10
- * `OAUTH_ROUTES` set. The `/v1` surface is the operation registry, every entry
11
- * of which is projected into OpenAPI, the SDK, an MCP tool and the contract
12
- * mock; the MCP endpoint is the transport those tools are served over, and a
13
- * transport that appeared in its own tool list would be a tool an agent could
14
- * point at itself.
15
- */
16
- export declare const MCP_ROUTES: {
17
- /**
18
- * The single Streamable HTTP endpoint. POST only; `GET` and `DELETE` answer
19
- * `405`, because this endpoint opens no standalone event stream and mints no
20
- * session to terminate.
21
- *
22
- * Served at this path **and** at the same path with a trailing slash, without
23
- * a redirect between them. A `301`/`302` on a POST is rewritten by the fetch
24
- * standard into a GET with the body dropped, so a client configured with the
25
- * other spelling would have its opening request silently become the GET this
26
- * endpoint refuses — and read that as "no MCP server here".
27
- */
28
- readonly endpoint: "/mcp";
29
- /**
30
- * RFC 9728 metadata for the MCP endpoint specifically.
31
- *
32
- * Served *beside* the origin's own document rather than instead of it. RFC
33
- * 9728 §3.1 places the metadata for a resource with a path component at
34
- * `/.well-known/oauth-protected-resource` followed by that path, so a client
35
- * that treats `https://host/mcp` as the resource it is authorizing for
36
- * constructs exactly this URL — and one that follows the `resource_metadata`
37
- * link out of the `401` is sent here too. Both arrive at a document naming
38
- * the same authorization server.
39
- */
40
- readonly protectedResourceMetadata: "/.well-known/oauth-protected-resource/mcp";
41
- };
42
- /**
43
- * What the server tells a model about itself, once, before any tool call.
44
- *
45
- * MCP calls this `instructions` and hands it to the model as context. It is
46
- * kept short and factual: an agent that has read the tool descriptions knows
47
- * what each one does, and what it cannot work out from them is the shape of the
48
- * authority it is acting under — which is the one thing that decides whether an
49
- * instruction like "create this in production" is even expressible.
50
- */
51
- export declare const MCP_INSTRUCTIONS: string;
package/dist/mcp.js DELETED
@@ -1,56 +0,0 @@
1
- /**
2
- * Where the remote MCP surface lives, and what it tells an agent about itself.
3
- *
4
- * Stated once for the same reason `OAUTH_ROUTES` is: two readers must agree.
5
- * The router serves these paths and the protected-resource metadata document
6
- * advertises them, and a path that drifts from its advertisement is not a 404 —
7
- * it is a client that cannot authenticate and cannot say why.
8
- *
9
- * Outside `/v1`, following the convention `CLERK_WEBHOOK_PATH` and
10
- * `OAUTH_ROUTES` set. The `/v1` surface is the operation registry, every entry
11
- * of which is projected into OpenAPI, the SDK, an MCP tool and the contract
12
- * mock; the MCP endpoint is the transport those tools are served over, and a
13
- * transport that appeared in its own tool list would be a tool an agent could
14
- * point at itself.
15
- */
16
- export const MCP_ROUTES = {
17
- /**
18
- * The single Streamable HTTP endpoint. POST only; `GET` and `DELETE` answer
19
- * `405`, because this endpoint opens no standalone event stream and mints no
20
- * session to terminate.
21
- *
22
- * Served at this path **and** at the same path with a trailing slash, without
23
- * a redirect between them. A `301`/`302` on a POST is rewritten by the fetch
24
- * standard into a GET with the body dropped, so a client configured with the
25
- * other spelling would have its opening request silently become the GET this
26
- * endpoint refuses — and read that as "no MCP server here".
27
- */
28
- endpoint: "/mcp",
29
- /**
30
- * RFC 9728 metadata for the MCP endpoint specifically.
31
- *
32
- * Served *beside* the origin's own document rather than instead of it. RFC
33
- * 9728 §3.1 places the metadata for a resource with a path component at
34
- * `/.well-known/oauth-protected-resource` followed by that path, so a client
35
- * that treats `https://host/mcp` as the resource it is authorizing for
36
- * constructs exactly this URL — and one that follows the `resource_metadata`
37
- * link out of the `401` is sent here too. Both arrive at a document naming
38
- * the same authorization server.
39
- */
40
- protectedResourceMetadata: "/.well-known/oauth-protected-resource/mcp",
41
- };
42
- /**
43
- * What the server tells a model about itself, once, before any tool call.
44
- *
45
- * MCP calls this `instructions` and hands it to the model as context. It is
46
- * kept short and factual: an agent that has read the tool descriptions knows
47
- * what each one does, and what it cannot work out from them is the shape of the
48
- * authority it is acting under — which is the one thing that decides whether an
49
- * instruction like "create this in production" is even expressible.
50
- */
51
- export const MCP_INSTRUCTIONS = [
52
- "Greatwait schedules work and delivers it to an endpoint at the time asked for.",
53
- "Every tool acts inside one organization, project and environment, fixed by the credential this connection was authorized with; no argument can name another, and there is no tool that moves between them.",
54
- "Reads are safe to repeat. Writes are annotated, and the ones marked destructive settle or discard an occurrence that would otherwise have fired.",
55
- "A refusal comes back as a problem document with a stable `code`: act on the code, not on the wording.",
56
- ].join(" ");
@@ -1,116 +0,0 @@
1
- import { z } from "zod";
2
- /**
3
- * Inbound webhook authenticity.
4
- *
5
- * Clerk delivers organization and membership changes through Svix, which signs
6
- * `<svix-id>.<svix-timestamp>.<raw body>` with HMAC-SHA256 under a secret minted
7
- * per endpoint. This module owns the encoding and the header vocabulary; the
8
- * adapter owns the key material and the constant-time comparison, exactly as
9
- * `signatures.ts` splits outbound callback signing.
10
- *
11
- * The split matters. Keeping the signed-byte construction in the contract layer
12
- * means every verifier — the Worker, a test, a future replay tool — signs the
13
- * same bytes, and the reasoning below is stated once instead of being
14
- * rediscovered by whoever writes the second one.
15
- */
16
- export declare const SVIX_HEADERS: {
17
- readonly id: "svix-id";
18
- readonly signature: "svix-signature";
19
- readonly timestamp: "svix-timestamp";
20
- };
21
- /**
22
- * Where Clerk delivers organization and membership changes.
23
- *
24
- * Deliberately outside `/v1`. W1-07 enforces REST/MCP parity over every public
25
- * `/v1` operation, and a webhook ingress is not a customer-facing operation —
26
- * putting it there would either break that check or require an exception to it.
27
- * Follows the internal-route convention set by `/__greatwait/version`.
28
- *
29
- * **Here rather than in the Worker that serves it, since W4-08.** The smoke
30
- * asks a hosted stage whether this route is configured to accept a delivery,
31
- * and a script cannot import the API application. A second spelling of an
32
- * address Clerk is pointed at is one that can drift while both read as
33
- * correct.
34
- */
35
- export declare const CLERK_WEBHOOK_PATH = "/__greatwait/webhooks/clerk";
36
- /** Only `v1` exists. A new scheme is added alongside, never in place of. */
37
- export declare const WEBHOOK_SIGNATURE_SCHEME: "v1";
38
- /**
39
- * How far Clerk's clock may differ from ours before a delivery is stale.
40
- *
41
- * Svix's own guidance is five minutes, and matching it is deliberate: a tighter
42
- * window would reject legitimate deliveries during a retry burst, and a wider
43
- * one lengthens the replay window for a captured request.
44
- */
45
- export declare const WEBHOOK_TOLERANCE_SECONDS = 300;
46
- /**
47
- * A Svix message id. Deliberately a restricted charset rather than
48
- * `z.string().min(1)` — see `webhookSigningInput` for why the absence of `.`
49
- * here is a security property and not a formatting preference.
50
- */
51
- export declare const svixIdSchema: z.ZodString;
52
- /**
53
- * Seconds since the Unix epoch, as digits. No sign, no decimal point, no
54
- * exponent — `Number.parseInt` would happily accept `+1e9` and `12.5`, and both
55
- * would reach the signed bytes in a form we did not intend.
56
- */
57
- export declare const svixTimestampSchema: z.ZodString;
58
- /**
59
- * One `v1,<base64>` pair. Svix sends a space-delimited list so an endpoint can
60
- * be verified under both keys during rotation, which is why the verifier must
61
- * accept a match against *any* entry rather than parsing a single value.
62
- */
63
- export declare const svixSignatureEntryPattern: RegExp;
64
- export declare const svixSignatureHeaderSchema: z.ZodString;
65
- export declare const svixHeadersSchema: z.ZodObject<{
66
- "svix-id": z.ZodString;
67
- "svix-signature": z.ZodString;
68
- "svix-timestamp": z.ZodString;
69
- }, z.core.$strip>;
70
- /**
71
- * Every `v1` signature offered in the header, base64 still encoded.
72
- *
73
- * Entries in other schemes are dropped rather than rejected: Svix may add a
74
- * scheme before we support it, and a delivery that carries both `v1` and a
75
- * future `v2` must still verify under `v1` instead of failing closed on the
76
- * presence of something newer.
77
- */
78
- export declare function webhookSignatureCandidates(header: string): readonly string[];
79
- /**
80
- * The exact bytes Svix signs: `<id>.<timestamp>.` followed by the raw body,
81
- * with no re-serialization anywhere in the path.
82
- *
83
- * Both prefix fields are validated rather than trusted, and — as with
84
- * `signingInput` for outbound callbacks — that is a security property. The
85
- * delimiter is `.`, so if either field could itself contain a `.` the encoding
86
- * would not be injective: an attacker holding one legitimate signature could
87
- * shift bytes across a delimiter, lengthening the id header while shortening
88
- * the body, and still verify.
89
- *
90
- * The argument here is slightly different from the outbound one, which relies
91
- * on both fields being fixed-width. `svixIdSchema` and `svixTimestampSchema`
92
- * admit no `.` at all, so splitting the signed content on its first two dots
93
- * recovers the id and the timestamp uniquely whatever their lengths. Variable
94
- * width is fine; a dot would not be.
95
- *
96
- * **Throws** on malformed input. Use `tryWebhookSigningInput` when verifying
97
- * headers off the wire, or a malformed-header probe becomes a 500.
98
- */
99
- export declare function webhookSigningInput(svixId: string, svixTimestamp: string, rawBody: Uint8Array): Uint8Array;
100
- /**
101
- * `webhookSigningInput` for untrusted input: `undefined` instead of a throw when
102
- * the headers are malformed.
103
- */
104
- export declare function tryWebhookSigningInput(svixId: string, svixTimestamp: string, rawBody: Uint8Array): Uint8Array | undefined;
105
- /**
106
- * Whether a delivery's timestamp is close enough to now.
107
- *
108
- * Rejects the future as firmly as the past. A signature dated an hour ahead is
109
- * not a clock-skew inconvenience — accepting it would hand an attacker who
110
- * captures one request an hour of replay window, so the comparison is on
111
- * absolute difference.
112
- *
113
- * Replay protection beyond this window belongs to the receiver, keyed on the
114
- * Svix message id; freshness alone does not make a delivery unique.
115
- */
116
- export declare function isWebhookTimestampFresh(svixTimestamp: string, nowMs: number, toleranceSeconds?: number): boolean;
package/dist/webhooks.js DELETED
@@ -1,161 +0,0 @@
1
- import { z } from "zod";
2
- /**
3
- * Inbound webhook authenticity.
4
- *
5
- * Clerk delivers organization and membership changes through Svix, which signs
6
- * `<svix-id>.<svix-timestamp>.<raw body>` with HMAC-SHA256 under a secret minted
7
- * per endpoint. This module owns the encoding and the header vocabulary; the
8
- * adapter owns the key material and the constant-time comparison, exactly as
9
- * `signatures.ts` splits outbound callback signing.
10
- *
11
- * The split matters. Keeping the signed-byte construction in the contract layer
12
- * means every verifier — the Worker, a test, a future replay tool — signs the
13
- * same bytes, and the reasoning below is stated once instead of being
14
- * rediscovered by whoever writes the second one.
15
- */
16
- export const SVIX_HEADERS = {
17
- id: "svix-id",
18
- signature: "svix-signature",
19
- timestamp: "svix-timestamp",
20
- };
21
- /**
22
- * Where Clerk delivers organization and membership changes.
23
- *
24
- * Deliberately outside `/v1`. W1-07 enforces REST/MCP parity over every public
25
- * `/v1` operation, and a webhook ingress is not a customer-facing operation —
26
- * putting it there would either break that check or require an exception to it.
27
- * Follows the internal-route convention set by `/__greatwait/version`.
28
- *
29
- * **Here rather than in the Worker that serves it, since W4-08.** The smoke
30
- * asks a hosted stage whether this route is configured to accept a delivery,
31
- * and a script cannot import the API application. A second spelling of an
32
- * address Clerk is pointed at is one that can drift while both read as
33
- * correct.
34
- */
35
- export const CLERK_WEBHOOK_PATH = "/__greatwait/webhooks/clerk";
36
- /** Only `v1` exists. A new scheme is added alongside, never in place of. */
37
- export const WEBHOOK_SIGNATURE_SCHEME = "v1";
38
- /**
39
- * How far Clerk's clock may differ from ours before a delivery is stale.
40
- *
41
- * Svix's own guidance is five minutes, and matching it is deliberate: a tighter
42
- * window would reject legitimate deliveries during a retry burst, and a wider
43
- * one lengthens the replay window for a captured request.
44
- */
45
- export const WEBHOOK_TOLERANCE_SECONDS = 300;
46
- /**
47
- * A Svix message id. Deliberately a restricted charset rather than
48
- * `z.string().min(1)` — see `webhookSigningInput` for why the absence of `.`
49
- * here is a security property and not a formatting preference.
50
- */
51
- export const svixIdSchema = z
52
- .string()
53
- .regex(/^msg_[A-Za-z0-9]{1,64}$/u, 'must be "msg_" followed by up to 64 alphanumeric characters');
54
- /**
55
- * Seconds since the Unix epoch, as digits. No sign, no decimal point, no
56
- * exponent — `Number.parseInt` would happily accept `+1e9` and `12.5`, and both
57
- * would reach the signed bytes in a form we did not intend.
58
- */
59
- export const svixTimestampSchema = z
60
- .string()
61
- .regex(/^[0-9]{1,15}$/u, "must be a whole number of seconds since the epoch");
62
- /**
63
- * One `v1,<base64>` pair. Svix sends a space-delimited list so an endpoint can
64
- * be verified under both keys during rotation, which is why the verifier must
65
- * accept a match against *any* entry rather than parsing a single value.
66
- */
67
- export const svixSignatureEntryPattern = /^v1,[A-Za-z0-9+/]+={0,2}$/u;
68
- export const svixSignatureHeaderSchema = z
69
- .string()
70
- .min(1)
71
- .refine((header) => header
72
- .split(" ")
73
- .filter((entry) => entry.length > 0)
74
- .some((entry) => svixSignatureEntryPattern.test(entry)), "must contain at least one v1,<base64> signature");
75
- export const svixHeadersSchema = z.object({
76
- [SVIX_HEADERS.id]: svixIdSchema,
77
- [SVIX_HEADERS.signature]: svixSignatureHeaderSchema,
78
- [SVIX_HEADERS.timestamp]: svixTimestampSchema,
79
- });
80
- /**
81
- * Every `v1` signature offered in the header, base64 still encoded.
82
- *
83
- * Entries in other schemes are dropped rather than rejected: Svix may add a
84
- * scheme before we support it, and a delivery that carries both `v1` and a
85
- * future `v2` must still verify under `v1` instead of failing closed on the
86
- * presence of something newer.
87
- */
88
- export function webhookSignatureCandidates(header) {
89
- return header
90
- .split(" ")
91
- .filter((entry) => svixSignatureEntryPattern.test(entry))
92
- .map((entry) => entry.slice(`${WEBHOOK_SIGNATURE_SCHEME},`.length));
93
- }
94
- /**
95
- * The exact bytes Svix signs: `<id>.<timestamp>.` followed by the raw body,
96
- * with no re-serialization anywhere in the path.
97
- *
98
- * Both prefix fields are validated rather than trusted, and — as with
99
- * `signingInput` for outbound callbacks — that is a security property. The
100
- * delimiter is `.`, so if either field could itself contain a `.` the encoding
101
- * would not be injective: an attacker holding one legitimate signature could
102
- * shift bytes across a delimiter, lengthening the id header while shortening
103
- * the body, and still verify.
104
- *
105
- * The argument here is slightly different from the outbound one, which relies
106
- * on both fields being fixed-width. `svixIdSchema` and `svixTimestampSchema`
107
- * admit no `.` at all, so splitting the signed content on its first two dots
108
- * recovers the id and the timestamp uniquely whatever their lengths. Variable
109
- * width is fine; a dot would not be.
110
- *
111
- * **Throws** on malformed input. Use `tryWebhookSigningInput` when verifying
112
- * headers off the wire, or a malformed-header probe becomes a 500.
113
- */
114
- export function webhookSigningInput(svixId, svixTimestamp, rawBody) {
115
- const checkedId = svixIdSchema.parse(svixId);
116
- const checkedTimestamp = svixTimestampSchema.parse(svixTimestamp);
117
- const prefix = `${checkedId}.${checkedTimestamp}.`;
118
- // Both fields are ASCII by construction, now that both have been validated, so
119
- // UTF-8 encoding them is byte-identical to their code points.
120
- const prefixBytes = new TextEncoder().encode(prefix);
121
- const input = new Uint8Array(prefixBytes.length + rawBody.length);
122
- input.set(prefixBytes, 0);
123
- input.set(rawBody, prefixBytes.length);
124
- return input;
125
- }
126
- /**
127
- * `webhookSigningInput` for untrusted input: `undefined` instead of a throw when
128
- * the headers are malformed.
129
- */
130
- export function tryWebhookSigningInput(svixId, svixTimestamp, rawBody) {
131
- try {
132
- return webhookSigningInput(svixId, svixTimestamp, rawBody);
133
- }
134
- catch (error) {
135
- // Only a validation failure means "malformed headers". Anything else — an
136
- // allocation failure on an oversized body, say — is a real fault and must
137
- // not be reported to the caller as a bad signature.
138
- if (error instanceof z.ZodError) {
139
- return undefined;
140
- }
141
- throw error;
142
- }
143
- }
144
- /**
145
- * Whether a delivery's timestamp is close enough to now.
146
- *
147
- * Rejects the future as firmly as the past. A signature dated an hour ahead is
148
- * not a clock-skew inconvenience — accepting it would hand an attacker who
149
- * captures one request an hour of replay window, so the comparison is on
150
- * absolute difference.
151
- *
152
- * Replay protection beyond this window belongs to the receiver, keyed on the
153
- * Svix message id; freshness alone does not make a delivery unique.
154
- */
155
- export function isWebhookTimestampFresh(svixTimestamp, nowMs, toleranceSeconds = WEBHOOK_TOLERANCE_SECONDS) {
156
- if (!svixTimestampSchema.safeParse(svixTimestamp).success) {
157
- return false;
158
- }
159
- const signedMs = Number(svixTimestamp) * 1000;
160
- return Math.abs(nowMs - signedMs) <= toleranceSeconds * 1000;
161
- }