@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/index.ts CHANGED
@@ -2367,6 +2367,139 @@ export interface BulkOperationResult {
2367
2367
  message: string;
2368
2368
  }
2369
2369
 
2370
+ // =====================================================
2371
+ // Webhook Subscription Types
2372
+ // =====================================================
2373
+
2374
+ /**
2375
+ * Record event name constants — use these when constructing `events` on a
2376
+ * webhook subscription instead of hand-typing strings.
2377
+ *
2378
+ * @example
2379
+ * ```ts
2380
+ * await centrali.webhookSubscriptions.create({
2381
+ * name: 'Order created',
2382
+ * url: 'https://my-app.example.com/webhooks/centrali',
2383
+ * events: [RecordEvents.CREATED, RecordEvents.UPDATED],
2384
+ * });
2385
+ * ```
2386
+ */
2387
+ export const RecordEvents = {
2388
+ CREATED: 'record_created',
2389
+ UPDATED: 'record_updated',
2390
+ DELETED: 'record_deleted',
2391
+ BULK_CREATED: 'records_bulk_created',
2392
+ } as const;
2393
+
2394
+ export type WebhookDeliveryStatus = 'success' | 'failed' | 'retrying';
2395
+
2396
+ export interface WebhookSubscription {
2397
+ id: string;
2398
+ name: string;
2399
+ url: string;
2400
+ events: RecordEventType[];
2401
+ /**
2402
+ * Optional list of record slugs to scope the subscription to. When null or
2403
+ * empty, the subscription receives events for all collections.
2404
+ */
2405
+ recordSlugs?: string[] | null;
2406
+ active: boolean;
2407
+ /**
2408
+ * Signing secret (`whsec_` prefix). Returned by `create` and `rotateSecret`;
2409
+ * undefined on subsequent reads so it is safe to pass subscriptions around.
2410
+ */
2411
+ secret?: string;
2412
+ workspaceSlug: string;
2413
+ createdAt?: string;
2414
+ updatedAt?: string;
2415
+ }
2416
+
2417
+ /**
2418
+ * Full webhook delivery record including request payload and response body.
2419
+ * Returned by `deliveries.get()`.
2420
+ */
2421
+ export interface WebhookDelivery {
2422
+ id: string;
2423
+ webhookSubscriptionId: string;
2424
+ workspaceSlug: string;
2425
+ event: string;
2426
+ attemptCount: number;
2427
+ status: WebhookDeliveryStatus;
2428
+ lastError?: string | null;
2429
+ httpStatus?: number | null;
2430
+ lastAttemptAt: string;
2431
+ nextAttemptAt?: string | null;
2432
+ requestPayload?: any;
2433
+ responseBody?: string | null;
2434
+ /** Delivery ID the row was replayed from, or null for original deliveries. */
2435
+ replayedFrom?: string | null;
2436
+ createdAt: string;
2437
+ updatedAt: string;
2438
+ }
2439
+
2440
+ /**
2441
+ * Trimmed delivery row returned by `deliveries.list()` — omits `requestPayload`
2442
+ * and `responseBody`. Fetch individual deliveries with `deliveries.get()` to get
2443
+ * the full payload and response body.
2444
+ */
2445
+ export type WebhookDeliverySummary = Omit<WebhookDelivery, 'requestPayload' | 'responseBody'>;
2446
+
2447
+ export interface CreateWebhookSubscriptionInput {
2448
+ name: string;
2449
+ url: string;
2450
+ events: RecordEventType[];
2451
+ /** Optional — restrict to a subset of collections. Omit for all collections. */
2452
+ recordSlugs?: string[];
2453
+ active?: boolean;
2454
+ }
2455
+
2456
+ export interface UpdateWebhookSubscriptionInput {
2457
+ name?: string;
2458
+ url?: string;
2459
+ events?: RecordEventType[];
2460
+ /**
2461
+ * Update the collection scope. Pass an empty array (`[]`) to clear the
2462
+ * restriction so the subscription receives events for all collections.
2463
+ * Omit the field to leave the scope unchanged.
2464
+ */
2465
+ recordSlugs?: string[];
2466
+ active?: boolean;
2467
+ }
2468
+
2469
+ export interface ListWebhookDeliveriesOptions {
2470
+ status?: WebhookDeliveryStatus;
2471
+ /** ISO 8601 datetime or Date — include deliveries created at or after this time. */
2472
+ since?: string | Date;
2473
+ /** ISO 8601 datetime or Date — include deliveries created at or before this time. */
2474
+ until?: string | Date;
2475
+ limit?: number;
2476
+ offset?: number;
2477
+ }
2478
+
2479
+ /**
2480
+ * Pagination metadata returned alongside `deliveries.list()` rows. The backend
2481
+ * surfaces `{ data, meta }` which the SDK normalizer passes through as the
2482
+ * `ApiResponse` itself — so `result.data` is the array and `result.meta` is
2483
+ * this object.
2484
+ */
2485
+ export interface WebhookDeliveriesListMeta {
2486
+ total: number;
2487
+ limit: number;
2488
+ offset: number;
2489
+ }
2490
+
2491
+ export interface WebhookReplayResponse {
2492
+ deliveryId: string;
2493
+ replayedFrom: string | null;
2494
+ status: WebhookDeliveryStatus;
2495
+ }
2496
+
2497
+ export interface WebhookCancelResponse {
2498
+ deliveryId: string;
2499
+ status: WebhookDeliveryStatus;
2500
+ lastError: string | null;
2501
+ }
2502
+
2370
2503
  /**
2371
2504
  * Generate the API URL from the base URL by adding the 'api.' subdomain.
2372
2505
  * E.g., https://centrali.io -> https://api.centrali.io
@@ -3066,6 +3199,49 @@ export function getAllowedDomainsApiPath(workspaceId: string, domainId?: string)
3066
3199
  return domainId ? `${basePath}/${domainId}` : basePath;
3067
3200
  }
3068
3201
 
3202
+ // =====================================================
3203
+ // Webhook Subscription API Paths
3204
+ // =====================================================
3205
+
3206
+ /**
3207
+ * Generate Webhook Subscriptions API URL PATH.
3208
+ */
3209
+ export function getWebhookSubscriptionsApiPath(workspaceId: string, subscriptionId?: string): string {
3210
+ const basePath = `data/workspace/${workspaceId}/api/v1/webhook-subscriptions`;
3211
+ return subscriptionId ? `${basePath}/${subscriptionId}` : basePath;
3212
+ }
3213
+
3214
+ /**
3215
+ * Generate rotate-secret API URL PATH for a webhook subscription.
3216
+ */
3217
+ export function getWebhookSubscriptionRotateSecretApiPath(workspaceId: string, subscriptionId: string): string {
3218
+ return `data/workspace/${workspaceId}/api/v1/webhook-subscriptions/${subscriptionId}/rotate-secret`;
3219
+ }
3220
+
3221
+ /**
3222
+ * Generate deliveries API URL PATH scoped to a webhook subscription.
3223
+ */
3224
+ export function getWebhookSubscriptionDeliveriesApiPath(workspaceId: string, subscriptionId: string, deliveryId?: string): string {
3225
+ const basePath = `data/workspace/${workspaceId}/api/v1/webhook-subscriptions/${subscriptionId}/deliveries`;
3226
+ return deliveryId ? `${basePath}/${deliveryId}` : basePath;
3227
+ }
3228
+
3229
+ /**
3230
+ * Generate retry API URL PATH for a webhook delivery.
3231
+ * Retry is workspace-scoped (not nested under a subscription) — only the delivery ID is needed.
3232
+ */
3233
+ export function getWebhookDeliveryRetryApiPath(workspaceId: string, deliveryId: string): string {
3234
+ return `data/workspace/${workspaceId}/api/v1/webhook-subscriptions/deliveries/${deliveryId}/retry`;
3235
+ }
3236
+
3237
+ /**
3238
+ * Generate cancel API URL PATH for a webhook delivery retry.
3239
+ * Cancel is workspace-scoped — only the delivery ID is needed.
3240
+ */
3241
+ export function getWebhookDeliveryCancelApiPath(workspaceId: string, deliveryId: string): string {
3242
+ return `data/workspace/${workspaceId}/api/v1/webhook-subscriptions/deliveries/${deliveryId}/cancel`;
3243
+ }
3244
+
3069
3245
  // =====================================================
