@dukkan.one/app-sdk/webhooks
Signature verification on raw bytes, the envelope schema, typed events, and the receiver pipeline with store binding and dedupe.
- الحزمة
@dukkan.one/app-sdk@0.3.0- مراجعة الواجهة
2026-09-10- الاستيراد
import { … } from "@dukkan.one/app-sdk/webhooks"
في هذه الصفحة
- الدوال
- constructEvent
- createWebhookReceiver
- eventFromEnvelope
- isKnownTopic
- parseEnvelope
- readWebhookHeaders
- verifyWebhookSignature
- الأصناف
- MemoryInboxStore
- الواجهات
- DukkanEvent
- EventDataByTopic
- InboxRecord
- InboxStore
- InstallSecrets
- ReceiveResult
- VerifyWebhookInput
- WebhookHeaders
- WebhookReceiverOptions
- WebhookResponse
- الأنواع
- VerifyWebhookResult
- WebhookEnvelope
- WebhookHandler
- WebhookHandlers
- الثوابت
- envelopeSchema
- WEBHOOK_MAX_BODY_BYTES
- WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS
الدوال
constructEvent#
One-shot verification for apps that route deliveries themselves: verify,
parse and type in a single call, throwing DukkanWebhookError on refusal.
createWebhookReceiver#
Framework-neutral receiver (docs/apps/webhooks): size cap, signature on raw bytes, envelope parse, store binding, dedupe on the signed id, then typed dispatch. The answer is always small JSON; the platform treats large or HTML error bodies as failures.
eventFromEnvelope#
Turns a verified envelope plus the unsigned delivery headers into a typed DukkanEvent.
isKnownTopic#
True when the topic is one this SDK build knows; unknown topics still reach onUnknownTopic.
parseEnvelope#
Parses and validates the signed body; returns null for anything that is not a v1 envelope.
readWebhookHeaders#
Reads the delivery headers from any headers-like source, case-insensitively.
verifyWebhookSignature#
The platform's exact scheme (packages/contracts/vectors/webhook-signing.json):
hex(HMAC-SHA256(secret, ${timestamp}.${rawBody})), verified on the raw
bytes before any JSON parsing, in constant time, inside a freshness window.
الأصناف
MemoryInboxStore#
new MemoryInboxStore(options?
In-memory inbox for tests and single-process tools; bounded so it never grows forever.
| الاسم | النوع | الوصف |
|---|---|---|
insertIfNewمطلوب | (record: InboxRecord): Promise<boolean> | |
markFailedمطلوب | (eventId: string, error: string): Promise<void> | Called when a handler failed AFTER the event was recorded, so the app can keep the row visible for a replay instead of losing the event behind the dedupe. Optional; the receiver never throws past the acknowledgement. |
failuresمطلوب | (): Map<string, string> | Events whose handler failed, with the error message, for replays and assertions. |
sizeمطلوب | number | |
recordsمطلوب | (): InboxRecord[] |
الواجهات
DukkanEvent#
A verified, parsed delivery. data is typed by topic once narrowed.
| الاسم | النوع | الوصف |
|---|---|---|
idمطلوب | string | |
topicمطلوب | Topic | |
storeIdمطلوب | string | |
installIdمطلوب | string | |
sequenceمطلوب | number | |
occurredAtمطلوب | string | |
testمطلوب | boolean | True on sandbox rehearsals (POST /webhooks/test); never in production. |
dataمطلوب | EventDataByTopic[Topic] | |
deliveryIdمطلوب | string | null | Delivery attempt id from the unsigned header (stable across retries). |
apiVersionمطلوب | string | null | |
envelopeمطلوب | { [x: string]: unknown; id: string; api_version: "v1"; topic: string; store_id: string; install_id: string; sequence: number; occurred_at: string; data: Record<string, unknown>; test?: boolean | undefined; } | The parsed envelope including any fields added after this SDK was built. |
EventDataByTopic#
data type for each topic, straight from the contract's x-dukkan-topic-payloads.
| الاسم | النوع | الوصف |
|---|---|---|
order.createdمطلوب | { order_id: string; order_number: string; status: string; total_minor: number; currency: string; source?: string | null; } & { [key: string]: unknown; } | |
order.status_changedمطلوب | { order_id: string; from_status: string; status: string; } & { [key: string]: unknown; } | |
order.paidمطلوب | { order_id: string; amount_minor: number; currency: string; } & { [key: string]: unknown; } | |
product.createdمطلوب | { product_id: string; } & { [key: string]: unknown; } | |
product.updatedمطلوب | { product_id: string; variant_id?: string; operation?: "insert" | "update" | "delete"; } & { [key: string]: unknown; } | |
product.deletedمطلوب | { product_id: string; } & { [key: string]: unknown; } | |
inventory.movement_createdمطلوب | { movement_id: string; variant_id: string | null; location_id: string; delta: number; stock_after: number; reason: string; } & { [key: string]: unknown; } | |
fulfillment.requestedمطلوب | { fulfillment_id: string; order_id: string; lines: ({ order_item_id: string; quantity: number; } & { [key: string]: unknown; })[]; } & { [key: string]: unknown; } | |
fulfillment.createdمطلوب | { fulfillment_id: string; order_id: string; status: "pending" | "shipped"; tracking_number: string | null; lines: ({ order_item_id: string; quantity: number; } & { [key: string]: unknown; })[]; order_fully_fulfilled: boolean; } & { [key: string]: unknown; } | |
fulfillment.updatedمطلوب | { fulfillment_id: string; order_id: string; status: "pending" | "shipped" | "delivered" | "cancelled"; previous_status: string; tracking_number: string | null; } & { [key: string]: unknown; } | |
refund.createdمطلوب | { refund_id: string; order_id: string; amount_minor: number; currency: string; lines?: ({ order_item_id: string | null; quantity: number; amount_minor: number; } & { [key: string]: unknown; })[]; source?: "pos"; } & { [key: string]: unknown; } | |
app.uninstalledمطلوب | { app_id: string; install_id: string; } & { [key: string]: unknown; } |
InboxRecord#
One accepted delivery as the receiver hands it to the inbox: the signed event id, the install and store, the topic, and the payload.
| الاسم | النوع | الوصف |
|---|---|---|
eventIdمطلوب | string | |
installIdمطلوب | string | |
storeIdمطلوب | string | |
topicمطلوب | string | |
sequenceمطلوب | number | |
occurredAtمطلوب | string | |
testمطلوب | boolean | |
payloadمطلوب | Record<string, unknown> | |
receivedAtمطلوب | string |
InboxStore#
Durable inbox the webhook receiver deduplicates on. insertIfNew must be
atomic (a unique index on the event id in SQL, a conditional put in KV).
| الاسم | النوع | الوصف |
|---|---|---|
insertIfNewمطلوب | (record: InboxRecord): Promise<boolean> | |
markFailed | ((eventId: string, error: string) => Promise<void>) | undefined | Called when a handler failed AFTER the event was recorded, so the app can keep the row visible for a replay instead of losing the event behind the dedupe. Optional; the receiver never throws past the acknowledgement. |
InstallSecrets#
What the receiver knows about the install a delivery claims to be for.
| الاسم | النوع | الوصف |
|---|---|---|
secretsمطلوب | ReadonlyArray<string> | Current signing secret first, then any previous one still in its grace window. |
storeIdمطلوب | string | null | The store the app bound this install to; a mismatching envelope is refused. |
ReceiveResult#
What receive returns: the small JSON answer to send, the accepted event (also for duplicates) and the deferred process step.
| الاسم | النوع | الوصف |
|---|---|---|
responseمطلوب | WebhookResponse | |
eventمطلوب | DukkanEvent<"order.created" | "order.status_changed" | "order.paid" | "product.created" | "product.updated" | "product.deleted" | "inventory.movement_created" | "fulfillment.requested" | "fulfillment.created" | "fulfillment.updated" | "refund.created" | "app.uninstalled"> | null | The accepted event, when verification passed (also for duplicates). |
processمطلوب | () => Promise<void> | Dispatches handlers. Await it before responding for inline processing,
or hand it to |
VerifyWebhookInput#
Input of verifyWebhookSignature: the raw bytes, the two signature headers, and the secret or secrets to try.
| الاسم | النوع | الوصف |
|---|---|---|
rawBodyمطلوب | string | ArrayBuffer | Uint8Array<ArrayBufferLike> | The request body EXACTLY as received (bytes or the raw string); never re-serialized JSON. |
timestampمطلوب | string | null | undefined |
|
signatureمطلوب | string | null | undefined |
|
secretمطلوب | string | ReadonlyArray<string> | The install's signing secret, or candidates when you hold more than one (rotation). |
toleranceSeconds | number | undefined | |
nowMs | number | undefined | Injectable clock (milliseconds) for tests. |
WebhookHeaders#
The delivery headers as read from the request, case-insensitively.
| الاسم | النوع | الوصف |
|---|---|---|
deliveryIdمطلوب | string | null | |
installIdمطلوب | string | null | Unsigned hint; the signed |
topicمطلوب | string | null | |
timestampمطلوب | string | null | |
apiVersionمطلوب | string | null | |
signatureمطلوب | string | null | |
contentLengthمطلوب | string | null |
WebhookReceiverOptions#
Configuration of createWebhookReceiver: where secrets and the inbox live, the typed handlers, and the hooks around them.
| الاسم | النوع | الوصف |
|---|---|---|
secretsForمطلوب | (installId: string) => Promise<InstallSecrets | null> | Looks up the signing secret(s) for the install named in the header hint. Return null for an install you do not know; the delivery is answered 404 and the platform retries until the app learns about the install. |
inbox | InboxStore | undefined | Durable dedupe on the signed event id (default: none, every delivery dispatches). |
handlers | WebhookHandlers | undefined | |
onUnknownTopic | ((event: DukkanEvent) => Promise<void> | void) | undefined | |
onEvent | ((event: DukkanEvent, outcome: "accepted" | "duplicate") => Promise<void> | void) | undefined | Runs before handlers on every accepted event (metrics, logging). |
onHandlerError | ((error: unknown, event: DukkanEvent) => Promise<void> | void) | undefined | Handler errors are caught so the delivery stays acknowledged: the event
is already recorded in the inbox and the platform would otherwise retry
into a |
maxBodyBytes | number | undefined | |
toleranceSeconds | number | undefined | |
logger | DukkanLogger | undefined | |
nowMs | (() => number) | undefined |
WebhookResponse#
The status and body the receiver wants returned to the platform. Always small JSON.
| الاسم | النوع | الوصف |
|---|---|---|
statusمطلوب | number | |
bodyمطلوب | { ok: true; duplicate?: boolean; } | { error: WebhookRejection; } |
الأنواع
VerifyWebhookResult#
WebhookEnvelope#
WebhookHandler#
WebhookHandlers#
الثوابت
envelopeSchema#
The signed body (docs/apps/webhooks#envelope). Loose on purpose: v1 is
additive, so a field added later must parse on an older SDK. topic is
NOT restricted to the known list here so an unknown topic reaches
onUnknownTopic instead of failing verification-adjacent parsing.
WEBHOOK_MAX_BODY_BYTES#
Largest delivery body the receiver accepts (256 KB); the platform never sends more.
WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS#
How far the signed timestamp may drift from now (5 minutes) before a delivery is refused as stale.