@newhomestar/sdk 0.8.1 → 0.8.3
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/dist/events.d.ts +87 -0
- package/dist/events.js +160 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/integration.d.ts +129 -0
- package/dist/integration.js +38 -0
- package/dist/integrationSpec.d.ts +13 -0
- package/dist/integrationSpec.js +14 -0
- package/package.json +58 -58
package/dist/events.d.ts
CHANGED
|
@@ -195,6 +195,93 @@ export declare function queueEvent(payload: QueueEventPayload): Promise<{
|
|
|
195
195
|
* startOutboxRelay(db);
|
|
196
196
|
*/
|
|
197
197
|
export declare function startOutboxRelay(db: any, options?: OutboxRelayOptions): ReturnType<typeof setInterval>;
|
|
198
|
+
/**
|
|
199
|
+
* Options for processing an inbound webhook event.
|
|
200
|
+
*/
|
|
201
|
+
export interface WebhookEventOptions {
|
|
202
|
+
/**
|
|
203
|
+
* Unique idempotency key derived from provider-supplied data.
|
|
204
|
+
* Used as the UNIQUE constraint for deduplication.
|
|
205
|
+
*
|
|
206
|
+
* Recommended formats:
|
|
207
|
+
* BambooHR: `bamboohr:${webhookId}:${timestamp}`
|
|
208
|
+
* Jira: `jira:${webhookEvent.id}`
|
|
209
|
+
* Generic: SHA-256 hash of the raw payload body
|
|
210
|
+
*/
|
|
211
|
+
idempotencyKey: string;
|
|
212
|
+
/**
|
|
213
|
+
* Event type string for classification and routing.
|
|
214
|
+
* e.g., "bamboohr.employee.updated", "jira.issue.created"
|
|
215
|
+
*/
|
|
216
|
+
eventType: string;
|
|
217
|
+
/**
|
|
218
|
+
* Queue name for categorization (defaults to "webhook").
|
|
219
|
+
* Useful for filtering/querying inbound events by source.
|
|
220
|
+
*/
|
|
221
|
+
queueName?: string;
|
|
222
|
+
/**
|
|
223
|
+
* Raw webhook payload — stored as-is in nova_inbound_events for audit.
|
|
224
|
+
*/
|
|
225
|
+
payload: Record<string, unknown>;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Process an inbound webhook with full ACID guarantee.
|
|
229
|
+
*
|
|
230
|
+
* Atomically:
|
|
231
|
+
* 1. Checks idempotency (UNIQUE on idempotency_key in nova_inbound_events)
|
|
232
|
+
* 2. Inserts the raw payload into nova_inbound_events
|
|
233
|
+
* 3. Runs the handler callback (business logic + domain writes)
|
|
234
|
+
* 4. Writes outbox rows for any staged events
|
|
235
|
+
* 5. Marks the inbound event as 'processed'
|
|
236
|
+
*
|
|
237
|
+
* If the handler throws, the entire transaction rolls back — no partial
|
|
238
|
+
* writes, no phantom inbound events, no orphaned outbox rows.
|
|
239
|
+
*
|
|
240
|
+
* The caller should:
|
|
241
|
+
* - Return HTTP 200 when status is 'processed' or 'duplicate'
|
|
242
|
+
* - Return HTTP 500 when withWebhookEvent() throws (triggers provider retry)
|
|
243
|
+
*
|
|
244
|
+
* ## Idempotency
|
|
245
|
+
*
|
|
246
|
+
* The idempotency_key is a UNIQUE column on nova_inbound_events.
|
|
247
|
+
* If the same key is seen again:
|
|
248
|
+
* - After successful processing: returns { status: 'duplicate' } immediately
|
|
249
|
+
* - After a rolled-back failure: key doesn't exist, full retry occurs
|
|
250
|
+
*
|
|
251
|
+
* ## Outbox Integration
|
|
252
|
+
*
|
|
253
|
+
* The `emit` function passed to the handler stages outbox events.
|
|
254
|
+
* They are written to nova_outbound_events inside the same $transaction.
|
|
255
|
+
* Post-commit, they are relayed (fire-and-forget) by the integration's
|
|
256
|
+
* outbox relay (startHrisOutboxRelay or startOutboxRelay).
|
|
257
|
+
*
|
|
258
|
+
* @param db - Prisma client (must have inboundEvent + eventOutbox models)
|
|
259
|
+
* @param opts - Idempotency key, event type, payload
|
|
260
|
+
* @param handler - Business logic callback; receives (tx, emit), runs inside $transaction
|
|
261
|
+
* @returns - { status: 'processed', result } or { status: 'duplicate' }
|
|
262
|
+
* @throws - On transaction failure (caller should return HTTP 500)
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* // In an integration's handleWebhook action:
|
|
267
|
+
* const { status } = await withWebhookEvent(db, {
|
|
268
|
+
* idempotencyKey: `bamboohr:${webhookId}:${timestamp}`,
|
|
269
|
+
* eventType: 'bamboohr.employee.updated',
|
|
270
|
+
* queueName: 'bamboohr_webhooks',
|
|
271
|
+
* payload: rawBody,
|
|
272
|
+
* }, async (tx, emit) => {
|
|
273
|
+
* // All of this runs inside $transaction:
|
|
274
|
+
* await tx.bambooEmployee.upsert({ ... });
|
|
275
|
+
* emit('employee.upserted', buildEmployeeHrisEvent(emp).payload);
|
|
276
|
+
* });
|
|
277
|
+
*
|
|
278
|
+
* return { status }; // 200 for both 'processed' and 'duplicate'
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
export declare function withWebhookEvent<R>(db: any, opts: WebhookEventOptions, handler: (tx: any, emit: ServiceEmitFn) => Promise<R>): Promise<{
|
|
282
|
+
status: 'processed' | 'duplicate';
|
|
283
|
+
result?: R;
|
|
284
|
+
}>;
|
|
198
285
|
/**
|
|
199
286
|
* A single PGMQ message received from the Nova Events Service SSE stream.
|
|
200
287
|
* Passed to InboundHandler and withInboundEvent().
|
package/dist/events.js
CHANGED
|
@@ -422,6 +422,166 @@ export function startOutboxRelay(db, options = {}) {
|
|
|
422
422
|
}, intervalMs);
|
|
423
423
|
return handle;
|
|
424
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Process an inbound webhook with full ACID guarantee.
|
|
427
|
+
*
|
|
428
|
+
* Atomically:
|
|
429
|
+
* 1. Checks idempotency (UNIQUE on idempotency_key in nova_inbound_events)
|
|
430
|
+
* 2. Inserts the raw payload into nova_inbound_events
|
|
431
|
+
* 3. Runs the handler callback (business logic + domain writes)
|
|
432
|
+
* 4. Writes outbox rows for any staged events
|
|
433
|
+
* 5. Marks the inbound event as 'processed'
|
|
434
|
+
*
|
|
435
|
+
* If the handler throws, the entire transaction rolls back — no partial
|
|
436
|
+
* writes, no phantom inbound events, no orphaned outbox rows.
|
|
437
|
+
*
|
|
438
|
+
* The caller should:
|
|
439
|
+
* - Return HTTP 200 when status is 'processed' or 'duplicate'
|
|
440
|
+
* - Return HTTP 500 when withWebhookEvent() throws (triggers provider retry)
|
|
441
|
+
*
|
|
442
|
+
* ## Idempotency
|
|
443
|
+
*
|
|
444
|
+
* The idempotency_key is a UNIQUE column on nova_inbound_events.
|
|
445
|
+
* If the same key is seen again:
|
|
446
|
+
* - After successful processing: returns { status: 'duplicate' } immediately
|
|
447
|
+
* - After a rolled-back failure: key doesn't exist, full retry occurs
|
|
448
|
+
*
|
|
449
|
+
* ## Outbox Integration
|
|
450
|
+
*
|
|
451
|
+
* The `emit` function passed to the handler stages outbox events.
|
|
452
|
+
* They are written to nova_outbound_events inside the same $transaction.
|
|
453
|
+
* Post-commit, they are relayed (fire-and-forget) by the integration's
|
|
454
|
+
* outbox relay (startHrisOutboxRelay or startOutboxRelay).
|
|
455
|
+
*
|
|
456
|
+
* @param db - Prisma client (must have inboundEvent + eventOutbox models)
|
|
457
|
+
* @param opts - Idempotency key, event type, payload
|
|
458
|
+
* @param handler - Business logic callback; receives (tx, emit), runs inside $transaction
|
|
459
|
+
* @returns - { status: 'processed', result } or { status: 'duplicate' }
|
|
460
|
+
* @throws - On transaction failure (caller should return HTTP 500)
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* ```typescript
|
|
464
|
+
* // In an integration's handleWebhook action:
|
|
465
|
+
* const { status } = await withWebhookEvent(db, {
|
|
466
|
+
* idempotencyKey: `bamboohr:${webhookId}:${timestamp}`,
|
|
467
|
+
* eventType: 'bamboohr.employee.updated',
|
|
468
|
+
* queueName: 'bamboohr_webhooks',
|
|
469
|
+
* payload: rawBody,
|
|
470
|
+
* }, async (tx, emit) => {
|
|
471
|
+
* // All of this runs inside $transaction:
|
|
472
|
+
* await tx.bambooEmployee.upsert({ ... });
|
|
473
|
+
* emit('employee.upserted', buildEmployeeHrisEvent(emp).payload);
|
|
474
|
+
* });
|
|
475
|
+
*
|
|
476
|
+
* return { status }; // 200 for both 'processed' and 'duplicate'
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
export async function withWebhookEvent(db, opts, handler) {
|
|
480
|
+
const serviceSlug = process.env.NOVA_SERVICE_SLUG;
|
|
481
|
+
// Rows to relay post-commit
|
|
482
|
+
const outboxRows = [];
|
|
483
|
+
let result;
|
|
484
|
+
let isDuplicate = false;
|
|
485
|
+
try {
|
|
486
|
+
await db.$transaction(async (tx) => {
|
|
487
|
+
// ── 1. Idempotency check ──────────────────────────────────────────
|
|
488
|
+
const existing = await tx.inboundEvent.findUnique({
|
|
489
|
+
where: { idempotencyKey: opts.idempotencyKey },
|
|
490
|
+
select: { id: true, status: true },
|
|
491
|
+
});
|
|
492
|
+
if (existing) {
|
|
493
|
+
isDuplicate = true;
|
|
494
|
+
return; // Transaction commits as no-op
|
|
495
|
+
}
|
|
496
|
+
// ── 2. Insert inbound event (status: processing) ─────────────────
|
|
497
|
+
await tx.inboundEvent.create({
|
|
498
|
+
data: {
|
|
499
|
+
idempotencyKey: opts.idempotencyKey,
|
|
500
|
+
queueName: opts.queueName ?? 'webhook',
|
|
501
|
+
eventType: opts.eventType,
|
|
502
|
+
payload: opts.payload,
|
|
503
|
+
status: 'processing',
|
|
504
|
+
attempts: 1,
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
// ── 3. Run handler (business logic) ───────────────────────────────
|
|
508
|
+
const stagedEmits = [];
|
|
509
|
+
const emit = (topic, payload, idempotencyKey) => {
|
|
510
|
+
stagedEmits.push({ topic, payload, idempotencyKey });
|
|
511
|
+
};
|
|
512
|
+
result = await handler(tx, emit);
|
|
513
|
+
// ── 4. Write outbox rows for staged events ────────────────────────
|
|
514
|
+
for (const staged of stagedEmits) {
|
|
515
|
+
const fullPayload = {
|
|
516
|
+
event_slug: staged.topic,
|
|
517
|
+
source_service: serviceSlug ?? undefined,
|
|
518
|
+
attributes: staged.payload,
|
|
519
|
+
metadata: { source: 'webhook' },
|
|
520
|
+
};
|
|
521
|
+
const row = await tx.eventOutbox.create({
|
|
522
|
+
data: {
|
|
523
|
+
eventType: staged.topic,
|
|
524
|
+
payload: fullPayload,
|
|
525
|
+
...(staged.idempotencyKey
|
|
526
|
+
? { idempotencyKey: staged.idempotencyKey }
|
|
527
|
+
: {}),
|
|
528
|
+
},
|
|
529
|
+
select: { id: true, idempotencyKey: true },
|
|
530
|
+
});
|
|
531
|
+
outboxRows.push({
|
|
532
|
+
id: row.id,
|
|
533
|
+
idempotencyKey: row.idempotencyKey,
|
|
534
|
+
payload: fullPayload,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
// ── 5. Mark inbound event as processed ────────────────────────────
|
|
538
|
+
await tx.inboundEvent.update({
|
|
539
|
+
where: { idempotencyKey: opts.idempotencyKey },
|
|
540
|
+
data: { status: 'processed', processedAt: new Date() },
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
// Transaction rolled back — record error best-effort (outside tx)
|
|
546
|
+
try {
|
|
547
|
+
await db.inboundEvent.upsert({
|
|
548
|
+
where: { idempotencyKey: opts.idempotencyKey },
|
|
549
|
+
create: {
|
|
550
|
+
idempotencyKey: opts.idempotencyKey,
|
|
551
|
+
queueName: opts.queueName ?? 'webhook',
|
|
552
|
+
eventType: opts.eventType,
|
|
553
|
+
payload: opts.payload,
|
|
554
|
+
status: 'failed',
|
|
555
|
+
error: String(err).slice(0, 500),
|
|
556
|
+
attempts: 1,
|
|
557
|
+
},
|
|
558
|
+
update: {
|
|
559
|
+
status: 'failed',
|
|
560
|
+
error: String(err).slice(0, 500),
|
|
561
|
+
attempts: { increment: 1 },
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
// Non-critical — the provider will retry on 500
|
|
567
|
+
}
|
|
568
|
+
// Re-throw so the HTTP handler returns 500
|
|
569
|
+
throw err;
|
|
570
|
+
}
|
|
571
|
+
if (isDuplicate)
|
|
572
|
+
return { status: 'duplicate' };
|
|
573
|
+
// ── Post-commit: relay outbox rows (fire-and-forget) ──────────────────
|
|
574
|
+
// The integration's startHrisOutboxRelay() or startOutboxRelay() is the
|
|
575
|
+
// safety net for any rows that fail immediate relay.
|
|
576
|
+
if (outboxRows.length > 0) {
|
|
577
|
+
const eventsUrl = process.env.NOVA_EVENTS_SERVICE_URL;
|
|
578
|
+
const serviceToken = process.env.NOVA_SERVICE_TOKEN;
|
|
579
|
+
if (eventsUrl && serviceToken) {
|
|
580
|
+
Promise.allSettled(outboxRows.map(row => _relayRow(db, row, eventsUrl, serviceToken))).catch(() => { });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return { status: 'processed', result };
|
|
584
|
+
}
|
|
425
585
|
// ── withInboundEvent ───────────────────────────────────────────────────────────
|
|
426
586
|
/**
|
|
427
587
|
* Atomically store an inbound PGMQ message + run the handler in one Prisma
|
package/dist/index.d.ts
CHANGED
|
@@ -357,13 +357,13 @@ export declare function runHttpServer<T extends WorkerDef>(def: T, opts?: HttpSe
|
|
|
357
357
|
export declare function runDualMode<T extends WorkerDef>(def: T, opts?: {
|
|
358
358
|
port?: number;
|
|
359
359
|
}): void;
|
|
360
|
-
export { withServiceEventOutbox, withEventOutbox, isIntegrationSync, queueEvent, logEvent, startOutboxRelay, startInboundConsumer, startPollConsumer, NovaEventsClient, } from './events.js';
|
|
361
|
-
export type { QueueEventPayload, LogEventPayload, OutboxRelayOptions, ServiceEmitFn, InboundConsumerOptions, PollConsumerOptions, } from './events.js';
|
|
360
|
+
export { withServiceEventOutbox, withEventOutbox, withWebhookEvent, isIntegrationSync, queueEvent, logEvent, startOutboxRelay, startInboundConsumer, startPollConsumer, NovaEventsClient, } from './events.js';
|
|
361
|
+
export type { QueueEventPayload, LogEventPayload, OutboxRelayOptions, ServiceEmitFn, InboundConsumerOptions, PollConsumerOptions, WebhookEventOptions, } from './events.js';
|
|
362
362
|
export type { ZodTypeAny as SchemaAny, ZodTypeAny };
|
|
363
363
|
export { parseNovaSpec } from "./parseSpec.js";
|
|
364
364
|
export type { NovaSpec } from "./parseSpec.js";
|
|
365
365
|
export { defineIntegration, validateIntegration, integrationSchema, integrationEvent, integrationFunction, schema, event, IntegrationDefSchema, } from "./integration.js";
|
|
366
|
-
export type { IntegrationDef, IntegrationSchemaDef, IntegrationEventDef, IntegrationFunctionDef, ValidationResult, SchemaType, ParamMeta, ParamIn, ParamUiType, SyncMappingDef, SyncMappingFieldDef, } from "./integration.js";
|
|
366
|
+
export type { IntegrationDef, IntegrationSchemaDef, IntegrationEventDef, IntegrationFunctionDef, ValidationResult, SchemaType, ParamMeta, ParamIn, ParamUiType, SyncMappingDef, SyncMappingFieldDef, WebhookConfig, WebhookTypeDef, WebhookFieldDef, } from "./integration.js";
|
|
367
367
|
export { parseIntegrationSpec, IntegrationSpecSchema } from "./integrationSpec.js";
|
|
368
368
|
export type { IntegrationSpec } from "./integrationSpec.js";
|
|
369
369
|
export type WebhookCapability = {
|
package/dist/index.js
CHANGED
|
@@ -897,7 +897,7 @@ export function runDualMode(def, opts = {}) {
|
|
|
897
897
|
/*──────────────── Event Outbox (re-exports for convenience) ───────────────*/
|
|
898
898
|
// Full API is also available via '@newhomestar/sdk/events' subpath.
|
|
899
899
|
// These re-exports allow import { withServiceEventOutbox } from '@newhomestar/sdk'.
|
|
900
|
-
export { withServiceEventOutbox, withEventOutbox, isIntegrationSync, queueEvent, logEvent, startOutboxRelay, startInboundConsumer, startPollConsumer, NovaEventsClient, } from './events.js';
|
|
900
|
+
export { withServiceEventOutbox, withEventOutbox, withWebhookEvent, isIntegrationSync, queueEvent, logEvent, startOutboxRelay, startInboundConsumer, startPollConsumer, NovaEventsClient, } from './events.js';
|
|
901
901
|
// YAML spec parsing utility
|
|
902
902
|
export { parseNovaSpec } from "./parseSpec.js";
|
|
903
903
|
// Integration definition API
|
package/dist/integration.d.ts
CHANGED
|
@@ -245,6 +245,88 @@ export interface SyncMappingDef {
|
|
|
245
245
|
/** Per-field mapping rules */
|
|
246
246
|
fields: SyncMappingFieldDef[];
|
|
247
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* A configuration field that users fill out when subscribing to a webhook type.
|
|
250
|
+
* Rendered as a dynamic form in the Odyssey UI.
|
|
251
|
+
*/
|
|
252
|
+
export interface WebhookFieldDef {
|
|
253
|
+
/** Field name (used as the key in the config JSON) */
|
|
254
|
+
name: string;
|
|
255
|
+
/** Human-readable label */
|
|
256
|
+
label: string;
|
|
257
|
+
/** Input widget type */
|
|
258
|
+
type: 'text' | 'select' | 'multiselect' | 'boolean' | 'url';
|
|
259
|
+
/** Whether this field is required */
|
|
260
|
+
required?: boolean;
|
|
261
|
+
/** Help text shown below the input */
|
|
262
|
+
description?: string;
|
|
263
|
+
/** Default value */
|
|
264
|
+
defaultValue?: unknown;
|
|
265
|
+
/** Options for select/multiselect fields */
|
|
266
|
+
options?: Array<{
|
|
267
|
+
label: string;
|
|
268
|
+
value: string;
|
|
269
|
+
}>;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Declares a category of inbound webhook the provider supports.
|
|
273
|
+
*
|
|
274
|
+
* This is metadata — it tells the platform:
|
|
275
|
+
* "This integration can receive webhooks of this type, producing these events."
|
|
276
|
+
*
|
|
277
|
+
* The actual webhook processing is handled by the integration's handler action
|
|
278
|
+
* using withWebhookEvent() for ACID compliance.
|
|
279
|
+
*/
|
|
280
|
+
export interface WebhookTypeDef {
|
|
281
|
+
/** Human-readable label shown in the UI (e.g., "Employee Changes") */
|
|
282
|
+
label: string;
|
|
283
|
+
/** Description shown in the UI */
|
|
284
|
+
description?: string;
|
|
285
|
+
/**
|
|
286
|
+
* Event types this webhook category produces.
|
|
287
|
+
* These are the event_slug values emitted via the outbox when
|
|
288
|
+
* webhooks of this type are processed.
|
|
289
|
+
* e.g., ["bamboohr.employee.created", "bamboohr.employee.updated"]
|
|
290
|
+
*/
|
|
291
|
+
produces: string[];
|
|
292
|
+
/**
|
|
293
|
+
* How the provider authenticates/signs webhook payloads.
|
|
294
|
+
* Used for documentation and UI display (the integration itself
|
|
295
|
+
* verifies signatures in its handler action).
|
|
296
|
+
*/
|
|
297
|
+
authentication?: {
|
|
298
|
+
/** Signature algorithm */
|
|
299
|
+
method: 'hmac_sha256' | 'hmac_sha1' | 'basic' | 'bearer' | 'none';
|
|
300
|
+
/** Header name containing the signature (e.g., "X-BambooHR-Signature") */
|
|
301
|
+
signatureHeader?: string;
|
|
302
|
+
/** Where the signing secret comes from */
|
|
303
|
+
secretSource?: 'platform_generated' | 'provider_assigned' | 'user_provided';
|
|
304
|
+
};
|
|
305
|
+
/**
|
|
306
|
+
* Configuration fields the user must fill when subscribing.
|
|
307
|
+
* Values are passed to the integration's create-webhook action.
|
|
308
|
+
*/
|
|
309
|
+
fields?: WebhookFieldDef[];
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Top-level webhook configuration for an integration.
|
|
313
|
+
* Declares the handler endpoint and available subscription types.
|
|
314
|
+
*
|
|
315
|
+
* Added to IntegrationDef as an optional `webhooks` property.
|
|
316
|
+
*/
|
|
317
|
+
export interface WebhookConfig {
|
|
318
|
+
/**
|
|
319
|
+
* Action slug that handles ALL inbound webhook payloads.
|
|
320
|
+
* Must be defined in the integration's `actions` map.
|
|
321
|
+
* The action should use withWebhookEvent() for ACID compliance.
|
|
322
|
+
*/
|
|
323
|
+
handler: string;
|
|
324
|
+
/**
|
|
325
|
+
* Available webhook subscription types.
|
|
326
|
+
* Key = type slug (e.g., "employee_changes", "time_off")
|
|
327
|
+
*/
|
|
328
|
+
types: Record<string, WebhookTypeDef>;
|
|
329
|
+
}
|
|
248
330
|
/**
|
|
249
331
|
* Full integration definition — the single source of truth.
|
|
250
332
|
*
|
|
@@ -338,6 +420,12 @@ export interface IntegrationDef {
|
|
|
338
420
|
* ```
|
|
339
421
|
*/
|
|
340
422
|
syncMappings?: Record<string, SyncMappingDef>;
|
|
423
|
+
/**
|
|
424
|
+
* Webhook configuration — declares inbound webhook types
|
|
425
|
+
* and how the platform should display/manage them in the UI.
|
|
426
|
+
* The integration's handler action does the actual processing.
|
|
427
|
+
*/
|
|
428
|
+
webhooks?: WebhookConfig;
|
|
341
429
|
}
|
|
342
430
|
export declare const IntegrationDefSchema: z.ZodObject<{
|
|
343
431
|
slug: z.ZodString;
|
|
@@ -441,6 +529,47 @@ export declare const IntegrationDefSchema: z.ZodObject<{
|
|
|
441
529
|
transform: z.ZodOptional<z.ZodString>;
|
|
442
530
|
}, z.core.$strip>>;
|
|
443
531
|
}, z.core.$strip>>>;
|
|
532
|
+
webhooks: z.ZodOptional<z.ZodObject<{
|
|
533
|
+
handler: z.ZodString;
|
|
534
|
+
types: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
535
|
+
label: z.ZodString;
|
|
536
|
+
description: z.ZodOptional<z.ZodString>;
|
|
537
|
+
produces: z.ZodArray<z.ZodString>;
|
|
538
|
+
authentication: z.ZodOptional<z.ZodObject<{
|
|
539
|
+
method: z.ZodEnum<{
|
|
540
|
+
none: "none";
|
|
541
|
+
bearer: "bearer";
|
|
542
|
+
basic: "basic";
|
|
543
|
+
hmac_sha256: "hmac_sha256";
|
|
544
|
+
hmac_sha1: "hmac_sha1";
|
|
545
|
+
}>;
|
|
546
|
+
signatureHeader: z.ZodOptional<z.ZodString>;
|
|
547
|
+
secretSource: z.ZodOptional<z.ZodEnum<{
|
|
548
|
+
platform_generated: "platform_generated";
|
|
549
|
+
provider_assigned: "provider_assigned";
|
|
550
|
+
user_provided: "user_provided";
|
|
551
|
+
}>>;
|
|
552
|
+
}, z.core.$strip>>;
|
|
553
|
+
fields: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
554
|
+
name: z.ZodString;
|
|
555
|
+
label: z.ZodString;
|
|
556
|
+
type: z.ZodEnum<{
|
|
557
|
+
boolean: "boolean";
|
|
558
|
+
text: "text";
|
|
559
|
+
select: "select";
|
|
560
|
+
multiselect: "multiselect";
|
|
561
|
+
url: "url";
|
|
562
|
+
}>;
|
|
563
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
564
|
+
description: z.ZodOptional<z.ZodString>;
|
|
565
|
+
defaultValue: z.ZodOptional<z.ZodUnknown>;
|
|
566
|
+
options: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
567
|
+
label: z.ZodString;
|
|
568
|
+
value: z.ZodString;
|
|
569
|
+
}, z.core.$strip>>>;
|
|
570
|
+
}, z.core.$strip>>>;
|
|
571
|
+
}, z.core.$strip>>;
|
|
572
|
+
}, z.core.$strip>>;
|
|
444
573
|
}, z.core.$strip>;
|
|
445
574
|
/**
|
|
446
575
|
* Result of validating an integration definition before build/push.
|
package/dist/integration.js
CHANGED
|
@@ -203,6 +203,31 @@ export const IntegrationDefSchema = z.object({
|
|
|
203
203
|
events: z.record(z.string(), IntegrationEventDefSchema),
|
|
204
204
|
functions: z.record(z.string(), IntegrationFunctionDefSchema),
|
|
205
205
|
syncMappings: z.record(z.string(), SyncMappingDefSchema).optional(),
|
|
206
|
+
webhooks: z.object({
|
|
207
|
+
handler: z.string(),
|
|
208
|
+
types: z.record(z.string(), z.object({
|
|
209
|
+
label: z.string(),
|
|
210
|
+
description: z.string().optional(),
|
|
211
|
+
produces: z.array(z.string()).min(1),
|
|
212
|
+
authentication: z.object({
|
|
213
|
+
method: z.enum(['hmac_sha256', 'hmac_sha1', 'basic', 'bearer', 'none']),
|
|
214
|
+
signatureHeader: z.string().optional(),
|
|
215
|
+
secretSource: z.enum(['platform_generated', 'provider_assigned', 'user_provided']).optional(),
|
|
216
|
+
}).optional(),
|
|
217
|
+
fields: z.array(z.object({
|
|
218
|
+
name: z.string(),
|
|
219
|
+
label: z.string(),
|
|
220
|
+
type: z.enum(['text', 'select', 'multiselect', 'boolean', 'url']),
|
|
221
|
+
required: z.boolean().optional(),
|
|
222
|
+
description: z.string().optional(),
|
|
223
|
+
defaultValue: z.unknown().optional(),
|
|
224
|
+
options: z.array(z.object({
|
|
225
|
+
label: z.string(),
|
|
226
|
+
value: z.string(),
|
|
227
|
+
})).optional(),
|
|
228
|
+
})).optional(),
|
|
229
|
+
})),
|
|
230
|
+
}).optional(),
|
|
206
231
|
});
|
|
207
232
|
/**
|
|
208
233
|
* Validate an integration definition before build or push.
|
|
@@ -425,5 +450,18 @@ export function defineIntegration(def) {
|
|
|
425
450
|
`which is not defined in schemas. Available schemas: ${Object.keys(def.schemas).join(', ')}`);
|
|
426
451
|
}
|
|
427
452
|
}
|
|
453
|
+
// ── Phase 7: Cross-validate webhook handler reference ──
|
|
454
|
+
if (def.webhooks) {
|
|
455
|
+
const handlerSlug = camelToSnake(def.webhooks.handler);
|
|
456
|
+
const fnSlugs = new Set(Object.keys(def.functions));
|
|
457
|
+
if (!fnSlugs.has(handlerSlug)) {
|
|
458
|
+
// Also check the raw action key before snake_case conversion
|
|
459
|
+
const actionKeys = new Set(Object.keys(def.actions));
|
|
460
|
+
if (!actionKeys.has(def.webhooks.handler)) {
|
|
461
|
+
throw new Error(`webhooks.handler '${def.webhooks.handler}' is not defined in actions. ` +
|
|
462
|
+
`Available actions: ${Object.keys(def.actions).join(', ')}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
428
466
|
return def;
|
|
429
467
|
}
|
|
@@ -173,6 +173,19 @@ export declare const IntegrationSpecSchema: z.ZodObject<{
|
|
|
173
173
|
requiredScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
174
174
|
category: z.ZodOptional<z.ZodString>;
|
|
175
175
|
}, z.core.$strip>>>;
|
|
176
|
+
webhookTypes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
177
|
+
slug: z.ZodString;
|
|
178
|
+
label: z.ZodString;
|
|
179
|
+
description: z.ZodOptional<z.ZodString>;
|
|
180
|
+
produces: z.ZodArray<z.ZodString>;
|
|
181
|
+
handler: z.ZodString;
|
|
182
|
+
authentication: z.ZodOptional<z.ZodObject<{
|
|
183
|
+
method: z.ZodString;
|
|
184
|
+
signatureHeader: z.ZodOptional<z.ZodString>;
|
|
185
|
+
secretSource: z.ZodOptional<z.ZodString>;
|
|
186
|
+
}, z.core.$strip>>;
|
|
187
|
+
fields: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
188
|
+
}, z.core.$strip>>>;
|
|
176
189
|
}, z.core.$strip>;
|
|
177
190
|
build: z.ZodOptional<z.ZodObject<{
|
|
178
191
|
dockerfile: z.ZodString;
|
package/dist/integrationSpec.js
CHANGED
|
@@ -69,6 +69,19 @@ const CapabilitySchema = z.discriminatedUnion('type', [
|
|
|
69
69
|
QueueCapabilitySchema,
|
|
70
70
|
StreamCapabilitySchema,
|
|
71
71
|
]);
|
|
72
|
+
const WebhookTypeSpecSchema = z.object({
|
|
73
|
+
slug: z.string(),
|
|
74
|
+
label: z.string(),
|
|
75
|
+
description: z.string().optional(),
|
|
76
|
+
produces: z.array(z.string()),
|
|
77
|
+
handler: z.string(),
|
|
78
|
+
authentication: z.object({
|
|
79
|
+
method: z.string(),
|
|
80
|
+
signatureHeader: z.string().optional(),
|
|
81
|
+
secretSource: z.string().optional(),
|
|
82
|
+
}).optional(),
|
|
83
|
+
fields: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
84
|
+
});
|
|
72
85
|
export const IntegrationSpecSchema = z.object({
|
|
73
86
|
apiVersion: z.string(),
|
|
74
87
|
kind: z.literal('Integration'),
|
|
@@ -133,6 +146,7 @@ export const IntegrationSpecSchema = z.object({
|
|
|
133
146
|
schemas: z.array(IntegrationSchemaSpecSchema).optional(),
|
|
134
147
|
events: z.array(IntegrationEventSpecSchema).optional(),
|
|
135
148
|
functions: z.array(IntegrationFunctionSpecSchema).optional(),
|
|
149
|
+
webhookTypes: z.array(WebhookTypeSpecSchema).optional(),
|
|
136
150
|
}),
|
|
137
151
|
build: z.object({
|
|
138
152
|
dockerfile: z.string(),
|
package/package.json
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@newhomestar/sdk",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "Type-safe SDK for building Nova pipelines (workers & functions)",
|
|
5
|
-
"homepage": "https://github.com/newhomestar/nova-node-sdk#readme",
|
|
6
|
-
"bugs": {
|
|
7
|
-
"url": "https://github.com/newhomestar/nova-node-sdk/issues"
|
|
8
|
-
},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/newhomestar/nova-node-sdk.git"
|
|
12
|
-
},
|
|
13
|
-
"license": "ISC",
|
|
14
|
-
"author": "Christian Gomez",
|
|
15
|
-
"type": "module",
|
|
16
|
-
"main": "dist/index.js",
|
|
17
|
-
"types": "dist/index.d.ts",
|
|
18
|
-
"exports": {
|
|
19
|
-
".": {
|
|
20
|
-
"import": "./dist/index.js",
|
|
21
|
-
"types": "./dist/index.d.ts"
|
|
22
|
-
},
|
|
23
|
-
"./next": {
|
|
24
|
-
"import": "./dist/next.js",
|
|
25
|
-
"types": "./dist/next.d.ts"
|
|
26
|
-
},
|
|
27
|
-
"./events": {
|
|
28
|
-
"import": "./dist/events.js",
|
|
29
|
-
"types": "./dist/events.d.ts"
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"files": [
|
|
33
|
-
"dist"
|
|
34
|
-
],
|
|
35
|
-
"scripts": {
|
|
36
|
-
"build": "tsc"
|
|
37
|
-
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"@openfga/sdk": "^0.9.0",
|
|
40
|
-
"@orpc/openapi": "1.7.4",
|
|
41
|
-
"@orpc/server": "1.7.4",
|
|
42
|
-
"@supabase/supabase-js": "^2.39.0",
|
|
43
|
-
"body-parser": "^1.20.2",
|
|
44
|
-
"dotenv": "^16.4.3",
|
|
45
|
-
"express": "^4.18.2",
|
|
46
|
-
"express-oauth2-jwt-bearer": "^1.7.4",
|
|
47
|
-
"undici": "^7.24.4",
|
|
48
|
-
"yaml": "^2.7.1"
|
|
49
|
-
},
|
|
50
|
-
"peerDependencies": {
|
|
51
|
-
"zod": ">=4.0.0"
|
|
52
|
-
},
|
|
53
|
-
"devDependencies": {
|
|
54
|
-
"@types/node": "^20.11.17",
|
|
55
|
-
"typescript": "^5.4.4",
|
|
56
|
-
"zod": "^4.3.0"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@newhomestar/sdk",
|
|
3
|
+
"version": "0.8.3",
|
|
4
|
+
"description": "Type-safe SDK for building Nova pipelines (workers & functions)",
|
|
5
|
+
"homepage": "https://github.com/newhomestar/nova-node-sdk#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/newhomestar/nova-node-sdk/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/newhomestar/nova-node-sdk.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "Christian Gomez",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./next": {
|
|
24
|
+
"import": "./dist/next.js",
|
|
25
|
+
"types": "./dist/next.d.ts"
|
|
26
|
+
},
|
|
27
|
+
"./events": {
|
|
28
|
+
"import": "./dist/events.js",
|
|
29
|
+
"types": "./dist/events.d.ts"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@openfga/sdk": "^0.9.0",
|
|
40
|
+
"@orpc/openapi": "1.7.4",
|
|
41
|
+
"@orpc/server": "1.7.4",
|
|
42
|
+
"@supabase/supabase-js": "^2.39.0",
|
|
43
|
+
"body-parser": "^1.20.2",
|
|
44
|
+
"dotenv": "^16.4.3",
|
|
45
|
+
"express": "^4.18.2",
|
|
46
|
+
"express-oauth2-jwt-bearer": "^1.7.4",
|
|
47
|
+
"undici": "^7.24.4",
|
|
48
|
+
"yaml": "^2.7.1"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"zod": ">=4.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.11.17",
|
|
55
|
+
"typescript": "^5.4.4",
|
|
56
|
+
"zod": "^4.3.0"
|
|
57
|
+
}
|
|
58
|
+
}
|