3070
3246
  // Orchestrations Manager
3071
3247
  // =====================================================
@@ -5146,6 +5322,188 @@ export class FunctionRunsManager {
5146
5322
  }
5147
5323
  }
5148
5324
 
5325
+ // =====================================================
5326
+ // Webhook Subscriptions Manager
5327
+ // =====================================================
5328
+
5329
+ /**
5330
+ * WebhookSubscriptionsManager provides methods for managing outbound webhook
5331
+ * subscriptions and inspecting delivery history. Access via
5332
+ * `centrali.webhookSubscriptions`.
5333
+ *
5334
+ * Subscriptions listen for record events (`record_created`, `record_updated`,
5335
+ * `record_deleted`, `records_bulk_created`) and POST a signed JSON payload to
5336
+ * your URL. Payloads are signed with HMAC-SHA256 using the subscription's
5337
+ * `whsec_` secret and delivered in the `X-Signature` header.
5338
+ *
5339
+ * Usage:
5340
+ * ```ts
5341
+ * // Create a subscription
5342
+ * const sub = await centrali.webhookSubscriptions.create({
5343
+ * name: 'Orders webhook',
5344
+ * url: 'https://my-app.example.com/webhooks/centrali',
5345
+ * events: [RecordEvents.CREATED, RecordEvents.UPDATED],
5346
+ * recordSlugs: ['orders'],
5347
+ * });
5348
+ * // The signing secret is returned on create — copy it now, it's not shown again.
5349
+ * // `secret` is typed `string | undefined` (reads omit it), so assert here.
5350
+ * console.log('Signing secret:', sub.data.secret!);
5351
+ *
5352
+ * // Rotate the secret (immediate cutover)
5353
+ * const rotated = await centrali.webhookSubscriptions.rotateSecret(sub.data.id);
5354
+ *
5355
+ * // Inspect delivery history
5356
+ * const deliveries = await centrali.webhookSubscriptions.deliveries.list(sub.data.id);
5357
+ *
5358
+ * // Replay a delivery
5359
+ * await centrali.webhookSubscriptions.deliveries.retry(deliveryId);
5360
+ * ```
5361
+ */
5362
+ export class WebhookSubscriptionsManager {
5363
+ private requestFn: <T>(method: Method, path: string, data?: any, queryParams?: Record<string, any>) => Promise<ApiResponse<T>>;
5364
+ private workspaceId: string;
5365
+
5366
+ /**
5367
+ * Delivery history and replay controls. Deliveries are per-subscription for
5368
+ * list/get, but retry/cancel are workspace-scoped — the backend routes
5369
+ * those under `/webhook-subscriptions/deliveries/{id}/retry|cancel`, so the
5370
+ * subscription ID is not needed to replay or cancel.
5371
+ */
5372
+ public readonly deliveries: {
5373
+ /**
5374
+ * List deliveries for a subscription. Rows omit `requestPayload` and
5375
+ * `responseBody` — use `deliveries.get()` to fetch the full record.
5376
+ * `result.data` is the array of trimmed rows; `result.meta` carries
5377
+ * the pagination counters (`total`, `limit`, `offset`).
5378
+ */
5379
+ list: (subscriptionId: string, options?: ListWebhookDeliveriesOptions) => Promise<ApiResponse<WebhookDeliverySummary[]> & { meta?: WebhookDeliveriesListMeta }>;
5380
+ /** Get a single delivery including the full payload and response body. */
5381
+ get: (subscriptionId: string, deliveryId: string) => Promise<ApiResponse<WebhookDelivery>>;
5382
+ /**
5383
+ * Replay a previously recorded delivery. Queues a new delivery row
5384
+ * pointing at `replayedFrom` the original. Returns the new delivery ID.
5385
+ */
5386
+ retry: (deliveryId: string) => Promise<ApiResponse<WebhookReplayResponse>>;
5387
+ /**
5388
+ * Cancel a delivery that is currently in `retrying` status. Flips the
5389
+ * row to `failed` with `lastError = 'Cancelled by user'`.
5390
+ */
5391
+ cancel: (deliveryId: string) => Promise<ApiResponse<WebhookCancelResponse>>;
5392
+ };
5393
+
5394
+ constructor(
5395
+ workspaceId: string,
5396
+ requestFn: <T>(method: Method, path: string, data?: any, queryParams?: Record<string, any>) => Promise<ApiResponse<T>>
5397
+ ) {
5398
+ this.workspaceId = workspaceId;
5399
+ this.requestFn = requestFn;
5400
+
5401
+ const toIso = (v: string | Date | undefined): string | undefined => {
5402
+ if (!v) return undefined;
5403
+ return v instanceof Date ? v.toISOString() : v;
5404
+ };
5405
+
5406
+ this.deliveries = {
5407
+ list: (subscriptionId: string, options?: ListWebhookDeliveriesOptions) => {
5408
+ const path = getWebhookSubscriptionDeliveriesApiPath(this.workspaceId, subscriptionId);
5409
+ const queryParams: Record<string, any> = {};
5410
+ if (options?.status) queryParams.status = options.status;
5411
+ if (options?.since) queryParams.since = toIso(options.since);
5412
+ if (options?.until) queryParams.until = toIso(options.until);
5413
+ if (options?.limit !== undefined) queryParams.limit = options.limit;
5414
+ if (options?.offset !== undefined) queryParams.offset = options.offset;
5415
+ return this.requestFn<WebhookDeliverySummary[]>(
5416
+ 'GET',
5417
+ path,
5418
+ null,
5419
+ Object.keys(queryParams).length > 0 ? queryParams : undefined
5420
+ ) as Promise<ApiResponse<WebhookDeliverySummary[]> & { meta?: WebhookDeliveriesListMeta }>;
5421
+ },
5422
+ get: (subscriptionId: string, deliveryId: string) => {
5423
+ const path = getWebhookSubscriptionDeliveriesApiPath(this.workspaceId, subscriptionId, deliveryId);
5424
+ return this.requestFn<WebhookDelivery>('GET', path);
5425
+ },
5426
+ retry: (deliveryId: string) => {
5427
+ const path = getWebhookDeliveryRetryApiPath(this.workspaceId, deliveryId);
5428
+ return this.requestFn<WebhookReplayResponse>('POST', path);
5429
+ },
5430
+ cancel: (deliveryId: string) => {
5431
+ const path = getWebhookDeliveryCancelApiPath(this.workspaceId, deliveryId);
5432
+ return this.requestFn<WebhookCancelResponse>('POST', path);
5433
+ },
5434
+ };
5435
+ }
5436
+
5437
+ /**
5438
+ * List all webhook subscriptions in the workspace.
5439
+ *
5440
+ * @example
5441
+ * ```ts
5442
+ * const subs = await centrali.webhookSubscriptions.list();
5443
+ * for (const sub of subs.data) console.log(sub.name, sub.url);
5444
+ * ```
5445
+ */
5446
+ public list(): Promise<ApiResponse<WebhookSubscription[]>> {
5447
+ const path = getWebhookSubscriptionsApiPath(this.workspaceId);
5448
+ return this.requestFn<WebhookSubscription[]>('GET', path);
5449
+ }
5450
+
5451
+ /**
5452
+ * Get a webhook subscription by ID.
5453
+ * @param subscriptionId - The subscription UUID
5454
+ */
5455
+ public get(subscriptionId: string): Promise<ApiResponse<WebhookSubscription>> {
5456
+ const path = getWebhookSubscriptionsApiPath(this.workspaceId, subscriptionId);
5457
+ return this.requestFn<WebhookSubscription>('GET', path);
5458
+ }
5459
+
5460
+ /**
5461
+ * Create a webhook subscription. The response `secret` field is only
5462
+ * populated on this call (and on `rotateSecret`) — copy it immediately;
5463
+ * subsequent `get`/`list` responses do not return the secret.
5464
+ *
5465
+ * @example
5466
+ * ```ts
5467
+ * const sub = await centrali.webhookSubscriptions.create({
5468
+ * name: 'Order notifications',
5469
+ * url: 'https://api.example.com/hooks/centrali',
5470
+ * events: [RecordEvents.CREATED, RecordEvents.UPDATED],
5471
+ * recordSlugs: ['orders'],
5472
+ * });
5473
+ * const signingSecret = sub.data.secret; // copy once — not returned on reads
5474
+ * ```
5475
+ */
5476
+ public create(input: CreateWebhookSubscriptionInput): Promise<ApiResponse<WebhookSubscription>> {
5477
+ const path = getWebhookSubscriptionsApiPath(this.workspaceId);
5478
+ return this.requestFn<WebhookSubscription>('POST', path, input);
5479
+ }
5480
+
5481
+ /**
5482
+ * Update fields on a webhook subscription. The signing secret is managed
5483
+ * by the server — use `rotateSecret()` to regenerate it.
5484
+ */
5485
+ public update(subscriptionId: string, patch: UpdateWebhookSubscriptionInput): Promise<ApiResponse<WebhookSubscription>> {
5486
+ const path = getWebhookSubscriptionsApiPath(this.workspaceId, subscriptionId);
5487
+ return this.requestFn<WebhookSubscription>('PATCH', path, patch);
5488
+ }
5489
+
5490
+ /** Delete a webhook subscription. Existing delivery history is retained. */
5491
+ public delete(subscriptionId: string): Promise<ApiResponse<void>> {
5492
+ const path = getWebhookSubscriptionsApiPath(this.workspaceId, subscriptionId);
5493
+ return this.requestFn<void>('DELETE', path);
5494
+ }
5495
+
5496
+ /**
5497
+ * Rotate the signing secret. Immediate cutover — the previous secret stops
5498
+ * signing on the next dispatch. The response includes the new secret;
5499
+ * capture it before closing the response.
5500
+ */
5501
+ public rotateSecret(subscriptionId: string): Promise<ApiResponse<WebhookSubscription>> {
5502
+ const path = getWebhookSubscriptionRotateSecretApiPath(this.workspaceId, subscriptionId);
5503
+ return this.requestFn<WebhookSubscription>('POST', path);
5504
+ }
5505
+ }
5506
+
5149
5507
  /**
5150
5508
  * Main Centrali SDK client.
5151
5509
  */
