hooklatch 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +438 -2
- package/dist/adapters/cloudflare.d.ts +11 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -0
- package/dist/adapters/cloudflare.js +27 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/express.d.ts +9 -0
- package/dist/adapters/express.d.ts.map +1 -0
- package/dist/adapters/express.js +63 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/hono.d.ts +12 -0
- package/dist/adapters/hono.d.ts.map +1 -0
- package/dist/adapters/hono.js +28 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/nextjs.d.ts +6 -0
- package/dist/adapters/nextjs.d.ts.map +1 -0
- package/dist/adapters/nextjs.js +56 -0
- package/dist/adapters/nextjs.js.map +1 -0
- package/dist/adapters/shared.d.ts +8 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +53 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/cjs/adapters/cloudflare.cjs +29 -0
- package/dist/cjs/adapters/cloudflare.d.cts +10 -0
- package/dist/cjs/adapters/express.cjs +65 -0
- package/dist/cjs/adapters/express.d.cts +8 -0
- package/dist/cjs/adapters/hono.cjs +30 -0
- package/dist/cjs/adapters/hono.d.cts +11 -0
- package/dist/cjs/adapters/nextjs.cjs +58 -0
- package/dist/cjs/adapters/nextjs.d.cts +5 -0
- package/dist/cjs/adapters/shared.cjs +58 -0
- package/dist/cjs/adapters/shared.d.cts +7 -0
- package/dist/cjs/core.cjs +30 -0
- package/dist/cjs/core.d.cts +5 -0
- package/dist/cjs/diagnostics/catalog.cjs +341 -0
- package/dist/cjs/diagnostics/catalog.d.cts +19 -0
- package/dist/cjs/diagnostics/index.cjs +90 -0
- package/dist/cjs/diagnostics/index.d.cts +5 -0
- package/dist/cjs/diagnostics/types.cjs +2 -0
- package/dist/cjs/diagnostics/types.d.cts +20 -0
- package/dist/cjs/errors.cjs +13 -0
- package/dist/cjs/errors.d.cts +6 -0
- package/dist/cjs/index.cjs +9 -0
- package/dist/cjs/index.d.cts +3 -0
- package/dist/cjs/internal/body.cjs +50 -0
- package/dist/cjs/internal/body.d.cts +2 -0
- package/dist/cjs/internal/bytes.cjs +43 -0
- package/dist/cjs/internal/bytes.d.cts +4 -0
- package/dist/cjs/internal/crypto.cjs +52 -0
- package/dist/cjs/internal/crypto.d.cts +3 -0
- package/dist/cjs/internal/headers.cjs +72 -0
- package/dist/cjs/internal/headers.d.cts +2 -0
- package/dist/cjs/internal/options.cjs +130 -0
- package/dist/cjs/internal/options.d.cts +7 -0
- package/dist/cjs/internal/prepare.cjs +11 -0
- package/dist/cjs/internal/prepare.d.cts +4 -0
- package/dist/cjs/internal/provider-options.cjs +72 -0
- package/dist/cjs/internal/provider-options.d.cts +6 -0
- package/dist/cjs/internal/provider.cjs +2 -0
- package/dist/cjs/internal/provider.d.cts +39 -0
- package/dist/cjs/internal/verification.cjs +71 -0
- package/dist/cjs/internal/verification.d.cts +12 -0
- package/dist/cjs/providers/clerk.cjs +15 -0
- package/dist/cjs/providers/clerk.d.cts +9 -0
- package/dist/cjs/providers/dodo-payments.cjs +26 -0
- package/dist/cjs/providers/dodo-payments.d.cts +20 -0
- package/dist/cjs/providers/doppler.cjs +71 -0
- package/dist/cjs/providers/doppler.d.cts +37 -0
- package/dist/cjs/providers/fal.cjs +80 -0
- package/dist/cjs/providers/fal.d.cts +13 -0
- package/dist/cjs/providers/github.cjs +52 -0
- package/dist/cjs/providers/github.d.cts +7 -0
- package/dist/cjs/providers/gitlab.cjs +26 -0
- package/dist/cjs/providers/gitlab.d.cts +20 -0
- package/dist/cjs/providers/grafana.cjs +180 -0
- package/dist/cjs/providers/grafana.d.cts +35 -0
- package/dist/cjs/providers/index.cjs +110 -0
- package/dist/cjs/providers/index.d.cts +26 -0
- package/dist/cjs/providers/lemon-squeezy.cjs +36 -0
- package/dist/cjs/providers/lemon-squeezy.d.cts +15 -0
- package/dist/cjs/providers/linear.cjs +58 -0
- package/dist/cjs/providers/linear.d.cts +36 -0
- package/dist/cjs/providers/paddle.cjs +105 -0
- package/dist/cjs/providers/paddle.d.cts +14 -0
- package/dist/cjs/providers/polar.cjs +80 -0
- package/dist/cjs/providers/polar.d.cts +25 -0
- package/dist/cjs/providers/razorpay.cjs +43 -0
- package/dist/cjs/providers/razorpay.d.cts +19 -0
- package/dist/cjs/providers/replicate.cjs +25 -0
- package/dist/cjs/providers/replicate.d.cts +19 -0
- package/dist/cjs/providers/sanity.cjs +161 -0
- package/dist/cjs/providers/sanity.d.cts +38 -0
- package/dist/cjs/providers/sentry.cjs +47 -0
- package/dist/cjs/providers/sentry.d.cts +25 -0
- package/dist/cjs/providers/shared.cjs +49 -0
- package/dist/cjs/providers/shared.d.cts +14 -0
- package/dist/cjs/providers/shopify.cjs +40 -0
- package/dist/cjs/providers/shopify.d.cts +8 -0
- package/dist/cjs/providers/standard-webhooks-protocol.cjs +97 -0
- package/dist/cjs/providers/standard-webhooks-protocol.d.cts +14 -0
- package/dist/cjs/providers/standard-webhooks.cjs +15 -0
- package/dist/cjs/providers/standard-webhooks.d.cts +9 -0
- package/dist/cjs/providers/stripe.cjs +81 -0
- package/dist/cjs/providers/stripe.d.cts +7 -0
- package/dist/cjs/providers/svix.cjs +63 -0
- package/dist/cjs/providers/svix.d.cts +9 -0
- package/dist/cjs/providers/vercel.cjs +39 -0
- package/dist/cjs/providers/vercel.d.cts +15 -0
- package/dist/cjs/providers/workos.cjs +120 -0
- package/dist/cjs/providers/workos.d.cts +32 -0
- package/dist/cjs/types.cjs +2 -0
- package/dist/cjs/types.d.cts +68 -0
- package/dist/cli/args.d.ts +86 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +358 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/filesystem.d.ts +15 -0
- package/dist/cli/filesystem.d.ts.map +1 -0
- package/dist/cli/filesystem.js +170 -0
- package/dist/cli/filesystem.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +64 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/templates.d.ts +13 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +297 -0
- package/dist/cli/templates.js.map +1 -0
- package/dist/core.d.ts +6 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +26 -0
- package/dist/core.js.map +1 -0
- package/dist/diagnostics/catalog.d.ts +20 -0
- package/dist/diagnostics/catalog.d.ts.map +1 -0
- package/dist/diagnostics/catalog.js +335 -0
- package/dist/diagnostics/catalog.js.map +1 -0
- package/dist/diagnostics/index.d.ts +6 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +88 -0
- package/dist/diagnostics/index.js.map +1 -0
- package/dist/diagnostics/types.d.ts +21 -0
- package/dist/diagnostics/types.d.ts.map +1 -0
- package/dist/diagnostics/types.js +2 -0
- package/dist/diagnostics/types.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/body.d.ts +3 -0
- package/dist/internal/body.d.ts.map +1 -0
- package/dist/internal/body.js +48 -0
- package/dist/internal/body.js.map +1 -0
- package/dist/internal/bytes.d.ts +5 -0
- package/dist/internal/bytes.d.ts.map +1 -0
- package/dist/internal/bytes.js +38 -0
- package/dist/internal/bytes.js.map +1 -0
- package/dist/internal/crypto.d.ts +4 -0
- package/dist/internal/crypto.d.ts.map +1 -0
- package/dist/internal/crypto.js +49 -0
- package/dist/internal/crypto.js.map +1 -0
- package/dist/internal/headers.d.ts +3 -0
- package/dist/internal/headers.d.ts.map +1 -0
- package/dist/internal/headers.js +70 -0
- package/dist/internal/headers.js.map +1 -0
- package/dist/internal/options.d.ts +8 -0
- package/dist/internal/options.d.ts.map +1 -0
- package/dist/internal/options.js +128 -0
- package/dist/internal/options.js.map +1 -0
- package/dist/internal/prepare.d.ts +5 -0
- package/dist/internal/prepare.d.ts.map +1 -0
- package/dist/internal/prepare.js +9 -0
- package/dist/internal/prepare.js.map +1 -0
- package/dist/internal/provider-options.d.ts +7 -0
- package/dist/internal/provider-options.d.ts.map +1 -0
- package/dist/internal/provider-options.js +69 -0
- package/dist/internal/provider-options.js.map +1 -0
- package/dist/internal/provider.d.ts +40 -0
- package/dist/internal/provider.d.ts.map +1 -0
- package/dist/internal/provider.js +2 -0
- package/dist/internal/provider.js.map +1 -0
- package/dist/internal/verification.d.ts +13 -0
- package/dist/internal/verification.d.ts.map +1 -0
- package/dist/internal/verification.js +66 -0
- package/dist/internal/verification.js.map +1 -0
- package/dist/node/adapters/cloudflare.d.ts +1 -0
- package/dist/node/adapters/cloudflare.js +3 -0
- package/dist/node/adapters/express.d.ts +1 -0
- package/dist/node/adapters/express.js +3 -0
- package/dist/node/adapters/hono.d.ts +1 -0
- package/dist/node/adapters/hono.js +3 -0
- package/dist/node/adapters/nextjs.d.ts +1 -0
- package/dist/node/adapters/nextjs.js +3 -0
- package/dist/node/diagnostics/index.d.ts +1 -0
- package/dist/node/diagnostics/index.js +3 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +6 -0
- package/dist/providers/clerk.d.ts +10 -0
- package/dist/providers/clerk.d.ts.map +1 -0
- package/dist/providers/clerk.js +13 -0
- package/dist/providers/clerk.js.map +1 -0
- package/dist/providers/dodo-payments.d.ts +21 -0
- package/dist/providers/dodo-payments.d.ts.map +1 -0
- package/dist/providers/dodo-payments.js +24 -0
- package/dist/providers/dodo-payments.js.map +1 -0
- package/dist/providers/doppler.d.ts +38 -0
- package/dist/providers/doppler.d.ts.map +1 -0
- package/dist/providers/doppler.js +69 -0
- package/dist/providers/doppler.js.map +1 -0
- package/dist/providers/fal.d.ts +14 -0
- package/dist/providers/fal.d.ts.map +1 -0
- package/dist/providers/fal.js +78 -0
- package/dist/providers/fal.js.map +1 -0
- package/dist/providers/github.d.ts +8 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +50 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/providers/gitlab.d.ts +21 -0
- package/dist/providers/gitlab.d.ts.map +1 -0
- package/dist/providers/gitlab.js +24 -0
- package/dist/providers/gitlab.js.map +1 -0
- package/dist/providers/grafana.d.ts +36 -0
- package/dist/providers/grafana.d.ts.map +1 -0
- package/dist/providers/grafana.js +178 -0
- package/dist/providers/grafana.js.map +1 -0
- package/dist/providers/index.d.ts +27 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +89 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/lemon-squeezy.d.ts +16 -0
- package/dist/providers/lemon-squeezy.d.ts.map +1 -0
- package/dist/providers/lemon-squeezy.js +34 -0
- package/dist/providers/lemon-squeezy.js.map +1 -0
- package/dist/providers/linear.d.ts +37 -0
- package/dist/providers/linear.d.ts.map +1 -0
- package/dist/providers/linear.js +56 -0
- package/dist/providers/linear.js.map +1 -0
- package/dist/providers/paddle.d.ts +15 -0
- package/dist/providers/paddle.d.ts.map +1 -0
- package/dist/providers/paddle.js +103 -0
- package/dist/providers/paddle.js.map +1 -0
- package/dist/providers/polar.d.ts +26 -0
- package/dist/providers/polar.d.ts.map +1 -0
- package/dist/providers/polar.js +78 -0
- package/dist/providers/polar.js.map +1 -0
- package/dist/providers/razorpay.d.ts +20 -0
- package/dist/providers/razorpay.d.ts.map +1 -0
- package/dist/providers/razorpay.js +41 -0
- package/dist/providers/razorpay.js.map +1 -0
- package/dist/providers/replicate.d.ts +20 -0
- package/dist/providers/replicate.d.ts.map +1 -0
- package/dist/providers/replicate.js +23 -0
- package/dist/providers/replicate.js.map +1 -0
- package/dist/providers/sanity.d.ts +39 -0
- package/dist/providers/sanity.d.ts.map +1 -0
- package/dist/providers/sanity.js +159 -0
- package/dist/providers/sanity.js.map +1 -0
- package/dist/providers/sentry.d.ts +26 -0
- package/dist/providers/sentry.d.ts.map +1 -0
- package/dist/providers/sentry.js +45 -0
- package/dist/providers/sentry.js.map +1 -0
- package/dist/providers/shared.d.ts +15 -0
- package/dist/providers/shared.d.ts.map +1 -0
- package/dist/providers/shared.js +40 -0
- package/dist/providers/shared.js.map +1 -0
- package/dist/providers/shopify.d.ts +9 -0
- package/dist/providers/shopify.d.ts.map +1 -0
- package/dist/providers/shopify.js +38 -0
- package/dist/providers/shopify.js.map +1 -0
- package/dist/providers/standard-webhooks-protocol.d.ts +15 -0
- package/dist/providers/standard-webhooks-protocol.d.ts.map +1 -0
- package/dist/providers/standard-webhooks-protocol.js +95 -0
- package/dist/providers/standard-webhooks-protocol.js.map +1 -0
- package/dist/providers/standard-webhooks.d.ts +10 -0
- package/dist/providers/standard-webhooks.d.ts.map +1 -0
- package/dist/providers/standard-webhooks.js +13 -0
- package/dist/providers/standard-webhooks.js.map +1 -0
- package/dist/providers/stripe.d.ts +8 -0
- package/dist/providers/stripe.d.ts.map +1 -0
- package/dist/providers/stripe.js +79 -0
- package/dist/providers/stripe.js.map +1 -0
- package/dist/providers/svix.d.ts +10 -0
- package/dist/providers/svix.d.ts.map +1 -0
- package/dist/providers/svix.js +61 -0
- package/dist/providers/svix.js.map +1 -0
- package/dist/providers/vercel.d.ts +16 -0
- package/dist/providers/vercel.d.ts.map +1 -0
- package/dist/providers/vercel.js +37 -0
- package/dist/providers/vercel.js.map +1 -0
- package/dist/providers/workos.d.ts +33 -0
- package/dist/providers/workos.d.ts.map +1 -0
- package/dist/providers/workos.js +118 -0
- package/dist/providers/workos.js.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +200 -4
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.linearDescriptor = void 0;
|
|
4
|
+
const shared_js_1 = require("./shared.cjs");
|
|
5
|
+
const SIGNATURE_HEADER = 'linear-signature';
|
|
6
|
+
/**
|
|
7
|
+
* Linear webhook signature authentication: `Linear-Signature` is a single
|
|
8
|
+
* 64-character hex HMAC-SHA256 over the exact visible raw request body, keyed
|
|
9
|
+
* with the literal, untrimmed UTF-8 webhook secret. Missing or trim-blank values
|
|
10
|
+
* are `missing-signature`; prefixes, outer whitespace, commas, lists, non-hex,
|
|
11
|
+
* wrong length and all other nonconforming values fail closed as
|
|
12
|
+
* `malformed-signature`. Either hex case is accepted as Hooklatch receiver
|
|
13
|
+
* policy, not as a claim of exact Linear SDK parity.
|
|
14
|
+
*
|
|
15
|
+
* `Linear-Delivery`, `Linear-Event`, `linear-timestamp` and all alien
|
|
16
|
+
* authentication headers are outside the MAC. They cannot establish freshness or
|
|
17
|
+
* trusted identity. Every identity field is null and `authenticatedFields` is
|
|
18
|
+
* empty: this descriptor intentionally performs body-only authentication, does
|
|
19
|
+
* not parse JSON, and never derives identity or time from body content. Old or
|
|
20
|
+
* future unsigned timestamps therefore cannot change authenticity, and this
|
|
21
|
+
* descriptor does not provide replay protection or business idempotency.
|
|
22
|
+
*
|
|
23
|
+
* The secret is literal UTF-8 with no prefix stripping, Base64 decoding,
|
|
24
|
+
* trimming or inferred length bound. Empty secrets are invalid configuration;
|
|
25
|
+
* whitespace-only and non-ASCII secrets remain literal keys. The exact raw body
|
|
26
|
+
* bytes are returned as signed content, so callers must retain the received
|
|
27
|
+
* bytes rather than reserialize parsed JSON.
|
|
28
|
+
*
|
|
29
|
+
* Sources:
|
|
30
|
+
* - https://linear.app/developers/webhooks
|
|
31
|
+
* - https://linear.app/developers/sdk-webhooks
|
|
32
|
+
* - https://github.com/linear/linear/blob/4d9cdbf0beb1d1da1824deb5ff3cd458c71380c9/packages/sdk/src/webhooks/client.ts
|
|
33
|
+
*
|
|
34
|
+
* No Linear-published signature tuple is claimed; test anchors are synthetic.
|
|
35
|
+
*/
|
|
36
|
+
exports.linearDescriptor = {
|
|
37
|
+
provider: 'linear',
|
|
38
|
+
decodeSecret(secret) {
|
|
39
|
+
return (0, shared_js_1.decodeLiteralSecret)(secret, 'Linear');
|
|
40
|
+
},
|
|
41
|
+
parse(headers, rawBody) {
|
|
42
|
+
const raw = headers.get(SIGNATURE_HEADER);
|
|
43
|
+
if (raw === undefined || raw.trim() === '') {
|
|
44
|
+
return (0, shared_js_1.failure)('missing-signature', 'No Linear signature header was provided.');
|
|
45
|
+
}
|
|
46
|
+
const signature = (0, shared_js_1.decodeHexSignature)(raw);
|
|
47
|
+
if (signature === null) {
|
|
48
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Linear signature header is malformed.');
|
|
49
|
+
}
|
|
50
|
+
const identity = {
|
|
51
|
+
deliveryId: null,
|
|
52
|
+
eventId: null,
|
|
53
|
+
timestamp: null,
|
|
54
|
+
authenticatedFields: [],
|
|
55
|
+
};
|
|
56
|
+
return { ok: true, signatures: [signature], signedContent: rawBody, identity };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { NormalizedHeaders, ParseResult } from '../internal/provider.cjs';
|
|
2
|
+
/**
|
|
3
|
+
* Linear webhook signature authentication: `Linear-Signature` is a single
|
|
4
|
+
* 64-character hex HMAC-SHA256 over the exact visible raw request body, keyed
|
|
5
|
+
* with the literal, untrimmed UTF-8 webhook secret. Missing or trim-blank values
|
|
6
|
+
* are `missing-signature`; prefixes, outer whitespace, commas, lists, non-hex,
|
|
7
|
+
* wrong length and all other nonconforming values fail closed as
|
|
8
|
+
* `malformed-signature`. Either hex case is accepted as Hooklatch receiver
|
|
9
|
+
* policy, not as a claim of exact Linear SDK parity.
|
|
10
|
+
*
|
|
11
|
+
* `Linear-Delivery`, `Linear-Event`, `linear-timestamp` and all alien
|
|
12
|
+
* authentication headers are outside the MAC. They cannot establish freshness or
|
|
13
|
+
* trusted identity. Every identity field is null and `authenticatedFields` is
|
|
14
|
+
* empty: this descriptor intentionally performs body-only authentication, does
|
|
15
|
+
* not parse JSON, and never derives identity or time from body content. Old or
|
|
16
|
+
* future unsigned timestamps therefore cannot change authenticity, and this
|
|
17
|
+
* descriptor does not provide replay protection or business idempotency.
|
|
18
|
+
*
|
|
19
|
+
* The secret is literal UTF-8 with no prefix stripping, Base64 decoding,
|
|
20
|
+
* trimming or inferred length bound. Empty secrets are invalid configuration;
|
|
21
|
+
* whitespace-only and non-ASCII secrets remain literal keys. The exact raw body
|
|
22
|
+
* bytes are returned as signed content, so callers must retain the received
|
|
23
|
+
* bytes rather than reserialize parsed JSON.
|
|
24
|
+
*
|
|
25
|
+
* Sources:
|
|
26
|
+
* - https://linear.app/developers/webhooks
|
|
27
|
+
* - https://linear.app/developers/sdk-webhooks
|
|
28
|
+
* - https://github.com/linear/linear/blob/4d9cdbf0beb1d1da1824deb5ff3cd458c71380c9/packages/sdk/src/webhooks/client.ts
|
|
29
|
+
*
|
|
30
|
+
* No Linear-published signature tuple is claimed; test anchors are synthetic.
|
|
31
|
+
*/
|
|
32
|
+
export declare const linearDescriptor: {
|
|
33
|
+
provider: "linear";
|
|
34
|
+
decodeSecret(secret: string): Uint8Array;
|
|
35
|
+
parse(headers: NormalizedHeaders, rawBody: Uint8Array): ParseResult;
|
|
36
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.paddleDescriptor = void 0;
|
|
4
|
+
const bytes_js_1 = require("../internal/bytes.cjs");
|
|
5
|
+
const shared_js_1 = require("./shared.cjs");
|
|
6
|
+
const SIGNATURE_HEADER = 'paddle-signature';
|
|
7
|
+
const TIMESTAMP_KEY = 'ts';
|
|
8
|
+
const SIGNATURE_KEY = 'h1';
|
|
9
|
+
/**
|
|
10
|
+
* Opaque nonempty printable ASCII unknown-field value: no whitespace, commas,
|
|
11
|
+
* semicolons, or additional equals signs. This is a bounded Hooklatch grammar,
|
|
12
|
+
* not a promise to support an unspecified future signature version.
|
|
13
|
+
*/
|
|
14
|
+
const OPAQUE_VALUE = /^[\x21-\x2b\x2d-\x3a\x3c\x3e-\x7e]+$/;
|
|
15
|
+
/**
|
|
16
|
+
* Paddle Billing: `Paddle-Signature: ts=<unix seconds>;h1=<64 hex>`.
|
|
17
|
+
* The signature covers the exact `ts` text, a colon, and the untouched raw
|
|
18
|
+
* body. Repeated `h1` values are a Hooklatch rotation accept-set; every `h1`
|
|
19
|
+
* must be valid. Unknown fields such as `h2` are validated then ignored.
|
|
20
|
+
* Freshness is enforced by core, never here.
|
|
21
|
+
* Source: https://developer.paddle.com/webhooks/about/signature-verification/
|
|
22
|
+
*/
|
|
23
|
+
exports.paddleDescriptor = {
|
|
24
|
+
provider: 'paddle',
|
|
25
|
+
decodeSecret(secret) {
|
|
26
|
+
return (0, shared_js_1.decodeLiteralSecret)(secret, 'Paddle');
|
|
27
|
+
},
|
|
28
|
+
parse(headers, rawBody) {
|
|
29
|
+
const raw = headers.get(SIGNATURE_HEADER);
|
|
30
|
+
if (raw === undefined || raw.trim() === '') {
|
|
31
|
+
return (0, shared_js_1.failure)('missing-signature', 'No Paddle signature header was provided.');
|
|
32
|
+
}
|
|
33
|
+
// HTTP comma-combined duplicate header values never form a valid field list.
|
|
34
|
+
if (raw.includes(',')) {
|
|
35
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Paddle signature header is malformed.');
|
|
36
|
+
}
|
|
37
|
+
const signatures = [];
|
|
38
|
+
let timestampText;
|
|
39
|
+
let timestampCount = 0;
|
|
40
|
+
let unknownSchemes = 0;
|
|
41
|
+
for (const rawSegment of raw.split(';')) {
|
|
42
|
+
// Trim outer segment whitespace only; keys and values stay exact.
|
|
43
|
+
const segment = rawSegment.trim();
|
|
44
|
+
if (segment === '') {
|
|
45
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Paddle signature header is malformed.');
|
|
46
|
+
}
|
|
47
|
+
// Reject missing keys, empty values, and extra equals signs.
|
|
48
|
+
const separator = segment.indexOf('=');
|
|
49
|
+
if (separator <= 0 || separator === segment.length - 1 || segment.indexOf('=', separator + 1) !== -1) {
|
|
50
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Paddle signature header is malformed.');
|
|
51
|
+
}
|
|
52
|
+
const key = segment.slice(0, separator);
|
|
53
|
+
const value = segment.slice(separator + 1);
|
|
54
|
+
if (!(0, shared_js_1.isSchemeToken)(key)) {
|
|
55
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Paddle signature header is malformed.');
|
|
56
|
+
}
|
|
57
|
+
if (key === TIMESTAMP_KEY) {
|
|
58
|
+
timestampCount += 1;
|
|
59
|
+
if (timestampCount > 1) {
|
|
60
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Paddle signature header is malformed.');
|
|
61
|
+
}
|
|
62
|
+
if (!(0, shared_js_1.isCanonicalTimestamp)(value)) {
|
|
63
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Paddle signature header is malformed.');
|
|
64
|
+
}
|
|
65
|
+
timestampText = value;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (key === SIGNATURE_KEY) {
|
|
69
|
+
const decoded = (0, shared_js_1.decodeHexSignature)(value);
|
|
70
|
+
// One malformed h1 poisons the whole header, even beside a valid h1.
|
|
71
|
+
if (decoded === null) {
|
|
72
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Paddle signature header is malformed.');
|
|
73
|
+
}
|
|
74
|
+
signatures.push(decoded);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Unknown fields are structurally validated, then ignored.
|
|
78
|
+
if (!OPAQUE_VALUE.test(value)) {
|
|
79
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Paddle signature header is malformed.');
|
|
80
|
+
}
|
|
81
|
+
unknownSchemes += 1;
|
|
82
|
+
}
|
|
83
|
+
// Required-field checks run only after all structure/token validation.
|
|
84
|
+
if (signatures.length === 0) {
|
|
85
|
+
return unknownSchemes > 0
|
|
86
|
+
? (0, shared_js_1.failure)('unsupported-scheme', 'No supported Paddle signature scheme was provided.')
|
|
87
|
+
: (0, shared_js_1.failure)('missing-signature', 'No Paddle signature value was provided.');
|
|
88
|
+
}
|
|
89
|
+
if (timestampText === undefined) {
|
|
90
|
+
return (0, shared_js_1.failure)('missing-timestamp', 'No Paddle timestamp was provided.');
|
|
91
|
+
}
|
|
92
|
+
const identity = {
|
|
93
|
+
deliveryId: null,
|
|
94
|
+
eventId: null,
|
|
95
|
+
timestamp: Number(timestampText),
|
|
96
|
+
authenticatedFields: ['timestamp'],
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
signatures,
|
|
101
|
+
signedContent: (0, bytes_js_1.concatBytes)((0, bytes_js_1.utf8)(`${timestampText}:`), rawBody),
|
|
102
|
+
identity,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NormalizedHeaders, ParseResult } from '../internal/provider.cjs';
|
|
2
|
+
/**
|
|
3
|
+
* Paddle Billing: `Paddle-Signature: ts=<unix seconds>;h1=<64 hex>`.
|
|
4
|
+
* The signature covers the exact `ts` text, a colon, and the untouched raw
|
|
5
|
+
* body. Repeated `h1` values are a Hooklatch rotation accept-set; every `h1`
|
|
6
|
+
* must be valid. Unknown fields such as `h2` are validated then ignored.
|
|
7
|
+
* Freshness is enforced by core, never here.
|
|
8
|
+
* Source: https://developer.paddle.com/webhooks/about/signature-verification/
|
|
9
|
+
*/
|
|
10
|
+
export declare const paddleDescriptor: {
|
|
11
|
+
provider: "paddle";
|
|
12
|
+
decodeSecret(secret: string): Uint8Array;
|
|
13
|
+
parse(headers: NormalizedHeaders, rawBody: Uint8Array): ParseResult;
|
|
14
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPolarDescriptor = createPolarDescriptor;
|
|
4
|
+
const errors_js_1 = require("../errors.cjs");
|
|
5
|
+
const shared_js_1 = require("./shared.cjs");
|
|
6
|
+
const standard_webhooks_protocol_js_1 = require("./standard-webhooks-protocol.cjs");
|
|
7
|
+
const CANONICAL_HEADERS = {
|
|
8
|
+
id: 'webhook-id',
|
|
9
|
+
timestamp: 'webhook-timestamp',
|
|
10
|
+
signature: 'webhook-signature',
|
|
11
|
+
};
|
|
12
|
+
const currentDescriptor = (0, standard_webhooks_protocol_js_1.createStandardWebhooksProtocol)('polar', 'Polar', CANONICAL_HEADERS);
|
|
13
|
+
const legacyDescriptor = {
|
|
14
|
+
provider: 'polar',
|
|
15
|
+
decodeSecret(secret) {
|
|
16
|
+
return (0, shared_js_1.decodeLiteralSecret)(secret, 'Polar');
|
|
17
|
+
},
|
|
18
|
+
parse: currentDescriptor.parse,
|
|
19
|
+
};
|
|
20
|
+
function invalidOptions() {
|
|
21
|
+
throw new errors_js_1.HooklatchError('invalid-options', 'Invalid Polar provider options.');
|
|
22
|
+
}
|
|
23
|
+
function isConfigObject(value) {
|
|
24
|
+
if (value === null || typeof value !== 'object')
|
|
25
|
+
return false;
|
|
26
|
+
try {
|
|
27
|
+
return !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read the selected mode as an own field and snapshot it before returning the
|
|
35
|
+
* descriptor. Inherited fields are not configuration; an own `secretMode:
|
|
36
|
+
* undefined` is invalid rather than an omitted optional field.
|
|
37
|
+
*/
|
|
38
|
+
function snapshotSecretMode(options) {
|
|
39
|
+
if (options === undefined)
|
|
40
|
+
return 'current';
|
|
41
|
+
if (!isConfigObject(options))
|
|
42
|
+
invalidOptions();
|
|
43
|
+
let keys = [];
|
|
44
|
+
let hasMode = false;
|
|
45
|
+
let mode = undefined;
|
|
46
|
+
try {
|
|
47
|
+
keys = Reflect.ownKeys(options).filter((key) => Object.getOwnPropertyDescriptor(options, key)?.enumerable);
|
|
48
|
+
hasMode = Object.hasOwn(options, 'secretMode');
|
|
49
|
+
mode = hasMode ? options.secretMode : undefined;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
invalidOptions();
|
|
53
|
+
}
|
|
54
|
+
if (keys.some((key) => key !== 'secretMode'))
|
|
55
|
+
invalidOptions();
|
|
56
|
+
if (!hasMode)
|
|
57
|
+
return 'current';
|
|
58
|
+
if (mode !== 'current' && mode !== 'legacy')
|
|
59
|
+
invalidOptions();
|
|
60
|
+
return mode;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Polar webhooks use the canonical Standard Webhooks headers and signed
|
|
64
|
+
* content: `${webhook-id}.${webhook-timestamp}.${rawBody}`.
|
|
65
|
+
*
|
|
66
|
+
* `current` (the default) keeps the shared Standard Webhooks decoder: an
|
|
67
|
+
* optional `whsec_` prefix is removed and the remainder must decode as
|
|
68
|
+
* canonical Base64 to 24..64 bytes. `legacy` treats the complete secret string
|
|
69
|
+
* as literal UTF-8 bytes, including any `whsec_` prefix, with only the shared
|
|
70
|
+
* nonempty policy. Exactly one mode applies to every secret configured for the
|
|
71
|
+
* descriptor; there is no prefix inference or cross-mode fallback.
|
|
72
|
+
*
|
|
73
|
+
* Polar's SDK tries both key interpretations and floors fractional timestamps.
|
|
74
|
+
* Hooklatch deliberately does neither, and does not decode opaque bodies as
|
|
75
|
+
* text. Source:
|
|
76
|
+
* https://raw.githubusercontent.com/polarsource/polar/89917ad4fe752b06ff84bd31c313d2a53a6085f1/sdk/typescript/src/webhooks.ts
|
|
77
|
+
*/
|
|
78
|
+
function createPolarDescriptor(options) {
|
|
79
|
+
return snapshotSecretMode(options) === 'current' ? currentDescriptor : legacyDescriptor;
|
|
80
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProviderDescriptor } from '../internal/provider.cjs';
|
|
2
|
+
export interface PolarDescriptorOptions {
|
|
3
|
+
readonly secretMode?: 'current' | 'legacy';
|
|
4
|
+
}
|
|
5
|
+
type PolarDescriptor = {
|
|
6
|
+
readonly provider: 'polar';
|
|
7
|
+
} & Pick<ProviderDescriptor, 'decodeSecret' | 'parse'>;
|
|
8
|
+
/**
|
|
9
|
+
* Polar webhooks use the canonical Standard Webhooks headers and signed
|
|
10
|
+
* content: `${webhook-id}.${webhook-timestamp}.${rawBody}`.
|
|
11
|
+
*
|
|
12
|
+
* `current` (the default) keeps the shared Standard Webhooks decoder: an
|
|
13
|
+
* optional `whsec_` prefix is removed and the remainder must decode as
|
|
14
|
+
* canonical Base64 to 24..64 bytes. `legacy` treats the complete secret string
|
|
15
|
+
* as literal UTF-8 bytes, including any `whsec_` prefix, with only the shared
|
|
16
|
+
* nonempty policy. Exactly one mode applies to every secret configured for the
|
|
17
|
+
* descriptor; there is no prefix inference or cross-mode fallback.
|
|
18
|
+
*
|
|
19
|
+
* Polar's SDK tries both key interpretations and floors fractional timestamps.
|
|
20
|
+
* Hooklatch deliberately does neither, and does not decode opaque bodies as
|
|
21
|
+
* text. Source:
|
|
22
|
+
* https://raw.githubusercontent.com/polarsource/polar/89917ad4fe752b06ff84bd31c313d2a53a6085f1/sdk/typescript/src/webhooks.ts
|
|
23
|
+
*/
|
|
24
|
+
export declare function createPolarDescriptor(options?: PolarDescriptorOptions): PolarDescriptor;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.razorpayDescriptor = void 0;
|
|
4
|
+
const shared_js_1 = require("./shared.cjs");
|
|
5
|
+
const SIGNATURE_HEADER = 'x-razorpay-signature';
|
|
6
|
+
const EVENT_ID_HEADER = 'x-razorpay-event-id';
|
|
7
|
+
/**
|
|
8
|
+
* Razorpay `X-Razorpay-Signature`: a single hex HMAC-SHA256 over the raw request
|
|
9
|
+
* body only, keyed with the literal dashboard webhook secret. A missing,
|
|
10
|
+
* blank, prefixed, non-hex, wrong-length or comma-combined value fails closed.
|
|
11
|
+
* The optional `x-razorpay-event-id` is unsigned metadata and never proves
|
|
12
|
+
* authenticity.
|
|
13
|
+
*
|
|
14
|
+
* Sources:
|
|
15
|
+
* - https://razorpay.com/docs/webhooks/validate-test/
|
|
16
|
+
* - https://github.com/razorpay/razorpay-node/blob/b9e8527225a7322ba2671a3bc0b848fd019d2d8c/lib/utils/razorpay-utils.js
|
|
17
|
+
*
|
|
18
|
+
* No Razorpay-published signature vector is claimed; test anchors are synthetic.
|
|
19
|
+
*/
|
|
20
|
+
exports.razorpayDescriptor = {
|
|
21
|
+
provider: 'razorpay',
|
|
22
|
+
decodeSecret(secret) {
|
|
23
|
+
return (0, shared_js_1.decodeLiteralSecret)(secret, 'Razorpay');
|
|
24
|
+
},
|
|
25
|
+
parse(headers, rawBody) {
|
|
26
|
+
const raw = headers.get(SIGNATURE_HEADER);
|
|
27
|
+
if (raw === undefined || raw.trim() === '') {
|
|
28
|
+
return (0, shared_js_1.failure)('missing-signature', 'No Razorpay signature header was provided.');
|
|
29
|
+
}
|
|
30
|
+
const signature = (0, shared_js_1.decodeHexSignature)(raw);
|
|
31
|
+
if (signature === null) {
|
|
32
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Razorpay signature header is malformed.');
|
|
33
|
+
}
|
|
34
|
+
const eventId = headers.get(EVENT_ID_HEADER);
|
|
35
|
+
const identity = {
|
|
36
|
+
deliveryId: null,
|
|
37
|
+
eventId: eventId === undefined || eventId.trim() === '' ? null : eventId,
|
|
38
|
+
timestamp: null,
|
|
39
|
+
authenticatedFields: [],
|
|
40
|
+
};
|
|
41
|
+
return { ok: true, signatures: [signature], signedContent: rawBody, identity };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { NormalizedHeaders, ParseResult } from '../internal/provider.cjs';
|
|
2
|
+
/**
|
|
3
|
+
* Razorpay `X-Razorpay-Signature`: a single hex HMAC-SHA256 over the raw request
|
|
4
|
+
* body only, keyed with the literal dashboard webhook secret. A missing,
|
|
5
|
+
* blank, prefixed, non-hex, wrong-length or comma-combined value fails closed.
|
|
6
|
+
* The optional `x-razorpay-event-id` is unsigned metadata and never proves
|
|
7
|
+
* authenticity.
|
|
8
|
+
*
|
|
9
|
+
* Sources:
|
|
10
|
+
* - https://razorpay.com/docs/webhooks/validate-test/
|
|
11
|
+
* - https://github.com/razorpay/razorpay-node/blob/b9e8527225a7322ba2671a3bc0b848fd019d2d8c/lib/utils/razorpay-utils.js
|
|
12
|
+
*
|
|
13
|
+
* No Razorpay-published signature vector is claimed; test anchors are synthetic.
|
|
14
|
+
*/
|
|
15
|
+
export declare const razorpayDescriptor: {
|
|
16
|
+
provider: "razorpay";
|
|
17
|
+
decodeSecret(secret: string): Uint8Array;
|
|
18
|
+
parse(headers: NormalizedHeaders, rawBody: Uint8Array): ParseResult;
|
|
19
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.replicateDescriptor = void 0;
|
|
4
|
+
const standard_webhooks_protocol_js_1 = require("./standard-webhooks-protocol.cjs");
|
|
5
|
+
/**
|
|
6
|
+
* Replicate webhooks use the canonical Standard Webhooks `webhook-id`,
|
|
7
|
+
* `webhook-timestamp` and `webhook-signature` headers. The secret is
|
|
8
|
+
* Base64-decoded (an optional `whsec_` prefix is stripped) and must decode to
|
|
9
|
+
* 24-64 bytes; the signature covers the exact
|
|
10
|
+
* `${webhook-id}.${webhook-timestamp}.` bytes followed by the raw body.
|
|
11
|
+
*
|
|
12
|
+
* These headers authenticate protocol bytes, not a unique Replicate identity.
|
|
13
|
+
* A signature minted by another canonical alias verifies here when the same
|
|
14
|
+
* secret is configured, so use endpoint-specific secrets. No prediction or
|
|
15
|
+
* event payload is parsed and no resource id is inferred.
|
|
16
|
+
*
|
|
17
|
+
* No Replicate-published signature vector is claimed; the direct tests use
|
|
18
|
+
* synthetic anchors. Source:
|
|
19
|
+
* https://replicate.com/docs/topics/webhooks/verify-webhook
|
|
20
|
+
*/
|
|
21
|
+
exports.replicateDescriptor = (0, standard_webhooks_protocol_js_1.createStandardWebhooksProtocol)('replicate', 'Replicate', {
|
|
22
|
+
id: 'webhook-id',
|
|
23
|
+
timestamp: 'webhook-timestamp',
|
|
24
|
+
signature: 'webhook-signature',
|
|
25
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replicate webhooks use the canonical Standard Webhooks `webhook-id`,
|
|
3
|
+
* `webhook-timestamp` and `webhook-signature` headers. The secret is
|
|
4
|
+
* Base64-decoded (an optional `whsec_` prefix is stripped) and must decode to
|
|
5
|
+
* 24-64 bytes; the signature covers the exact
|
|
6
|
+
* `${webhook-id}.${webhook-timestamp}.` bytes followed by the raw body.
|
|
7
|
+
*
|
|
8
|
+
* These headers authenticate protocol bytes, not a unique Replicate identity.
|
|
9
|
+
* A signature minted by another canonical alias verifies here when the same
|
|
10
|
+
* secret is configured, so use endpoint-specific secrets. No prediction or
|
|
11
|
+
* event payload is parsed and no resource id is inferred.
|
|
12
|
+
*
|
|
13
|
+
* No Replicate-published signature vector is claimed; the direct tests use
|
|
14
|
+
* synthetic anchors. Source:
|
|
15
|
+
* https://replicate.com/docs/topics/webhooks/verify-webhook
|
|
16
|
+
*/
|
|
17
|
+
export declare const replicateDescriptor: {
|
|
18
|
+
readonly provider: "replicate";
|
|
19
|
+
} & Pick<import("../internal/provider.cjs").ProviderDescriptor, "decodeSecret" | "parse">;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanityDescriptor = void 0;
|
|
4
|
+
const bytes_js_1 = require("../internal/bytes.cjs");
|
|
5
|
+
const shared_js_1 = require("./shared.cjs");
|
|
6
|
+
const SIGNATURE_HEADER = 'sanity-webhook-signature';
|
|
7
|
+
const TIMESTAMP_FIELD = 't=';
|
|
8
|
+
const SIGNATURE_FIELD = 'v1=';
|
|
9
|
+
const DIGEST_CHARACTERS = 43;
|
|
10
|
+
const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
11
|
+
/**
|
|
12
|
+
* Sanity does not send signed payloads before 2021-01-01T00:00:00.000Z, so a
|
|
13
|
+
* millisecond timestamp below this bound is not a Sanity delivery. The bound
|
|
14
|
+
* mirrors the reference toolkit; the failure code is Hooklatch policy.
|
|
15
|
+
*/
|
|
16
|
+
const MINIMUM_TIMESTAMP_MS = 1609459200000;
|
|
17
|
+
/**
|
|
18
|
+
* Split the exact accepted header grammar `t=<ms>,v1=<digest>`.
|
|
19
|
+
*
|
|
20
|
+
* The fields are ordered, appear exactly once, and carry no whitespace, extra
|
|
21
|
+
* fields, duplicate fields, semicolon separators, or alternate versions. Any
|
|
22
|
+
* other structure is rejected instead of being repaired.
|
|
23
|
+
*/
|
|
24
|
+
function splitHeader(raw) {
|
|
25
|
+
const separator = raw.indexOf(',');
|
|
26
|
+
if (separator < 0)
|
|
27
|
+
return null;
|
|
28
|
+
const timestampField = raw.slice(0, separator);
|
|
29
|
+
const signatureField = raw.slice(separator + 1);
|
|
30
|
+
if (!timestampField.startsWith(TIMESTAMP_FIELD))
|
|
31
|
+
return null;
|
|
32
|
+
if (!signatureField.startsWith(SIGNATURE_FIELD))
|
|
33
|
+
return null;
|
|
34
|
+
const timestampText = timestampField.slice(TIMESTAMP_FIELD.length);
|
|
35
|
+
const digest = signatureField.slice(SIGNATURE_FIELD.length);
|
|
36
|
+
if (timestampText.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
return { timestampText, digest };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Encode the 32 decoded digest bytes as canonical unpadded Base64url.
|
|
42
|
+
*
|
|
43
|
+
* 32 bytes are 256 bits: 42 characters carry 252 of them and the last four
|
|
44
|
+
* bits close the 43rd character.
|
|
45
|
+
*/
|
|
46
|
+
function encodeBase64Url(bytes) {
|
|
47
|
+
let output = '';
|
|
48
|
+
let buffer = 0;
|
|
49
|
+
let bits = 0;
|
|
50
|
+
for (const byte of bytes) {
|
|
51
|
+
buffer = (buffer << 8) | byte;
|
|
52
|
+
bits += 8;
|
|
53
|
+
while (bits >= 6) {
|
|
54
|
+
bits -= 6;
|
|
55
|
+
output += BASE64URL_ALPHABET.charAt((buffer >>> bits) & 0x3f);
|
|
56
|
+
buffer &= (1 << bits) - 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return output + BASE64URL_ALPHABET.charAt((buffer << (6 - bits)) & 0x3f);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Decode exactly 43 canonical unpadded Base64url characters into 32 bytes.
|
|
63
|
+
*
|
|
64
|
+
* The 43 characters carry 258 bits, so 32 bytes consume 256 of them and the
|
|
65
|
+
* two remaining low bits of the final character are pad bits. Re-encoding the
|
|
66
|
+
* decoded bytes must reproduce the input exactly, which rejects a digest whose
|
|
67
|
+
* pad bits are not zero.
|
|
68
|
+
*/
|
|
69
|
+
function decodeBase64UrlDigest(value) {
|
|
70
|
+
if (value.length !== DIGEST_CHARACTERS)
|
|
71
|
+
return null;
|
|
72
|
+
const bytes = new Uint8Array(32);
|
|
73
|
+
let buffer = 0;
|
|
74
|
+
let bits = 0;
|
|
75
|
+
let offset = 0;
|
|
76
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
77
|
+
const digit = BASE64URL_ALPHABET.indexOf(value.charAt(index));
|
|
78
|
+
if (digit < 0)
|
|
79
|
+
return null;
|
|
80
|
+
buffer = (buffer << 6) | digit;
|
|
81
|
+
bits += 6;
|
|
82
|
+
if (bits < 8)
|
|
83
|
+
continue;
|
|
84
|
+
bits -= 8;
|
|
85
|
+
bytes[offset] = (buffer >>> bits) & 0xff;
|
|
86
|
+
offset += 1;
|
|
87
|
+
buffer &= (1 << bits) - 1;
|
|
88
|
+
}
|
|
89
|
+
return encodeBase64Url(bytes) === value ? bytes : null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Sanity: `sanity-webhook-signature: t=<unix milliseconds>,v1=<base64url>`.
|
|
93
|
+
*
|
|
94
|
+
* The HMAC-SHA256 covers UTF8(the original `t` text + ".") concatenated with
|
|
95
|
+
* the exact raw body. The secret is the literal, untrimmed UTF-8 text; it is
|
|
96
|
+
* never Base64-decoded. The digest is canonical unpadded Base64url for exactly
|
|
97
|
+
* 32 bytes, decoded by a descriptor-local decoder so the shared standard
|
|
98
|
+
* Base64 handling stays unchanged.
|
|
99
|
+
*
|
|
100
|
+
* The accepted header is exactly `t=<ms>,v1=<digest>`: ordered fields, one
|
|
101
|
+
* each, no whitespace anywhere or outer trimming, no extra or duplicate
|
|
102
|
+
* fields, no semicolon separators, no padding, and no alternate versions. A
|
|
103
|
+
* missing or blank header is `missing-signature`; every other nonmatching
|
|
104
|
+
* structure is `malformed-signature`, with no alternate reason precedence.
|
|
105
|
+
*
|
|
106
|
+
* `t` must be a canonical nonnegative safe decimal integer of milliseconds
|
|
107
|
+
* with no leading zeros, at or after 2021-01-01T00:00:00.000Z. Identity is
|
|
108
|
+
* `timestamp = Number(t) / 1000` with no delivery or event id; freshness is
|
|
109
|
+
* enforced by core and is never evaluated here.
|
|
110
|
+
*
|
|
111
|
+
* The reference SDK verifies UTF-8 text and refuses to sign an empty payload.
|
|
112
|
+
* This descriptor accepts empty, NUL, invalid UTF-8, and non-JSON bodies as
|
|
113
|
+
* opaque bytes, so it extends rather than matches that behavior, and it does
|
|
114
|
+
* not claim exact SDK parity or live interoperability. The strict grammar
|
|
115
|
+
* is checked at parse time. The reference decoder regex accepts spaces and
|
|
116
|
+
* trimming, but full SDK verification compares the original header to canonical
|
|
117
|
+
* re-encoding and also rejects these noncanonical header variants.
|
|
118
|
+
*
|
|
119
|
+
* Sources:
|
|
120
|
+
* - https://www.sanity.io/docs/content-lake/webhook-best-practices
|
|
121
|
+
* - https://github.com/sanity-io/webhook-toolkit/blob/e0c211ed6cd80465a70829e335c9e219f37839ba/src/signature.ts
|
|
122
|
+
*/
|
|
123
|
+
exports.sanityDescriptor = {
|
|
124
|
+
provider: 'sanity',
|
|
125
|
+
decodeSecret(secret) {
|
|
126
|
+
return (0, shared_js_1.decodeLiteralSecret)(secret, 'Sanity');
|
|
127
|
+
},
|
|
128
|
+
parse(headers, rawBody) {
|
|
129
|
+
const raw = headers.get(SIGNATURE_HEADER);
|
|
130
|
+
if (raw === undefined || raw.trim() === '') {
|
|
131
|
+
return (0, shared_js_1.failure)('missing-signature', 'No Sanity signature header was provided.');
|
|
132
|
+
}
|
|
133
|
+
const fields = splitHeader(raw);
|
|
134
|
+
if (fields === null) {
|
|
135
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Sanity signature header is malformed.');
|
|
136
|
+
}
|
|
137
|
+
if (!(0, shared_js_1.isCanonicalTimestamp)(fields.timestampText)) {
|
|
138
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Sanity signature header is malformed.');
|
|
139
|
+
}
|
|
140
|
+
const timestamp = Number(fields.timestampText);
|
|
141
|
+
if (timestamp < MINIMUM_TIMESTAMP_MS) {
|
|
142
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Sanity signature header is malformed.');
|
|
143
|
+
}
|
|
144
|
+
const digest = decodeBase64UrlDigest(fields.digest);
|
|
145
|
+
if (digest === null) {
|
|
146
|
+
return (0, shared_js_1.failure)('malformed-signature', 'The Sanity signature header is malformed.');
|
|
147
|
+
}
|
|
148
|
+
const identity = {
|
|
149
|
+
deliveryId: null,
|
|
150
|
+
eventId: null,
|
|
151
|
+
timestamp: timestamp / 1000,
|
|
152
|
+
authenticatedFields: ['timestamp'],
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
signatures: [digest],
|
|
157
|
+
signedContent: (0, bytes_js_1.concatBytes)((0, bytes_js_1.utf8)(`${fields.timestampText}.`), rawBody),
|
|
158
|
+
identity,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { NormalizedHeaders, ParseResult } from '../internal/provider.cjs';
|
|
2
|
+
/**
|
|
3
|
+
* Sanity: `sanity-webhook-signature: t=<unix milliseconds>,v1=<base64url>`.
|
|
4
|
+
*
|
|
5
|
+
* The HMAC-SHA256 covers UTF8(the original `t` text + ".") concatenated with
|
|
6
|
+
* the exact raw body. The secret is the literal, untrimmed UTF-8 text; it is
|
|
7
|
+
* never Base64-decoded. The digest is canonical unpadded Base64url for exactly
|
|
8
|
+
* 32 bytes, decoded by a descriptor-local decoder so the shared standard
|
|
9
|
+
* Base64 handling stays unchanged.
|
|
10
|
+
*
|
|
11
|
+
* The accepted header is exactly `t=<ms>,v1=<digest>`: ordered fields, one
|
|
12
|
+
* each, no whitespace anywhere or outer trimming, no extra or duplicate
|
|
13
|
+
* fields, no semicolon separators, no padding, and no alternate versions. A
|
|
14
|
+
* missing or blank header is `missing-signature`; every other nonmatching
|
|
15
|
+
* structure is `malformed-signature`, with no alternate reason precedence.
|
|
16
|
+
*
|
|
17
|
+
* `t` must be a canonical nonnegative safe decimal integer of milliseconds
|
|
18
|
+
* with no leading zeros, at or after 2021-01-01T00:00:00.000Z. Identity is
|
|
19
|
+
* `timestamp = Number(t) / 1000` with no delivery or event id; freshness is
|
|
20
|
+
* enforced by core and is never evaluated here.
|
|
21
|
+
*
|
|
22
|
+
* The reference SDK verifies UTF-8 text and refuses to sign an empty payload.
|
|
23
|
+
* This descriptor accepts empty, NUL, invalid UTF-8, and non-JSON bodies as
|
|
24
|
+
* opaque bytes, so it extends rather than matches that behavior, and it does
|
|
25
|
+
* not claim exact SDK parity or live interoperability. The strict grammar
|
|
26
|
+
* is checked at parse time. The reference decoder regex accepts spaces and
|
|
27
|
+
* trimming, but full SDK verification compares the original header to canonical
|
|
28
|
+
* re-encoding and also rejects these noncanonical header variants.
|
|
29
|
+
*
|
|
30
|
+
* Sources:
|
|
31
|
+
* - https://www.sanity.io/docs/content-lake/webhook-best-practices
|
|
32
|
+
* - https://github.com/sanity-io/webhook-toolkit/blob/e0c211ed6cd80465a70829e335c9e219f37839ba/src/signature.ts
|
|
33
|
+
*/
|
|
34
|
+
export declare const sanityDescriptor: {
|
|
35
|
+
provider: "sanity";
|
|
36
|
+
decodeSecret(secret: string): Uint8Array;
|
|
37
|
+
parse(headers: NormalizedHeaders, rawBody: Uint8Array): ParseResult;
|
|
38
|
+
};
|