@objectstack/plugin-webhooks 14.5.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;
@@ -749,6 +750,15 @@ declare const SysWebhook: Omit<{
749
750
  helpText?: string | undefined;
750
751
  defaultValue?: unknown;
751
752
  defaultFromRow?: boolean | undefined;
753
+ visible?: {
754
+ dialect: "cel" | "js" | "cron" | "template";
755
+ source?: string | undefined;
756
+ ast?: unknown;
757
+ meta?: {
758
+ rationale?: string | undefined;
759
+ generatedBy?: string | undefined;
760
+ } | undefined;
761
+ } | undefined;
752
762
  }[] | undefined;
753
763
  variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
754
764
  order?: number | undefined;
@@ -955,6 +965,7 @@ declare const SysWebhook: Omit<{
955
965
  readonly precision?: number | undefined;
956
966
  readonly required?: boolean | undefined;
957
967
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
968
+ readonly widget?: string | undefined;
958
969
  readonly multiple?: boolean | undefined;
959
970
  readonly dependencies?: string[] | undefined;
960
971
  readonly externalId?: boolean | undefined;
@@ -1138,6 +1149,7 @@ declare const SysWebhook: Omit<{
1138
1149
  readonly precision?: number | undefined;
1139
1150
  readonly required?: boolean | undefined;
1140
1151
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1152
+ readonly widget?: string | undefined;
1141
1153
  readonly multiple?: boolean | undefined;
1142
1154
  readonly dependencies?: string[] | undefined;
1143
1155
  readonly externalId?: boolean | undefined;
@@ -1321,6 +1333,7 @@ declare const SysWebhook: Omit<{
1321
1333
  readonly precision?: number | undefined;
1322
1334
  readonly required?: boolean | undefined;
1323
1335
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1336
+ readonly widget?: string | undefined;
1324
1337
  readonly multiple?: boolean | undefined;
1325
1338
  readonly dependencies?: string[] | undefined;
1326
1339
  readonly externalId?: boolean | undefined;
@@ -1504,6 +1517,7 @@ declare const SysWebhook: Omit<{
1504
1517
  readonly precision?: number | undefined;
1505
1518
  readonly required?: boolean | undefined;
1506
1519
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1520
+ readonly widget?: string | undefined;
1507
1521
  readonly multiple?: boolean | undefined;
1508
1522
  readonly dependencies?: string[] | undefined;
1509
1523
  readonly externalId?: boolean | undefined;
@@ -1666,7 +1680,7 @@ declare const SysWebhook: Omit<{
1666
1680
  readonly triggers: {
1667
1681
  readonly readonly?: boolean | undefined;
1668
1682
  readonly format?: string | undefined;
1669
- readonly options?: {
1683
+ options: {
1670
1684
  label: string;
1671
1685
  value: string;
1672
1686
  color?: string | undefined;
@@ -1680,13 +1694,14 @@ declare const SysWebhook: Omit<{
1680
1694
  generatedBy?: string | undefined;
1681
1695
  } | undefined;
1682
1696
  } | undefined;
1683
- }[] | undefined;
1697
+ }[];
1684
1698
  readonly description?: string | undefined;
1685
1699
  readonly label?: string | undefined;
1686
1700
  readonly name?: string | undefined;
1687
1701
  readonly precision?: number | undefined;
1688
1702
  readonly required?: boolean | undefined;
1689
1703
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1704
+ readonly widget?: string | undefined;
1690
1705
  readonly multiple?: boolean | undefined;
1691
1706
  readonly dependencies?: string[] | undefined;
1692
1707
  readonly externalId?: boolean | undefined;
@@ -1844,7 +1859,7 @@ declare const SysWebhook: Omit<{
1844
1859
  readonly inlineHelpText?: string | undefined;
1845
1860
  readonly autonumberFormat?: string | undefined;
1846
1861
  readonly index?: boolean | undefined;
1847
- readonly type: "text";
1862
+ readonly type: "select";
1848
1863
  };
1849
1864
  readonly url: {
1850
1865
  readonly readonly?: boolean | undefined;
@@ -1870,6 +1885,7 @@ declare const SysWebhook: Omit<{
1870
1885
  readonly precision?: number | undefined;
1871
1886
  readonly required?: boolean | undefined;
1872
1887
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1888
+ readonly widget?: string | undefined;
1873
1889
  readonly multiple?: boolean | undefined;
1874
1890
  readonly dependencies?: string[] | undefined;
1875
1891
  readonly externalId?: boolean | undefined;
@@ -2032,7 +2048,7 @@ declare const SysWebhook: Omit<{
2032
2048
  readonly method: {
2033
2049
  readonly readonly?: boolean | undefined;
2034
2050
  readonly format?: string | undefined;
2035
- readonly options?: {
2051
+ options: {
2036
2052
  label: string;
2037
2053
  value: string;
2038
2054
  color?: string | undefined;
@@ -2046,13 +2062,14 @@ declare const SysWebhook: Omit<{
2046
2062
  generatedBy?: string | undefined;
2047
2063
  } | undefined;
2048
2064
  } | undefined;
2049
- }[] | undefined;
2065
+ }[];
2050
2066
  readonly description?: string | undefined;
2051
2067
  readonly label?: string | undefined;
2052
2068
  readonly name?: string | undefined;
2053
2069
  readonly precision?: number | undefined;
2054
2070
  readonly required?: boolean | undefined;
2055
2071
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2072
+ readonly widget?: string | undefined;
2056
2073
  readonly multiple?: boolean | undefined;
2057
2074
  readonly dependencies?: string[] | undefined;
2058
2075
  readonly externalId?: boolean | undefined;
@@ -2210,7 +2227,7 @@ declare const SysWebhook: Omit<{
2210
2227
  readonly inlineHelpText?: string | undefined;
2211
2228
  readonly autonumberFormat?: string | undefined;
2212
2229
  readonly index?: boolean | undefined;
2213
- readonly type: "text";
2230
+ readonly type: "select";
2214
2231
  };
2215
2232
  readonly description: {
2216
2233
  readonly readonly?: boolean | undefined;
@@ -2236,6 +2253,7 @@ declare const SysWebhook: Omit<{
2236
2253
  readonly precision?: number | undefined;
2237
2254
  readonly required?: boolean | undefined;
2238
2255
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2256
+ readonly widget?: string | undefined;
2239
2257
  readonly multiple?: boolean | undefined;
2240
2258
  readonly dependencies?: string[] | undefined;
2241
2259
  readonly externalId?: boolean | undefined;
@@ -2419,6 +2437,7 @@ declare const SysWebhook: Omit<{
2419
2437
  readonly precision?: number | undefined;
2420
2438
  readonly required?: boolean | undefined;
2421
2439
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2440
+ readonly widget?: string | undefined;
2422
2441
  readonly multiple?: boolean | undefined;
2423
2442
  readonly dependencies?: string[] | undefined;
2424
2443
  readonly externalId?: boolean | undefined;
@@ -2602,6 +2621,7 @@ declare const SysWebhook: Omit<{
2602
2621
  readonly precision?: number | undefined;
2603
2622
  readonly required?: boolean | undefined;
2604
2623
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2624
+ readonly widget?: string | undefined;
2605
2625
  readonly multiple?: boolean | undefined;
2606
2626
  readonly dependencies?: string[] | undefined;
2607
2627
  readonly externalId?: boolean | undefined;
@@ -2785,6 +2805,7 @@ declare const SysWebhook: Omit<{
2785
2805
  readonly precision?: number | undefined;
2786
2806
  readonly required?: boolean | undefined;
2787
2807
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2808
+ readonly widget?: string | undefined;
2788
2809
  readonly multiple?: boolean | undefined;
2789
2810
  readonly dependencies?: string[] | undefined;
2790
2811
  readonly externalId?: boolean | undefined;
@@ -2968,6 +2989,7 @@ declare const SysWebhook: Omit<{
2968
2989
  readonly precision?: number | undefined;
2969
2990
  readonly required?: boolean | undefined;
2970
2991
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2992
+ readonly widget?: string | undefined;
2971
2993
  readonly multiple?: boolean | undefined;
2972
2994
  readonly dependencies?: string[] | undefined;
2973
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;
@@ -749,6 +750,15 @@ declare const SysWebhook: Omit<{
749
750
  helpText?: string | undefined;
750
751
  defaultValue?: unknown;
751
752
  defaultFromRow?: boolean | undefined;
753
+ visible?: {
754
+ dialect: "cel" | "js" | "cron" | "template";
755
+ source?: string | undefined;
756
+ ast?: unknown;
757
+ meta?: {
758
+ rationale?: string | undefined;
759
+ generatedBy?: string | undefined;
760
+ } | undefined;
761
+ } | undefined;
752
762
  }[] | undefined;
753
763
  variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
754
764
  order?: number | undefined;
@@ -955,6 +965,7 @@ declare const SysWebhook: Omit<{
955
965
  readonly precision?: number | undefined;
956
966
  readonly required?: boolean | undefined;
957
967
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
968
+ readonly widget?: string | undefined;
958
969
  readonly multiple?: boolean | undefined;
959
970
  readonly dependencies?: string[] | undefined;
960
971
  readonly externalId?: boolean | undefined;
@@ -1138,6 +1149,7 @@ declare const SysWebhook: Omit<{
1138
1149
  readonly precision?: number | undefined;
1139
1150
  readonly required?: boolean | undefined;
1140
1151
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1152
+ readonly widget?: string | undefined;
1141
1153
  readonly multiple?: boolean | undefined;
1142
1154
  readonly dependencies?: string[] | undefined;
1143
1155
  readonly externalId?: boolean | undefined;
@@ -1321,6 +1333,7 @@ declare const SysWebhook: Omit<{
1321
1333
  readonly precision?: number | undefined;
1322
1334
  readonly required?: boolean | undefined;
1323
1335
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1336
+ readonly widget?: string | undefined;
1324
1337
  readonly multiple?: boolean | undefined;
1325
1338
  readonly dependencies?: string[] | undefined;
1326
1339
  readonly externalId?: boolean | undefined;
@@ -1504,6 +1517,7 @@ declare const SysWebhook: Omit<{
1504
1517
  readonly precision?: number | undefined;
1505
1518
  readonly required?: boolean | undefined;
1506
1519
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1520
+ readonly widget?: string | undefined;
1507
1521
  readonly multiple?: boolean | undefined;
1508
1522
  readonly dependencies?: string[] | undefined;
1509
1523
  readonly externalId?: boolean | undefined;
@@ -1666,7 +1680,7 @@ declare const SysWebhook: Omit<{
1666
1680
  readonly triggers: {
1667
1681
  readonly readonly?: boolean | undefined;
1668
1682
  readonly format?: string | undefined;
1669
- readonly options?: {
1683
+ options: {
1670
1684
  label: string;
1671
1685
  value: string;
1672
1686
  color?: string | undefined;
@@ -1680,13 +1694,14 @@ declare const SysWebhook: Omit<{
1680
1694
  generatedBy?: string | undefined;
1681
1695
  } | undefined;
1682
1696
  } | undefined;
1683
- }[] | undefined;
1697
+ }[];
1684
1698
  readonly description?: string | undefined;
1685
1699
  readonly label?: string | undefined;
1686
1700
  readonly name?: string | undefined;
1687
1701
  readonly precision?: number | undefined;
1688
1702
  readonly required?: boolean | undefined;
1689
1703
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1704
+ readonly widget?: string | undefined;
1690
1705
  readonly multiple?: boolean | undefined;
1691
1706
  readonly dependencies?: string[] | undefined;
1692
1707
  readonly externalId?: boolean | undefined;
@@ -1844,7 +1859,7 @@ declare const SysWebhook: Omit<{
1844
1859
  readonly inlineHelpText?: string | undefined;
1845
1860
  readonly autonumberFormat?: string | undefined;
1846
1861
  readonly index?: boolean | undefined;
1847
- readonly type: "text";
1862
+ readonly type: "select";
1848
1863
  };
1849
1864
  readonly url: {
1850
1865
  readonly readonly?: boolean | undefined;
@@ -1870,6 +1885,7 @@ declare const SysWebhook: Omit<{
1870
1885
  readonly precision?: number | undefined;
1871
1886
  readonly required?: boolean | undefined;
1872
1887
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
1888
+ readonly widget?: string | undefined;
1873
1889
  readonly multiple?: boolean | undefined;
1874
1890
  readonly dependencies?: string[] | undefined;
1875
1891
  readonly externalId?: boolean | undefined;
@@ -2032,7 +2048,7 @@ declare const SysWebhook: Omit<{
2032
2048
  readonly method: {
2033
2049
  readonly readonly?: boolean | undefined;
2034
2050
  readonly format?: string | undefined;
2035
- readonly options?: {
2051
+ options: {
2036
2052
  label: string;
2037
2053
  value: string;
2038
2054
  color?: string | undefined;
@@ -2046,13 +2062,14 @@ declare const SysWebhook: Omit<{
2046
2062
  generatedBy?: string | undefined;
2047
2063
  } | undefined;
2048
2064
  } | undefined;
2049
- }[] | undefined;
2065
+ }[];
2050
2066
  readonly description?: string | undefined;
2051
2067
  readonly label?: string | undefined;
2052
2068
  readonly name?: string | undefined;
2053
2069
  readonly precision?: number | undefined;
2054
2070
  readonly required?: boolean | undefined;
2055
2071
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2072
+ readonly widget?: string | undefined;
2056
2073
  readonly multiple?: boolean | undefined;
2057
2074
  readonly dependencies?: string[] | undefined;
2058
2075
  readonly externalId?: boolean | undefined;
@@ -2210,7 +2227,7 @@ declare const SysWebhook: Omit<{
2210
2227
  readonly inlineHelpText?: string | undefined;
2211
2228
  readonly autonumberFormat?: string | undefined;
2212
2229
  readonly index?: boolean | undefined;
2213
- readonly type: "text";
2230
+ readonly type: "select";
2214
2231
  };
2215
2232
  readonly description: {
2216
2233
  readonly readonly?: boolean | undefined;
@@ -2236,6 +2253,7 @@ declare const SysWebhook: Omit<{
2236
2253
  readonly precision?: number | undefined;
2237
2254
  readonly required?: boolean | undefined;
2238
2255
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2256
+ readonly widget?: string | undefined;
2239
2257
  readonly multiple?: boolean | undefined;
2240
2258
  readonly dependencies?: string[] | undefined;
2241
2259
  readonly externalId?: boolean | undefined;
@@ -2419,6 +2437,7 @@ declare const SysWebhook: Omit<{
2419
2437
  readonly precision?: number | undefined;
2420
2438
  readonly required?: boolean | undefined;
2421
2439
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2440
+ readonly widget?: string | undefined;
2422
2441
  readonly multiple?: boolean | undefined;
2423
2442
  readonly dependencies?: string[] | undefined;
2424
2443
  readonly externalId?: boolean | undefined;
@@ -2602,6 +2621,7 @@ declare const SysWebhook: Omit<{
2602
2621
  readonly precision?: number | undefined;
2603
2622
  readonly required?: boolean | undefined;
2604
2623
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2624
+ readonly widget?: string | undefined;
2605
2625
  readonly multiple?: boolean | undefined;
2606
2626
  readonly dependencies?: string[] | undefined;
2607
2627
  readonly externalId?: boolean | undefined;
@@ -2785,6 +2805,7 @@ declare const SysWebhook: Omit<{
2785
2805
  readonly precision?: number | undefined;
2786
2806
  readonly required?: boolean | undefined;
2787
2807
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2808
+ readonly widget?: string | undefined;
2788
2809
  readonly multiple?: boolean | undefined;
2789
2810
  readonly dependencies?: string[] | undefined;
2790
2811
  readonly externalId?: boolean | undefined;
@@ -2968,6 +2989,7 @@ declare const SysWebhook: Omit<{
2968
2989
  readonly precision?: number | undefined;
2969
2990
  readonly required?: boolean | undefined;
2970
2991
  readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
2992
+ readonly widget?: string | undefined;
2971
2993
  readonly multiple?: boolean | undefined;
2972
2994
  readonly dependencies?: string[] | undefined;
2973
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.5.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,12 +19,12 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@objectstack/core": "14.5.0",
23
- "@objectstack/service-messaging": "14.5.0",
24
- "@objectstack/spec": "14.5.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
- "@types/node": "^26.1.0",
27
+ "@types/node": "^26.1.1",
28
28
  "typescript": "^6.0.3",
29
29
  "vitest": "^4.1.10"
30
30
  },
@@ -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,