@greatwait/contracts 0.1.0-alpha.1 → 0.1.0-alpha.3
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.
- package/dist/authority.d.ts +57 -0
- package/dist/authority.js +35 -0
- package/dist/callback-receiver.d.ts +50 -0
- package/dist/callback-receiver.js +51 -0
- package/dist/first-value-journey.d.ts +156 -0
- package/dist/first-value-journey.js +213 -0
- package/dist/identity.d.ts +4 -4
- package/dist/index.d.ts +4 -5
- package/dist/index.js +4 -5
- package/dist/mcp.d.ts +148 -0
- package/dist/mcp.js +158 -0
- package/dist/oauth.js +83 -7
- package/dist/operations.d.ts +157 -3
- package/dist/operations.js +104 -2
- package/dist/scheduling.d.ts +2 -2
- package/dist/test-sink.d.ts +24 -4
- package/dist/test-sink.js +13 -2
- package/generated/v1/callback-receiver.json +1 -1
- package/generated/v1/examples/context.read.response.json +6 -0
- package/generated/v1/examples/credentials.connection.response.json +16 -0
- package/generated/v1/manifest.json +53 -0
- package/generated/v1/openapi.json +690 -1
- package/generated/v1/policy-matrix.json +14 -0
- package/generated/v1/schemas/context.read.response.json +43 -0
- package/generated/v1/schemas/credentials.connection.response.json +64 -0
- package/generated/v1/snippets/attempts.list.json +1 -0
- package/generated/v1/snippets/attempts.read.json +1 -0
- package/generated/v1/snippets/context.read.json +6 -0
- package/generated/v1/snippets/credentials.connection.json +6 -0
- package/generated/v1/snippets/credentials.create.json +1 -0
- package/generated/v1/snippets/credentials.list.json +1 -0
- package/generated/v1/snippets/credentials.revoke.json +1 -0
- package/generated/v1/snippets/credentials.rotate.json +1 -0
- package/generated/v1/snippets/endpoints.create.json +1 -0
- package/generated/v1/snippets/endpoints.disable.json +1 -0
- package/generated/v1/snippets/endpoints.list.json +1 -0
- package/generated/v1/snippets/endpoints.read.json +1 -0
- package/generated/v1/snippets/endpoints.rotateSigningKey.json +1 -0
- package/generated/v1/snippets/endpoints.update.json +1 -0
- package/generated/v1/snippets/endpoints.verify.json +1 -0
- package/generated/v1/snippets/forecast.read.json +1 -0
- package/generated/v1/snippets/forecast.windows.json +1 -0
- package/generated/v1/snippets/grants.list.json +1 -0
- package/generated/v1/snippets/grants.revoke.json +1 -0
- package/generated/v1/snippets/journey/await-occurrence.json +6 -0
- package/generated/v1/snippets/journey/check-credential.json +6 -0
- package/generated/v1/snippets/journey/check-service.json +6 -0
- package/generated/v1/snippets/journey/clean-up.json +6 -0
- package/generated/v1/snippets/journey/create-destination.json +6 -0
- package/generated/v1/snippets/journey/create-one-shot.json +6 -0
- package/generated/v1/snippets/journey/read-attempt.json +6 -0
- package/generated/v1/snippets/journey/read-schedule.json +6 -0
- package/generated/v1/snippets/journey/replay-one-shot.json +6 -0
- package/generated/v1/snippets/meta.scaffold.json +1 -0
- package/generated/v1/snippets/meta.version.json +1 -0
- package/generated/v1/snippets/occurrences.list.json +1 -0
- package/generated/v1/snippets/occurrences.read.json +1 -0
- package/generated/v1/snippets/occurrences.redeliver.json +1 -0
- package/generated/v1/snippets/projects.create.json +1 -0
- package/generated/v1/snippets/projects.list.json +1 -0
- package/generated/v1/snippets/schedules.archive.json +1 -0
- package/generated/v1/snippets/schedules.cancel.json +1 -0
- package/generated/v1/snippets/schedules.create.json +1 -0
- package/generated/v1/snippets/schedules.list.json +1 -0
- package/generated/v1/snippets/schedules.pause.json +1 -0
- package/generated/v1/snippets/schedules.read.json +1 -0
- package/generated/v1/snippets/schedules.replace.json +1 -0
- package/generated/v1/snippets/schedules.resume.json +1 -0
- package/generated/v1/snippets/sinkdeliveries.list.json +1 -0
- package/generated/v1/snippets/sinkdeliveries.read.json +1 -0
- package/generated/v1/snippets/testsinks.configure.json +1 -0
- package/generated/v1/snippets/testsinks.create.json +1 -0
- package/generated/v1/snippets/testsinks.delete.json +1 -0
- package/generated/v1/snippets/testsinks.list.json +1 -0
- package/generated/v1/snippets/testsinks.read.json +1 -0
- package/generated/v1/snippets/usage.read.json +1 -0
- package/package.json +7 -2
- package/dist/webhooks.d.ts +0 -116
- package/dist/webhooks.js +0 -161
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"curl": "curl -sS -X GET \"$GREATWAIT_API_URL/v1/usage\" \\\n --variable %GREATWAIT_API_KEY \\\n --expand-header \"Authorization: Bearer {{GREATWAIT_API_KEY}}\"",
|
|
3
|
+
"environments": ["test", "live"],
|
|
3
4
|
"javascript": "import { createGreatwaitClient } from \"@greatwait/api-client\";\n\nconst greatwait = createGreatwaitClient({\n baseUrl: process.env.GREATWAIT_API_URL,\n credential: process.env.GREATWAIT_API_KEY,\n fetch,\n});\n\nconst result = await greatwait.request(\"usage_read\");",
|
|
4
5
|
"mcp": "// Any MCP client, against $GREATWAIT_API_URL/mcp.\n{\n \"arguments\": {},\n \"name\": \"usage_read\"\n}"
|
|
5
6
|
}
|
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",
|
|
@@ -140,7 +145,7 @@
|
|
|
140
145
|
"name": "@greatwait/contracts",
|
|
141
146
|
"publishConfig": {
|
|
142
147
|
"access": "public",
|
|
143
|
-
"tag": "
|
|
148
|
+
"tag": "latest"
|
|
144
149
|
},
|
|
145
150
|
"repository": {
|
|
146
151
|
"type": "git",
|
|
@@ -149,5 +154,5 @@
|
|
|
149
154
|
},
|
|
150
155
|
"sideEffects": false,
|
|
151
156
|
"type": "module",
|
|
152
|
-
"version": "0.1.0-alpha.
|
|
157
|
+
"version": "0.1.0-alpha.3"
|
|
153
158
|
}
|
package/dist/webhooks.d.ts
DELETED
|
@@ -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
|
-
}
|