@voltro/plugin-webhooks 0.1.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/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +4596 -0
- package/dist/errors.d.ts +93 -0
- package/dist/errors.js +14 -0
- package/dist/index.d.ts +849 -0
- package/dist/index.js +1009 -0
- package/dist/mixin.d.ts +51 -0
- package/dist/mixin.js +68 -0
- package/dist/providers/index.d.ts +166 -0
- package/dist/providers/index.js +88 -0
- package/dist/signing-BOUxHdAo.js +120 -0
- package/package.json +64 -0
package/dist/mixin.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { TableLike } from '@voltro/database';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One row per delivery attempt — NOT per emit. An emit fans out to
|
|
5
|
+
* N targets; each target then runs ≤ `maxAttempts` deliveries. The
|
|
6
|
+
* primary key is `(deliveryId, attempt)`; `deliveryId` is shared
|
|
7
|
+
* across retries of the SAME (event, payload, target) tuple so the
|
|
8
|
+
* dashboard groups them.
|
|
9
|
+
*
|
|
10
|
+
* Status lifecycle: `pending` (queued — the target was paused at emit
|
|
11
|
+
* time, or the attempt is rate-deferred to the next window) →
|
|
12
|
+
* `inFlight` → `succeeded` | `failed` | `retryScheduled`. On retry the
|
|
13
|
+
* workflow creates a new `(deliveryId, attempt+1)` row; on
|
|
14
|
+
* resume/deferral the SAME attempt-1 row transitions out of `pending`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const _voltroWebhookDeliveriesTable: TableLike;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fixed-window rate-limit counters — one row per (scope, minute
|
|
20
|
+
* bucket), where scope is `target:<targetId>` (per-target
|
|
21
|
+
* `rateLimitPerMinute`) or `event:<eventId>` (an outgoing event's
|
|
22
|
+
* `globalRateLimit`). The delivery workflow claims a slot via a CAS
|
|
23
|
+
* loop over `count` (see `rateLimit.ts`) BEFORE every wire POST, so
|
|
24
|
+
* the cap holds across replicas — the counter lives here, never in
|
|
25
|
+
* process memory.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately NOT tenant-scoped: rows carry only a scope key + an
|
|
28
|
+
* integer count (no payload, no secret, no tenant data), and the
|
|
29
|
+
* background delivery workflow that writes them has no request
|
|
30
|
+
* subject. The deterministic PK `<scope>@<bucket>` is the
|
|
31
|
+
* `insertIgnore` conflict target for the first-in-window create race.
|
|
32
|
+
*/
|
|
33
|
+
export declare const _voltroWebhookRateWindowsTable: TableLike;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One row per subscribed delivery target. Created via
|
|
37
|
+
* `webhooks.subscribe(...)`. Read-only from app code; mutate via
|
|
38
|
+
* the `webhooks` service so the framework can run validation +
|
|
39
|
+
* generate secrets + invalidate caches.
|
|
40
|
+
*/
|
|
41
|
+
export declare const _voltroWebhookTargetsTable: TableLike;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Return the framework-managed tables the webhooks plugin adds.
|
|
45
|
+
* Consumed by the plugin entry's `registerSchema` hook so the
|
|
46
|
+
* bootstrap migration creates the tables BEFORE any user
|
|
47
|
+
* migration runs.
|
|
48
|
+
*/
|
|
49
|
+
export declare const webhookTables: () => ReadonlyArray<TableLike>;
|
|
50
|
+
|
|
51
|
+
export { }
|
package/dist/mixin.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { boolean as e, id as t, integer as n, json as r, registerRetention as i, retentionTtlMsFromEnv as a, table as o, text as s, timestamp as c } from "@voltro/database";
|
|
2
|
+
import { tenant as l } from "@voltro/plugin-multitenancy/mixin";
|
|
3
|
+
//#region src/mixin.ts
|
|
4
|
+
var u = o("_voltro_webhook_targets", {
|
|
5
|
+
id: t({ prefix: "whtarget" }),
|
|
6
|
+
event: s(),
|
|
7
|
+
url: s(),
|
|
8
|
+
secret: s(),
|
|
9
|
+
signing: r(),
|
|
10
|
+
retry: r(),
|
|
11
|
+
filter: r().nullable(),
|
|
12
|
+
headers: r().nullable(),
|
|
13
|
+
rateLimitPerMinute: n().nullable(),
|
|
14
|
+
active: e().default(!0),
|
|
15
|
+
format: s().oneOf([
|
|
16
|
+
"json",
|
|
17
|
+
"form",
|
|
18
|
+
"xml"
|
|
19
|
+
]).default("json"),
|
|
20
|
+
autoDisableAfter: n().nullable(),
|
|
21
|
+
consecutiveFailures: n().default(0),
|
|
22
|
+
autoDisabledAt: c().nullable(),
|
|
23
|
+
autoDisableReason: s().nullable(),
|
|
24
|
+
payloadVersion: n().default(1),
|
|
25
|
+
description: s().nullable()
|
|
26
|
+
}).with(l()), d = o("_voltro_webhook_deliveries", {
|
|
27
|
+
id: t({ prefix: "whdeliv" }),
|
|
28
|
+
deliveryId: s(),
|
|
29
|
+
targetId: s(),
|
|
30
|
+
event: s(),
|
|
31
|
+
eventId: s().nullable(),
|
|
32
|
+
attempt: n(),
|
|
33
|
+
status: s().oneOf([
|
|
34
|
+
"pending",
|
|
35
|
+
"inFlight",
|
|
36
|
+
"succeeded",
|
|
37
|
+
"failed",
|
|
38
|
+
"retryScheduled"
|
|
39
|
+
]),
|
|
40
|
+
payload: r(),
|
|
41
|
+
responseStatus: n().nullable(),
|
|
42
|
+
responseBody: s().nullable(),
|
|
43
|
+
errorMessage: s().nullable(),
|
|
44
|
+
latencyMs: n().nullable(),
|
|
45
|
+
scheduledAt: c(),
|
|
46
|
+
nextAttemptAt: c().nullable()
|
|
47
|
+
}).with(l()), f = o("_voltro_webhook_rate_windows", {
|
|
48
|
+
id: t({ prefix: "whrate" }),
|
|
49
|
+
scope: s().maxLength(191),
|
|
50
|
+
bucket: n(),
|
|
51
|
+
count: n().default(0),
|
|
52
|
+
createdAt: c(),
|
|
53
|
+
updatedAt: c()
|
|
54
|
+
}), p = () => (i({
|
|
55
|
+
table: "_voltro_webhook_deliveries",
|
|
56
|
+
timeColumn: "createdAt",
|
|
57
|
+
ttlMs: a(process.env.VOLTRO_WEBHOOK_DELIVERIES_TTL_HOURS, 2160)
|
|
58
|
+
}), i({
|
|
59
|
+
table: "_voltro_webhook_rate_windows",
|
|
60
|
+
timeColumn: "createdAt",
|
|
61
|
+
ttlMs: 24 * 36e5
|
|
62
|
+
}), [
|
|
63
|
+
u,
|
|
64
|
+
d,
|
|
65
|
+
f
|
|
66
|
+
]);
|
|
67
|
+
//#endregion
|
|
68
|
+
export { d as _voltroWebhookDeliveriesTable, f as _voltroWebhookRateWindowsTable, u as _voltroWebhookTargetsTable, p as webhookTables };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
|
|
3
|
+
declare interface CustomSignatureScheme {
|
|
4
|
+
readonly _tag: 'custom';
|
|
5
|
+
readonly header: string;
|
|
6
|
+
readonly sign: (rawBody: Uint8Array, secret: string) => string;
|
|
7
|
+
readonly verify: (rawBody: Uint8Array, secret: string, signatureHeader: string) => boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export declare const genericProvider: (options?: {
|
|
11
|
+
readonly signatureHeader?: string;
|
|
12
|
+
readonly replayWindowSeconds?: number;
|
|
13
|
+
}) => WebhookProviderDescriptor;
|
|
14
|
+
|
|
15
|
+
export declare const githubProvider: () => WebhookProviderDescriptor;
|
|
16
|
+
|
|
17
|
+
declare type HmacAlgorithm = 'hmacSha256' | 'hmacSha1';
|
|
18
|
+
|
|
19
|
+
declare interface HmacSignatureScheme {
|
|
20
|
+
readonly _tag: 'hmac';
|
|
21
|
+
readonly algorithm: HmacAlgorithm;
|
|
22
|
+
/** Header the signature is written to / read from. Stripe uses
|
|
23
|
+
* `Stripe-Signature`, GitHub uses `X-Hub-Signature-256`, generic
|
|
24
|
+
* apps use `X-Webhook-Signature`. */
|
|
25
|
+
readonly header: string;
|
|
26
|
+
/** When `true`, the signed payload is `<timestamp>.<rawBody>` and
|
|
27
|
+
* the header carries both (`t=...,v1=...`). Recipients reject
|
|
28
|
+
* signatures whose `t` is older than `replayWindowSeconds`. */
|
|
29
|
+
readonly includeTimestamp: boolean;
|
|
30
|
+
/** Window during which a signed payload is replay-safe. Older
|
|
31
|
+
* signatures get rejected. Default 5 minutes. Only relevant when
|
|
32
|
+
* `includeTimestamp: true`. */
|
|
33
|
+
readonly replayWindowSeconds?: number;
|
|
34
|
+
/** Encoding the signature is rendered in. `hex` is most common;
|
|
35
|
+
* Slack uses `hex` prefixed with `v0=`. */
|
|
36
|
+
readonly encoding?: 'hex' | 'base64';
|
|
37
|
+
/** Prefix written before the hex/base64 signature in the header
|
|
38
|
+
* value. Slack: `v0=`. GitHub: `sha256=`. Default empty. */
|
|
39
|
+
readonly versionPrefix?: string;
|
|
40
|
+
/** Optional secret-rotation hint: a SECOND secret accepted during
|
|
41
|
+
* verification but never used for outgoing signing. Lets you
|
|
42
|
+
* rotate the primary secret without breaking in-flight inbound
|
|
43
|
+
* deliveries. */
|
|
44
|
+
readonly previousSecret?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare interface IncomingWebhookContext<Body> {
|
|
48
|
+
/** The fully-decoded body, validated against `payload`. */
|
|
49
|
+
readonly body: Body;
|
|
50
|
+
/** Raw bytes — needed to recompute signatures. Already used by
|
|
51
|
+
* the framework's middleware to verify the inbound signature
|
|
52
|
+
* before this handler runs; passed along in case the user wants
|
|
53
|
+
* to compute additional MAC's for downstream relays. */
|
|
54
|
+
readonly rawBody: Uint8Array;
|
|
55
|
+
/** Request headers (lowercased keys). The middleware has already
|
|
56
|
+
* consumed signature / idempotency headers. */
|
|
57
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
58
|
+
/** Idempotency key extracted by the middleware (provider-specific
|
|
59
|
+
* extraction — Stripe uses `Stripe-Signature`'s `t=`, GitHub uses
|
|
60
|
+
* `X-GitHub-Delivery`, generic uses `Idempotency-Key`). */
|
|
61
|
+
readonly idempotencyKey: string;
|
|
62
|
+
/** Workflow facade supplied by the framework runtime. Use this for
|
|
63
|
+
* verified external incoming calls that should start or signal
|
|
64
|
+
* durable workflows after signature + idempotency checks pass. */
|
|
65
|
+
readonly workflows?: IncomingWorkflowFacade;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
declare interface IncomingWebhookDescriptor<Body> {
|
|
69
|
+
readonly _tag: 'incomingWebhook';
|
|
70
|
+
readonly id: WebhookId;
|
|
71
|
+
/** URL path the framework mounts the route on. Defaults to
|
|
72
|
+
* `/webhooks/<id>` if absent. Use a custom path for legacy
|
|
73
|
+
* integrations (`/integrations/stripe/v1`). */
|
|
74
|
+
readonly path?: `/${string}`;
|
|
75
|
+
/** Signature scheme used to authenticate inbound requests. Reject
|
|
76
|
+
* on mismatch with a 401. `undefined` (the default) means NO
|
|
77
|
+
* verification — only acceptable when behind a separate trust
|
|
78
|
+
* boundary (gateway + IP allow-list). The dashboard surfaces a
|
|
79
|
+
* warning for unsigned incoming webhooks. */
|
|
80
|
+
readonly signature?: SignatureScheme;
|
|
81
|
+
/** Idempotency key extraction. Default `'Idempotency-Key'` header.
|
|
82
|
+
* Provider-templates override this to match the provider's wire
|
|
83
|
+
* format. */
|
|
84
|
+
readonly idempotency?: {
|
|
85
|
+
/** Header name OR a function that reads from headers/body. */
|
|
86
|
+
readonly from: string | ((headers: Readonly<Record<string, string>>, rawBody: Uint8Array) => string | undefined);
|
|
87
|
+
/** TTL the framework retains the key for de-dup. Default 7 days. */
|
|
88
|
+
readonly ttl?: '5m' | '1h' | '6h' | '1d' | '7d' | '30d';
|
|
89
|
+
};
|
|
90
|
+
/** Validated body schema. The framework decodes the request body
|
|
91
|
+
* AFTER signature verification, BEFORE the handler runs. */
|
|
92
|
+
readonly payload: Schema.Schema<Body>;
|
|
93
|
+
/** Body parser — JSON by default. Stripe / GitHub send `application/json`
|
|
94
|
+
* but other providers use `application/x-www-form-urlencoded`. */
|
|
95
|
+
readonly bodyType?: 'json' | 'form' | 'raw';
|
|
96
|
+
/** Provider preset — when set, the framework fills `signature` +
|
|
97
|
+
* `idempotency` + `bodyType` from the provider's known shape.
|
|
98
|
+
* Explicit fields above always win. */
|
|
99
|
+
readonly provider?: WebhookProviderDescriptor;
|
|
100
|
+
/** Typed handler. Returns void on success, throws to reject. The
|
|
101
|
+
* HTTP status is 2xx for success, 4xx for typed validation
|
|
102
|
+
* errors, 5xx for handler exceptions. The framework retries
|
|
103
|
+
* 5xx-classified failures via the provider's expected behavior
|
|
104
|
+
* (most providers retry their own POST on 5xx). */
|
|
105
|
+
readonly handler: (context: IncomingWebhookContext<Body>) => Promise<void> | void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
declare interface IncomingWorkflowFacade {
|
|
109
|
+
start<Payload = unknown>(workflowName: string, payload: Payload): Promise<{
|
|
110
|
+
readonly id: string;
|
|
111
|
+
readonly workflowName: string;
|
|
112
|
+
readonly executionId: string;
|
|
113
|
+
readonly status: 'running';
|
|
114
|
+
}>;
|
|
115
|
+
signal(target: {
|
|
116
|
+
readonly id?: string;
|
|
117
|
+
readonly executionId?: string;
|
|
118
|
+
readonly workflowName?: string;
|
|
119
|
+
}, signalName: string, payload?: unknown): Promise<{
|
|
120
|
+
readonly eventId: string;
|
|
121
|
+
}>;
|
|
122
|
+
update(target: {
|
|
123
|
+
readonly id?: string;
|
|
124
|
+
readonly executionId?: string;
|
|
125
|
+
readonly workflowName?: string;
|
|
126
|
+
}, updateName: string, payload?: unknown, options?: {
|
|
127
|
+
readonly timeoutMs?: number;
|
|
128
|
+
readonly pollIntervalMs?: number;
|
|
129
|
+
}): Promise<{
|
|
130
|
+
readonly eventId: string;
|
|
131
|
+
readonly updateId: string;
|
|
132
|
+
readonly completedEventId: string;
|
|
133
|
+
readonly result: unknown;
|
|
134
|
+
}>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
declare type SignatureScheme = HmacSignatureScheme | CustomSignatureScheme;
|
|
138
|
+
|
|
139
|
+
export declare const slackProvider: () => WebhookProviderDescriptor;
|
|
140
|
+
|
|
141
|
+
export declare const stripeProvider: (options?: {
|
|
142
|
+
/** Override the replay window. Stripe's default is 5 min;
|
|
143
|
+
* callers running tests sometimes want a wider window. */
|
|
144
|
+
readonly replayWindowSeconds?: number;
|
|
145
|
+
}) => WebhookProviderDescriptor;
|
|
146
|
+
|
|
147
|
+
/** Stable identifier for an event or webhook. Drives the database
|
|
148
|
+
* primary key, the inspect endpoint URL, the dashboard listing. Use
|
|
149
|
+
* dotted-camelCase (`order.completed`, `user.signedUp`). */
|
|
150
|
+
declare type WebhookId = string;
|
|
151
|
+
|
|
152
|
+
declare interface WebhookProviderDescriptor {
|
|
153
|
+
readonly _tag: 'webhookProvider';
|
|
154
|
+
readonly id: string;
|
|
155
|
+
/** Display name for the dashboard. */
|
|
156
|
+
readonly name: string;
|
|
157
|
+
readonly signature: SignatureScheme;
|
|
158
|
+
readonly idempotency: NonNullable<IncomingWebhookDescriptor<unknown>['idempotency']>;
|
|
159
|
+
readonly bodyType: 'json' | 'form' | 'raw';
|
|
160
|
+
/** Optional discriminator: provider-specific event-type extraction
|
|
161
|
+
* (e.g. Stripe's top-level `type` field). Used by typed handlers
|
|
162
|
+
* to narrow on payload variant. */
|
|
163
|
+
readonly eventTypeFrom?: (body: unknown) => string | undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { }
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { a as e, l as t, n, t as r } from "../signing-BOUxHdAo.js";
|
|
2
|
+
import { createHmac as i, timingSafeEqual as a } from "node:crypto";
|
|
3
|
+
//#region src/providers/stripe.ts
|
|
4
|
+
var o = (n) => t({
|
|
5
|
+
id: "stripe",
|
|
6
|
+
name: "Stripe",
|
|
7
|
+
signature: {
|
|
8
|
+
...e(),
|
|
9
|
+
...n?.replayWindowSeconds === void 0 ? {} : { replayWindowSeconds: n.replayWindowSeconds }
|
|
10
|
+
},
|
|
11
|
+
idempotency: {
|
|
12
|
+
from: (e, t) => {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(new TextDecoder().decode(t)).id;
|
|
15
|
+
} catch {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
ttl: "30d"
|
|
20
|
+
},
|
|
21
|
+
bodyType: "json",
|
|
22
|
+
eventTypeFrom: (e) => {
|
|
23
|
+
if (typeof e == "object" && e && "type" in e) {
|
|
24
|
+
let t = e.type;
|
|
25
|
+
return typeof t == "string" ? t : void 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}), s = () => t({
|
|
29
|
+
id: "github",
|
|
30
|
+
name: "GitHub",
|
|
31
|
+
signature: n(),
|
|
32
|
+
idempotency: {
|
|
33
|
+
from: "x-github-delivery",
|
|
34
|
+
ttl: "7d"
|
|
35
|
+
},
|
|
36
|
+
bodyType: "json",
|
|
37
|
+
eventTypeFrom: (e) => {}
|
|
38
|
+
}), c = () => ({
|
|
39
|
+
_tag: "custom",
|
|
40
|
+
header: "X-Slack-Signature",
|
|
41
|
+
sign: (e, t) => {
|
|
42
|
+
let n = `v0:${Math.floor(Date.now() / 1e3)}:${new TextDecoder().decode(e)}`;
|
|
43
|
+
return `v0=${i("sha256", t).update(n).digest("hex")}`;
|
|
44
|
+
},
|
|
45
|
+
verify: (e, t, n) => {
|
|
46
|
+
let r = n.indexOf("|");
|
|
47
|
+
if (r === -1) return !1;
|
|
48
|
+
let o = n.slice(0, r), s = n.slice(r + 1);
|
|
49
|
+
if (!/^\d+$/.test(o)) return !1;
|
|
50
|
+
let c = Math.floor(Date.now() / 1e3);
|
|
51
|
+
if (Math.abs(c - Number(o)) > 300) return !1;
|
|
52
|
+
let l = `v0:${o}:${new TextDecoder().decode(e)}`, u = `v0=${i("sha256", t).update(l).digest("hex")}`, d = Buffer.from(u, "utf8"), f = Buffer.from(s, "utf8");
|
|
53
|
+
return d.length === f.length ? a(d, f) : (a(d, Buffer.alloc(d.length)), !1);
|
|
54
|
+
}
|
|
55
|
+
}), l = () => t({
|
|
56
|
+
id: "slack",
|
|
57
|
+
name: "Slack",
|
|
58
|
+
signature: c(),
|
|
59
|
+
idempotency: {
|
|
60
|
+
from: "x-slack-request-timestamp",
|
|
61
|
+
ttl: "5m"
|
|
62
|
+
},
|
|
63
|
+
bodyType: "json",
|
|
64
|
+
eventTypeFrom: (e) => {
|
|
65
|
+
if (typeof e == "object" && e && "event" in e) {
|
|
66
|
+
let t = e.event;
|
|
67
|
+
if (typeof t == "object" && t && "type" in t) {
|
|
68
|
+
let e = t.type;
|
|
69
|
+
return typeof e == "string" ? e : void 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}), u = (e) => t({
|
|
74
|
+
id: "generic",
|
|
75
|
+
name: "Generic",
|
|
76
|
+
signature: {
|
|
77
|
+
...r(),
|
|
78
|
+
...e?.signatureHeader === void 0 ? {} : { header: e.signatureHeader },
|
|
79
|
+
...e?.replayWindowSeconds === void 0 ? {} : { replayWindowSeconds: e.replayWindowSeconds }
|
|
80
|
+
},
|
|
81
|
+
idempotency: {
|
|
82
|
+
from: "idempotency-key",
|
|
83
|
+
ttl: "7d"
|
|
84
|
+
},
|
|
85
|
+
bodyType: "json"
|
|
86
|
+
});
|
|
87
|
+
//#endregion
|
|
88
|
+
export { u as genericProvider, s as githubProvider, l as slackProvider, o as stripeProvider };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHmac as e, timingSafeEqual as t } from "node:crypto";
|
|
2
|
+
//#region src/dsl.ts
|
|
3
|
+
var n = (e) => ({
|
|
4
|
+
_tag: "outgoingEvent",
|
|
5
|
+
...e
|
|
6
|
+
}), r = (e) => ({
|
|
7
|
+
_tag: "incomingWebhook",
|
|
8
|
+
...e
|
|
9
|
+
}), i = (e) => ({
|
|
10
|
+
_tag: "webhookProvider",
|
|
11
|
+
...e
|
|
12
|
+
}), a = (e) => {
|
|
13
|
+
if (typeof e != "object" || !e) return !1;
|
|
14
|
+
let t = e._tag;
|
|
15
|
+
return t === "outgoingEvent" || t === "incomingWebhook" || t === "webhookProvider";
|
|
16
|
+
}, o = (e) => e === "hmacSha256" ? "sha256" : "sha1", s = (t, n, r, i = "hex") => {
|
|
17
|
+
let a = e(o(t), n);
|
|
18
|
+
return a.update(r), a.digest(i);
|
|
19
|
+
}, c = (e, t, n) => {
|
|
20
|
+
if (e._tag === "custom") return {
|
|
21
|
+
headerName: e.header,
|
|
22
|
+
headerValue: e.sign(t, n)
|
|
23
|
+
};
|
|
24
|
+
let r = e.encoding ?? "hex", i = e.versionPrefix ?? "";
|
|
25
|
+
if (e.includeTimestamp) {
|
|
26
|
+
let a = Math.floor(Date.now() / 1e3), o = Buffer.concat([Buffer.from(`${a}.`, "utf8"), Buffer.from(t)]), c = s(e.algorithm, n, o, r);
|
|
27
|
+
return {
|
|
28
|
+
headerName: e.header,
|
|
29
|
+
headerValue: `t=${a},${i}${c}`,
|
|
30
|
+
timestamp: a
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
let a = s(e.algorithm, n, t, r);
|
|
34
|
+
return {
|
|
35
|
+
headerName: e.header,
|
|
36
|
+
headerValue: `${i}${a}`
|
|
37
|
+
};
|
|
38
|
+
}, l = (e, t, n, r) => {
|
|
39
|
+
if (!r) return {
|
|
40
|
+
ok: !1,
|
|
41
|
+
reason: "missing signature header"
|
|
42
|
+
};
|
|
43
|
+
if (e._tag === "custom") return e.verify(t, n, r) ? { ok: !0 } : {
|
|
44
|
+
ok: !1,
|
|
45
|
+
reason: "custom verifier rejected"
|
|
46
|
+
};
|
|
47
|
+
let i = e.encoding ?? "hex", a = e.versionPrefix ?? "", o = (n) => {
|
|
48
|
+
if (e.includeTimestamp) {
|
|
49
|
+
let o = r.split(",").map((e) => e.trim()), c = o.find((e) => e.startsWith("t="));
|
|
50
|
+
if (!c) return {
|
|
51
|
+
ok: !1,
|
|
52
|
+
reason: "no timestamp in signature header"
|
|
53
|
+
};
|
|
54
|
+
let l = Number(c.slice(2));
|
|
55
|
+
if (!Number.isFinite(l)) return {
|
|
56
|
+
ok: !1,
|
|
57
|
+
reason: "invalid timestamp"
|
|
58
|
+
};
|
|
59
|
+
let d = e.replayWindowSeconds ?? 300, f = Math.floor(Date.now() / 1e3);
|
|
60
|
+
if (Math.abs(f - l) > d) return {
|
|
61
|
+
ok: !1,
|
|
62
|
+
reason: `replay window exceeded (${Math.abs(f - l)}s > ${d}s)`
|
|
63
|
+
};
|
|
64
|
+
let p = a.length > 0 ? o.find((e) => e.startsWith(a))?.slice(a.length) : o.find((e) => !e.startsWith("t="));
|
|
65
|
+
if (!p) return {
|
|
66
|
+
ok: !1,
|
|
67
|
+
reason: "no signature in header"
|
|
68
|
+
};
|
|
69
|
+
let m = Buffer.concat([Buffer.from(`${l}.`, "utf8"), Buffer.from(t)]);
|
|
70
|
+
return u(s(e.algorithm, n, m, i), p) ? { ok: !0 } : {
|
|
71
|
+
ok: !1,
|
|
72
|
+
reason: "signature mismatch"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
let o = a.length > 0 && r.startsWith(a) ? r.slice(a.length) : r;
|
|
76
|
+
return u(s(e.algorithm, n, t, i), o) ? { ok: !0 } : {
|
|
77
|
+
ok: !1,
|
|
78
|
+
reason: "signature mismatch"
|
|
79
|
+
};
|
|
80
|
+
}, c = o(n);
|
|
81
|
+
if (c.ok) return c;
|
|
82
|
+
if (e.previousSecret !== void 0) {
|
|
83
|
+
let t = o(e.previousSecret);
|
|
84
|
+
if (t.ok) return t;
|
|
85
|
+
}
|
|
86
|
+
return c;
|
|
87
|
+
}, u = (e, n) => {
|
|
88
|
+
let r = Buffer.from(e, "utf8"), i = Buffer.from(n, "utf8");
|
|
89
|
+
return r.length === i.length ? t(r, i) : (t(r, Buffer.alloc(r.length)), !1);
|
|
90
|
+
}, d = (e = "Stripe-Signature") => ({
|
|
91
|
+
_tag: "hmac",
|
|
92
|
+
algorithm: "hmacSha256",
|
|
93
|
+
header: e,
|
|
94
|
+
includeTimestamp: !0,
|
|
95
|
+
replayWindowSeconds: 300,
|
|
96
|
+
encoding: "hex",
|
|
97
|
+
versionPrefix: "v1="
|
|
98
|
+
}), f = () => ({
|
|
99
|
+
_tag: "hmac",
|
|
100
|
+
algorithm: "hmacSha256",
|
|
101
|
+
header: "X-Hub-Signature-256",
|
|
102
|
+
includeTimestamp: !1,
|
|
103
|
+
encoding: "hex",
|
|
104
|
+
versionPrefix: "sha256="
|
|
105
|
+
}), p = () => ({
|
|
106
|
+
_tag: "custom",
|
|
107
|
+
header: "X-Slack-Signature",
|
|
108
|
+
sign: (e, t) => `v0=${s("hmacSha256", t, `v0:${Math.floor(Date.now() / 1e3)}:${Buffer.from(e).toString("utf8")}`)}`,
|
|
109
|
+
verify: (e, t, n) => !1
|
|
110
|
+
}), m = () => ({
|
|
111
|
+
_tag: "hmac",
|
|
112
|
+
algorithm: "hmacSha256",
|
|
113
|
+
header: "X-Webhook-Signature",
|
|
114
|
+
includeTimestamp: !0,
|
|
115
|
+
replayWindowSeconds: 300,
|
|
116
|
+
encoding: "hex",
|
|
117
|
+
versionPrefix: "v1="
|
|
118
|
+
});
|
|
119
|
+
//#endregion
|
|
120
|
+
export { d as a, n as c, p as i, i as l, f as n, l as o, c as r, r as s, m as t, a as u };
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voltro/plugin-webhooks",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Webhooks plugin — first-class outgoing (multi-target, durable-workflow delivery, HMAC signing, retry policies, rate limits) + incoming (signature verification, idempotency, provider templates for Stripe / GitHub / Slack / generic).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"voltro",
|
|
7
|
+
"typescript",
|
|
8
|
+
"framework"
|
|
9
|
+
],
|
|
10
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
11
|
+
"homepage": "https://voltro.dev",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"email": "support@voltro.dev"
|
|
14
|
+
},
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Voltro UG",
|
|
17
|
+
"url": "https://voltro.dev"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./mixin": {
|
|
27
|
+
"types": "./dist/mixin.d.ts",
|
|
28
|
+
"import": "./dist/mixin.js",
|
|
29
|
+
"default": "./dist/mixin.js"
|
|
30
|
+
},
|
|
31
|
+
"./providers": {
|
|
32
|
+
"types": "./dist/providers/index.d.ts",
|
|
33
|
+
"import": "./dist/providers/index.js",
|
|
34
|
+
"default": "./dist/providers/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./errors": {
|
|
37
|
+
"types": "./dist/errors.d.ts",
|
|
38
|
+
"import": "./dist/errors.js",
|
|
39
|
+
"default": "./dist/errors.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"main": "./dist/index.js",
|
|
43
|
+
"module": "./dist/index.js",
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=24.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@effect/workflow": "^0.18.2",
|
|
51
|
+
"@voltro/database": "0.1.0",
|
|
52
|
+
"@voltro/integration-http": "0.1.0",
|
|
53
|
+
"@voltro/logger": "0.1.0",
|
|
54
|
+
"@voltro/plugin-multitenancy": "0.1.0",
|
|
55
|
+
"@voltro/protocol": "0.1.0",
|
|
56
|
+
"@voltro/runtime": "0.1.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"effect": "^3.21.4"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|