@centrali-io/centrali-sdk 5.2.0 → 5.3.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/README.md CHANGED
@@ -898,6 +898,110 @@ await centrali.orchestrations.delete('orch-id');
898
898
  | `completed` | Run finished successfully |
899
899
  | `failed` | Run failed with error |
900
900
 
901
+ ### Webhook Subscriptions
902
+
903
+ Outbound webhooks for record events. Centrali POSTs a signed JSON payload to your URL and records every delivery attempt; failed deliveries retry with backoff and can be replayed or cancelled individually.
904
+
905
+ ```typescript
906
+ import { CentraliSDK, RecordEvents } from '@centrali-io/centrali-sdk';
907
+
908
+ // Create a subscription — the signing secret is returned ONCE on create
909
+ const { data: sub } = await centrali.webhookSubscriptions.create({
910
+ name: 'Order notifications',
911
+ url: 'https://api.example.com/hooks/centrali',
912
+ events: [RecordEvents.CREATED, RecordEvents.UPDATED],
913
+ recordSlugs: ['orders'], // omit for all collections
914
+ });
915
+ // `secret` is typed `string | undefined` because reads omit it; create/rotate
916
+ // always populate it, so assert here to keep strict TypeScript happy.
917
+ console.log('Signing secret:', sub.secret!); // copy now — not returned on reads
918
+ ```
919
+
920
+ ```typescript
921
+ // Rotate the signing secret (immediate cutover)
922
+ const { data: rotated } = await centrali.webhookSubscriptions.rotateSecret(sub.id);
923
+ console.log('New secret:', rotated.secret!);
924
+
925
+ // List, update, delete
926
+ const { data: all } = await centrali.webhookSubscriptions.list();
927
+ await centrali.webhookSubscriptions.update(sub.id, { active: false });
928
+ await centrali.webhookSubscriptions.delete(sub.id);
929
+ ```
930
+
931
+ #### Delivery History & Replay
932
+
933
+ ```typescript
934
+ // List deliveries (rows omit requestPayload/responseBody — use .get() for those)
935
+ const deliveries = await centrali.webhookSubscriptions.deliveries.list(sub.id, {
936
+ status: 'failed',
937
+ since: new Date(Date.now() - 24 * 60 * 60 * 1000),
938
+ limit: 50,
939
+ });
940
+
941
+ // Fetch a single delivery with full payload and response body
942
+ const delivery = await centrali.webhookSubscriptions.deliveries.get(sub.id, deliveryId);
943
+ console.log('Payload:', delivery.data.requestPayload);
944
+ console.log('Response:', delivery.data.httpStatus, delivery.data.responseBody);
945
+
946
+ // Replay a failed delivery — reuses the original payload and signature
947
+ await centrali.webhookSubscriptions.deliveries.retry(deliveryId);
948
+
949
+ // Cancel a delivery that is currently retrying
950
+ await centrali.webhookSubscriptions.deliveries.cancel(deliveryId);
951
+ ```
952
+
953
+ #### Event Types
954
+
955
+ | Event | Emitted When |
956
+ |-------|-------------|
957
+ | `record_created` | A new record is inserted |
958
+ | `record_updated` | An existing record is modified |
959
+ | `record_deleted` | A record is deleted (soft or hard) |
960
+ | `records_bulk_created` | Multiple records are inserted in one batch |
961
+
962
+ #### Outbound Payload
963
+
964
+ Centrali POSTs this JSON body to your URL:
965
+
966
+ ```jsonc
967
+ {
968
+ "event": "record_created", // 'record_created' | 'record_updated' | 'record_deleted' | 'records_bulk_created'
969
+ "workspaceSlug": "acme",
970
+ "recordSlug": "orders", // collection slug (may be absent on some bulk events)
971
+ "recordId": "3f2c…-…-…", // UUID of the affected record
972
+ "data": { /* record snapshot */ },// present on create/update; absent on delete
973
+ "timestamp": "2026-04-22T09:31:04.112Z",
974
+ "createdBy": "user_abc" // actor — only the field for the matching event
975
+ // (createdBy for create, updatedBy for update, deletedBy for delete)
976
+ }
977
+ ```
978
+
979
+ Verify the signature against the raw bytes of this body **before** parsing it.
980
+
981
+ #### Signature Verification
982
+
983
+ Every dispatch includes an `X-Signature` header containing a base64 HMAC-SHA256 of the raw request body. The signing secret has the form `whsec_<base64url>`; strip the prefix and base64url-decode the rest to get the raw key bytes.
984
+
985
+ ```typescript
986
+ import crypto from 'crypto';
987
+
988
+ function verifyCentraliSignature(rawBody: string, header: string, secret: string): boolean {
989
+ const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64url');
990
+ const expected = crypto.createHmac('sha256', key).update(rawBody).digest('base64');
991
+ const received = Buffer.from(header);
992
+ const expectedBuf = Buffer.from(expected);
993
+ return received.length === expectedBuf.length && crypto.timingSafeEqual(received, expectedBuf);
994
+ }
995
+ ```
996
+
997
+ #### Delivery Statuses
998
+
999
+ | Status | Description |
1000
+ |--------|-------------|
1001
+ | `success` | Endpoint returned 2xx |
1002
+ | `retrying` | Awaiting next retry attempt |
1003
+ | `failed` | Exhausted all retries, or cancelled |
1004
+
901
1005
  ## TypeScript Support
902
1006
 
903
1007
  The SDK includes full TypeScript definitions for type-safe development: