@objectstack/plugin-webhooks 14.6.0 → 14.7.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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/auto-enqueuer.ts","../src/webhook-outbox-plugin.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IDataEngine, IRealtimeService, RealtimeEventPayload } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\n\n/**\n * Enqueue callback into the shared `service-messaging` HTTP outbox (ADR-0018 M3).\n * The plugin supplies one bound to `messaging.enqueueHttp(...)`; webhooks no\n * longer own a delivery outbox/dispatcher — they share the generic substrate.\n */\nexport type HttpEnqueueFn = (input: EnqueueHttpInput) => Promise<string>;\n\n/**\n * Optional logger interface (subset of console / kernel logger).\n */\ninterface OptionalLogger {\n info?(msg: string, meta?: unknown): void;\n warn?(msg: string, meta?: unknown): void;\n debug?(msg: string, meta?: unknown): void;\n error?(msg: string, err?: unknown, meta?: unknown): void;\n}\n\n/**\n * Per-row subscription cached in memory. Mirrors a subset of the\n * `sys_webhook` object — only what the auto-enqueuer needs to match an\n * event and build an `EnqueueInput`.\n */\ninterface CachedSubscription {\n id: string;\n name: string;\n objectName: string | undefined; // empty = matches all objects (manual-only is filtered out earlier)\n triggers: Set<'create' | 'update' | 'delete' | 'undelete'>;\n url: string;\n method?: string;\n headers?: Record<string, string>;\n secret?: string;\n timeoutMs?: number;\n}\n\nexport interface AutoEnqueuerOptions {\n /**\n * Object name holding webhook subscriptions. Defaults to `sys_webhook`,\n * the platform-objects schema authored in apps.\n */\n subscriptionsObject?: string;\n\n /**\n * Periodic full-cache refresh interval (ms). Belt-and-braces in case\n * the subscription-change event is missed. Default 60s.\n */\n refreshIntervalMs?: number;\n\n logger?: OptionalLogger;\n}\n\n/**\n * Bridge between `IRealtimeService` (`data.record.*` events emitted by\n * the engine) and `IWebhookOutbox` (durable delivery rows the dispatcher\n * picks up).\n *\n * ## Why a separate class\n * Keeps `WebhookOutboxPlugin` lean: the plugin wires services, this\n * class owns the runtime fan-out logic + subscription cache.\n *\n * ## Hot path\n * Every `engine.insert/update/delete` fires a `data.record.*` event.\n * The handler:\n * 1. Looks up matching subscriptions in an in-memory `Map<object, sub[]>`\n * — O(1) per event, no DB hit on the write path.\n * 2. Calls `outbox.enqueue()` fire-and-forget for each match. The\n * enqueue itself is a single INSERT, which runs *after* the user's\n * request has already returned.\n *\n * Net cost on the write path: one synchronous Map lookup (~microseconds).\n *\n * ## Cache freshness\n * The cache is rebuilt:\n * 1. Once on `start()`.\n * 2. On every `data.record.{created,updated,deleted}` event whose\n * object is `sys_webhook` (self-healing — when a user toggles a\n * webhook, the handler refreshes the cache before returning).\n * 3. Periodically (default 60s) as belt-and-braces.\n *\n * For multi-node clusters this is *eventually consistent* — node B may\n * not see node A's edit for up to one cycle. That's acceptable for\n * webhook configuration changes (humans don't expect millisecond\n * propagation) and matches Hasura's behaviour.\n *\n * ## Determinism\n * `eventId` is computed from `${object}:${recordId}:${type}:${timestamp}`\n * so the outbox dedup index catches duplicates that could arise from\n * upstream replay or buggy producers — and is stable across nodes.\n */\nexport class AutoEnqueuer {\n private readonly subscriptions = new Map<string, CachedSubscription[]>();\n private readonly subscriptionsObject: string;\n private readonly refreshIntervalMs: number;\n private readonly logger: OptionalLogger;\n private subId: string | undefined;\n private subIdSelfHeal: string | undefined;\n private refreshTimer: ReturnType<typeof setInterval> | undefined;\n private running = false;\n private refreshing: Promise<void> | undefined;\n\n constructor(\n private readonly engine: IDataEngine,\n private readonly realtime: IRealtimeService,\n private readonly enqueue: HttpEnqueueFn,\n opts: AutoEnqueuerOptions = {},\n ) {\n this.subscriptionsObject = opts.subscriptionsObject ?? 'sys_webhook';\n this.refreshIntervalMs = opts.refreshIntervalMs ?? 60_000;\n this.logger = opts.logger ?? {};\n }\n\n /**\n * Load the subscription cache and start listening for events.\n */\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n\n await this.refresh();\n\n // Main subscription: every data event → match → enqueue.\n this.subId = await this.realtime.subscribe(\n 'webhook-auto-enqueuer',\n (event) => this.handleEvent(event),\n );\n\n // Self-healing: any change to sys_webhook refreshes the cache.\n this.subIdSelfHeal = await this.realtime.subscribe(\n 'webhook-auto-enqueuer-self-heal',\n (event) => this.handleSelfHealEvent(event),\n { object: this.subscriptionsObject },\n );\n\n if (this.refreshIntervalMs > 0) {\n this.refreshTimer = setInterval(() => {\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] periodic refresh failed', err),\n );\n }, this.refreshIntervalMs);\n // Don't keep the process alive solely for this timer.\n this.refreshTimer.unref?.();\n }\n }\n\n async stop(): Promise<void> {\n if (!this.running) return;\n this.running = false;\n if (this.subId) await this.realtime.unsubscribe(this.subId);\n if (this.subIdSelfHeal) await this.realtime.unsubscribe(this.subIdSelfHeal);\n if (this.refreshTimer) clearInterval(this.refreshTimer);\n this.subId = undefined;\n this.subIdSelfHeal = undefined;\n this.refreshTimer = undefined;\n }\n\n /**\n * Force-refresh the subscription cache from storage. Concurrent\n * callers share a single in-flight refresh.\n */\n async refresh(): Promise<void> {\n if (this.refreshing) return this.refreshing;\n this.refreshing = this.doRefresh().finally(() => {\n this.refreshing = undefined;\n });\n return this.refreshing;\n }\n\n private async doRefresh(): Promise<void> {\n let rows: any[];\n try {\n rows = await this.engine.find(this.subscriptionsObject, {\n where: { active: true },\n });\n } catch (err) {\n this.logger.warn?.(\n `[webhook-auto-enqueuer] failed to load ${this.subscriptionsObject}`,\n err,\n );\n return;\n }\n\n const next = new Map<string, CachedSubscription[]>();\n for (const row of rows) {\n const sub = this.parseRow(row);\n if (!sub) continue;\n // Empty objectName == \"any object\" → indexed under '*'.\n const key = sub.objectName ?? '*';\n const arr = next.get(key) ?? [];\n arr.push(sub);\n next.set(key, arr);\n }\n\n this.subscriptions.clear();\n for (const [k, v] of next) this.subscriptions.set(k, v);\n\n this.logger.debug?.('[webhook-auto-enqueuer] cache refreshed', {\n objects: this.subscriptions.size,\n rows: rows.length,\n });\n }\n\n private parseRow(row: any): CachedSubscription | null {\n if (!row?.id || !row?.url) return null;\n const triggersField = (row.triggers ?? '') as string;\n const triggers = new Set(\n triggersField\n .split(',')\n .map((s: string) => s.trim().toLowerCase())\n .filter(Boolean) as Array<'create' | 'update' | 'delete' | 'undelete'>,\n );\n if (triggers.size === 0) {\n // Manual-only webhook (no triggers) — skip auto-enqueue.\n return null;\n }\n\n // The \"definition_json\" field carries advanced config (headers,\n // secret, timeout); attempt a best-effort parse. Fall back to\n // top-level fields where present.\n let defn: Record<string, any> = {};\n if (typeof row.definition_json === 'string' && row.definition_json.length > 0) {\n try {\n defn = JSON.parse(row.definition_json) ?? {};\n } catch {\n defn = {};\n }\n }\n\n return {\n id: row.id as string,\n name: (row.name as string) ?? row.id,\n objectName: row.object_name ? String(row.object_name) : undefined,\n triggers,\n url: String(row.url),\n method: row.method ?? defn.method ?? 'POST',\n headers: defn.headers,\n secret: defn.secret,\n timeoutMs: defn.timeoutMs,\n };\n }\n\n /**\n * Handler for the firehose subscription.\n *\n * NOTE: we intentionally `void` the inner enqueue() so the realtime\n * publisher (and therefore the user's request) is never blocked on\n * webhook persistence.\n */\n private handleEvent(event: RealtimeEventPayload): void {\n if (!event.type?.startsWith('data.record.')) return;\n if (!event.object) return;\n if (event.object === this.subscriptionsObject) return; // self-heal handles its own\n\n const action = event.type.slice('data.record.'.length) as\n | 'created' | 'updated' | 'deleted' | 'undeleted' | string;\n const trigger = mapActionToTrigger(action);\n if (!trigger) return;\n\n const subs = [\n ...(this.subscriptions.get(event.object) ?? []),\n ...(this.subscriptions.get('*') ?? []),\n ];\n if (subs.length === 0) return;\n\n const payload = event.payload ?? {};\n const recordId =\n (payload as any).recordId ??\n (payload as any).id ??\n (payload as any).after?.id ??\n (payload as any).before?.id ??\n 'unknown';\n\n // Deterministic eventId — same input on any node → same id.\n // Includes timestamp so two distinct updates to the same record\n // don't accidentally dedup.\n const eventId = `${event.object}:${recordId}:${action}:${event.timestamp}`;\n\n for (const sub of subs) {\n if (!sub.triggers.has(trigger)) continue;\n\n // Fire-and-forget — never await on the hot path. Map the webhook\n // delivery onto the generic HTTP-outbox shape (ADR-0018 M3):\n // - source 'webhook' + dedupKey '<webhookId>:<eventId>' preserves\n // the old (event_id, webhook_id) at-most-once enqueue;\n // - refId = webhookId keeps per-webhook partition affinity / ordering;\n // - label = event type → X-Objectstack-Event header.\n void this.enqueue({\n source: 'webhook',\n refId: sub.id,\n dedupKey: `${sub.id}:${eventId}`,\n label: event.type,\n url: sub.url,\n method: sub.method,\n headers: sub.headers,\n signingSecret: sub.secret,\n timeoutMs: sub.timeoutMs,\n payload: {\n object: event.object,\n recordId,\n action,\n timestamp: event.timestamp,\n ...payload,\n },\n }).catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] enqueue failed', {\n webhook: sub.name,\n eventId,\n err: (err as Error)?.message ?? err,\n }),\n );\n }\n }\n\n private handleSelfHealEvent(event: RealtimeEventPayload): void {\n if (event.object !== this.subscriptionsObject) return;\n if (!event.type?.startsWith('data.record.')) return;\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] self-heal refresh failed', err),\n );\n }\n\n /** Test / admin accessor. */\n snapshot(): ReadonlyMap<string, ReadonlyArray<CachedSubscription>> {\n return this.subscriptions;\n }\n}\n\nfunction mapActionToTrigger(\n action: string,\n): 'create' | 'update' | 'delete' | 'undelete' | null {\n switch (action) {\n case 'created':\n return 'create';\n case 'updated':\n return 'update';\n case 'deleted':\n return 'delete';\n case 'undeleted':\n return 'undelete';\n default:\n return null;\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { IDataEngine, IRealtimeService } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\nimport { AutoEnqueuer, type AutoEnqueuerOptions } from './auto-enqueuer.js';\nimport { SysWebhook } from './sys-webhook.object.js';\n\n/**\n * Structural view of `@objectstack/service-messaging`'s HTTP-outbox surface\n * (ADR-0018 M3) — declared locally so this plugin doesn't take a hard runtime\n * import on the service. Webhook deliveries are enqueued onto the shared\n * `sys_http_delivery` outbox and drained by the messaging `HttpDispatcher`.\n */\ninterface MessagingHttpSurface {\n isHttpDeliveryReady(): boolean;\n enqueueHttp(input: EnqueueHttpInput): Promise<string>;\n redeliverHttp(id: string): Promise<{ id: string; status: string }>;\n}\n\nexport interface WebhookOutboxPluginOptions {\n /**\n * Auto-enqueue config. When enabled (default `true` if the realtime + data\n * engine services are available), the plugin subscribes to `data.record.*`\n * events and enqueues a delivery onto the shared messaging HTTP outbox for\n * every matching `sys_webhook` row.\n *\n * Set `false` to disable and enqueue webhooks imperatively elsewhere.\n */\n autoEnqueue?: boolean | AutoEnqueuerOptions;\n}\n\n/**\n * Wires webhook fan-out on top of the shared outbound-HTTP delivery substrate\n * (ADR-0018 M3).\n *\n * Webhooks are no longer their own delivery engine: the durable outbox, the\n * cluster-coordinated dispatcher, the retry/backoff/dead-letter schedule, and\n * the retention sweep all live in `@objectstack/service-messaging`\n * (`sys_http_delivery` + `HttpDispatcher`). This plugin owns only the\n * webhook-specific concerns:\n * - the `sys_webhook` configuration object,\n * - the {@link AutoEnqueuer} that turns `data.record.*` events into outbox\n * rows (`source: 'webhook'`), and\n * - the redeliver admin endpoint.\n *\n * End-to-end flow:\n *\n * engine.insert('contact', {...})\n * → engine publishes data.record.created via IRealtimeService\n * → AutoEnqueuer matches active sys_webhook rows in O(1)\n * → messaging.enqueueHttp() runs fire-and-forget (off the write path)\n * → messaging HttpDispatcher claims and POSTs (cluster-coordinated, retried)\n *\n * **Requires** `MessagingServicePlugin` (`@objectstack/service-messaging`),\n * which is a foundational, always-on capability.\n */\nexport class WebhookOutboxPlugin implements Plugin {\n name = 'com.objectstack.plugin-webhook-outbox';\n version = '2.0.0';\n type = 'standard' as const;\n dependencies = ['com.objectstack.service.messaging'];\n\n private autoEnqueuer: AutoEnqueuer | undefined;\n\n constructor(private readonly options: WebhookOutboxPluginOptions = {}) {}\n\n async init(ctx: PluginContext): Promise<void> {\n // Register the webhook config object (ADR-0029 K2.a). The delivery\n // telemetry now lives in messaging's `sys_http_delivery`, so the nav's\n // \"Deliveries\" entry points there (filtered to source=webhook in views).\n const manifest = ctx.getService<{ register(m: any): void }>('manifest');\n if (manifest && typeof manifest.register === 'function') {\n manifest.register({\n id: 'com.objectstack.plugin-webhook-outbox.schema',\n namespace: 'sys',\n version: this.version,\n type: 'plugin',\n scope: 'system',\n name: 'Webhook Schemas',\n description: 'Registers sys_webhook (configuration). Deliveries use messaging\\'s sys_http_delivery outbox.',\n objects: [SysWebhook],\n navigationContributions: [\n {\n app: 'setup',\n group: 'group_integrations',\n priority: 100,\n items: [\n { id: 'nav_webhooks', type: 'object', label: 'Webhooks', objectName: 'sys_webhook', icon: 'webhook', requiresObject: 'sys_webhook' },\n { id: 'nav_http_deliveries', type: 'object', label: 'HTTP Deliveries', objectName: 'sys_http_delivery', icon: 'send', requiresObject: 'sys_http_delivery' },\n ],\n },\n ],\n });\n } else {\n ctx.logger.warn?.(\n '[webhook-outbox] manifest service unavailable — sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin.',\n );\n }\n\n // ADR-0029 D8 — contribute object translations once i18n is up.\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n try {\n const i18n = ctx.getService<any>('i18n');\n if (i18n && typeof i18n.loadTranslations === 'function') {\n const { WebhooksTranslations } = await import('./translations/index.js');\n for (const [locale, data] of Object.entries(WebhooksTranslations)) {\n i18n.loadTranslations(locale, data as Record<string, unknown>);\n }\n }\n } catch { /* i18n optional */ }\n });\n }\n\n const autoEnqueueOpt = this.options.autoEnqueue ?? true;\n\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n await this.bootAutoEnqueue(ctx, autoEnqueueOpt);\n this.registerAdminRoutes(ctx);\n });\n }\n\n ctx.logger.info?.('[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)', {\n autoEnqueue: autoEnqueueOpt !== false,\n });\n }\n\n async dispose(): Promise<void> {\n await this.autoEnqueuer?.stop();\n }\n\n private getMessaging(ctx: PluginContext): MessagingHttpSurface | undefined {\n const svc = this.tryGetService<MessagingHttpSurface>(ctx, ['messaging']);\n return svc && typeof svc.enqueueHttp === 'function' ? svc : undefined;\n }\n\n private async bootAutoEnqueue(\n ctx: PluginContext,\n opt: boolean | AutoEnqueuerOptions,\n ): Promise<void> {\n if (opt === false) return;\n const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);\n const realtime = this.tryGetService<IRealtimeService>(ctx, ['realtime']);\n const messaging = this.getMessaging(ctx);\n if (!engine || !realtime || !messaging) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] disabled — ObjectQL, Realtime, or Messaging service not available',\n { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging },\n );\n return;\n }\n if (!messaging.isHttpDeliveryReady()) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) — webhook deliveries will not be durable',\n );\n }\n\n const enqOpts = (typeof opt === 'object' ? opt : {}) as AutoEnqueuerOptions;\n this.autoEnqueuer = new AutoEnqueuer(\n engine,\n realtime,\n (input) => messaging.enqueueHttp(input),\n { ...enqOpts, logger: ctx.logger },\n );\n await this.autoEnqueuer.start();\n ctx.registerService('webhook.autoEnqueuer', this.autoEnqueuer);\n ctx.logger.info?.('[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)');\n }\n\n private tryGetService<T>(ctx: PluginContext, names: string[]): T | undefined {\n for (const n of names) {\n try {\n const svc = ctx.getService<T>(n);\n if (svc) return svc;\n } catch {\n // fall through\n }\n }\n return undefined;\n }\n\n /**\n * Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is\n * available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is the\n * better-auth session cookie — every authenticated user counts.\n */\n private registerAdminRoutes(ctx: PluginContext): void {\n const http = this.tryGetService<any>(ctx, ['http-server']);\n if (!http || typeof http.getRawApp !== 'function') {\n ctx.logger.debug?.('[webhook-outbox] HTTP server not available; redeliver endpoint not mounted');\n return;\n }\n const rawApp = http.getRawApp();\n const messaging = this.getMessaging(ctx);\n if (!rawApp || !messaging) return;\n\n rawApp.post('/api/v1/webhooks/redeliver', async (c: any) => {\n const userId = await this.resolveSessionUserId(ctx, c);\n if (!userId) {\n return c.json(\n { success: false, error: 'unauthenticated', message: 'Sign in to redeliver webhook deliveries.' },\n 401,\n );\n }\n let body: any;\n try {\n body = await c.req.json();\n } catch {\n return c.json({ success: false, error: 'invalid_body', message: 'Request body must be JSON.' }, 400);\n }\n const deliveryId = typeof body?.deliveryId === 'string' ? body.deliveryId.trim() : '';\n if (!deliveryId) {\n return c.json(\n { success: false, error: 'missing_delivery_id', message: 'Body must include `deliveryId: string`.' },\n 400,\n );\n }\n try {\n const row = await messaging.redeliverHttp(deliveryId);\n ctx.logger.info?.('[webhook-outbox] redelivered', { deliveryId, requestedBy: userId });\n return c.json({ success: true, data: { id: row.id, status: row.status } });\n } catch (err: any) {\n const code = err?.code;\n if (code === 'not_found') {\n return c.json({ success: false, error: 'not_found', message: err.message }, 404);\n }\n if (code === 'not_eligible') {\n return c.json({ success: false, error: 'not_eligible', message: err.message }, 409);\n }\n ctx.logger.error?.('[webhook-outbox] redeliver failed', err as Error);\n return c.json(\n { success: false, error: 'internal_error', message: err?.message ?? String(err) },\n 500,\n );\n }\n });\n\n ctx.logger.info?.('[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver');\n }\n\n private async resolveSessionUserId(ctx: PluginContext, c: any): Promise<string | undefined> {\n try {\n const authService: any = this.tryGetService<any>(ctx, ['auth']);\n if (!authService) return undefined;\n let api: any = authService.api;\n if (!api && typeof authService.getApi === 'function') {\n api = await authService.getApi();\n }\n if (!api?.getSession) return undefined;\n const session = await api.getSession({ headers: c.req.raw.headers });\n const uid = session?.user?.id;\n return typeof uid === 'string' && uid.length > 0 ? uid : undefined;\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/auto-enqueuer.ts","../src/webhook-outbox-plugin.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IDataEngine, IRealtimeService, RealtimeEventPayload } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\n\n/**\n * Enqueue callback into the shared `service-messaging` HTTP outbox (ADR-0018 M3).\n * The plugin supplies one bound to `messaging.enqueueHttp(...)`; webhooks no\n * longer own a delivery outbox/dispatcher — they share the generic substrate.\n */\nexport type HttpEnqueueFn = (input: EnqueueHttpInput) => Promise<string>;\n\n/**\n * Optional logger interface (subset of console / kernel logger).\n */\ninterface OptionalLogger {\n info?(msg: string, meta?: unknown): void;\n warn?(msg: string, meta?: unknown): void;\n debug?(msg: string, meta?: unknown): void;\n error?(msg: string, err?: unknown, meta?: unknown): void;\n}\n\n/**\n * Per-row subscription cached in memory. Mirrors a subset of the\n * `sys_webhook` object — only what the auto-enqueuer needs to match an\n * event and build an `EnqueueInput`.\n */\ninterface CachedSubscription {\n id: string;\n name: string;\n objectName: string | undefined; // empty = matches all objects (manual-only is filtered out earlier)\n triggers: Set<'create' | 'update' | 'delete' | 'undelete'>;\n url: string;\n method?: string;\n headers?: Record<string, string>;\n secret?: string;\n timeoutMs?: number;\n}\n\nexport interface AutoEnqueuerOptions {\n /**\n * Object name holding webhook subscriptions. Defaults to `sys_webhook`,\n * the platform-objects schema authored in apps.\n */\n subscriptionsObject?: string;\n\n /**\n * Periodic full-cache refresh interval (ms). Belt-and-braces in case\n * the subscription-change event is missed. Default 60s.\n */\n refreshIntervalMs?: number;\n\n logger?: OptionalLogger;\n}\n\n/**\n * Bridge between `IRealtimeService` (`data.record.*` events emitted by\n * the engine) and `IWebhookOutbox` (durable delivery rows the dispatcher\n * picks up).\n *\n * ## Why a separate class\n * Keeps `WebhookOutboxPlugin` lean: the plugin wires services, this\n * class owns the runtime fan-out logic + subscription cache.\n *\n * ## Hot path\n * Every `engine.insert/update/delete` fires a `data.record.*` event.\n * The handler:\n * 1. Looks up matching subscriptions in an in-memory `Map<object, sub[]>`\n * — O(1) per event, no DB hit on the write path.\n * 2. Calls `outbox.enqueue()` fire-and-forget for each match. The\n * enqueue itself is a single INSERT, which runs *after* the user's\n * request has already returned.\n *\n * Net cost on the write path: one synchronous Map lookup (~microseconds).\n *\n * ## Cache freshness\n * The cache is rebuilt:\n * 1. Once on `start()`.\n * 2. On every `data.record.{created,updated,deleted}` event whose\n * object is `sys_webhook` (self-healing — when a user toggles a\n * webhook, the handler refreshes the cache before returning).\n * 3. Periodically (default 60s) as belt-and-braces.\n *\n * For multi-node clusters this is *eventually consistent* — node B may\n * not see node A's edit for up to one cycle. That's acceptable for\n * webhook configuration changes (humans don't expect millisecond\n * propagation) and matches Hasura's behaviour.\n *\n * ## Determinism\n * `eventId` is computed from `${object}:${recordId}:${type}:${timestamp}`\n * so the outbox dedup index catches duplicates that could arise from\n * upstream replay or buggy producers — and is stable across nodes.\n */\nexport class AutoEnqueuer {\n private readonly subscriptions = new Map<string, CachedSubscription[]>();\n private readonly subscriptionsObject: string;\n private readonly refreshIntervalMs: number;\n private readonly logger: OptionalLogger;\n private subId: string | undefined;\n private subIdSelfHeal: string | undefined;\n private refreshTimer: ReturnType<typeof setInterval> | undefined;\n private running = false;\n private refreshing: Promise<void> | undefined;\n\n constructor(\n private readonly engine: IDataEngine,\n private readonly realtime: IRealtimeService,\n private readonly enqueue: HttpEnqueueFn,\n opts: AutoEnqueuerOptions = {},\n ) {\n this.subscriptionsObject = opts.subscriptionsObject ?? 'sys_webhook';\n this.refreshIntervalMs = opts.refreshIntervalMs ?? 60_000;\n this.logger = opts.logger ?? {};\n }\n\n /**\n * Load the subscription cache and start listening for events.\n */\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n\n await this.refresh();\n\n // Main subscription: every data event → match → enqueue.\n this.subId = await this.realtime.subscribe(\n 'webhook-auto-enqueuer',\n (event) => this.handleEvent(event),\n );\n\n // Self-healing: any change to sys_webhook refreshes the cache.\n this.subIdSelfHeal = await this.realtime.subscribe(\n 'webhook-auto-enqueuer-self-heal',\n (event) => this.handleSelfHealEvent(event),\n { object: this.subscriptionsObject },\n );\n\n if (this.refreshIntervalMs > 0) {\n this.refreshTimer = setInterval(() => {\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] periodic refresh failed', err),\n );\n }, this.refreshIntervalMs);\n // Don't keep the process alive solely for this timer.\n this.refreshTimer.unref?.();\n }\n }\n\n async stop(): Promise<void> {\n if (!this.running) return;\n this.running = false;\n if (this.subId) await this.realtime.unsubscribe(this.subId);\n if (this.subIdSelfHeal) await this.realtime.unsubscribe(this.subIdSelfHeal);\n if (this.refreshTimer) clearInterval(this.refreshTimer);\n this.subId = undefined;\n this.subIdSelfHeal = undefined;\n this.refreshTimer = undefined;\n }\n\n /**\n * Force-refresh the subscription cache from storage. Concurrent\n * callers share a single in-flight refresh.\n */\n async refresh(): Promise<void> {\n if (this.refreshing) return this.refreshing;\n this.refreshing = this.doRefresh().finally(() => {\n this.refreshing = undefined;\n });\n return this.refreshing;\n }\n\n private async doRefresh(): Promise<void> {\n let rows: any[];\n try {\n rows = await this.engine.find(this.subscriptionsObject, {\n where: { active: true },\n });\n } catch (err) {\n this.logger.warn?.(\n `[webhook-auto-enqueuer] failed to load ${this.subscriptionsObject}`,\n err,\n );\n return;\n }\n\n const next = new Map<string, CachedSubscription[]>();\n for (const row of rows) {\n const sub = this.parseRow(row);\n if (!sub) continue;\n // Empty objectName == \"any object\" → indexed under '*'.\n const key = sub.objectName ?? '*';\n const arr = next.get(key) ?? [];\n arr.push(sub);\n next.set(key, arr);\n }\n\n this.subscriptions.clear();\n for (const [k, v] of next) this.subscriptions.set(k, v);\n\n this.logger.debug?.('[webhook-auto-enqueuer] cache refreshed', {\n objects: this.subscriptions.size,\n rows: rows.length,\n });\n }\n\n private parseRow(row: any): CachedSubscription | null {\n if (!row?.id || !row?.url) return null;\n // `triggers` is now authored as a multi-select (stored as an array), but\n // legacy rows stored a comma-separated string (and some drivers hand a\n // JSON-encoded array back as a string). Accept all three shapes so a\n // schema change never silently drops a subscription's events.\n const rawTriggers = row.triggers;\n let triggerList: string[];\n if (Array.isArray(rawTriggers)) {\n triggerList = rawTriggers.map((t) => String(t));\n } else {\n const s = String(rawTriggers ?? '').trim();\n if (s.startsWith('[')) {\n try {\n const parsed = JSON.parse(s);\n triggerList = Array.isArray(parsed) ? parsed.map((t) => String(t)) : [s];\n } catch {\n triggerList = s.split(',');\n }\n } else {\n triggerList = s.split(',');\n }\n }\n const triggers = new Set(\n triggerList\n .map((t) => t.trim().toLowerCase())\n .filter(Boolean) as Array<'create' | 'update' | 'delete' | 'undelete'>,\n );\n if (triggers.size === 0) {\n // Manual-only webhook (no triggers) — skip auto-enqueue.\n return null;\n }\n\n // The \"definition_json\" field carries advanced config (headers,\n // secret, timeout); attempt a best-effort parse. Fall back to\n // top-level fields where present.\n let defn: Record<string, any> = {};\n if (typeof row.definition_json === 'string' && row.definition_json.length > 0) {\n try {\n defn = JSON.parse(row.definition_json) ?? {};\n } catch {\n defn = {};\n }\n }\n\n return {\n id: row.id as string,\n name: (row.name as string) ?? row.id,\n objectName: row.object_name ? String(row.object_name) : undefined,\n triggers,\n url: String(row.url),\n // Method is authored via a select whose option values are lowercased\n // (get/post/…); upper-case here so delivery uses a canonical HTTP\n // method regardless of whether the row was authored before or after\n // the select change (legacy rows stored 'POST').\n method: String(row.method ?? defn.method ?? 'POST').toUpperCase(),\n headers: defn.headers,\n secret: defn.secret,\n timeoutMs: defn.timeoutMs,\n };\n }\n\n /**\n * Handler for the firehose subscription.\n *\n * NOTE: we intentionally `void` the inner enqueue() so the realtime\n * publisher (and therefore the user's request) is never blocked on\n * webhook persistence.\n */\n private handleEvent(event: RealtimeEventPayload): void {\n if (!event.type?.startsWith('data.record.')) return;\n if (!event.object) return;\n if (event.object === this.subscriptionsObject) return; // self-heal handles its own\n\n const action = event.type.slice('data.record.'.length) as\n | 'created' | 'updated' | 'deleted' | 'undeleted' | string;\n const trigger = mapActionToTrigger(action);\n if (!trigger) return;\n\n const subs = [\n ...(this.subscriptions.get(event.object) ?? []),\n ...(this.subscriptions.get('*') ?? []),\n ];\n if (subs.length === 0) return;\n\n const payload = event.payload ?? {};\n const recordId =\n (payload as any).recordId ??\n (payload as any).id ??\n (payload as any).after?.id ??\n (payload as any).before?.id ??\n 'unknown';\n\n // Deterministic eventId — same input on any node → same id.\n // Includes timestamp so two distinct updates to the same record\n // don't accidentally dedup.\n const eventId = `${event.object}:${recordId}:${action}:${event.timestamp}`;\n\n for (const sub of subs) {\n if (!sub.triggers.has(trigger)) continue;\n\n // Fire-and-forget — never await on the hot path. Map the webhook\n // delivery onto the generic HTTP-outbox shape (ADR-0018 M3):\n // - source 'webhook' + dedupKey '<webhookId>:<eventId>' preserves\n // the old (event_id, webhook_id) at-most-once enqueue;\n // - refId = webhookId keeps per-webhook partition affinity / ordering;\n // - label = event type → X-Objectstack-Event header.\n void this.enqueue({\n source: 'webhook',\n refId: sub.id,\n dedupKey: `${sub.id}:${eventId}`,\n label: event.type,\n url: sub.url,\n method: sub.method,\n headers: sub.headers,\n signingSecret: sub.secret,\n timeoutMs: sub.timeoutMs,\n payload: {\n object: event.object,\n recordId,\n action,\n timestamp: event.timestamp,\n ...payload,\n },\n }).catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] enqueue failed', {\n webhook: sub.name,\n eventId,\n err: (err as Error)?.message ?? err,\n }),\n );\n }\n }\n\n private handleSelfHealEvent(event: RealtimeEventPayload): void {\n if (event.object !== this.subscriptionsObject) return;\n if (!event.type?.startsWith('data.record.')) return;\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] self-heal refresh failed', err),\n );\n }\n\n /** Test / admin accessor. */\n snapshot(): ReadonlyMap<string, ReadonlyArray<CachedSubscription>> {\n return this.subscriptions;\n }\n}\n\nfunction mapActionToTrigger(\n action: string,\n): 'create' | 'update' | 'delete' | 'undelete' | null {\n switch (action) {\n case 'created':\n return 'create';\n case 'updated':\n return 'update';\n case 'deleted':\n return 'delete';\n case 'undeleted':\n return 'undelete';\n default:\n return null;\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { IDataEngine, IRealtimeService } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\nimport { AutoEnqueuer, type AutoEnqueuerOptions } from './auto-enqueuer.js';\nimport { SysWebhook } from './sys-webhook.object.js';\n\n/**\n * Structural view of `@objectstack/service-messaging`'s HTTP-outbox surface\n * (ADR-0018 M3) — declared locally so this plugin doesn't take a hard runtime\n * import on the service. Webhook deliveries are enqueued onto the shared\n * `sys_http_delivery` outbox and drained by the messaging `HttpDispatcher`.\n */\ninterface MessagingHttpSurface {\n isHttpDeliveryReady(): boolean;\n enqueueHttp(input: EnqueueHttpInput): Promise<string>;\n redeliverHttp(id: string): Promise<{ id: string; status: string }>;\n}\n\nexport interface WebhookOutboxPluginOptions {\n /**\n * Auto-enqueue config. When enabled (default `true` if the realtime + data\n * engine services are available), the plugin subscribes to `data.record.*`\n * events and enqueues a delivery onto the shared messaging HTTP outbox for\n * every matching `sys_webhook` row.\n *\n * Set `false` to disable and enqueue webhooks imperatively elsewhere.\n */\n autoEnqueue?: boolean | AutoEnqueuerOptions;\n}\n\n/**\n * Wires webhook fan-out on top of the shared outbound-HTTP delivery substrate\n * (ADR-0018 M3).\n *\n * Webhooks are no longer their own delivery engine: the durable outbox, the\n * cluster-coordinated dispatcher, the retry/backoff/dead-letter schedule, and\n * the retention sweep all live in `@objectstack/service-messaging`\n * (`sys_http_delivery` + `HttpDispatcher`). This plugin owns only the\n * webhook-specific concerns:\n * - the `sys_webhook` configuration object,\n * - the {@link AutoEnqueuer} that turns `data.record.*` events into outbox\n * rows (`source: 'webhook'`), and\n * - the redeliver admin endpoint.\n *\n * End-to-end flow:\n *\n * engine.insert('contact', {...})\n * → engine publishes data.record.created via IRealtimeService\n * → AutoEnqueuer matches active sys_webhook rows in O(1)\n * → messaging.enqueueHttp() runs fire-and-forget (off the write path)\n * → messaging HttpDispatcher claims and POSTs (cluster-coordinated, retried)\n *\n * **Requires** `MessagingServicePlugin` (`@objectstack/service-messaging`),\n * which is a foundational, always-on capability.\n */\nexport class WebhookOutboxPlugin implements Plugin {\n name = 'com.objectstack.plugin-webhook-outbox';\n version = '2.0.0';\n type = 'standard' as const;\n dependencies = ['com.objectstack.service.messaging'];\n\n private autoEnqueuer: AutoEnqueuer | undefined;\n\n constructor(private readonly options: WebhookOutboxPluginOptions = {}) {}\n\n async init(ctx: PluginContext): Promise<void> {\n // Register the webhook config object (ADR-0029 K2.a). The delivery\n // telemetry now lives in messaging's `sys_http_delivery`, so the nav's\n // \"Deliveries\" entry points there (filtered to source=webhook in views).\n const manifest = ctx.getService<{ register(m: any): void }>('manifest');\n if (manifest && typeof manifest.register === 'function') {\n manifest.register({\n id: 'com.objectstack.plugin-webhook-outbox.schema',\n namespace: 'sys',\n version: this.version,\n type: 'plugin',\n scope: 'system',\n name: 'Webhook Schemas',\n description: 'Registers sys_webhook (configuration). Deliveries use messaging\\'s sys_http_delivery outbox.',\n objects: [SysWebhook],\n navigationContributions: [\n {\n app: 'setup',\n group: 'group_integrations',\n priority: 100,\n items: [\n { id: 'nav_webhooks', type: 'object', label: 'Webhooks', objectName: 'sys_webhook', icon: 'webhook', requiresObject: 'sys_webhook' },\n { id: 'nav_http_deliveries', type: 'object', label: 'HTTP Deliveries', objectName: 'sys_http_delivery', icon: 'send', requiresObject: 'sys_http_delivery' },\n ],\n },\n ],\n });\n } else {\n ctx.logger.warn?.(\n '[webhook-outbox] manifest service unavailable — sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin.',\n );\n }\n\n // ADR-0029 D8 — contribute object translations once i18n is up.\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n try {\n const i18n = ctx.getService<any>('i18n');\n if (i18n && typeof i18n.loadTranslations === 'function') {\n const { WebhooksTranslations } = await import('./translations/index.js');\n for (const [locale, data] of Object.entries(WebhooksTranslations)) {\n i18n.loadTranslations(locale, data as Record<string, unknown>);\n }\n }\n } catch { /* i18n optional */ }\n });\n }\n\n const autoEnqueueOpt = this.options.autoEnqueue ?? true;\n\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n await this.bootAutoEnqueue(ctx, autoEnqueueOpt);\n this.registerAdminRoutes(ctx);\n });\n }\n\n ctx.logger.info?.('[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)', {\n autoEnqueue: autoEnqueueOpt !== false,\n });\n }\n\n async dispose(): Promise<void> {\n await this.autoEnqueuer?.stop();\n }\n\n private getMessaging(ctx: PluginContext): MessagingHttpSurface | undefined {\n const svc = this.tryGetService<MessagingHttpSurface>(ctx, ['messaging']);\n return svc && typeof svc.enqueueHttp === 'function' ? svc : undefined;\n }\n\n private async bootAutoEnqueue(\n ctx: PluginContext,\n opt: boolean | AutoEnqueuerOptions,\n ): Promise<void> {\n if (opt === false) return;\n const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);\n const realtime = this.tryGetService<IRealtimeService>(ctx, ['realtime']);\n const messaging = this.getMessaging(ctx);\n if (!engine || !realtime || !messaging) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] disabled — ObjectQL, Realtime, or Messaging service not available',\n { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging },\n );\n return;\n }\n if (!messaging.isHttpDeliveryReady()) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) — webhook deliveries will not be durable',\n );\n }\n\n const enqOpts = (typeof opt === 'object' ? opt : {}) as AutoEnqueuerOptions;\n this.autoEnqueuer = new AutoEnqueuer(\n engine,\n realtime,\n (input) => messaging.enqueueHttp(input),\n { ...enqOpts, logger: ctx.logger },\n );\n await this.autoEnqueuer.start();\n ctx.registerService('webhook.autoEnqueuer', this.autoEnqueuer);\n ctx.logger.info?.('[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)');\n }\n\n private tryGetService<T>(ctx: PluginContext, names: string[]): T | undefined {\n for (const n of names) {\n try {\n const svc = ctx.getService<T>(n);\n if (svc) return svc;\n } catch {\n // fall through\n }\n }\n return undefined;\n }\n\n /**\n * Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is\n * available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is the\n * better-auth session cookie — every authenticated user counts.\n */\n private registerAdminRoutes(ctx: PluginContext): void {\n const http = this.tryGetService<any>(ctx, ['http-server']);\n if (!http || typeof http.getRawApp !== 'function') {\n ctx.logger.debug?.('[webhook-outbox] HTTP server not available; redeliver endpoint not mounted');\n return;\n }\n const rawApp = http.getRawApp();\n const messaging = this.getMessaging(ctx);\n if (!rawApp || !messaging) return;\n\n rawApp.post('/api/v1/webhooks/redeliver', async (c: any) => {\n const userId = await this.resolveSessionUserId(ctx, c);\n if (!userId) {\n return c.json(\n { success: false, error: 'unauthenticated', message: 'Sign in to redeliver webhook deliveries.' },\n 401,\n );\n }\n let body: any;\n try {\n body = await c.req.json();\n } catch {\n return c.json({ success: false, error: 'invalid_body', message: 'Request body must be JSON.' }, 400);\n }\n const deliveryId = typeof body?.deliveryId === 'string' ? body.deliveryId.trim() : '';\n if (!deliveryId) {\n return c.json(\n { success: false, error: 'missing_delivery_id', message: 'Body must include `deliveryId: string`.' },\n 400,\n );\n }\n try {\n const row = await messaging.redeliverHttp(deliveryId);\n ctx.logger.info?.('[webhook-outbox] redelivered', { deliveryId, requestedBy: userId });\n return c.json({ success: true, data: { id: row.id, status: row.status } });\n } catch (err: any) {\n const code = err?.code;\n if (code === 'not_found') {\n return c.json({ success: false, error: 'not_found', message: err.message }, 404);\n }\n if (code === 'not_eligible') {\n return c.json({ success: false, error: 'not_eligible', message: err.message }, 409);\n }\n ctx.logger.error?.('[webhook-outbox] redeliver failed', err as Error);\n return c.json(\n { success: false, error: 'internal_error', message: err?.message ?? String(err) },\n 500,\n );\n }\n });\n\n ctx.logger.info?.('[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver');\n }\n\n private async resolveSessionUserId(ctx: PluginContext, c: any): Promise<string | undefined> {\n try {\n const authService: any = this.tryGetService<any>(ctx, ['auth']);\n if (!authService) return undefined;\n let api: any = authService.api;\n if (!api && typeof authService.getApi === 'function') {\n api = await authService.getApi();\n }\n if (!api?.getSession) return undefined;\n const session = await api.getSession({ headers: c.req.raw.headers });\n const uid = session?.user?.id;\n return typeof uid === 'string' && uid.length > 0 ? uid : undefined;\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":[]}
package/dist/schema.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunkRA3REIOAcjs = require('./chunk-RA3REIOA.cjs');
3
+ var _chunkI63ISOGOcjs = require('./chunk-I63ISOGO.cjs');
4
4
 
5
5
 
6
- exports.SysWebhook = _chunkRA3REIOAcjs.SysWebhook;
6
+ exports.SysWebhook = _chunkI63ISOGOcjs.SysWebhook;
7
7
  //# sourceMappingURL=schema.cjs.map
package/dist/schema.d.cts CHANGED
@@ -260,6 +260,7 @@ declare const SysWebhook: Omit<{
260
260
  generatedBy?: string | undefined;
261
261
  } | undefined;
262
262
  } | undefined;
