@voltro/plugin-webhooks 0.33.0 → 0.34.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 +1801 -0
- package/dist/index.d.ts +355 -37
- package/dist/index.js +398 -344
- 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,
|
|
@@ -168,6 +182,17 @@ declare interface DeliverWorkflowOptions {
|
|
|
168
182
|
/** The fixed window backing "per minute" — injectable for tests
|
|
169
183
|
* (production uses the 60s default). */
|
|
170
184
|
readonly rateWindowMs?: number;
|
|
185
|
+
/**
|
|
186
|
+
* Per-attempt wire timeout, milliseconds. Default 30 000.
|
|
187
|
+
*
|
|
188
|
+
* There was no timeout at all before this, so one receiver that accepted the
|
|
189
|
+
* connection and never answered held a durable workflow — and its rate-limit
|
|
190
|
+
* slot — indefinitely. Standard Webhooks recommends "somewhere between 15 and
|
|
191
|
+
* 30s"; the top of that band is the default because a slow-but-alive receiver
|
|
192
|
+
* being cut off produces a retry storm, which is the worse of the two
|
|
193
|
+
* failures. Also settable per deployment with `VOLTRO_WEBHOOK_TIMEOUT_MS`.
|
|
194
|
+
*/
|
|
195
|
+
readonly timeoutMs?: number;
|
|
171
196
|
}
|
|
172
197
|
|
|
173
198
|
/** `getDelivery` adds the two heavy columns `listDeliveries` omits. */
|
|
@@ -282,6 +307,26 @@ export declare const fastRetryPolicy: () => RetryPolicy;
|
|
|
282
307
|
|
|
283
308
|
export declare const generateSecret: () => string;
|
|
284
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Mint a spec-shaped signing secret: `whsec_` + base64 of 32 random bytes.
|
|
312
|
+
*
|
|
313
|
+
* 32 bytes sits inside the spec's 24..64 band and matches the SHA-256 block
|
|
314
|
+
* this key feeds. Per the spec, keys "should be unique per endpoint" — which is
|
|
315
|
+
* already how `subscribe` mints them.
|
|
316
|
+
*/
|
|
317
|
+
export declare const generateStandardWebhooksSecret: () => string;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The house HMAC shape: `X-Webhook-Signature: t=NNN,v1=hex` over
|
|
321
|
+
* `<timestamp>.<body>`.
|
|
322
|
+
*
|
|
323
|
+
* Kept as a named CHOICE, not as a default and not as a fallback: a receiver
|
|
324
|
+
* that already implemented this exact format should not have to change to keep
|
|
325
|
+
* receiving. It is a peer of `stripeSignature()` / `githubSignature()` — a
|
|
326
|
+
* specific counterparty's format — rather than a second general-purpose path.
|
|
327
|
+
*/
|
|
328
|
+
export declare const genericHmacSignature: () => HmacSignatureScheme;
|
|
329
|
+
|
|
285
330
|
export declare const getIdempotencyCache: () => IdempotencyCache;
|
|
286
331
|
|
|
287
332
|
/** GitHub-style: `X-Hub-Signature-256: sha256=hex` (no timestamp). */
|
|
@@ -402,11 +447,24 @@ export declare interface IncomingWebhookDescriptor<Body> {
|
|
|
402
447
|
* integrations (`/integrations/stripe/v1`). */
|
|
403
448
|
readonly path?: `/${string}`;
|
|
404
449
|
/** 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. */
|
|
450
|
+
* on mismatch with a 401. Usually filled in by `provider`. */
|
|
409
451
|
readonly signature?: SignatureScheme;
|
|
452
|
+
/**
|
|
453
|
+
* How this endpoint authenticates its caller. An incoming webhook is a
|
|
454
|
+
* PUBLIC POST that runs your application code, so the framework will not
|
|
455
|
+
* mount one that has made no decision here: `mountIncomingWebhook` throws
|
|
456
|
+
* at boot when there is no effective `signature` (from this descriptor or
|
|
457
|
+
* from `provider`) AND no explicit value below.
|
|
458
|
+
*
|
|
459
|
+
* - omitted → derived. `signature` / `provider` present → `'signature'`;
|
|
460
|
+
* nothing present → boot refuses.
|
|
461
|
+
* - `'provider'` → the handler verifies with the provider's own SDK
|
|
462
|
+
* (Stripe's `constructEvent`, etc.). The framework's generic HMAC layer
|
|
463
|
+
* is not the authority and does not require a framework-side secret.
|
|
464
|
+
* - `'none'` → deliberately unverified, because a gateway + IP allow-list
|
|
465
|
+
* owns the trust boundary. Logged as a warning at every boot, on purpose.
|
|
466
|
+
*/
|
|
467
|
+
readonly verification?: 'signature' | 'provider' | 'none';
|
|
410
468
|
/** Idempotency key extraction. Default `'Idempotency-Key'` header.
|
|
411
469
|
* Provider-templates override this to match the provider's wire
|
|
412
470
|
* format. */
|
|
@@ -434,6 +492,26 @@ export declare interface IncomingWebhookDescriptor<Body> {
|
|
|
434
492
|
readonly handler: (context: IncomingWebhookContext<Body>) => Promise<void> | void;
|
|
435
493
|
}
|
|
436
494
|
|
|
495
|
+
/** How a mounted incoming webhook authenticates its caller. Mirrors
|
|
496
|
+
* `@voltro/runtime`'s `WebhookVerification` STRUCTURALLY — this package must
|
|
497
|
+
* not depend on the runtime (it is imported by browser-safe descriptor files),
|
|
498
|
+
* and the runtime reads the stamped value back by shape. */
|
|
499
|
+
export declare type IncomingWebhookVerification = 'signature' | 'provider' | 'none';
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* The verification decision for a descriptor, WITHOUT mounting it.
|
|
503
|
+
*
|
|
504
|
+
* One function, two readers: `mountIncomingWebhook` (which turns `null` into a
|
|
505
|
+
* boot refusal) and `voltro doctor` (which reports it as a preflight, before a
|
|
506
|
+
* deploy discovers it). A doctor that re-derived this could disagree with the
|
|
507
|
+
* boot it is supposed to predict — the failure shape a preflight must not have.
|
|
508
|
+
*
|
|
509
|
+
* `null` = nothing authenticates the caller. Declaring nothing, and declaring
|
|
510
|
+
* `'signature'` with no scheme to verify AGAINST, are the same open endpoint,
|
|
511
|
+
* so both answer `null`.
|
|
512
|
+
*/
|
|
513
|
+
export declare const incomingWebhookVerification: <Body>(descriptor: IncomingWebhookDescriptor<Body>) => IncomingWebhookVerification | null;
|
|
514
|
+
|
|
437
515
|
export declare interface IncomingWorkflowFacade {
|
|
438
516
|
start<Payload = unknown>(workflowName: string, payload: Payload): Promise<WorkflowRunHandle>;
|
|
439
517
|
signal(target: {
|
|
@@ -458,6 +536,9 @@ export declare interface IncomingWorkflowFacade {
|
|
|
458
536
|
}>;
|
|
459
537
|
}
|
|
460
538
|
|
|
539
|
+
/** Is this a spec-shaped secret? Used to decide whether a target needs one minted. */
|
|
540
|
+
export declare const isStandardWebhooksSecret: (secret: string) => boolean;
|
|
541
|
+
|
|
461
542
|
/** Type guard for file-walker discovery — every `*.webhook.tsx`
|
|
462
543
|
* must default-export one of these. */
|
|
463
544
|
export declare const isWebhookDescriptor: (value: unknown) => value is WebhookDescriptor;
|
|
@@ -519,16 +600,24 @@ emitter: () => Pick<WebhooksServiceShape, "emit"> | undefined) => OutboxHandlerD
|
|
|
519
600
|
*/
|
|
520
601
|
export declare const matchesFilter: (filter: WebhookFilter | Readonly<Record<string, unknown>> | null, payload: unknown) => boolean;
|
|
521
602
|
|
|
603
|
+
/** A mounted handler, carrying its verification declaration. */
|
|
604
|
+
export declare type MountedIncomingWebhook = ((request: IncomingRequest) => Promise<IncomingResponse>) & {
|
|
605
|
+
readonly [WEBHOOK_VERIFICATION_PROPERTY]: IncomingWebhookVerification;
|
|
606
|
+
};
|
|
607
|
+
|
|
522
608
|
/**
|
|
523
609
|
* Build a request handler for a specific `IncomingWebhookDescriptor`.
|
|
524
610
|
* The returned function is what stage 4 mounts on the HTTP layer.
|
|
611
|
+
*
|
|
612
|
+
* @throws {UnverifiedIncomingWebhook} at mount time when the descriptor has no
|
|
613
|
+
* effective signature scheme and no explicit `verification`.
|
|
525
614
|
*/
|
|
526
|
-
export declare const mountIncomingWebhook: <Body>(descriptor: IncomingWebhookDescriptor<Body>, options: MountOptions) =>
|
|
615
|
+
export declare const mountIncomingWebhook: <Body>(descriptor: IncomingWebhookDescriptor<Body>, options: MountOptions) => MountedIncomingWebhook;
|
|
527
616
|
|
|
528
617
|
export declare interface MountOptions {
|
|
529
|
-
/** Resolves the per-webhook signing secret.
|
|
530
|
-
*
|
|
531
|
-
*
|
|
618
|
+
/** Resolves the per-webhook signing secret. Returning `null` for a
|
|
619
|
+
* webhook whose verification is `'signature'` makes every delivery
|
|
620
|
+
* answer 503 — it does NOT skip verification. */
|
|
532
621
|
readonly resolveSecret: (webhookId: string) => Promise<string | null>;
|
|
533
622
|
/** Optional idempotency cache override. Defaults to the
|
|
534
623
|
* process-singleton LRU. Pass a custom cache for tests or for a
|
|
@@ -562,9 +651,16 @@ export declare interface OutgoingEventDescriptor<Payload> {
|
|
|
562
651
|
* (pass the descriptor itself to `emit(descriptor, payload)` to
|
|
563
652
|
* type the payload; either way `emit` DECODES the payload against
|
|
564
653
|
* this schema and rejects a mismatch with `WebhookPayloadInvalid`
|
|
565
|
-
* before any delivery is created).
|
|
566
|
-
*
|
|
567
|
-
*
|
|
654
|
+
* before any delivery is created).
|
|
655
|
+
*
|
|
656
|
+
* The BODY on the wire is this payload and nothing else — no envelope. It
|
|
657
|
+
* used to say recipients receive `{ event, eventId, occurredAt, payload }`,
|
|
658
|
+
* which was never true of the code: `emit` posts `JSON.stringify(payload)`
|
|
659
|
+
* verbatim (asserted in `deliverWorkflow.integration.test.ts`). The event
|
|
660
|
+
* name, ids and attempt count ride in HEADERS (`x-voltro-event`,
|
|
661
|
+
* `webhook-id`, `x-voltro-attempt`), which is also where Standard Webhooks
|
|
662
|
+
* puts the delivery metadata — so the generated consumer package reads them
|
|
663
|
+
* from there. */
|
|
568
664
|
readonly payload: Schema.Schema<Payload>;
|
|
569
665
|
/** Schema version. Increment when the payload shape changes in a
|
|
570
666
|
* way subscribers must adapt to. The dashboard surfaces version
|
|
@@ -628,7 +724,17 @@ export declare interface RateScope {
|
|
|
628
724
|
* target when the auto-disable fires. Returns the post-write streak +
|
|
629
725
|
* whether THIS call auto-disabled.
|
|
630
726
|
*/
|
|
631
|
-
export declare const recordDeliveryOutcome: (store: DataStore, targetId: string, succeeded: boolean, reason: string, now?: Date
|
|
727
|
+
export declare const recordDeliveryOutcome: (store: DataStore, targetId: string, succeeded: boolean, reason: string, now?: Date,
|
|
728
|
+
/**
|
|
729
|
+
* Disable the target on THIS failure, whatever the streak says.
|
|
730
|
+
*
|
|
731
|
+
* Standard Webhooks is explicit that `410 Gone` means "disable the endpoint",
|
|
732
|
+
* and that is a different signal from a streak: the receiver has TOLD us the
|
|
733
|
+
* endpoint is gone, so waiting for `autoDisableAfter` more failures is us
|
|
734
|
+
* ignoring an answer we asked for. It also fires when `autoDisableAfter` is
|
|
735
|
+
* unset — the streak feature being off does not make a 410 ambiguous.
|
|
736
|
+
*/
|
|
737
|
+
disableNow?: boolean) => Promise<AutoDisableOutcome>;
|
|
632
738
|
|
|
633
739
|
/** Refund a slot claimed by `consumeRateSlot` — used when a later scope
|
|
634
740
|
* in the same acquisition denies, so a deferred delivery doesn't burn
|
|
@@ -699,26 +805,160 @@ export declare interface RetryPolicy {
|
|
|
699
805
|
|
|
700
806
|
export declare type RetryStrategy = 'fixed' | 'linear' | 'exponential';
|
|
701
807
|
|
|
702
|
-
export declare type SignatureScheme = HmacSignatureScheme | CustomSignatureScheme;
|
|
808
|
+
export declare type SignatureScheme = HmacSignatureScheme | CustomSignatureScheme | StandardWebhooksSignatureScheme;
|
|
703
809
|
|
|
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. */
|
|
810
|
+
export declare interface SignedRequest {
|
|
811
|
+
/** Every header the scheme contributes, lowercased. */
|
|
812
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
813
|
+
/** The instant embedded in the signature, when the scheme embeds one.
|
|
814
|
+
* Surfaced so callers can log it beside the delivery record. */
|
|
711
815
|
readonly timestamp?: number;
|
|
712
816
|
}
|
|
713
817
|
|
|
714
|
-
/**
|
|
715
|
-
|
|
818
|
+
/**
|
|
819
|
+
* Render the signature headers for an outbound request.
|
|
820
|
+
*
|
|
821
|
+
* ONE function for every scheme: a caller that has to know which scheme it is
|
|
822
|
+
* holding in order to assemble the right headers is a caller that will get a
|
|
823
|
+
* new scheme wrong.
|
|
824
|
+
*/
|
|
825
|
+
export declare const signRequest: (scheme: SignatureScheme, input: SignRequestInput) => SignedRequest;
|
|
826
|
+
|
|
827
|
+
/** Everything a scheme may need to render its headers. */
|
|
828
|
+
export declare interface SignRequestInput {
|
|
829
|
+
/** The exact bytes on the wire — the signature covers these, never the
|
|
830
|
+
* pre-encoded JSON. */
|
|
831
|
+
readonly rawBody: Uint8Array;
|
|
832
|
+
/** The signing secret for this target. */
|
|
833
|
+
readonly secret: string;
|
|
834
|
+
/**
|
|
835
|
+
* The unique message identifier. Standard Webhooks signs OVER it and sends it
|
|
836
|
+
* as `webhook-id`; the other schemes ignore it.
|
|
837
|
+
*
|
|
838
|
+
* Required rather than optional, and the reason is worth keeping: it is also
|
|
839
|
+
* the consumer's idempotency key, so a delivery without one cannot be
|
|
840
|
+
* de-duplicated by the receiver at all. Making it optional would let a call
|
|
841
|
+
* site omit it and produce a spec-shaped message that is missing the one
|
|
842
|
+
* field the spec tells consumers to rely on.
|
|
843
|
+
*/
|
|
844
|
+
readonly messageId: string;
|
|
845
|
+
/** Unix seconds. Defaults to now — a test pins it. */
|
|
846
|
+
readonly timestampSeconds?: number;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Slack-style: `X-Slack-Signature: v0=hex`, with the timestamp in the SEPARATE
|
|
851
|
+
* `X-Slack-Request-Timestamp` header and a signed content of
|
|
852
|
+
* `v0:<timestamp>:<body>`.
|
|
853
|
+
*
|
|
854
|
+
* This used to be a stub whose `verify` returned `false` unconditionally, with a
|
|
855
|
+
* comment explaining that the route adapter spliced the two header values into
|
|
856
|
+
* one string before calling it. It did — so `slackSignature()` was an exported,
|
|
857
|
+
* documented API that rejected every request when used as written, and the only
|
|
858
|
+
* thing that made Slack work was a special case in `incoming.ts` keyed on
|
|
859
|
+
* `_tag === 'custom'`. A verifier that receives the whole header map does not
|
|
860
|
+
* need either.
|
|
861
|
+
*/
|
|
862
|
+
export declare const slackSignature: (options?: {
|
|
863
|
+
readonly replayWindowSeconds?: number;
|
|
864
|
+
}) => CustomSignatureScheme;
|
|
865
|
+
|
|
866
|
+
/** Default replay tolerance. The spec requires A tolerance and names no number,
|
|
867
|
+
* so this is ours: 5 minutes, matching the window every other scheme here uses. */
|
|
868
|
+
export declare const STANDARD_WEBHOOKS_DEFAULT_TOLERANCE_SECONDS = 300;
|
|
869
|
+
|
|
870
|
+
/** The three headers, exactly as the spec names them (lowercase). */
|
|
871
|
+
export declare const STANDARD_WEBHOOKS_ID_HEADER = "webhook-id";
|
|
872
|
+
|
|
873
|
+
export declare const STANDARD_WEBHOOKS_MAX_KEY_BYTES = 64;
|
|
874
|
+
|
|
875
|
+
/** Spec: "Between 24 bytes (192 bits) and 64 bytes (512 bits)". */
|
|
876
|
+
export declare const STANDARD_WEBHOOKS_MIN_KEY_BYTES = 24;
|
|
877
|
+
|
|
878
|
+
/** Secret serialization prefix. */
|
|
879
|
+
export declare const STANDARD_WEBHOOKS_SECRET_PREFIX = "whsec_";
|
|
880
|
+
|
|
881
|
+
export declare const STANDARD_WEBHOOKS_SIGNATURE_HEADER = "webhook-signature";
|
|
882
|
+
|
|
883
|
+
export declare const STANDARD_WEBHOOKS_TIMESTAMP_HEADER = "webhook-timestamp";
|
|
884
|
+
|
|
885
|
+
/** The symmetric signature identifier. `v1a` is the asymmetric one — see the
|
|
886
|
+
* header for why it is refused rather than ignored. */
|
|
887
|
+
export declare const STANDARD_WEBHOOKS_VERSION = "v1";
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* The three outbound headers for one delivery.
|
|
891
|
+
*
|
|
892
|
+
* `secrets` may carry more than one: during a rotation the spec has the producer
|
|
893
|
+
* sign "with both the current and old keys" and space-delimit the tokens, so a
|
|
894
|
+
* consumer holding either key still verifies. Order is producer-chosen; a
|
|
895
|
+
* consumer tries each.
|
|
896
|
+
*/
|
|
897
|
+
export declare const standardWebhooksHeaders: (input: {
|
|
898
|
+
readonly secrets: ReadonlyArray<string>;
|
|
899
|
+
readonly messageId: string;
|
|
900
|
+
readonly timestampSeconds: number;
|
|
901
|
+
readonly rawBody: Uint8Array;
|
|
902
|
+
}) => Readonly<Record<string, string>>;
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* The raw HMAC key behind a `whsec_…` secret.
|
|
906
|
+
*
|
|
907
|
+
* The prefix is REQUIRED, and that strictness is deliberate. A hex secret (what
|
|
908
|
+
* this plugin's generic schemes mint) is also valid base64, so a lenient
|
|
909
|
+
* "decode if it looks like base64" rule would silently HMAC 48 bytes of garbage
|
|
910
|
+
* — self-consistently, so our own round-trip would pass while every conformant
|
|
911
|
+
* consumer library rejected the delivery. Refusing loudly at signing time is the
|
|
912
|
+
* only version of this that cannot ship a broken endpoint.
|
|
913
|
+
*/
|
|
914
|
+
export declare const standardWebhooksKey: (secret: string) => Buffer;
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Standard Webhooks v1.0.0 — the interoperable scheme.
|
|
918
|
+
*
|
|
919
|
+
* Use it on an INCOMING webhook whose sender signs to the spec, and read it as
|
|
920
|
+
* the OUTGOING default via `defaultOutgoingSignature()`.
|
|
921
|
+
*
|
|
922
|
+
* Symmetric (HMAC-SHA256, `v1`) only. The spec's asymmetric half (ed25519,
|
|
923
|
+
* `v1a`, `whsk_`/`whpk_`) is not implemented, and `verifyRequest` says so
|
|
924
|
+
* explicitly rather than reporting a generic mismatch.
|
|
925
|
+
*/
|
|
926
|
+
export declare const standardWebhooksSignature: (options?: {
|
|
927
|
+
readonly toleranceSeconds?: number;
|
|
928
|
+
readonly previousSecret?: string;
|
|
929
|
+
}) => StandardWebhooksSignatureScheme;
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* The Standard Webhooks (v1.0.0) scheme — three `webhook-*` headers, a
|
|
933
|
+
* `msg_id.timestamp.payload` signed content, base64 `v1,` signatures, a
|
|
934
|
+
* `whsec_`-prefixed base64 key. Implemented in `./standardWebhooks`, which
|
|
935
|
+
* carries the spec quotations and the interop vector it is verified against.
|
|
936
|
+
*/
|
|
937
|
+
export declare interface StandardWebhooksSignatureScheme {
|
|
938
|
+
readonly _tag: 'standardWebhooks';
|
|
939
|
+
/** Replay tolerance. The spec REQUIRES a tolerance and names no number; 300s
|
|
940
|
+
* is ours. */
|
|
941
|
+
readonly toleranceSeconds?: number;
|
|
942
|
+
/** A second key accepted on verify AND signed alongside on send — the spec's
|
|
943
|
+
* zero-downtime rotation, which is what the space-delimited signature list
|
|
944
|
+
* exists for. */
|
|
945
|
+
readonly previousSecret?: string;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** One `v1,<base64>` token. */
|
|
949
|
+
export declare const standardWebhooksSignatureToken: (secret: string, messageId: string, timestampSeconds: number, rawBody: Uint8Array) => string;
|
|
716
950
|
|
|
717
|
-
/**
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
951
|
+
/** The exact bytes the spec signs: `msg_id.timestamp.payload`. */
|
|
952
|
+
export declare const standardWebhooksSignedContent: (messageId: string, timestampSeconds: number, rawBody: Uint8Array) => Buffer;
|
|
953
|
+
|
|
954
|
+
export declare type StandardWebhooksVerifyResult = {
|
|
955
|
+
readonly ok: true;
|
|
956
|
+
readonly messageId: string;
|
|
957
|
+
readonly timestampSeconds: number;
|
|
958
|
+
} | {
|
|
959
|
+
readonly ok: false;
|
|
960
|
+
readonly reason: string;
|
|
961
|
+
};
|
|
722
962
|
|
|
723
963
|
/** Stripe-style: `Stripe-Signature: t=NNN,v1=hex`, 5-min replay window. */
|
|
724
964
|
export declare const stripeSignature: (header?: string) => HmacSignatureScheme;
|
|
@@ -876,6 +1116,15 @@ export declare interface TargetSummary {
|
|
|
876
1116
|
|
|
877
1117
|
declare const TTL_MS: Record<string, number>;
|
|
878
1118
|
|
|
1119
|
+
/** Thrown at mount (i.e. at boot) for a webhook that verifies nothing and never
|
|
1120
|
+
* said so. The message is the deliverable — it names the endpoint and every
|
|
1121
|
+
* way out of the failure. */
|
|
1122
|
+
export declare class UnverifiedIncomingWebhook extends Error {
|
|
1123
|
+
readonly webhookId: string;
|
|
1124
|
+
readonly name = "UnverifiedIncomingWebhook";
|
|
1125
|
+
constructor(webhookId: string);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
879
1128
|
/**
|
|
880
1129
|
* Convenience accessor for handlers. The framework's `AppContext`
|
|
881
1130
|
* types the `webhooks` slot as `unknown` to avoid a circular dep
|
|
@@ -897,6 +1146,24 @@ export declare const useWebhooksEffect: Effect.Effect<WebhooksServiceShape, neve
|
|
|
897
1146
|
|
|
898
1147
|
export declare const validateSubscribe: (input: SubscribeInput) => void;
|
|
899
1148
|
|
|
1149
|
+
/**
|
|
1150
|
+
* Constant-time verification of an incoming request.
|
|
1151
|
+
*
|
|
1152
|
+
* Returns the structured reason so a caller can attach it to a 401 (helpful in
|
|
1153
|
+
* dev, fine to elide in prod).
|
|
1154
|
+
*/
|
|
1155
|
+
export declare const verifyRequest: (scheme: SignatureScheme, input: VerifyRequestInput) => VerifyResult;
|
|
1156
|
+
|
|
1157
|
+
export declare interface VerifyRequestInput {
|
|
1158
|
+
readonly rawBody: Uint8Array;
|
|
1159
|
+
readonly secret: string;
|
|
1160
|
+
/** The request's headers, LOWERCASED keys. A scheme reads whichever of them
|
|
1161
|
+
* it needs (Slack needs two; Standard Webhooks needs three). */
|
|
1162
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
1163
|
+
/** Overridable clock for the replay window — tests pin it. */
|
|
1164
|
+
readonly nowSeconds?: number;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
900
1167
|
export declare type VerifyResult = {
|
|
901
1168
|
readonly ok: true;
|
|
902
1169
|
} | {
|
|
@@ -904,11 +1171,20 @@ export declare type VerifyResult = {
|
|
|
904
1171
|
readonly reason: string;
|
|
905
1172
|
};
|
|
906
1173
|
|
|
907
|
-
/**
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
|
|
1174
|
+
/**
|
|
1175
|
+
* Verify an inbound Standard-Webhooks request.
|
|
1176
|
+
*
|
|
1177
|
+
* Order matters and follows the spec's own reasoning: shape → timestamp
|
|
1178
|
+
* tolerance (a replay is rejected before any HMAC work) → constant-time compare
|
|
1179
|
+
* against every accepted key × every offered token.
|
|
1180
|
+
*/
|
|
1181
|
+
export declare const verifyStandardWebhooks: (input: {
|
|
1182
|
+
readonly secrets: ReadonlyArray<string>;
|
|
1183
|
+
readonly rawBody: Uint8Array;
|
|
1184
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
1185
|
+
readonly toleranceSeconds?: number;
|
|
1186
|
+
readonly nowSeconds?: number;
|
|
1187
|
+
}) => StandardWebhooksVerifyResult;
|
|
912
1188
|
|
|
913
1189
|
/** Compare a target's pinned `payloadVersion` against the event's
|
|
914
1190
|
* current `version`. Three states:
|
|
@@ -953,7 +1229,7 @@ export declare const _voltroWebhookDeliveriesTable: Table<"_voltro_webhook_deliv
|
|
|
953
1229
|
readonly eventId: ColumnBuilder<string | null, "text", boolean>;
|
|
954
1230
|
/** Attempt counter (1-indexed). */
|
|
955
1231
|
readonly attempt: ColumnBuilder<number, "integer", boolean>;
|
|
956
|
-
readonly status: ColumnBuilder<"failed" | "
|
|
1232
|
+
readonly status: ColumnBuilder<"failed" | "pending" | "succeeded" | "inFlight" | "retryScheduled", "text", boolean>;
|
|
957
1233
|
/** Payload as sent over the wire. Stored verbatim — re-rendering
|
|
958
1234
|
* from a referenced event row would lose the snapshot if the
|
|
959
1235
|
* source event was deleted. */
|
|
@@ -1184,6 +1460,9 @@ export declare const _voltroWebhookTargetsTable: Table<"_voltro_webhook_targets"
|
|
|
1184
1460
|
* duplicate-effect check is what enforces that. */
|
|
1185
1461
|
export declare const WEBHOOK_EMIT_EFFECT = "voltro.webhook.emit";
|
|
1186
1462
|
|
|
1463
|
+
/** The property name the runtime's boot gate reads off a mounted handler. */
|
|
1464
|
+
export declare const WEBHOOK_VERIFICATION_PROPERTY: "voltroWebhookVerification";
|
|
1465
|
+
|
|
1187
1466
|
/**
|
|
1188
1467
|
* Thrown by `WebhooksService.replay` when no delivery row exists for the
|
|
1189
1468
|
* given `deliveryId` — the row can't be re-triggered because the
|
|
@@ -1293,6 +1572,45 @@ export declare interface WebhookProviderDescriptor {
|
|
|
1293
1572
|
readonly eventTypeFrom?: (body: unknown) => string | undefined;
|
|
1294
1573
|
}
|
|
1295
1574
|
|
|
1575
|
+
/**
|
|
1576
|
+
* The permissions webhook delivery declares.
|
|
1577
|
+
*
|
|
1578
|
+
* `network:outbound:*` and nothing else, deliberately: this entry contributes
|
|
1579
|
+
* no interceptors, no inspect endpoints, no `extendSchema` tables (the webhook
|
|
1580
|
+
* tables ride the framework's feature-mix assembly the moment a `*.webhook.tsx`
|
|
1581
|
+
* file exists — see `cli/src/frameworkTableAssembly.ts`), so no other hook
|
|
1582
|
+
* permission would be truthful either.
|
|
1583
|
+
*/
|
|
1584
|
+
export declare const WEBHOOKS_PLUGIN_PERMISSIONS: ReadonlyArray<PluginPermission>;
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* Register webhook delivery with the plugin system.
|
|
1588
|
+
*
|
|
1589
|
+
* ```ts
|
|
1590
|
+
* // app.config.ts
|
|
1591
|
+
* import { webhooksPlugin } from '@voltro/plugin-webhooks'
|
|
1592
|
+
*
|
|
1593
|
+
* export default {
|
|
1594
|
+
* plugins: [webhooksPlugin()],
|
|
1595
|
+
* }
|
|
1596
|
+
* ```
|
|
1597
|
+
*
|
|
1598
|
+
* Adding it changes no behavior — `*.webhook.tsx` discovery, delivery and the
|
|
1599
|
+
* incoming routes work exactly as before. What it changes is the boot audit:
|
|
1600
|
+
* webhooks now appears in the plugin permission report and the plugin manifest
|
|
1601
|
+
* with its outbound declaration, instead of being invisible to both.
|
|
1602
|
+
*/
|
|
1603
|
+
export declare const webhooksPlugin: (options?: WebhooksPluginOptions) => VoltroPlugin;
|
|
1604
|
+
|
|
1605
|
+
export declare interface WebhooksPluginOptions {
|
|
1606
|
+
/**
|
|
1607
|
+
* Distinguishing suffix when an app registers the entry more than once
|
|
1608
|
+
* (matching the `@voltro/plugin-mail#<name>` convention). Plugin names must
|
|
1609
|
+
* be unique in one app's plugin list.
|
|
1610
|
+
*/
|
|
1611
|
+
readonly name?: string;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1296
1614
|
export declare class WebhooksService extends WebhooksService_base {
|
|
1297
1615
|
}
|
|
1298
1616
|
|
|
@@ -1564,7 +1882,7 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
|
|
|
1564
1882
|
readonly eventId: ColumnBuilder<string | null, "text", boolean>;
|
|
1565
1883
|
/** Attempt counter (1-indexed). */
|
|
1566
1884
|
readonly attempt: ColumnBuilder<number, "integer", boolean>;
|
|
1567
|
-
readonly status: ColumnBuilder<"failed" | "
|
|
1885
|
+
readonly status: ColumnBuilder<"failed" | "pending" | "succeeded" | "inFlight" | "retryScheduled", "text", boolean>;
|
|
1568
1886
|
/** Payload as sent over the wire. Stored verbatim — re-rendering
|
|
1569
1887
|
* from a referenced event row would lose the snapshot if the
|
|
1570
1888
|
* source event was deleted. */
|