@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 +104 -0
- package/dist/index.d.ts +4918 -0
- package/dist/index.js +239 -1
- package/index.ts +396 -0
- package/package.json +4 -4
- package/tsconfig.json +1 -1
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:
|