@voltro/plugin-webhooks 0.33.0 → 0.35.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 +1968 -0
- package/dist/index.d.ts +365 -38
- package/dist/index.js +400 -346
- package/dist/mixin.d.ts +2 -2
- package/dist/mixin.js +2 -0
- package/dist/providers/index.d.ts +39 -6
- package/dist/providers/index.js +16 -18
- package/dist/signing-DeHObNv6.js +234 -0
- package/package.json +7 -7
- package/dist/signing-DUG4JWwk.js +0 -117
package/dist/index.d.ts
CHANGED
|
@@ -8,8 +8,10 @@ import { EventDescriptor } from '@voltro/protocol';
|
|
|
8
8
|
import { EventWebhookSpec } from '@voltro/protocol';
|
|
9
9
|
import { FieldDefinitions } from '@voltro/database';
|
|
10
10
|
import { OutboxHandlerDefinition } from '@voltro/runtime';
|
|
11
|
+
import { PluginPermission } from '@voltro/protocol';
|
|
11
12
|
import { Schema } from 'effect';
|
|
12
13
|
import { Table } from '@voltro/database';
|
|
14
|
+
import { VoltroPlugin } from '@voltro/protocol';
|
|
13
15
|
import { Workflow } from '@effect/workflow';
|
|
14
16
|
import { WorkflowEngine } from '@effect/workflow/WorkflowEngine';
|
|
15
17
|
import { WorkflowInstance } from '@effect/workflow/WorkflowEngine';
|
|
@@ -93,7 +95,10 @@ export declare interface CustomSignatureScheme {
|
|
|
93
95
|
readonly _tag: 'custom';
|
|
94
96
|
readonly header: string;
|
|
95
97
|
readonly sign: (rawBody: Uint8Array, secret: string) => string;
|
|
96
|
-
|
|
98
|
+
/** Verify against the request's FULL header map (lowercased keys) — a scheme
|
|
99
|
+
* whose timestamp lives in a second header can read it here instead of
|
|
100
|
+
* relying on a caller to splice the two values together. */
|
|
101
|
+
readonly verify: (rawBody: Uint8Array, secret: string, headers: Readonly<Record<string, string>>) => boolean;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
/** The structural slice of a `defineEvent` descriptor this plugin needs.
|
|
@@ -117,8 +122,17 @@ export declare interface DeclaredEventLike {
|
|
|
117
122
|
* register routes. */
|
|
118
123
|
export declare const defaultIncomingPath: (webhookId: string) => `/${string}`;
|
|
119
124
|
|
|
120
|
-
/**
|
|
121
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The DEFAULT for a new outgoing subscription: Standard Webhooks v1.0.0.
|
|
127
|
+
*
|
|
128
|
+
* An interoperable spec beats a house format for the one signature shape a
|
|
129
|
+
* third party has to implement against. Every Standard-Webhooks consumer
|
|
130
|
+
* library — and the package `voltro webhooks consumer` generates — verifies
|
|
131
|
+
* these deliveries with no per-vendor code. That is the whole argument for a
|
|
132
|
+
* spec, and it only pays if the spec is what we send by DEFAULT rather than
|
|
133
|
+
* what you can opt into.
|
|
134
|
+
*/
|
|
135
|
+
export declare const defaultOutgoingSignature: () => StandardWebhooksSignatureScheme;
|
|
122
136
|
|
|
123
137
|
/** Sensible default for new outgoing subscriptions: exponential, 8
|
|
124
138
|
* attempts, 5s → 1h. Roughly: 5s, 10s, 20s, 40s, 80s, 160s, 320s,
|
|
@@ -154,7 +168,16 @@ declare interface DeliverInput {
|
|
|
154
168
|
readonly attemptEpoch?: number;
|
|
155
169
|
}
|
|
156
170
|
|
|
157
|
-
export declare const deliverWebhookWorkflow: Workflow.Workflow<"voltro.deliverWebhook",
|
|
171
|
+
export declare const deliverWebhookWorkflow: Workflow.Workflow<"voltro.deliverWebhook", Schema.Struct<{
|
|
172
|
+
deliveryId: typeof Schema.String;
|
|
173
|
+
targetId: typeof Schema.String;
|
|
174
|
+
event: typeof Schema.String;
|
|
175
|
+
eventId: typeof Schema.String;
|
|
176
|
+
payloadJson: typeof Schema.String;
|
|
177
|
+
attemptEpoch: Schema.optionalWith<typeof Schema.Number, {
|
|
178
|
+
default: () => number;
|
|
179
|
+
}>;
|
|
180
|
+
}>, Schema.Struct<{
|
|
158
181
|
finalStatus: Schema.Literal<["succeeded", "failed", "deferred"]>;
|
|
159
182
|
attempts: typeof Schema.Number;
|
|
160
183
|
}>, typeof Schema.Never>;
|
|
@@ -168,6 +191,17 @@ declare interface DeliverWorkflowOptions {
|
|
|
168
191
|
/** The fixed window backing "per minute" — injectable for tests
|
|
169
192
|
* (production uses the 60s default). */
|
|
170
193
|
readonly rateWindowMs?: number;
|
|
194
|
+
/**
|
|
195
|
+
* Per-attempt wire timeout, milliseconds. Default 30 000.
|
|
196
|
+
*
|
|
197
|
+
* There was no timeout at all before this, so one receiver that accepted the
|
|
198
|
+
* connection and never answered held a durable workflow — and its rate-limit
|
|
199
|
+
* slot — indefinitely. Standard Webhooks recommends "somewhere between 15 and
|
|
200
|
+
* 30s"; the top of that band is the default because a slow-but-alive receiver
|
|
201
|
+
* being cut off produces a retry storm, which is the worse of the two
|
|
202
|
+
* failures. Also settable per deployment with `VOLTRO_WEBHOOK_TIMEOUT_MS`.
|
|
203
|
+
*/
|
|
204
|
+
readonly timeoutMs?: number;
|
|
171
205
|
}
|
|
172
206
|
|
|
173
207
|
/** `getDelivery` adds the two heavy columns `listDeliveries` omits. */
|
|
@@ -282,6 +316,26 @@ export declare const fastRetryPolicy: () => RetryPolicy;
|
|
|
282
316
|
|
|
283
317
|
export declare const generateSecret: () => string;
|
|
284
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Mint a spec-shaped signing secret: `whsec_` + base64 of 32 random bytes.
|
|
321
|
+
*
|
|
322
|
+
* 32 bytes sits inside the spec's 24..64 band and matches the SHA-256 block
|
|
323
|
+
* this key feeds. Per the spec, keys "should be unique per endpoint" — which is
|
|
324
|
+
* already how `subscribe` mints them.
|
|
325
|
+
*/
|
|
326
|
+
export declare const generateStandardWebhooksSecret: () => string;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The house HMAC shape: `X-Webhook-Signature: t=NNN,v1=hex` over
|
|
330
|
+
* `<timestamp>.<body>`.
|
|
331
|
+
*
|
|
332
|
+
* Kept as a named CHOICE, not as a default and not as a fallback: a receiver
|
|
333
|
+
* that already implemented this exact format should not have to change to keep
|
|
334
|
+
* receiving. It is a peer of `stripeSignature()` / `githubSignature()` — a
|
|
335
|
+
* specific counterparty's format — rather than a second general-purpose path.
|
|
336
|
+
*/
|
|
337
|
+
export declare const genericHmacSignature: () => HmacSignatureScheme;
|
|
338
|
+
|
|
285
339
|
export declare const getIdempotencyCache: () => IdempotencyCache;
|
|
286
340
|
|
|
287
341
|
/** GitHub-style: `X-Hub-Signature-256: sha256=hex` (no timestamp). */
|
|
@@ -402,11 +456,24 @@ export declare interface IncomingWebhookDescriptor<Body> {
|
|
|
402
456
|
* integrations (`/integrations/stripe/v1`). */
|
|
403
457
|
readonly path?: `/${string}`;
|
|
404
458
|
/** Signature scheme used to authenticate inbound requests. Reject
|
|
405
|
-
* on mismatch with a 401.
|
|
406
|
-
* verification — only acceptable when behind a separate trust
|
|
407
|
-
* boundary (gateway + IP allow-list). The dashboard surfaces a
|
|
408
|
-
* warning for unsigned incoming webhooks. */
|
|
459
|
+
* on mismatch with a 401. Usually filled in by `provider`. */
|
|
409
460
|
readonly signature?: SignatureScheme;
|
|
461
|
+
/**
|
|
462
|
+
* How this endpoint authenticates its caller. An incoming webhook is a
|
|
463
|
+
* PUBLIC POST that runs your application code, so the framework will not
|
|
464
|
+
* mount one that has made no decision here: `mountIncomingWebhook` throws
|
|
465
|
+
* at boot when there is no effective `signature` (from this descriptor or
|
|
466
|
+
* from `provider`) AND no explicit value below.
|
|
467
|
+
*
|
|
468
|
+
* - omitted → derived. `signature` / `provider` present → `'signature'`;
|
|
469
|
+
* nothing present → boot refuses.
|
|
470
|
+
* - `'provider'` → the handler verifies with the provider's own SDK
|
|
471
|
+
* (Stripe's `constructEvent`, etc.). The framework's generic HMAC layer
|
|
472
|
+
* is not the authority and does not require a framework-side secret.
|
|
473
|
+
* - `'none'` → deliberately unverified, because a gateway + IP allow-list
|
|
474
|
+
* owns the trust boundary. Logged as a warning at every boot, on purpose.
|
|
475
|
+
*/
|
|
476
|
+
readonly verification?: 'signature' | 'provider' | 'none';
|
|
410
477
|
/** Idempotency key extraction. Default `'Idempotency-Key'` header.
|
|
411
478
|
* Provider-templates override this to match the provider's wire
|
|
412
479
|
* format. */
|
|
@@ -434,6 +501,26 @@ export declare interface IncomingWebhookDescriptor<Body> {
|
|
|
434
501
|
readonly handler: (context: IncomingWebhookContext<Body>) => Promise<void> | void;
|
|
435
502
|
}
|
|
436
503
|
|
|
504
|
+
/** How a mounted incoming webhook authenticates its caller. Mirrors
|
|
505
|
+
* `@voltro/runtime`'s `WebhookVerification` STRUCTURALLY — this package must
|
|
506
|
+
* not depend on the runtime (it is imported by browser-safe descriptor files),
|
|
507
|
+
* and the runtime reads the stamped value back by shape. */
|
|
508
|
+
export declare type IncomingWebhookVerification = 'signature' | 'provider' | 'none';
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* The verification decision for a descriptor, WITHOUT mounting it.
|
|
512
|
+
*
|
|
513
|
+
* One function, two readers: `mountIncomingWebhook` (which turns `null` into a
|
|
514
|
+
* boot refusal) and `voltro doctor` (which reports it as a preflight, before a
|
|
515
|
+
* deploy discovers it). A doctor that re-derived this could disagree with the
|
|
516
|
+
* boot it is supposed to predict — the failure shape a preflight must not have.
|
|
517
|
+
*
|
|
518
|
+
* `null` = nothing authenticates the caller. Declaring nothing, and declaring
|
|
519
|
+
* `'signature'` with no scheme to verify AGAINST, are the same open endpoint,
|
|
520
|
+
* so both answer `null`.
|
|
521
|
+
*/
|
|
522
|
+
export declare const incomingWebhookVerification: <Body>(descriptor: IncomingWebhookDescriptor<Body>) => IncomingWebhookVerification | null;
|
|
523
|
+
|
|
437
524
|
export declare interface IncomingWorkflowFacade {
|
|
438
525
|
start<Payload = unknown>(workflowName: string, payload: Payload): Promise<WorkflowRunHandle>;
|
|
439
526
|
signal(target: {
|
|
@@ -458,6 +545,9 @@ export declare interface IncomingWorkflowFacade {
|
|
|
458
545
|
}>;
|
|
459
546
|
}
|
|
460
547
|
|
|
548
|
+
/** Is this a spec-shaped secret? Used to decide whether a target needs one minted. */
|
|
549
|
+
export declare const isStandardWebhooksSecret: (secret: string) => boolean;
|
|
550
|
+
|
|
461
551
|
/** Type guard for file-walker discovery — every `*.webhook.tsx`
|
|
462
552
|
* must default-export one of these. */
|
|
463
553
|
export declare const isWebhookDescriptor: (value: unknown) => value is WebhookDescriptor;
|
|
@@ -519,16 +609,24 @@ emitter: () => Pick<WebhooksServiceShape, "emit"> | undefined) => OutboxHandlerD
|
|
|
519
609
|
*/
|
|
520
610
|
export declare const matchesFilter: (filter: WebhookFilter | Readonly<Record<string, unknown>> | null, payload: unknown) => boolean;
|
|
521
611
|
|
|
612
|
+
/** A mounted handler, carrying its verification declaration. */
|
|
613
|
+
export declare type MountedIncomingWebhook = ((request: IncomingRequest) => Promise<IncomingResponse>) & {
|
|
614
|
+
readonly [WEBHOOK_VERIFICATION_PROPERTY]: IncomingWebhookVerification;
|
|
615
|
+
};
|
|
616
|
+
|
|
522
617
|
/**
|
|
523
618
|
* Build a request handler for a specific `IncomingWebhookDescriptor`.
|
|
524
619
|
* The returned function is what stage 4 mounts on the HTTP layer.
|
|
620
|
+
*
|
|
621
|
+
* @throws {UnverifiedIncomingWebhook} at mount time when the descriptor has no
|
|
622
|
+
* effective signature scheme and no explicit `verification`.
|
|
525
623
|
*/
|
|
526
|
-
export declare const mountIncomingWebhook: <Body>(descriptor: IncomingWebhookDescriptor<Body>, options: MountOptions) =>
|
|
624
|
+
export declare const mountIncomingWebhook: <Body>(descriptor: IncomingWebhookDescriptor<Body>, options: MountOptions) => MountedIncomingWebhook;
|
|
527
625
|
|
|
528
626
|
export declare interface MountOptions {
|
|
529
|
-
/** Resolves the per-webhook signing secret.
|
|
530
|
-
*
|
|
531
|
-
*
|
|
627
|
+
/** Resolves the per-webhook signing secret. Returning `null` for a
|
|
628
|
+
* webhook whose verification is `'signature'` makes every delivery
|
|
629
|
+
* answer 503 — it does NOT skip verification. */
|
|
532
630
|
readonly resolveSecret: (webhookId: string) => Promise<string | null>;
|
|
533
631
|
/** Optional idempotency cache override. Defaults to the
|
|
534
632
|
* process-singleton LRU. Pass a custom cache for tests or for a
|
|
@@ -562,9 +660,16 @@ export declare interface OutgoingEventDescriptor<Payload> {
|
|
|
562
660
|
* (pass the descriptor itself to `emit(descriptor, payload)` to
|
|
563
661
|
* type the payload; either way `emit` DECODES the payload against
|
|
564
662
|
* this schema and rejects a mismatch with `WebhookPayloadInvalid`
|
|
565
|
-
* before any delivery is created).
|
|
566
|
-
*
|
|
567
|
-
*
|
|
663
|
+
* before any delivery is created).
|
|
664
|
+
*
|
|
665
|
+
* The BODY on the wire is this payload and nothing else — no envelope. It
|
|
666
|
+
* used to say recipients receive `{ event, eventId, occurredAt, payload }`,
|
|
667
|
+
* which was never true of the code: `emit` posts `JSON.stringify(payload)`
|
|
668
|
+
* verbatim (asserted in `deliverWorkflow.integration.test.ts`). The event
|
|
669
|
+
* name, ids and attempt count ride in HEADERS (`x-voltro-event`,
|
|
670
|
+
* `webhook-id`, `x-voltro-attempt`), which is also where Standard Webhooks
|
|
671
|
+
* puts the delivery metadata — so the generated consumer package reads them
|
|
672
|
+
* from there. */
|
|
568
673
|
readonly payload: Schema.Schema<Payload>;
|
|
569
674
|
/** Schema version. Increment when the payload shape changes in a
|
|
570
675
|
* way subscribers must adapt to. The dashboard surfaces version
|
|
@@ -628,7 +733,17 @@ export declare interface RateScope {
|
|
|
628
733
|
* target when the auto-disable fires. Returns the post-write streak +
|
|
629
734
|
* whether THIS call auto-disabled.
|
|
630
735
|
*/
|
|
631
|
-
export declare const recordDeliveryOutcome: (store: DataStore, targetId: string, succeeded: boolean, reason: string, now?: Date
|
|
736
|
+
export declare const recordDeliveryOutcome: (store: DataStore, targetId: string, succeeded: boolean, reason: string, now?: Date,
|
|
737
|
+
/**
|
|
738
|
+
* Disable the target on THIS failure, whatever the streak says.
|
|
739
|
+
*
|
|
740
|
+
* Standard Webhooks is explicit that `410 Gone` means "disable the endpoint",
|
|
741
|
+
* and that is a different signal from a streak: the receiver has TOLD us the
|
|
742
|
+
* endpoint is gone, so waiting for `autoDisableAfter` more failures is us
|
|
743
|
+
* ignoring an answer we asked for. It also fires when `autoDisableAfter` is
|
|
744
|
+
* unset — the streak feature being off does not make a 410 ambiguous.
|
|
745
|
+
*/
|
|
746
|
+
disableNow?: boolean) => Promise<AutoDisableOutcome>;
|
|
632
747
|
|
|
633
748
|
/** Refund a slot claimed by `consumeRateSlot` — used when a later scope
|
|
634
749
|
* in the same acquisition denies, so a deferred delivery doesn't burn
|
|
@@ -699,26 +814,160 @@ export declare interface RetryPolicy {
|
|
|
699
814
|
|
|
700
815
|
export declare type RetryStrategy = 'fixed' | 'linear' | 'exponential';
|
|
701
816
|
|
|
702
|
-
export declare type SignatureScheme = HmacSignatureScheme | CustomSignatureScheme;
|
|
817
|
+
export declare type SignatureScheme = HmacSignatureScheme | CustomSignatureScheme | StandardWebhooksSignatureScheme;
|
|
703
818
|
|
|
704
|
-
export declare interface
|
|
705
|
-
/**
|
|
706
|
-
readonly
|
|
707
|
-
/**
|
|
708
|
-
|
|
709
|
-
/** Timestamp embedded in the header (when `includeTimestamp`).
|
|
710
|
-
* Surfaced so callers can log it alongside delivery records. */
|
|
819
|
+
export declare interface SignedRequest {
|
|
820
|
+
/** Every header the scheme contributes, lowercased. */
|
|
821
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
822
|
+
/** The instant embedded in the signature, when the scheme embeds one.
|
|
823
|
+
* Surfaced so callers can log it beside the delivery record. */
|
|
711
824
|
readonly timestamp?: number;
|
|
712
825
|
}
|
|
713
826
|
|
|
714
|
-
/**
|
|
715
|
-
|
|
827
|
+
/**
|
|
828
|
+
* Render the signature headers for an outbound request.
|
|
829
|
+
*
|
|
830
|
+
* ONE function for every scheme: a caller that has to know which scheme it is
|
|
831
|
+
* holding in order to assemble the right headers is a caller that will get a
|
|
832
|
+
* new scheme wrong.
|
|
833
|
+
*/
|
|
834
|
+
export declare const signRequest: (scheme: SignatureScheme, input: SignRequestInput) => SignedRequest;
|
|
716
835
|
|
|
717
|
-
/**
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
836
|
+
/** Everything a scheme may need to render its headers. */
|
|
837
|
+
export declare interface SignRequestInput {
|
|
838
|
+
/** The exact bytes on the wire — the signature covers these, never the
|
|
839
|
+
* pre-encoded JSON. */
|
|
840
|
+
readonly rawBody: Uint8Array;
|
|
841
|
+
/** The signing secret for this target. */
|
|
842
|
+
readonly secret: string;
|
|
843
|
+
/**
|
|
844
|
+
* The unique message identifier. Standard Webhooks signs OVER it and sends it
|
|
845
|
+
* as `webhook-id`; the other schemes ignore it.
|
|
846
|
+
*
|
|
847
|
+
* Required rather than optional, and the reason is worth keeping: it is also
|
|
848
|
+
* the consumer's idempotency key, so a delivery without one cannot be
|
|
849
|
+
* de-duplicated by the receiver at all. Making it optional would let a call
|
|
850
|
+
* site omit it and produce a spec-shaped message that is missing the one
|
|
851
|
+
* field the spec tells consumers to rely on.
|
|
852
|
+
*/
|
|
853
|
+
readonly messageId: string;
|
|
854
|
+
/** Unix seconds. Defaults to now — a test pins it. */
|
|
855
|
+
readonly timestampSeconds?: number;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Slack-style: `X-Slack-Signature: v0=hex`, with the timestamp in the SEPARATE
|
|
860
|
+
* `X-Slack-Request-Timestamp` header and a signed content of
|
|
861
|
+
* `v0:<timestamp>:<body>`.
|
|
862
|
+
*
|
|
863
|
+
* This used to be a stub whose `verify` returned `false` unconditionally, with a
|
|
864
|
+
* comment explaining that the route adapter spliced the two header values into
|
|
865
|
+
* one string before calling it. It did — so `slackSignature()` was an exported,
|
|
866
|
+
* documented API that rejected every request when used as written, and the only
|
|
867
|
+
* thing that made Slack work was a special case in `incoming.ts` keyed on
|
|
868
|
+
* `_tag === 'custom'`. A verifier that receives the whole header map does not
|
|
869
|
+
* need either.
|
|
870
|
+
*/
|
|
871
|
+
export declare const slackSignature: (options?: {
|
|
872
|
+
readonly replayWindowSeconds?: number;
|
|
873
|
+
}) => CustomSignatureScheme;
|
|
874
|
+
|
|
875
|
+
/** Default replay tolerance. The spec requires A tolerance and names no number,
|
|
876
|
+
* so this is ours: 5 minutes, matching the window every other scheme here uses. */
|
|
877
|
+
export declare const STANDARD_WEBHOOKS_DEFAULT_TOLERANCE_SECONDS = 300;
|
|
878
|
+
|
|
879
|
+
/** The three headers, exactly as the spec names them (lowercase). */
|
|
880
|
+
export declare const STANDARD_WEBHOOKS_ID_HEADER = "webhook-id";
|
|
881
|
+
|
|
882
|
+
export declare const STANDARD_WEBHOOKS_MAX_KEY_BYTES = 64;
|
|
883
|
+
|
|
884
|
+
/** Spec: "Between 24 bytes (192 bits) and 64 bytes (512 bits)". */
|
|
885
|
+
export declare const STANDARD_WEBHOOKS_MIN_KEY_BYTES = 24;
|
|
886
|
+
|
|
887
|
+
/** Secret serialization prefix. */
|
|
888
|
+
export declare const STANDARD_WEBHOOKS_SECRET_PREFIX = "whsec_";
|
|
889
|
+
|
|
890
|
+
export declare const STANDARD_WEBHOOKS_SIGNATURE_HEADER = "webhook-signature";
|
|
891
|
+
|
|
892
|
+
export declare const STANDARD_WEBHOOKS_TIMESTAMP_HEADER = "webhook-timestamp";
|
|
893
|
+
|
|
894
|
+
/** The symmetric signature identifier. `v1a` is the asymmetric one — see the
|
|
895
|
+
* header for why it is refused rather than ignored. */
|
|
896
|
+
export declare const STANDARD_WEBHOOKS_VERSION = "v1";
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* The three outbound headers for one delivery.
|
|
900
|
+
*
|
|
901
|
+
* `secrets` may carry more than one: during a rotation the spec has the producer
|
|
902
|
+
* sign "with both the current and old keys" and space-delimit the tokens, so a
|
|
903
|
+
* consumer holding either key still verifies. Order is producer-chosen; a
|
|
904
|
+
* consumer tries each.
|
|
905
|
+
*/
|
|
906
|
+
export declare const standardWebhooksHeaders: (input: {
|
|
907
|
+
readonly secrets: ReadonlyArray<string>;
|
|
908
|
+
readonly messageId: string;
|
|
909
|
+
readonly timestampSeconds: number;
|
|
910
|
+
readonly rawBody: Uint8Array;
|
|
911
|
+
}) => Readonly<Record<string, string>>;
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* The raw HMAC key behind a `whsec_…` secret.
|
|
915
|
+
*
|
|
916
|
+
* The prefix is REQUIRED, and that strictness is deliberate. A hex secret (what
|
|
917
|
+
* this plugin's generic schemes mint) is also valid base64, so a lenient
|
|
918
|
+
* "decode if it looks like base64" rule would silently HMAC 48 bytes of garbage
|
|
919
|
+
* — self-consistently, so our own round-trip would pass while every conformant
|
|
920
|
+
* consumer library rejected the delivery. Refusing loudly at signing time is the
|
|
921
|
+
* only version of this that cannot ship a broken endpoint.
|
|
922
|
+
*/
|
|
923
|
+
export declare const standardWebhooksKey: (secret: string) => Buffer;
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Standard Webhooks v1.0.0 — the interoperable scheme.
|
|
927
|
+
*
|
|
928
|
+
* Use it on an INCOMING webhook whose sender signs to the spec, and read it as
|
|
929
|
+
* the OUTGOING default via `defaultOutgoingSignature()`.
|
|
930
|
+
*
|
|
931
|
+
* Symmetric (HMAC-SHA256, `v1`) only. The spec's asymmetric half (ed25519,
|
|
932
|
+
* `v1a`, `whsk_`/`whpk_`) is not implemented, and `verifyRequest` says so
|
|
933
|
+
* explicitly rather than reporting a generic mismatch.
|
|
934
|
+
*/
|
|
935
|
+
export declare const standardWebhooksSignature: (options?: {
|
|
936
|
+
readonly toleranceSeconds?: number;
|
|
937
|
+
readonly previousSecret?: string;
|
|
938
|
+
}) => StandardWebhooksSignatureScheme;
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* The Standard Webhooks (v1.0.0) scheme — three `webhook-*` headers, a
|
|
942
|
+
* `msg_id.timestamp.payload` signed content, base64 `v1,` signatures, a
|
|
943
|
+
* `whsec_`-prefixed base64 key. Implemented in `./standardWebhooks`, which
|
|
944
|
+
* carries the spec quotations and the interop vector it is verified against.
|
|
945
|
+
*/
|
|
946
|
+
export declare interface StandardWebhooksSignatureScheme {
|
|
947
|
+
readonly _tag: 'standardWebhooks';
|
|
948
|
+
/** Replay tolerance. The spec REQUIRES a tolerance and names no number; 300s
|
|
949
|
+
* is ours. */
|
|
950
|
+
readonly toleranceSeconds?: number;
|
|
951
|
+
/** A second key accepted on verify AND signed alongside on send — the spec's
|
|
952
|
+
* zero-downtime rotation, which is what the space-delimited signature list
|
|
953
|
+
* exists for. */
|
|
954
|
+
readonly previousSecret?: string;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** One `v1,<base64>` token. */
|
|
958
|
+
export declare const standardWebhooksSignatureToken: (secret: string, messageId: string, timestampSeconds: number, rawBody: Uint8Array) => string;
|
|
959
|
+
|
|
960
|
+
/** The exact bytes the spec signs: `msg_id.timestamp.payload`. */
|
|
961
|
+
export declare const standardWebhooksSignedContent: (messageId: string, timestampSeconds: number, rawBody: Uint8Array) => Buffer;
|
|
962
|
+
|
|
963
|
+
export declare type StandardWebhooksVerifyResult = {
|
|
964
|
+
readonly ok: true;
|
|
965
|
+
readonly messageId: string;
|
|
966
|
+
readonly timestampSeconds: number;
|
|
967
|
+
} | {
|
|
968
|
+
readonly ok: false;
|
|
969
|
+
readonly reason: string;
|
|
970
|
+
};
|
|
722
971
|
|
|
723
972
|
/** Stripe-style: `Stripe-Signature: t=NNN,v1=hex`, 5-min replay window. */
|
|
724
973
|
export declare const stripeSignature: (header?: string) => HmacSignatureScheme;
|
|
@@ -876,6 +1125,15 @@ export declare interface TargetSummary {
|
|
|
876
1125
|
|
|
877
1126
|
declare const TTL_MS: Record<string, number>;
|
|
878
1127
|
|
|
1128
|
+
/** Thrown at mount (i.e. at boot) for a webhook that verifies nothing and never
|
|
1129
|
+
* said so. The message is the deliverable — it names the endpoint and every
|
|
1130
|
+
* way out of the failure. */
|
|
1131
|
+
export declare class UnverifiedIncomingWebhook extends Error {
|
|
1132
|
+
readonly webhookId: string;
|
|
1133
|
+
readonly name = "UnverifiedIncomingWebhook";
|
|
1134
|
+
constructor(webhookId: string);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
879
1137
|
/**
|
|
880
1138
|
* Convenience accessor for handlers. The framework's `AppContext`
|
|
881
1139
|
* types the `webhooks` slot as `unknown` to avoid a circular dep
|
|
@@ -897,6 +1155,24 @@ export declare const useWebhooksEffect: Effect.Effect<WebhooksServiceShape, neve
|
|
|
897
1155
|
|
|
898
1156
|
export declare const validateSubscribe: (input: SubscribeInput) => void;
|
|
899
1157
|
|
|
1158
|
+
/**
|
|
1159
|
+
* Constant-time verification of an incoming request.
|
|
1160
|
+
*
|
|
1161
|
+
* Returns the structured reason so a caller can attach it to a 401 (helpful in
|
|
1162
|
+
* dev, fine to elide in prod).
|
|
1163
|
+
*/
|
|
1164
|
+
export declare const verifyRequest: (scheme: SignatureScheme, input: VerifyRequestInput) => VerifyResult;
|
|
1165
|
+
|
|
1166
|
+
export declare interface VerifyRequestInput {
|
|
1167
|
+
readonly rawBody: Uint8Array;
|
|
1168
|
+
readonly secret: string;
|
|
1169
|
+
/** The request's headers, LOWERCASED keys. A scheme reads whichever of them
|
|
1170
|
+
* it needs (Slack needs two; Standard Webhooks needs three). */
|
|
1171
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
1172
|
+
/** Overridable clock for the replay window — tests pin it. */
|
|
1173
|
+
readonly nowSeconds?: number;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
900
1176
|
export declare type VerifyResult = {
|
|
901
1177
|
readonly ok: true;
|
|
902
1178
|
} | {
|
|
@@ -904,11 +1180,20 @@ export declare type VerifyResult = {
|
|
|
904
1180
|
readonly reason: string;
|
|
905
1181
|
};
|
|
906
1182
|
|
|
907
|
-
/**
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
|
|
1183
|
+
/**
|
|
1184
|
+
* Verify an inbound Standard-Webhooks request.
|
|
1185
|
+
*
|
|
1186
|
+
* Order matters and follows the spec's own reasoning: shape → timestamp
|
|
1187
|
+
* tolerance (a replay is rejected before any HMAC work) → constant-time compare
|
|
1188
|
+
* against every accepted key × every offered token.
|
|
1189
|
+
*/
|
|
1190
|
+
export declare const verifyStandardWebhooks: (input: {
|
|
1191
|
+
readonly secrets: ReadonlyArray<string>;
|
|
1192
|
+
readonly rawBody: Uint8Array;
|
|
1193
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
1194
|
+
readonly toleranceSeconds?: number;
|
|
1195
|
+
readonly nowSeconds?: number;
|
|
1196
|
+
}) => StandardWebhooksVerifyResult;
|
|
912
1197
|
|
|
913
1198
|
/** Compare a target's pinned `payloadVersion` against the event's
|
|
914
1199
|
* current `version`. Three states:
|
|
@@ -953,7 +1238,7 @@ export declare const _voltroWebhookDeliveriesTable: Table<"_voltro_webhook_deliv
|
|
|
953
1238
|
readonly eventId: ColumnBuilder<string | null, "text", boolean>;
|
|
954
1239
|
/** Attempt counter (1-indexed). */
|
|
955
1240
|
readonly attempt: ColumnBuilder<number, "integer", boolean>;
|
|
956
|
-
readonly status: ColumnBuilder<"failed" | "
|
|
1241
|
+
readonly status: ColumnBuilder<"failed" | "pending" | "succeeded" | "inFlight" | "retryScheduled", "text", boolean>;
|
|
957
1242
|
/** Payload as sent over the wire. Stored verbatim — re-rendering
|
|
958
1243
|
* from a referenced event row would lose the snapshot if the
|
|
959
1244
|
* source event was deleted. */
|
|
@@ -1184,6 +1469,9 @@ export declare const _voltroWebhookTargetsTable: Table<"_voltro_webhook_targets"
|
|
|
1184
1469
|
* duplicate-effect check is what enforces that. */
|
|
1185
1470
|
export declare const WEBHOOK_EMIT_EFFECT = "voltro.webhook.emit";
|
|
1186
1471
|
|
|
1472
|
+
/** The property name the runtime's boot gate reads off a mounted handler. */
|
|
1473
|
+
export declare const WEBHOOK_VERIFICATION_PROPERTY: "voltroWebhookVerification";
|
|
1474
|
+
|
|
1187
1475
|
/**
|
|
1188
1476
|
* Thrown by `WebhooksService.replay` when no delivery row exists for the
|
|
1189
1477
|
* given `deliveryId` — the row can't be re-triggered because the
|
|
@@ -1293,6 +1581,45 @@ export declare interface WebhookProviderDescriptor {
|
|
|
1293
1581
|
readonly eventTypeFrom?: (body: unknown) => string | undefined;
|
|
1294
1582
|
}
|
|
1295
1583
|
|
|
1584
|
+
/**
|
|
1585
|
+
* The permissions webhook delivery declares.
|
|
1586
|
+
*
|
|
1587
|
+
* `network:outbound:*` and nothing else, deliberately: this entry contributes
|
|
1588
|
+
* no interceptors, no inspect endpoints, no `extendSchema` tables (the webhook
|
|
1589
|
+
* tables ride the framework's feature-mix assembly the moment a `*.webhook.tsx`
|
|
1590
|
+
* file exists — see `cli/src/frameworkTableAssembly.ts`), so no other hook
|
|
1591
|
+
* permission would be truthful either.
|
|
1592
|
+
*/
|
|
1593
|
+
export declare const WEBHOOKS_PLUGIN_PERMISSIONS: ReadonlyArray<PluginPermission>;
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* Register webhook delivery with the plugin system.
|
|
1597
|
+
*
|
|
1598
|
+
* ```ts
|
|
1599
|
+
* // app.config.ts
|
|
1600
|
+
* import { webhooksPlugin } from '@voltro/plugin-webhooks'
|
|
1601
|
+
*
|
|
1602
|
+
* export default {
|
|
1603
|
+
* plugins: [webhooksPlugin()],
|
|
1604
|
+
* }
|
|
1605
|
+
* ```
|
|
1606
|
+
*
|
|
1607
|
+
* Adding it changes no behavior — `*.webhook.tsx` discovery, delivery and the
|
|
1608
|
+
* incoming routes work exactly as before. What it changes is the boot audit:
|
|
1609
|
+
* webhooks now appears in the plugin permission report and the plugin manifest
|
|
1610
|
+
* with its outbound declaration, instead of being invisible to both.
|
|
1611
|
+
*/
|
|
1612
|
+
export declare const webhooksPlugin: (options?: WebhooksPluginOptions) => VoltroPlugin;
|
|
1613
|
+
|
|
1614
|
+
export declare interface WebhooksPluginOptions {
|
|
1615
|
+
/**
|
|
1616
|
+
* Distinguishing suffix when an app registers the entry more than once
|
|
1617
|
+
* (matching the `@voltro/plugin-mail#<name>` convention). Plugin names must
|
|
1618
|
+
* be unique in one app's plugin list.
|
|
1619
|
+
*/
|
|
1620
|
+
readonly name?: string;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1296
1623
|
export declare class WebhooksService extends WebhooksService_base {
|
|
1297
1624
|
}
|
|
1298
1625
|
|
|
@@ -1564,7 +1891,7 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
|
|
|
1564
1891
|
readonly eventId: ColumnBuilder<string | null, "text", boolean>;
|
|
1565
1892
|
/** Attempt counter (1-indexed). */
|
|
1566
1893
|
readonly attempt: ColumnBuilder<number, "integer", boolean>;
|
|
1567
|
-
readonly status: ColumnBuilder<"failed" | "
|
|
1894
|
+
readonly status: ColumnBuilder<"failed" | "pending" | "succeeded" | "inFlight" | "retryScheduled", "text", boolean>;
|
|
1568
1895
|
/** Payload as sent over the wire. Stored verbatim — re-rendering
|
|
1569
1896
|
* from a referenced event row would lose the snapshot if the
|
|
1570
1897
|
* source event was deleted. */
|