@@ -5164,6 +5522,7 @@ export class CentraliSDK {
5164
5522
  private _collections: CollectionsManager | null = null;
5165
5523
  private _functions: ComputeFunctionsManager | null = null;
5166
5524
  private _runs: FunctionRunsManager | null = null;
5525
+ private _webhookSubscriptions: WebhookSubscriptionsManager | null = null;
5167
5526
  private isRefreshingToken: boolean = false;
5168
5527
  private tokenRefreshPromise: Promise<string> | null = null;
5169
5528
 
@@ -5661,6 +6020,43 @@ export class CentraliSDK {
5661
6020
  return this._runs;
5662
6021
  }
5663
6022
 
6023
+ /**
6024
+ * Webhook subscriptions namespace for outbound webhooks — create, update,
6025
+ * rotate signing secrets, inspect delivery history, and replay/cancel
6026
+ * individual deliveries.
6027
+ *
6028
+ * Usage:
6029
+ * ```ts
6030
+ * // Create a subscription (capture secret immediately — not returned on reads)
6031
+ * const sub = await centrali.webhookSubscriptions.create({
6032
+ * name: 'Order notifications',
6033
+ * url: 'https://api.example.com/hooks/centrali',
6034
+ * events: [RecordEvents.CREATED, RecordEvents.UPDATED],
6035
+ * recordSlugs: ['orders'],
6036
+ * });
6037
+ *
6038
+ * // Rotate the signing secret (immediate cutover)
6039
+ * const rotated = await centrali.webhookSubscriptions.rotateSecret(sub.data.id);
6040
+ *
6041
+ * // Inspect deliveries
6042
+ * const deliveries = await centrali.webhookSubscriptions.deliveries.list(sub.data.id, {
6043
+ * status: 'failed'
6044
+ * });
6045
+ *
6046
+ * // Replay a failed delivery
6047
+ * await centrali.webhookSubscriptions.deliveries.retry(deliveries.data[0].id);
6048
+ * ```
6049
+ */
6050
+ public get webhookSubscriptions(): WebhookSubscriptionsManager {
6051
+ if (!this._webhookSubscriptions) {
6052
+ this._webhookSubscriptions = new WebhookSubscriptionsManager(
6053
+ this.options.workspaceId,
6054
+ this.request.bind(this)
6055
+ );
6056
+ }
6057
+ return this._webhookSubscriptions;
6058
+ }
6059
+
5664
6060
  /**
5665
6061
  * Manually set or update the bearer token for subsequent requests.
5666
6062
  */
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@centrali-io/centrali-sdk",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Centrali Node SDK",
5
5
  "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
6
7
  "type": "commonjs",
7
8
  "repository": {
8
9
  "type": "git",
@@ -10,8 +11,7 @@
10
11
  },
11
12
  "scripts": {
12
13
  "build": "tsc --project tsconfig.json",
13
- "prepublishOnly": "npm run build",
14
- "test": "echo \"Error: no test specified\" && exit 1"
14
+ "prepublishOnly": "npm run build"
15
15
  },
16
16
  "keywords": [],
17
17
  "author": "Blueinit",
@@ -25,7 +25,7 @@
25
25
  "@types/eventsource": "^1.1.15",
26
26
  "@types/q": "^1.5.8",
27
27
  "@types/qs": "^6.14.0",
28
- "typescript": "^5.8.3"
28
+ "typescript": "5.9.3"
29
29
  },
30
30
  "publishConfig": {
31
31
  "registry": "https://registry.npmjs.org/",
package/tsconfig.json CHANGED
@@ -52,7 +52,7 @@
52
52
  // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
53
53
 
54
54
  /* Emit */
55
- // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
55
+ "declaration": true, /* Emit .d.ts alongside .js so consumers don't re-parse index.ts (~6.6k lines) on every build. */
56
56
  // "declarationMap": true, /* Create sourcemaps for d.ts files. */
57
57
  // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
58
58
  // "sourceMap": true, /* Create source map files for emitted JavaScript files. */