263
+ widget?: string | undefined;
263
264
  requiredPermissions?: string[] | undefined;
264
265
  system?: boolean | undefined;
265
266
  inlineHelpText?: string | undefined;
@@ -964,6 +965,7 @@ declare const SysWebhook: Omit<{
964
965
  readonly precision?: number | undefined;
965
966
  readonly required?: boolean | undefined;
966
967
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
968
+ readonly widget?: string | undefined;
967
969
  readonly multiple?: boolean | undefined;
968
970
  readonly dependencies?: string[] | undefined;
969
971
  readonly externalId?: boolean | undefined;
@@ -1147,6 +1149,7 @@ declare const SysWebhook: Omit<{
1147
1149
  readonly precision?: number | undefined;
1148
1150
  readonly required?: boolean | undefined;
1149
1151
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1152
+ readonly widget?: string | undefined;
1150
1153
  readonly multiple?: boolean | undefined;
1151
1154
  readonly dependencies?: string[] | undefined;
1152
1155
  readonly externalId?: boolean | undefined;
@@ -1330,6 +1333,7 @@ declare const SysWebhook: Omit<{
1330
1333
  readonly precision?: number | undefined;
1331
1334
  readonly required?: boolean | undefined;
1332
1335
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1336
+ readonly widget?: string | undefined;
1333
1337
  readonly multiple?: boolean | undefined;
1334
1338
  readonly dependencies?: string[] | undefined;
1335
1339
  readonly externalId?: boolean | undefined;
@@ -1513,6 +1517,7 @@ declare const SysWebhook: Omit<{
1513
1517
  readonly precision?: number | undefined;
1514
1518
  readonly required?: boolean | undefined;
1515
1519
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1520
+ readonly widget?: string | undefined;
1516
1521
  readonly multiple?: boolean | undefined;
1517
1522
  readonly dependencies?: string[] | undefined;
1518
1523
  readonly externalId?: boolean | undefined;
@@ -1675,7 +1680,7 @@ declare const SysWebhook: Omit<{
1675
1680
  readonly triggers: {
1676
1681
  readonly readonly?: boolean | undefined;
1677
1682
  readonly format?: string | undefined;
1678
- readonly options?: {
1683
+ options: {
1679
1684
  label: string;
1680
1685
  value: string;
1681
1686
  color?: string | undefined;
@@ -1689,13 +1694,14 @@ declare const SysWebhook: Omit<{
1689
1694
  generatedBy?: string | undefined;
1690
1695
  } | undefined;
1691
1696
  } | undefined;
1692
- }[] | undefined;
1697
+ }[];
1693
1698
  readonly description?: string | undefined;
1694
1699
  readonly label?: string | undefined;
1695
1700
  readonly name?: string | undefined;
1696
1701
  readonly precision?: number | undefined;
1697
1702
  readonly required?: boolean | undefined;
1698
1703
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1704
+ readonly widget?: string | undefined;
1699
1705
  readonly multiple?: boolean | undefined;
1700
1706
  readonly dependencies?: string[] | undefined;
1701
1707
  readonly externalId?: boolean | undefined;
@@ -1853,7 +1859,7 @@ declare const SysWebhook: Omit<{
1853
1859
  readonly inlineHelpText?: string | undefined;
1854
1860
  readonly autonumberFormat?: string | undefined;
1855
1861
  readonly index?: boolean | undefined;
1856
- readonly type: "text";
1862
+ readonly type: "select";
1857
1863
  };
1858
1864
  readonly url: {
1859
1865
  readonly readonly?: boolean | undefined;
@@ -1879,6 +1885,7 @@ declare const SysWebhook: Omit<{
1879
1885
  readonly precision?: number | undefined;
1880
1886
  readonly required?: boolean | undefined;
1881
1887
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1888
+ readonly widget?: string | undefined;
1882
1889
  readonly multiple?: boolean | undefined;
1883
1890
  readonly dependencies?: string[] | undefined;
1884
1891
  readonly externalId?: boolean | undefined;
@@ -2041,7 +2048,7 @@ declare const SysWebhook: Omit<{
2041
2048
  readonly method: {
2042
2049
  readonly readonly?: boolean | undefined;
2043
2050
  readonly format?: string | undefined;
2044
- readonly options?: {
2051
+ options: {
2045
2052
  label: string;
2046
2053
  value: string;
2047
2054
  color?: string | undefined;
@@ -2055,13 +2062,14 @@ declare const SysWebhook: Omit<{
2055
2062
  generatedBy?: string | undefined;
2056
2063
  } | undefined;
2057
2064
  } | undefined;
2058
- }[] | undefined;
2065
+ }[];
2059
2066
  readonly description?: string | undefined;
2060
2067
  readonly label?: string | undefined;
2061
2068
  readonly name?: string | undefined;
2062
2069
  readonly precision?: number | undefined;
2063
2070
  readonly required?: boolean | undefined;
2064
2071
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2072
+ readonly widget?: string | undefined;
2065
2073
  readonly multiple?: boolean | undefined;
2066
2074
  readonly dependencies?: string[] | undefined;
2067
2075
  readonly externalId?: boolean | undefined;
@@ -2219,7 +2227,7 @@ declare const SysWebhook: Omit<{
2219
2227
  readonly inlineHelpText?: string | undefined;
2220
2228
  readonly autonumberFormat?: string | undefined;
2221
2229
  readonly index?: boolean | undefined;
2222
- readonly type: "text";
2230
+ readonly type: "select";
2223
2231
  };
2224
2232
  readonly description: {
2225
2233
  readonly readonly?: boolean | undefined;
@@ -2245,6 +2253,7 @@ declare const SysWebhook: Omit<{
2245
2253
  readonly precision?: number | undefined;
2246
2254
  readonly required?: boolean | undefined;
2247
2255
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2256
+ readonly widget?: string | undefined;
2248
2257
  readonly multiple?: boolean | undefined;
2249
2258
  readonly dependencies?: string[] | undefined;
2250
2259
  readonly externalId?: boolean | undefined;
@@ -2428,6 +2437,7 @@ declare const SysWebhook: Omit<{
2428
2437
  readonly precision?: number | undefined;
2429
2438
  readonly required?: boolean | undefined;
2430
2439
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2440
+ readonly widget?: string | undefined;
2431
2441
  readonly multiple?: boolean | undefined;
2432
2442
  readonly dependencies?: string[] | undefined;
2433
2443
  readonly externalId?: boolean | undefined;
@@ -2611,6 +2621,7 @@ declare const SysWebhook: Omit<{
2611
2621
  readonly precision?: number | undefined;
2612
2622
  readonly required?: boolean | undefined;
2613
2623
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2624
+ readonly widget?: string | undefined;
2614
2625
  readonly multiple?: boolean | undefined;
2615
2626
  readonly dependencies?: string[] | undefined;
2616
2627
  readonly externalId?: boolean | undefined;
@@ -2794,6 +2805,7 @@ declare const SysWebhook: Omit<{
2794
2805
  readonly precision?: number | undefined;
2795
2806
  readonly required?: boolean | undefined;
2796
2807
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2808
+ readonly widget?: string | undefined;
2797
2809
  readonly multiple?: boolean | undefined;
2798
2810
  readonly dependencies?: string[] | undefined;
2799
2811
  readonly externalId?: boolean | undefined;
@@ -2977,6 +2989,7 @@ declare const SysWebhook: Omit<{
2977
2989
  readonly precision?: number | undefined;
2978
2990
  readonly required?: boolean | undefined;
2979
2991
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2992
+ readonly widget?: string | undefined;
2980
2993
  readonly multiple?: boolean | undefined;
2981
2994
  readonly dependencies?: string[] | undefined;
2982
2995
  readonly externalId?: boolean | undefined;
package/dist/schema.d.ts CHANGED
@@ -260,6 +260,7 @@ declare const SysWebhook: Omit<{
260
260
  generatedBy?: string | undefined;
261
261
  } | undefined;
262
262
  } | undefined;
263
+ widget?: string | undefined;
263
264
  requiredPermissions?: string[] | undefined;
264
265
  system?: boolean | undefined;
265
266
  inlineHelpText?: string | undefined;
@@ -964,6 +965,7 @@ declare const SysWebhook: Omit<{
964
965
  readonly precision?: number | undefined;
965
966
  readonly required?: boolean | undefined;
966
967
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
968
+ readonly widget?: string | undefined;
967
969
  readonly multiple?: boolean | undefined;
968
970
  readonly dependencies?: string[] | undefined;
969
971
  readonly externalId?: boolean | undefined;
@@ -1147,6 +1149,7 @@ declare const SysWebhook: Omit<{
1147
1149
  readonly precision?: number | undefined;
1148
1150
  readonly required?: boolean | undefined;
1149
1151
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1152
+ readonly widget?: string | undefined;
1150
1153
  readonly multiple?: boolean | undefined;
1151
1154
  readonly dependencies?: string[] | undefined;
1152
1155
  readonly externalId?: boolean | undefined;
@@ -1330,6 +1333,7 @@ declare const SysWebhook: Omit<{
1330
1333
  readonly precision?: number | undefined;
1331
1334
  readonly required?: boolean | undefined;
1332
1335
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1336
+ readonly widget?: string | undefined;
1333
1337
  readonly multiple?: boolean | undefined;
1334
1338
  readonly dependencies?: string[] | undefined;
1335
1339
  readonly externalId?: boolean | undefined;
@@ -1513,6 +1517,7 @@ declare const SysWebhook: Omit<{
1513
1517
  readonly precision?: number | undefined;
1514
1518
  readonly required?: boolean | undefined;
1515
1519
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1520
+ readonly widget?: string | undefined;
1516
1521
  readonly multiple?: boolean | undefined;
1517
1522
  readonly dependencies?: string[] | undefined;
1518
1523
  readonly externalId?: boolean | undefined;
@@ -1675,7 +1680,7 @@ declare const SysWebhook: Omit<{
1675
1680
  readonly triggers: {
1676
1681
  readonly readonly?: boolean | undefined;
1677
1682
  readonly format?: string | undefined;
1678
- readonly options?: {
1683
+ options: {
1679
1684
  label: string;
1680
1685
  value: string;
1681
1686
  color?: string | undefined;
@@ -1689,13 +1694,14 @@ declare const SysWebhook: Omit<{
1689
1694
  generatedBy?: string | undefined;
1690
1695
  } | undefined;
1691
1696
  } | undefined;
1692
- }[] | undefined;
1697
+ }[];
1693
1698
  readonly description?: string | undefined;
1694
1699
  readonly label?: string | undefined;
1695
1700
  readonly name?: string | undefined;
1696
1701
  readonly precision?: number | undefined;
1697
1702
  readonly required?: boolean | undefined;
1698
1703
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1704
+ readonly widget?: string | undefined;
1699
1705
  readonly multiple?: boolean | undefined;
1700
1706
  readonly dependencies?: string[] | undefined;
1701
1707
  readonly externalId?: boolean | undefined;
@@ -1853,7 +1859,7 @@ declare const SysWebhook: Omit<{
1853
1859
  readonly inlineHelpText?: string | undefined;
1854
1860
  readonly autonumberFormat?: string | undefined;
1855
1861
  readonly index?: boolean | undefined;
1856
- readonly type: "text";
1862
+ readonly type: "select";
1857
1863
  };
1858
1864
  readonly url: {
1859
1865
  readonly readonly?: boolean | undefined;
@@ -1879,6 +1885,7 @@ declare const SysWebhook: Omit<{
1879
1885
  readonly precision?: number | undefined;
1880
1886
  readonly required?: boolean | undefined;
1881
1887
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1888
+ readonly widget?: string | undefined;
1882
1889
  readonly multiple?: boolean | undefined;
1883
1890
  readonly dependencies?: string[] | undefined;
1884
1891
  readonly externalId?: boolean | undefined;
@@ -2041,7 +2048,7 @@ declare const SysWebhook: Omit<{
2041
2048
  readonly method: {
2042
2049
  readonly readonly?: boolean | undefined;
2043
2050
  readonly format?: string | undefined;
2044
- readonly options?: {
2051
+ options: {
2045
2052
  label: string;
2046
2053
  value: string;
2047
2054
  color?: string | undefined;
@@ -2055,13 +2062,14 @@ declare const SysWebhook: Omit<{
2055
2062
  generatedBy?: string | undefined;
2056
2063
  } | undefined;
2057
2064
  } | undefined;
2058
- }[] | undefined;
2065
+ }[];
2059
2066
  readonly description?: string | undefined;
2060
2067
  readonly label?: string | undefined;
2061
2068
  readonly name?: string | undefined;
2062
2069
  readonly precision?: number | undefined;
2063
2070
  readonly required?: boolean | undefined;
2064
2071
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2072
+ readonly widget?: string | undefined;
2065
2073
  readonly multiple?: boolean | undefined;
2066
2074
  readonly dependencies?: string[] | undefined;
2067
2075
  readonly externalId?: boolean | undefined;
@@ -2219,7 +2227,7 @@ declare const SysWebhook: Omit<{
2219
2227
  readonly inlineHelpText?: string | undefined;
2220
2228
  readonly autonumberFormat?: string | undefined;
2221
2229
  readonly index?: boolean | undefined;
2222
- readonly type: "text";
2230
+ readonly type: "select";
2223
2231
  };
2224
2232
  readonly description: {
2225
2233
  readonly readonly?: boolean | undefined;
@@ -2245,6 +2253,7 @@ declare const SysWebhook: Omit<{
2245
2253
  readonly precision?: number | undefined;
2246
2254
  readonly required?: boolean | undefined;
2247
2255
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2256
+ readonly widget?: string | undefined;
2248
2257
  readonly multiple?: boolean | undefined;
2249
2258
  readonly dependencies?: string[] | undefined;
2250
2259
  readonly externalId?: boolean | undefined;
@@ -2428,6 +2437,7 @@ declare const SysWebhook: Omit<{
2428
2437
  readonly precision?: number | undefined;
2429
2438
  readonly required?: boolean | undefined;
2430
2439
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2440
+ readonly widget?: string | undefined;
2431
2441
  readonly multiple?: boolean | undefined;
2432
2442
  readonly dependencies?: string[] | undefined;
2433
2443
  readonly externalId?: boolean | undefined;
@@ -2611,6 +2621,7 @@ declare const SysWebhook: Omit<{
2611
2621
  readonly precision?: number | undefined;
2612
2622
  readonly required?: boolean | undefined;
2613
2623
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2624
+ readonly widget?: string | undefined;
2614
2625
  readonly multiple?: boolean | undefined;
2615
2626
  readonly dependencies?: string[] | undefined;
2616
2627
  readonly externalId?: boolean | undefined;
@@ -2794,6 +2805,7 @@ declare const SysWebhook: Omit<{
2794
2805
  readonly precision?: number | undefined;
2795
2806
  readonly required?: boolean | undefined;
2796
2807
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2808
+ readonly widget?: string | undefined;
2797
2809
  readonly multiple?: boolean | undefined;
2798
2810
  readonly dependencies?: string[] | undefined;
2799
2811
  readonly externalId?: boolean | undefined;
@@ -2977,6 +2989,7 @@ declare const SysWebhook: Omit<{
2977
2989
  readonly precision?: number | undefined;
2978
2990
  readonly required?: boolean | undefined;
2979
2991
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2992
+ readonly widget?: string | undefined;
2980
2993
  readonly multiple?: boolean | undefined;
2981
2994
  readonly dependencies?: string[] | undefined;
2982
2995
  readonly externalId?: boolean | undefined;
package/dist/schema.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  SysWebhook
3
- } from "./chunk-HSSV22TO.js";
3
+ } from "./chunk-YQG6SUP3.js";
4
4
  export {
5
5
  SysWebhook
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/plugin-webhooks",
3
- "version": "14.6.0",
3
+ "version": "14.7.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Persistent, cluster-aware webhook dispatcher. Durable outbox + per-partition cluster.lock for exactly-once-ish delivery across nodes. See content/docs/concepts/webhook-delivery.mdx.",
6
6
  "type": "module",
@@ -19,9 +19,9 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@objectstack/core": "14.6.0",
23
- "@objectstack/service-messaging": "14.6.0",
24
- "@objectstack/spec": "14.6.0"
22
+ "@objectstack/core": "14.7.0",
23
+ "@objectstack/service-messaging": "14.7.0",
24
+ "@objectstack/spec": "14.7.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.1",
@@ -205,6 +205,42 @@ describe('AutoEnqueuer', () => {
205
205
  await ae.stop();
206
206
  });
207
207
 
208
+ it('parses triggers authored as a multi-select array', async () => {
209
+ // The `triggers` field is now a multi-select stored as an array; the
210
+ // parser must treat it identically to the legacy CSV form.
211
+ const engine = new FakeEngine({ sys_webhook: [webhook({ triggers: ['create'] })] });
212
+ const realtime = new FakeRealtime();
213
+ const { enqueue, calls } = makeRecorder();
214
+ const ae = new AutoEnqueuer(engine, realtime, enqueue, { refreshIntervalMs: 0 });
215
+ await ae.start();
216
+
217
+ await realtime.publish(event('created', 'contact', { id: 'c-1' }));
218
+ await realtime.publish(event('updated', 'contact', { id: 'c-1' }, '2026-05-24T00:00:01.000Z'));
219
+ await flush();
220
+
221
+ expect(calls).toHaveLength(1);
222
+ expect(calls[0].label).toBe('data.record.created');
223
+ await ae.stop();
224
+ });
225
+
226
+ it('parses triggers stored as a JSON-encoded array string', async () => {
227
+ // Some drivers hand a JSON array column back as a string — accept it.
228
+ const engine = new FakeEngine({ sys_webhook: [webhook({ triggers: '["create","update"]' })] });
229
+ const realtime = new FakeRealtime();
230
+ const { enqueue, calls } = makeRecorder();
231
+ const ae = new AutoEnqueuer(engine, realtime, enqueue, { refreshIntervalMs: 0 });
232
+ await ae.start();
233
+
234
+ await realtime.publish(event('created', 'contact', { id: 'c-1' }));
235
+ await realtime.publish(event('deleted', 'contact', { id: 'c-1' }, '2026-05-24T00:00:02.000Z'));
236
+ await flush();
237
+
238
+ // create + update are subscribed; delete is not.
239
+ expect(calls).toHaveLength(1);
240
+ expect(calls[0].label).toBe('data.record.created');
241
+ await ae.stop();
242
+ });
243
+
208
244
  it('fans out to multiple matching webhooks', async () => {
209
245
  const engine = new FakeEngine({
210
246
  sys_webhook: [
@@ -205,11 +205,30 @@ export class AutoEnqueuer {
205
205
 
206
206
  private parseRow(row: any): CachedSubscription | null {
207
207
  if (!row?.id || !row?.url) return null;
208
- const triggersField = (row.triggers ?? '') as string;
208
+ // `triggers` is now authored as a multi-select (stored as an array), but
209
+ // legacy rows stored a comma-separated string (and some drivers hand a
210
+ // JSON-encoded array back as a string). Accept all three shapes so a
211
+ // schema change never silently drops a subscription's events.
212
+ const rawTriggers = row.triggers;
213
+ let triggerList: string[];
214
+ if (Array.isArray(rawTriggers)) {
215
+ triggerList = rawTriggers.map((t) => String(t));
216
+ } else {
217
+ const s = String(rawTriggers ?? '').trim();
218
+ if (s.startsWith('[')) {
219
+ try {
220
+ const parsed = JSON.parse(s);
221
+ triggerList = Array.isArray(parsed) ? parsed.map((t) => String(t)) : [s];
222
+ } catch {
223
+ triggerList = s.split(',');
224
+ }
225
+ } else {
226
+ triggerList = s.split(',');
227
+ }
228
+ }
209
229
  const triggers = new Set(
210
- triggersField
211
- .split(',')
212
- .map((s: string) => s.trim().toLowerCase())
230
+ triggerList
231
+ .map((t) => t.trim().toLowerCase())
213
232
  .filter(Boolean) as Array<'create' | 'update' | 'delete' | 'undelete'>,
214
233
  );
215
234
  if (triggers.size === 0) {
@@ -235,7 +254,11 @@ export class AutoEnqueuer {
235
254
  objectName: row.object_name ? String(row.object_name) : undefined,
236
255
  triggers,
237
256
  url: String(row.url),
238
- method: row.method ?? defn.method ?? 'POST',
257
+ // Method is authored via a select whose option values are lowercased
258
+ // (get/post/…); upper-case here so delivery uses a canonical HTTP
259
+ // method regardless of whether the row was authored before or after
260
+ // the select change (legacy rows stored 'POST').
261
+ method: String(row.method ?? defn.method ?? 'POST').toUpperCase(),
239
262
  headers: defn.headers,
240
263
  secret: defn.secret,
241
264
  timeoutMs: defn.timeoutMs,