@pattern-stack/codegen 0.16.0 → 0.16.1

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/consumer-skills/integration/change-sources-and-sinks.md +1 -1
  3. package/dist/{chunk-32DOFN3T.js → chunk-2WDX6I7T.js} +2 -2
  4. package/dist/{chunk-WWGYCIJX.js → chunk-43SBT72G.js} +2 -2
  5. package/dist/{chunk-VNBC3VXM.js → chunk-5LXOJGO2.js} +6 -6
  6. package/dist/{chunk-3NMCDN7L.js → chunk-5TK7MEN4.js} +2 -2
  7. package/dist/chunk-5TK7MEN4.js.map +1 -0
  8. package/dist/{chunk-RUYLXR5F.js → chunk-AYC2HEAL.js} +9 -9
  9. package/dist/{chunk-CEWLVVAH.js → chunk-CRBVI4GE.js} +5 -5
  10. package/dist/{chunk-DKKFTHHI.js → chunk-CZQUOIDY.js} +4 -4
  11. package/dist/{chunk-3RWMQC3K.js → chunk-DGYTSCKN.js} +14 -14
  12. package/dist/{chunk-EBKVKN75.js → chunk-DLG62MQY.js} +7 -7
  13. package/dist/{chunk-4PFF3ED4.js → chunk-H6FO2ZDJ.js} +4 -4
  14. package/dist/{chunk-OTR44OH6.js → chunk-IT6FRTEW.js} +10 -10
  15. package/dist/{chunk-XDIIVIIK.js → chunk-JM3T27ZW.js} +7 -7
  16. package/dist/{chunk-KSTZIULO.js → chunk-K2I6XIK5.js} +4 -4
  17. package/dist/{chunk-DRCLNYH7.js → chunk-NXNVTXKG.js} +4 -4
  18. package/dist/{chunk-BHZP6LOV.js → chunk-QSJ3J4HE.js} +7 -7
  19. package/dist/{chunk-BK5ICA2F.js → chunk-RUSUZZAF.js} +4 -4
  20. package/dist/{chunk-BULPAAD3.js → chunk-T4YJRD22.js} +4 -4
  21. package/dist/{chunk-YLPAPPLW.js → chunk-TIZXQU26.js} +36 -9
  22. package/dist/chunk-TIZXQU26.js.map +1 -0
  23. package/dist/{chunk-EJBK7I4F.js → chunk-TKVTEUBD.js} +2 -2
  24. package/dist/runtime/shared/openapi/index.js +3 -3
  25. package/dist/runtime/subsystems/analytics/analytics.module.js +2 -2
  26. package/dist/runtime/subsystems/analytics/index.js +4 -4
  27. package/dist/runtime/subsystems/auth/auth.module.js +1 -1
  28. package/dist/runtime/subsystems/auth/index.js +6 -6
  29. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +2 -2
  30. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +2 -2
  31. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +6 -6
  32. package/dist/runtime/subsystems/bridge/bridge.module.js +19 -19
  33. package/dist/runtime/subsystems/bridge/index.js +21 -21
  34. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +2 -2
  35. package/dist/runtime/subsystems/events/events.module.js +3 -3
  36. package/dist/runtime/subsystems/events/index.js +3 -3
  37. package/dist/runtime/subsystems/index.js +60 -60
  38. package/dist/runtime/subsystems/integration/build-change-source.js +2 -2
  39. package/dist/runtime/subsystems/integration/detection-config.schema.d.ts +23 -15
  40. package/dist/runtime/subsystems/integration/detection-config.schema.js +1 -1
  41. package/dist/runtime/subsystems/integration/index.js +3 -3
  42. package/dist/runtime/subsystems/integration/webhook-change-source.d.ts +36 -6
  43. package/dist/runtime/subsystems/integration/webhook-change-source.js +1 -1
  44. package/dist/runtime/subsystems/jobs/index.js +43 -43
  45. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +6 -6
  46. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +3 -3
  47. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +3 -3
  48. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +3 -3
  49. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +3 -3
  50. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +3 -3
  51. package/dist/runtime/subsystems/jobs/job-worker.js +3 -3
  52. package/dist/runtime/subsystems/jobs/job-worker.module.js +13 -13
  53. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +11 -11
  54. package/dist/runtime/subsystems/storage/index.js +4 -4
  55. package/dist/runtime/subsystems/storage/storage.module.js +2 -2
  56. package/dist/src/cli/index.js +4 -4
  57. package/dist/src/index.d.ts +11 -11
  58. package/dist/src/index.js +4 -4
  59. package/package.json +1 -1
  60. package/runtime/subsystems/integration/detection-config.schema.ts +64 -54
  61. package/runtime/subsystems/integration/webhook-change-source.ts +187 -133
  62. package/dist/chunk-3NMCDN7L.js.map +0 -1
  63. package/dist/chunk-YLPAPPLW.js.map +0 -1
  64. /package/dist/{chunk-32DOFN3T.js.map → chunk-2WDX6I7T.js.map} +0 -0
  65. /package/dist/{chunk-WWGYCIJX.js.map → chunk-43SBT72G.js.map} +0 -0
  66. /package/dist/{chunk-VNBC3VXM.js.map → chunk-5LXOJGO2.js.map} +0 -0
  67. /package/dist/{chunk-RUYLXR5F.js.map → chunk-AYC2HEAL.js.map} +0 -0
  68. /package/dist/{chunk-CEWLVVAH.js.map → chunk-CRBVI4GE.js.map} +0 -0
  69. /package/dist/{chunk-DKKFTHHI.js.map → chunk-CZQUOIDY.js.map} +0 -0
  70. /package/dist/{chunk-3RWMQC3K.js.map → chunk-DGYTSCKN.js.map} +0 -0
  71. /package/dist/{chunk-EBKVKN75.js.map → chunk-DLG62MQY.js.map} +0 -0
  72. /package/dist/{chunk-4PFF3ED4.js.map → chunk-H6FO2ZDJ.js.map} +0 -0
  73. /package/dist/{chunk-OTR44OH6.js.map → chunk-IT6FRTEW.js.map} +0 -0
  74. /package/dist/{chunk-XDIIVIIK.js.map → chunk-JM3T27ZW.js.map} +0 -0
  75. /package/dist/{chunk-KSTZIULO.js.map → chunk-K2I6XIK5.js.map} +0 -0
  76. /package/dist/{chunk-DRCLNYH7.js.map → chunk-NXNVTXKG.js.map} +0 -0
  77. /package/dist/{chunk-BHZP6LOV.js.map → chunk-QSJ3J4HE.js.map} +0 -0
  78. /package/dist/{chunk-BK5ICA2F.js.map → chunk-RUSUZZAF.js.map} +0 -0
  79. /package/dist/{chunk-BULPAAD3.js.map → chunk-T4YJRD22.js.map} +0 -0
  80. /package/dist/{chunk-EJBK7I4F.js.map → chunk-TKVTEUBD.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,46 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.16.1] — 2026-06-04
8
+
9
+ **`WebhookFetchCallback<T>` yields `{ record, eventId?, cursor? }`** —
10
+ `WebhookChangeSource` now prefers a queue-yielded `eventId` for
11
+ `Change<T>.dedupKey` (gap #6 follow-through, swe-brain ADR-0009 Amendment B
12
+ §B5). swe-brain's Slack inbound drain documented an `eventIdField: 'externalId'`
13
+ substitution — delivery dedup happens at the receiver, so record identity stood
14
+ in for event identity. That substitution becomes genuinely unsafe once a message
15
+ and its edit (same `externalId`, different vendor event) can share one drain
16
+ batch — they'd collapse to a single `dedupKey`. Vendor delivery metadata (the
17
+ event id) should never need a field on the vendor-neutral canonical record; the
18
+ queue-yield is the right channel for it. Backward-compatible: callbacks that
19
+ yield `{ record, cursor? }` and configs that set `webhook.eventIdField` are
20
+ unchanged; opting into `eventId` is the only migration, and it's optional.
21
+
22
+ ### Changed
23
+
24
+ - **`WebhookFetchCallback<T>` yield shape** is now `{ record: T; eventId?:
25
+ string; cursor?: WebhookCursor }` (was `{ record: T; cursor?: WebhookCursor }`)
26
+ — the new `eventId` is opt-in and additive; existing callbacks compile and run
27
+ unchanged.
28
+ - **`WebhookChangeSource.dedupKey` precedence** is now **yielded `eventId` >
29
+ `webhook.eventIdField` record extraction > undefined**. A non-empty yielded
30
+ `eventId` always wins; otherwise the configured `eventIdField` is read off the
31
+ emitted record (and still throws if the field is configured but absent on the
32
+ record); with neither, `dedupKey` is `undefined` (the orchestrator simply has
33
+ no delivery-level dedup signal for that change).
34
+ - **`detection-config.schema.ts`: `webhook.eventIdField` is now optional**
35
+ (`z.string().min(1).optional()`). A callback that always yields `eventId` need
36
+ not declare a record field for it. The `webhook` block itself stays
37
+ structurally required in webhook mode (an empty `{}` is valid; a missing block
38
+ is not), so the existing "webhook-mode missing webhook block" validation is
39
+ unaffected.
40
+
41
+ ### Docs
42
+
43
+ - ADR-033 §1 amended (the `webhook: { eventIdField? }` shape + the yield channel
44
+ + dedupKey precedence), integration `SKILL.md` + consumer
45
+ `change-sources-and-sinks.md` updated to the yield-preferred precedence.
46
+
7
47
  ## [0.16.0] — 2026-06-04
8
48
 
9
49
  **Postgres LISTEN/NOTIFY wakeups** for the jobs worker + events outbox drainer
@@ -75,7 +75,7 @@ interface Change<T> {
75
75
  record: T; // canonical shape — provider mapping happens in the adapter
76
76
  cursor: unknown; // typed internally; opaque at the seam
77
77
  source: 'poll' | 'cdc' | 'webhook'; // provenance for the run-log audit
78
- dedupKey?: string; // CDC replay_id / webhook event_id when available
78
+ dedupKey?: string; // CDC replay_id / webhook event id when available (webhook: prefers the eventId yielded by WebhookFetchCallback, else webhook.eventIdField)
79
79
  providerChangedFields?: string[]; // CDC-only hint; lets the differ skip untouched fields
80
80
  }
81
81
  ```
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DetectionConfigSchema
3
- } from "./chunk-3NMCDN7L.js";
3
+ } from "./chunk-5TK7MEN4.js";
4
4
 
5
5
  // src/parser/load-entities.ts
6
6
  import { resolve as resolve2 } from "path";
@@ -4039,4 +4039,4 @@ export {
4039
4039
  analyzeDomain,
4040
4040
  validateEntities
4041
4041
  };
4042
- //# sourceMappingURL=chunk-32DOFN3T.js.map
4042
+ //# sourceMappingURL=chunk-2WDX6I7T.js.map
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-4MF3HKJA.js";
4
4
  import {
5
5
  WebhookChangeSource
6
- } from "./chunk-YLPAPPLW.js";
6
+ } from "./chunk-TIZXQU26.js";
7
7
 
8
8
  // runtime/subsystems/integration/build-change-source.ts
9
9
  function buildChangeSource(cfg, fetch, middlewares = []) {
@@ -26,4 +26,4 @@ function buildChangeSource(cfg, fetch, middlewares = []) {
26
26
  export {
27
27
  buildChangeSource
28
28
  };
29
- //# sourceMappingURL=chunk-WWGYCIJX.js.map
29
+ //# sourceMappingURL=chunk-43SBT72G.js.map
@@ -1,19 +1,19 @@
1
- import {
2
- MissingTenantIdError
3
- } from "./chunk-T4BIIU5E.js";
4
1
  import {
5
2
  clampLimit,
6
3
  decodeKeysetCursor,
7
4
  encodeKeysetCursor,
8
5
  toJobRunSummary
9
6
  } from "./chunk-L3LZWWSX.js";
7
+ import {
8
+ jobRuns
9
+ } from "./chunk-OKXZ63IA.js";
10
10
  import {
11
11
  JOBS_MULTI_TENANT,
12
12
  JOB_ORCHESTRATOR
13
13
  } from "./chunk-ZPL74UQN.js";
14
14
  import {
15
- jobRuns
16
- } from "./chunk-OKXZ63IA.js";
15
+ MissingTenantIdError
16
+ } from "./chunk-T4BIIU5E.js";
17
17
  import {
18
18
  DRIZZLE
19
19
  } from "./chunk-U64T4YZE.js";
@@ -198,4 +198,4 @@ DrizzleJobRunService = __decorateClass([
198
198
  export {
199
199
  DrizzleJobRunService
200
200
  };
201
- //# sourceMappingURL=chunk-VNBC3VXM.js.map
201
+ //# sourceMappingURL=chunk-5LXOJGO2.js.map
@@ -58,7 +58,7 @@ var PollDetectionSchema = z.object({
58
58
  provenance: z.enum(["poll", "cdc"]).optional()
59
59
  });
60
60
  var WebhookDetectionSchema = z.object({
61
- eventIdField: z.string().min(1)
61
+ eventIdField: z.string().min(1).optional()
62
62
  });
63
63
  var PollModeSchema = z.object({
64
64
  mode: z.literal("poll"),
@@ -87,4 +87,4 @@ export {
87
87
  WebhookDetectionSchema,
88
88
  DetectionConfigSchema
89
89
  };
90
- //# sourceMappingURL=chunk-3NMCDN7L.js.map
90
+ //# sourceMappingURL=chunk-5TK7MEN4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/integration/detection-config.schema.ts"],"sourcesContent":["/**\n * Integration subsystem — DetectionConfig schema (#226-1)\n *\n * Canonical Zod schema for per-entity integration detection config. The schema is\n * the single source of truth for filter/mapping shape and is consumed by:\n *\n * 1. Runtime primitives — `PollChangeSource<T>`, `WebhookChangeSource<T>`\n * (#226-3, #226-4) accept a parsed `DetectionConfig` at construction.\n * 2. Codegen — `src/schema/entity-definition.schema.ts` (#226-6) imports\n * this schema so per-entity YAML `detection:` blocks validate against\n * the same shape the runtime enforces.\n *\n * Locked decisions (see ADR-033 + decision memo Q1–Q6):\n * - Filter vocabulary is flat AND of `{ field, op, value }` triples; richer\n * boolean expressions (OR / NOT / nested) are deferred per epic open Q3.\n * - Cursor strategy is a tagged union over the four shapes the three modes\n * need (`systemModstamp`, `replayId`, `timestamp`, `eventId`). Each\n * strategy types its cursor internally; the orchestrator persists what\n * the iterator last yielded (integration skill rule 2).\n * - `mode: 'poll'` may opt into `provenance: 'cdc'` so Stripe-style event\n * endpoints (mechanically a poll, semantically CDC) reuse the poll\n * primitive while emitting `Change<T>.source = 'cdc'`. Long-lived\n * streaming CDC (SFDC Pub-Sub, Debezium) is a separate primitive\n * deferred to #226-8.\n * - `webhook` mode's `eventIdField` is optional: `WebhookChangeSource<T>`\n * prefers an `eventId` yielded by the queue iterator and falls back to the\n * `eventIdField` record extraction (precedence: yielded eventId >\n * eventIdField extraction > undefined dedupKey).\n */\nimport { z } from \"zod\";\n\n// ============================================================================\n// Field mapping — provider field → canonical target\n// ============================================================================\n\n/**\n * Maps a single provider field onto the canonical record. `transform` is an\n * opt-in tag the adapter callback may inspect (`date-iso`, `decimal-string`,\n * etc.); the schema does not enumerate transforms — adapters interpret them.\n */\nexport const FieldMappingSchema = z.object({\n\tsource: z.string().min(1),\n\ttarget: z.string().min(1),\n\ttransform: z.string().min(1).optional(),\n});\n\nexport type FieldMapping = z.infer<typeof FieldMappingSchema>;\n\n// ============================================================================\n// Resolved filter — flat-AND triple\n// ============================================================================\n\n/**\n * A single resolved filter clause applied at fetch time. `value` is `unknown`\n * to admit primitives, arrays (for `in` / `nin`), and dates as ISO strings —\n * adapters interpret per provider.\n */\nexport const ResolvedFilterSchema = z.object({\n\tfield: z.string().min(1),\n\top: z.enum([\"eq\", \"neq\", \"in\", \"nin\", \"gt\", \"gte\", \"lt\", \"lte\"]),\n\tvalue: z.unknown(),\n});\n\nexport type ResolvedFilter = z.infer<typeof ResolvedFilterSchema>;\n\n// ============================================================================\n// Cursor strategy — tagged union over the four shapes the modes need\n// ============================================================================\n\nconst SystemModstampCursorSchema = z.object({\n\tkind: z.literal(\"systemModstamp\"),\n\tfield: z.string().min(1),\n});\n\nconst ReplayIdCursorSchema = z.object({\n\tkind: z.literal(\"replayId\"),\n\tfield: z.string().min(1),\n});\n\nconst TimestampCursorSchema = z.object({\n\tkind: z.literal(\"timestamp\"),\n\tfield: z.string().min(1),\n});\n\nconst EventIdCursorSchema = z.object({\n\tkind: z.literal(\"eventId\"),\n\tfield: z.string().min(1),\n});\n\n/**\n * Gmail `historyId` (RFC-0003 §3) — an opaque, atomic vendor token. The next\n * watermark only exists at end-of-walk; there is no resumable mid-walk value.\n * `field` is metadata for codegen/adapters (the response key the token lives on).\n */\nconst HistoryIdCursorSchema = z.object({\n\tkind: z.literal(\"historyId\"),\n\tfield: z.string().min(1),\n});\n\n/**\n * Google Calendar `syncToken` (RFC-0003 §3) — an opaque, atomic sync token,\n * same divisibility profile as `historyId`.\n */\nconst SyncTokenCursorSchema = z.object({\n\tkind: z.literal(\"syncToken\"),\n\tfield: z.string().min(1),\n});\n\nexport const CursorStrategySchema = z.discriminatedUnion(\"kind\", [\n\tSystemModstampCursorSchema,\n\tReplayIdCursorSchema,\n\tTimestampCursorSchema,\n\tEventIdCursorSchema,\n\tHistoryIdCursorSchema,\n\tSyncTokenCursorSchema,\n]);\n\nexport type CursorStrategy = z.infer<typeof CursorStrategySchema>;\n\n// ============================================================================\n// Cursor divisibility (RFC-0003 §3)\n// ============================================================================\n\n/**\n * Whether a cursor strategy is *divisible* — a property of the strategy, not\n * the read primitive. Divisible cursors are sortable/monotonic watermarks whose\n * value is meaningful AS OF any single record (HubSpot `systemModstamp`, a\n * `timestamp` field, a Salesforce CDC `replayId`); the read primitive may\n * checkpoint per-ref mid-walk, so a crash resumes from the last delivered ref.\n *\n * Atomic cursors are opaque vendor tokens (Gmail `historyId`, Calendar\n * `syncToken`, a generic `eventId`) whose next value only exists at end-of-walk.\n * The primitive must withhold per-ref cursors and emit the token only at a safe\n * boundary, so an interrupted run never persists an unresumable mid-walk token\n * (it resumes all-or-nothing from the prior token — see `IncrementalReadBase`).\n *\n * `eventId` is classified atomic conservatively: a generic opaque id is treated\n * all-or-nothing unless a concrete strategy proves it monotonically resumable.\n */\nexport const CURSOR_DIVISIBILITY: Readonly<\n\tRecord<CursorStrategy[\"kind\"], boolean>\n> = {\n\tsystemModstamp: true,\n\ttimestamp: true,\n\treplayId: true,\n\teventId: false,\n\thistoryId: false,\n\tsyncToken: false,\n};\n\n/** Predicate form of {@link CURSOR_DIVISIBILITY}. */\nexport function isDivisibleCursor(kind: CursorStrategy[\"kind\"]): boolean {\n\treturn CURSOR_DIVISIBILITY[kind];\n}\n\n// ============================================================================\n// Mode-specific blocks\n// ============================================================================\n\n/**\n * Poll-mode block. `provenance: 'cdc'` opts the poll primitive into stamping\n * `Change<T>.source = 'cdc'` and populating `dedupKey` from the cursor's\n * `field` — used for Stripe-style event endpoints. Defaults to `'poll'`.\n */\nexport const PollDetectionSchema = z.object({\n\tcursor: CursorStrategySchema,\n\tprovenance: z.enum([\"poll\", \"cdc\"]).optional(),\n});\n\nexport type PollDetection = z.infer<typeof PollDetectionSchema>;\n\n/**\n * Webhook-mode block. `eventIdField`, when present, names the field on the\n * emitted canonical record that `WebhookChangeSource<T>` reads to set\n * `Change<T>.dedupKey` — used only as the fallback when the queue iterator\n * does NOT yield an `eventId` alongside the record.\n *\n * `eventIdField` is **optional**: a queue iterator that always yields an\n * `eventId` (vendor delivery metadata, the preferred channel) need not declare\n * a record field for it. dedupKey precedence is: yielded `eventId` >\n * `eventIdField` record extraction > undefined.\n */\nexport const WebhookDetectionSchema = z.object({\n\teventIdField: z.string().min(1).optional(),\n});\n\nexport type WebhookDetection = z.infer<typeof WebhookDetectionSchema>;\n\n// ============================================================================\n// DetectionConfig — top-level discriminated union over `mode`\n// ============================================================================\n\nconst PollModeSchema = z.object({\n\tmode: z.literal(\"poll\"),\n\tpoll: PollDetectionSchema,\n\tmapping: z.array(FieldMappingSchema).min(1),\n\tfilters: z.array(ResolvedFilterSchema).default([]),\n});\n\nconst WebhookModeSchema = z.object({\n\tmode: z.literal(\"webhook\"),\n\twebhook: WebhookDetectionSchema,\n\tmapping: z.array(FieldMappingSchema).min(1),\n\tfilters: z.array(ResolvedFilterSchema).default([]),\n});\n\n/**\n * Top-level detection config. Discriminated on `mode` so the relevant\n * mode-block (poll/webhook) is structurally required for that mode. CDC as a\n * long-lived streaming primitive is deferred (#226-8); CDC-as-provenance\n * (Stripe-style event endpoints) is expressed via `mode: 'poll'` with\n * `poll.provenance: 'cdc'`.\n */\nexport const DetectionConfigSchema = z.discriminatedUnion(\"mode\", [\n\tPollModeSchema,\n\tWebhookModeSchema,\n]);\n\nexport type DetectionConfig = z.infer<typeof DetectionConfigSchema>;\n"],"mappings":";AA6BA,SAAS,SAAS;AAWX,IAAM,qBAAqB,EAAE,OAAO;AAAA,EAC1C,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACxB,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACxB,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AACvC,CAAC;AAaM,IAAM,uBAAuB,EAAE,OAAO;AAAA,EAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACvB,IAAI,EAAE,KAAK,CAAC,MAAM,OAAO,MAAM,OAAO,MAAM,OAAO,MAAM,KAAK,CAAC;AAAA,EAC/D,OAAO,EAAE,QAAQ;AAClB,CAAC;AAQD,IAAM,6BAA6B,EAAE,OAAO;AAAA,EAC3C,MAAM,EAAE,QAAQ,gBAAgB;AAAA,EAChC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AACxB,CAAC;AAED,IAAM,uBAAuB,EAAE,OAAO;AAAA,EACrC,MAAM,EAAE,QAAQ,UAAU;AAAA,EAC1B,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AACxB,CAAC;AAED,IAAM,wBAAwB,EAAE,OAAO;AAAA,EACtC,MAAM,EAAE,QAAQ,WAAW;AAAA,EAC3B,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AACxB,CAAC;AAED,IAAM,sBAAsB,EAAE,OAAO;AAAA,EACpC,MAAM,EAAE,QAAQ,SAAS;AAAA,EACzB,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AACxB,CAAC;AAOD,IAAM,wBAAwB,EAAE,OAAO;AAAA,EACtC,MAAM,EAAE,QAAQ,WAAW;AAAA,EAC3B,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AACxB,CAAC;AAMD,IAAM,wBAAwB,EAAE,OAAO;AAAA,EACtC,MAAM,EAAE,QAAQ,WAAW;AAAA,EAC3B,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AACxB,CAAC;AAEM,IAAM,uBAAuB,EAAE,mBAAmB,QAAQ;AAAA,EAChE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAwBM,IAAM,sBAET;AAAA,EACH,gBAAgB;AAAA,EAChB,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;AAAA,EACT,WAAW;AAAA,EACX,WAAW;AACZ;AAGO,SAAS,kBAAkB,MAAuC;AACxE,SAAO,oBAAoB,IAAI;AAChC;AAWO,IAAM,sBAAsB,EAAE,OAAO;AAAA,EAC3C,QAAQ;AAAA,EACR,YAAY,EAAE,KAAK,CAAC,QAAQ,KAAK,CAAC,EAAE,SAAS;AAC9C,CAAC;AAeM,IAAM,yBAAyB,EAAE,OAAO;AAAA,EAC9C,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAC1C,CAAC;AAQD,IAAM,iBAAiB,EAAE,OAAO;AAAA,EAC/B,MAAM,EAAE,QAAQ,MAAM;AAAA,EACtB,MAAM;AAAA,EACN,SAAS,EAAE,MAAM,kBAAkB,EAAE,IAAI,CAAC;AAAA,EAC1C,SAAS,EAAE,MAAM,oBAAoB,EAAE,QAAQ,CAAC,CAAC;AAClD,CAAC;AAED,IAAM,oBAAoB,EAAE,OAAO;AAAA,EAClC,MAAM,EAAE,QAAQ,SAAS;AAAA,EACzB,SAAS;AAAA,EACT,SAAS,EAAE,MAAM,kBAAkB,EAAE,IAAI,CAAC;AAAA,EAC1C,SAAS,EAAE,MAAM,oBAAoB,EAAE,QAAQ,CAAC,CAAC;AAClD,CAAC;AASM,IAAM,wBAAwB,EAAE,mBAAmB,QAAQ;AAAA,EACjE;AAAA,EACA;AACD,CAAC;","names":[]}
@@ -1,13 +1,9 @@
1
1
  import {
2
2
  JobWorker
3
- } from "./chunk-XDIIVIIK.js";
3
+ } from "./chunk-JM3T27ZW.js";
4
4
  import {
5
5
  JobsDomainModule
6
- } from "./chunk-3RWMQC3K.js";
7
- import {
8
- BootValidationError,
9
- ReservedPoolViolationError
10
- } from "./chunk-T4BIIU5E.js";
6
+ } from "./chunk-DGYTSCKN.js";
11
7
  import {
12
8
  BULLMQ_CONNECTION,
13
9
  BULLMQ_RESOLVED_CONFIG,
@@ -18,14 +14,18 @@ import {
18
14
  allPoolNames,
19
15
  loadPoolConfig
20
16
  } from "./chunk-RHVN6NA7.js";
17
+ import {
18
+ HandlerRegistry
19
+ } from "./chunk-CO6LUM72.js";
21
20
  import {
22
21
  JOB_ORCHESTRATOR,
23
22
  JOB_RUN_SERVICE,
24
23
  JOB_STEP_SERVICE
25
24
  } from "./chunk-ZPL74UQN.js";
26
25
  import {
27
- HandlerRegistry
28
- } from "./chunk-CO6LUM72.js";
26
+ BootValidationError,
27
+ ReservedPoolViolationError
28
+ } from "./chunk-T4BIIU5E.js";
29
29
  import {
30
30
  tokenKey
31
31
  } from "./chunk-GYGNEQSC.js";
@@ -290,4 +290,4 @@ export {
290
290
  JobWorkerOrchestrator,
291
291
  JobWorkerModule
292
292
  };
293
- //# sourceMappingURL=chunk-RUYLXR5F.js.map
293
+ //# sourceMappingURL=chunk-AYC2HEAL.js.map
@@ -9,22 +9,22 @@ import {
9
9
  } from "./chunk-EDKJU5BO.js";
10
10
  import {
11
11
  DrizzleBridgeDeliveryRepo
12
- } from "./chunk-KSTZIULO.js";
12
+ } from "./chunk-K2I6XIK5.js";
13
13
  import {
14
14
  MemoryBridgeDeliveryRepo
15
15
  } from "./chunk-4DOJBQTP.js";
16
16
  import {
17
17
  BridgeOutboxDrainHook
18
- } from "./chunk-EBKVKN75.js";
18
+ } from "./chunk-DLG62MQY.js";
19
19
  import {
20
20
  BridgeDeliveryHandler
21
- } from "./chunk-DRCLNYH7.js";
21
+ } from "./chunk-NXNVTXKG.js";
22
22
  import {
23
23
  BridgeReservedPoolsNotPolledError
24
24
  } from "./chunk-NXXDZ6ZF.js";
25
25
  import {
26
26
  JOB_WORKER_MODULE_OPTIONS
27
- } from "./chunk-RUYLXR5F.js";
27
+ } from "./chunk-AYC2HEAL.js";
28
28
  import {
29
29
  BRIDGE_DELIVERY_REPO,
30
30
  BRIDGE_MODULE_OPTIONS,
@@ -119,4 +119,4 @@ BridgeModule = __decorateClass([
119
119
  export {
120
120
  BridgeModule
121
121
  };
122
- //# sourceMappingURL=chunk-CEWLVVAH.js.map
122
+ //# sourceMappingURL=chunk-CRBVI4GE.js.map
@@ -1,6 +1,3 @@
1
- import {
2
- NoopAnalyticsBackend
3
- } from "./chunk-J37YWU7Y.js";
4
1
  import {
5
2
  CubeAnalyticsBackend
6
3
  } from "./chunk-7B3RYX45.js";
@@ -9,6 +6,9 @@ import {
9
6
  CUBE_API_SECRET,
10
7
  CUBE_API_URL
11
8
  } from "./chunk-6I7ULIN6.js";
9
+ import {
10
+ NoopAnalyticsBackend
11
+ } from "./chunk-J37YWU7Y.js";
12
12
  import {
13
13
  __decorateClass
14
14
  } from "./chunk-2E224ZSN.js";
@@ -50,4 +50,4 @@ AnalyticsModule = __decorateClass([
50
50
  export {
51
51
  AnalyticsModule
52
52
  };
53
- //# sourceMappingURL=chunk-DKKFTHHI.js.map
53
+ //# sourceMappingURL=chunk-CZQUOIDY.js.map
@@ -1,29 +1,29 @@
1
+ import {
2
+ DrizzleJobRunService
3
+ } from "./chunk-5LXOJGO2.js";
4
+ import {
5
+ MemoryJobRunService
6
+ } from "./chunk-QSJ3J4HE.js";
1
7
  import {
2
8
  DrizzleJobStepService
3
9
  } from "./chunk-DV4RV2DC.js";
10
+ import {
11
+ BULLMQ_CONNECTION,
12
+ BULLMQ_RESOLVED_CONFIG,
13
+ resolveBullMqConfig
14
+ } from "./chunk-I6MVCB5A.js";
4
15
  import {
5
16
  DrizzleJobOrchestrator
6
- } from "./chunk-OTR44OH6.js";
17
+ } from "./chunk-IT6FRTEW.js";
7
18
  import {
8
19
  MemoryJobOrchestrator
9
- } from "./chunk-BULPAAD3.js";
20
+ } from "./chunk-T4YJRD22.js";
10
21
  import {
11
22
  MemoryJobStepService
12
23
  } from "./chunk-PNZSGAB2.js";
13
- import {
14
- DrizzleJobRunService
15
- } from "./chunk-VNBC3VXM.js";
16
- import {
17
- MemoryJobRunService
18
- } from "./chunk-BHZP6LOV.js";
19
24
  import {
20
25
  MemoryJobStore
21
26
  } from "./chunk-SNQ3TOWP.js";
22
- import {
23
- BULLMQ_CONNECTION,
24
- BULLMQ_RESOLVED_CONFIG,
25
- resolveBullMqConfig
26
- } from "./chunk-I6MVCB5A.js";
27
27
  import {
28
28
  JOBS_LISTEN_NOTIFY,
29
29
  JOBS_MULTI_TENANT,
@@ -114,4 +114,4 @@ JobsDomainModule = __decorateClass([
114
114
  export {
115
115
  JobsDomainModule
116
116
  };
117
- //# sourceMappingURL=chunk-3RWMQC3K.js.map
117
+ //# sourceMappingURL=chunk-DGYTSCKN.js.map
@@ -1,22 +1,22 @@
1
1
  import {
2
2
  BRIDGE_DELIVERY_JOB_TYPE
3
- } from "./chunk-DRCLNYH7.js";
3
+ } from "./chunk-NXNVTXKG.js";
4
4
  import {
5
5
  bridgeDelivery
6
6
  } from "./chunk-2TVVBC53.js";
7
+ import {
8
+ jobRuns
9
+ } from "./chunk-OKXZ63IA.js";
7
10
  import {
8
11
  JOBS_LISTEN_NOTIFY
9
12
  } from "./chunk-ZPL74UQN.js";
10
13
  import {
11
- jobRuns
12
- } from "./chunk-OKXZ63IA.js";
14
+ BRIDGE_REGISTRY
15
+ } from "./chunk-4LH67P4U.js";
13
16
  import {
14
17
  JOBS_WAKE_CHANNEL,
15
18
  pgNotify
16
19
  } from "./chunk-MYQIQ27N.js";
17
- import {
18
- BRIDGE_REGISTRY
19
- } from "./chunk-4LH67P4U.js";
20
20
  import {
21
21
  __decorateClass,
22
22
  __decorateParam
@@ -151,4 +151,4 @@ BridgeOutboxDrainHook = __decorateClass([
151
151
  export {
152
152
  BridgeOutboxDrainHook
153
153
  };
154
- //# sourceMappingURL=chunk-EBKVKN75.js.map
154
+ //# sourceMappingURL=chunk-DLG62MQY.js.map
@@ -1,11 +1,11 @@
1
+ import {
2
+ BRIDGE_OUTBOX_DRAIN_HOOK
3
+ } from "./chunk-4LH67P4U.js";
1
4
  import {
2
5
  EVENTS_WAKE_CHANNEL,
3
6
  PgNotifyListener,
4
7
  pgNotify
5
8
  } from "./chunk-MYQIQ27N.js";
6
- import {
7
- BRIDGE_OUTBOX_DRAIN_HOOK
8
- } from "./chunk-4LH67P4U.js";
9
9
  import {
10
10
  domainEvents
11
11
  } from "./chunk-OFRRBC7M.js";
@@ -393,4 +393,4 @@ DrizzleEventBus = __decorateClass([
393
393
  export {
394
394
  DrizzleEventBus
395
395
  };
396
- //# sourceMappingURL=chunk-4PFF3ED4.js.map
396
+ //# sourceMappingURL=chunk-H6FO2ZDJ.js.map
@@ -1,3 +1,12 @@
1
+ import {
2
+ jobRuns,
3
+ jobSteps,
4
+ jobs
5
+ } from "./chunk-OKXZ63IA.js";
6
+ import {
7
+ JOBS_LISTEN_NOTIFY,
8
+ JOBS_MULTI_TENANT
9
+ } from "./chunk-ZPL74UQN.js";
1
10
  import {
2
11
  JobCollisionError,
3
12
  JobNotReplayableError,
@@ -5,15 +14,6 @@ import {
5
14
  JobTypeNotFoundError,
6
15
  MissingTenantIdError
7
16
  } from "./chunk-T4BIIU5E.js";
8
- import {
9
- JOBS_LISTEN_NOTIFY,
10
- JOBS_MULTI_TENANT
11
- } from "./chunk-ZPL74UQN.js";
12
- import {
13
- jobRuns,
14
- jobSteps,
15
- jobs
16
- } from "./chunk-OKXZ63IA.js";
17
17
  import {
18
18
  JOBS_WAKE_CHANNEL,
19
19
  pgNotify
@@ -372,4 +372,4 @@ export {
372
372
  evaluateKeyTemplate,
373
373
  DrizzleJobOrchestrator
374
374
  };
375
- //# sourceMappingURL=chunk-OTR44OH6.js.map
375
+ //# sourceMappingURL=chunk-IT6FRTEW.js.map
@@ -1,14 +1,14 @@
1
1
  import {
2
- JOB_ORCHESTRATOR,
3
- JOB_RUN_SERVICE,
4
- JOB_STEP_SERVICE
5
- } from "./chunk-ZPL74UQN.js";
2
+ jobRuns
3
+ } from "./chunk-OKXZ63IA.js";
6
4
  import {
7
5
  JOB_HANDLER_REGISTRY
8
6
  } from "./chunk-CO6LUM72.js";
9
7
  import {
10
- jobRuns
11
- } from "./chunk-OKXZ63IA.js";
8
+ JOB_ORCHESTRATOR,
9
+ JOB_RUN_SERVICE,
10
+ JOB_STEP_SERVICE
11
+ } from "./chunk-ZPL74UQN.js";
12
12
  import {
13
13
  JOBS_WAKE_CHANNEL,
14
14
  PgNotifyListener
@@ -518,4 +518,4 @@ export {
518
518
  buildStaleSweepQuery,
519
519
  JobWorker
520
520
  };
521
- //# sourceMappingURL=chunk-XDIIVIIK.js.map
521
+ //# sourceMappingURL=chunk-JM3T27ZW.js.map
@@ -1,9 +1,9 @@
1
- import {
2
- bridgeDelivery
3
- } from "./chunk-2TVVBC53.js";
4
1
  import {
5
2
  assertTenantId
6
3
  } from "./chunk-6DWFJNIK.js";
4
+ import {
5
+ bridgeDelivery
6
+ } from "./chunk-2TVVBC53.js";
7
7
  import {
8
8
  BRIDGE_MULTI_TENANT
9
9
  } from "./chunk-4LH67P4U.js";
@@ -119,4 +119,4 @@ DrizzleBridgeDeliveryRepo = __decorateClass([
119
119
  export {
120
120
  DrizzleBridgeDeliveryRepo
121
121
  };
122
- //# sourceMappingURL=chunk-KSTZIULO.js.map
122
+ //# sourceMappingURL=chunk-K2I6XIK5.js.map
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  assertTenantId
3
3
  } from "./chunk-6DWFJNIK.js";
4
- import {
5
- JOB_ORCHESTRATOR
6
- } from "./chunk-ZPL74UQN.js";
7
4
  import {
8
5
  JobHandler,
9
6
  JobHandlerBase
10
7
  } from "./chunk-CO6LUM72.js";
8
+ import {
9
+ JOB_ORCHESTRATOR
10
+ } from "./chunk-ZPL74UQN.js";
11
11
  import {
12
12
  BRIDGE_DELIVERY_REPO,
13
13
  BRIDGE_MULTI_TENANT,
@@ -118,4 +118,4 @@ export {
118
118
  BRIDGE_DELIVERY_JOB_TYPE,
119
119
  BridgeDeliveryHandler
120
120
  };
121
- //# sourceMappingURL=chunk-DRCLNYH7.js.map
121
+ //# sourceMappingURL=chunk-NXNVTXKG.js.map
@@ -1,19 +1,19 @@
1
- import {
2
- MemoryJobStore
3
- } from "./chunk-SNQ3TOWP.js";
4
- import {
5
- MissingTenantIdError
6
- } from "./chunk-T4BIIU5E.js";
7
1
  import {
8
2
  clampLimit,
9
3
  decodeKeysetCursor,
10
4
  encodeKeysetCursor,
11
5
  toJobRunSummary
12
6
  } from "./chunk-L3LZWWSX.js";
7
+ import {
8
+ MemoryJobStore
9
+ } from "./chunk-SNQ3TOWP.js";
13
10
  import {
14
11
  JOBS_MULTI_TENANT,
15
12
  JOB_ORCHESTRATOR
16
13
  } from "./chunk-ZPL74UQN.js";
14
+ import {
15
+ MissingTenantIdError
16
+ } from "./chunk-T4BIIU5E.js";
17
17
  import {
18
18
  __decorateClass,
19
19
  __decorateParam
@@ -209,4 +209,4 @@ function compareBy(a, b, order) {
209
209
  export {
210
210
  MemoryJobRunService
211
211
  };
212
- //# sourceMappingURL=chunk-BHZP6LOV.js.map
212
+ //# sourceMappingURL=chunk-QSJ3J4HE.js.map
@@ -1,12 +1,12 @@
1
+ import {
2
+ MemoryStorageBackend
3
+ } from "./chunk-3SZFUTXE.js";
1
4
  import {
2
5
  STORAGE
3
6
  } from "./chunk-NYBCQZC7.js";
4
7
  import {
5
8
  LocalStorageBackend
6
9
  } from "./chunk-JWNHNUYL.js";
7
- import {
8
- MemoryStorageBackend
9
- } from "./chunk-3SZFUTXE.js";
10
10
  import {
11
11
  __decorateClass
12
12
  } from "./chunk-2E224ZSN.js";
@@ -37,4 +37,4 @@ StorageModule = __decorateClass([
37
37
  export {
38
38
  StorageModule
39
39
  };
40
- //# sourceMappingURL=chunk-BK5ICA2F.js.map
40
+ //# sourceMappingURL=chunk-RUSUZZAF.js.map
@@ -4,6 +4,9 @@ import {
4
4
  import {
5
5
  MemoryJobStore
6
6
  } from "./chunk-SNQ3TOWP.js";
7
+ import {
8
+ JOBS_MULTI_TENANT
9
+ } from "./chunk-ZPL74UQN.js";
7
10
  import {
8
11
  JobCollisionError,
9
12
  JobNotReplayableError,
@@ -11,9 +14,6 @@ import {
11
14
  JobTypeNotFoundError,
12
15
  MissingTenantIdError
13
16
  } from "./chunk-T4BIIU5E.js";
14
- import {
15
- JOBS_MULTI_TENANT
16
- } from "./chunk-ZPL74UQN.js";
17
17
  import {
18
18
  __decorateClass,
19
19
  __decorateParam
@@ -632,4 +632,4 @@ function serialiseError(err, attempt, retryable) {
632
632
  export {
633
633
  MemoryJobOrchestrator
634
634
  };
635
- //# sourceMappingURL=chunk-BULPAAD3.js.map
635
+ //# sourceMappingURL=chunk-T4YJRD22.js.map
@@ -3,6 +3,11 @@ var WebhookChangeSource = class {
3
3
  label;
4
4
  queue;
5
5
  externalIdSourceField;
6
+ /**
7
+ * Record field carrying the event id, when `webhook.eventIdField` is
8
+ * configured. Used only as the fallback when the queue iterator does NOT
9
+ * yield an `eventId` — see {@link WebhookFetchCallback} for the precedence.
10
+ */
6
11
  eventIdSourceField;
7
12
  composed;
8
13
  constructor(opts) {
@@ -39,19 +44,18 @@ var WebhookChangeSource = class {
39
44
  subscription,
40
45
  cursor
41
46
  };
42
- for await (const { record, cursor: nextCursor } of this.queue(ctx)) {
47
+ for await (const {
48
+ record,
49
+ eventId: yieldedEventId,
50
+ cursor: nextCursor
51
+ } of this.queue(ctx)) {
43
52
  const externalIdRaw = record[this.externalIdSourceField];
44
53
  if (typeof externalIdRaw !== "string" || externalIdRaw.length === 0) {
45
54
  throw new Error(
46
55
  `WebhookChangeSource: record missing string '${this.externalIdSourceField}' \u2014 emitted records MUST carry the canonical external id keyed by the mapping source`
47
56
  );
48
57
  }
49
- const eventIdRaw = record[this.eventIdSourceField];
50
- if (typeof eventIdRaw !== "string" || eventIdRaw.length === 0) {
51
- throw new Error(
52
- `WebhookChangeSource: record missing string '${this.eventIdSourceField}' \u2014 webhook records MUST carry the event id (DetectionConfig.webhook.eventIdField) so Change<T>.dedupKey can be populated`
53
- );
54
- }
58
+ const dedupKey = this.deriveDedupKey(yieldedEventId, record);
55
59
  const change = {
56
60
  externalId: externalIdRaw,
57
61
  // Webhook mode cannot distinguish create vs. update vs. delete on
@@ -62,14 +66,37 @@ var WebhookChangeSource = class {
62
66
  record,
63
67
  cursor: nextCursor ?? null,
64
68
  source: "webhook",
65
- dedupKey: eventIdRaw
69
+ dedupKey
66
70
  };
67
71
  yield change;
68
72
  }
69
73
  }
74
+ /**
75
+ * Resolve `Change<T>.dedupKey` with the precedence: yielded `eventId` >
76
+ * `webhook.eventIdField` record extraction > `undefined`. A non-empty
77
+ * yielded `eventId` always wins; otherwise the configured field is read off
78
+ * the record (and must be a non-empty string when the field is configured);
79
+ * with neither, `dedupKey` is `undefined` (the orchestrator then has no
80
+ * delivery-level dedup signal for this change).
81
+ */
82
+ deriveDedupKey(yieldedEventId, record) {
83
+ if (yieldedEventId !== void 0 && yieldedEventId.length > 0) {
84
+ return yieldedEventId;
85
+ }
86
+ if (this.eventIdSourceField === void 0) {
87
+ return void 0;
88
+ }
89
+ const eventIdRaw = record[this.eventIdSourceField];
90
+ if (typeof eventIdRaw !== "string" || eventIdRaw.length === 0) {
91
+ throw new Error(
92
+ `WebhookChangeSource: record missing string '${this.eventIdSourceField}' \u2014 a webhook record MUST carry the event id (DetectionConfig.webhook.eventIdField) so Change<T>.dedupKey can be populated, unless the queue iterator yields an 'eventId' alongside the record`
93
+ );
94
+ }
95
+ return eventIdRaw;
96
+ }
70
97
  };
71
98
 
72
99
  export {
73
100
  WebhookChangeSource
74
101
  };
75
- //# sourceMappingURL=chunk-YLPAPPLW.js.map
102
+ //# sourceMappingURL=chunk-TIZXQU26.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/integration/webhook-change-source.ts"],"sourcesContent":["/**\n * Integration subsystem — `WebhookChangeSource<T>` primitive (#226-4, ADR-033).\n *\n * Generic webhook-mode `IChangeSource<T>` implementation parameterized by a\n * parsed `DetectionConfig` (webhook mode) and a consumer-supplied\n * `WebhookFetchCallback<T>` that iterates a consumer-owned inbound staging\n * queue. The primitive owns:\n *\n * - canonical `Change<T>.source = 'webhook'` stamping;\n * - `dedupKey` derivation, preferring the `eventId` yielded alongside the\n * record by the queue iterator, and falling back to the configured\n * `webhook.eventIdField` on the emitted record when no `eventId` is yielded\n * (precedence: yielded `eventId` > `eventIdField` record extraction >\n * undefined `dedupKey`);\n * - `externalId` derivation: the mapping entry whose `target === 'external_id'`\n * names — via its `source` — the field on the emitted record that carries\n * the canonical external id (mirrors `PollChangeSource`);\n * - middleware-chain composition via the locked `ChangeMiddleware<T>` shape\n * (#226-1) — same composition seam as the poll primitive.\n *\n * The primitive is **passive**: it iterates whatever the consumer-owned\n * queue yields. It does NOT synchronously drive the orchestrator, does NOT\n * own a transport, and does NOT manage acks. The inbound staging table\n * schema is consumer-owned and deferred per ADR-0002 §Phase 4 — the\n * `WebhookFetchCallback<T>` is the queue contract the consumer injects.\n *\n * Shape locks (decision memo Q5, mirrored from poll primitive):\n * - `WebhookFetchContext = { subscription, cursor }` — explicitly NO\n * `userId` / `tenantId`. Run-scope identity is closed over by the\n * consumer at queue construction or resolved inside the callback via\n * consumer services. There are no `filters` on the webhook context —\n * filtering is done at registration / on the staging row, not at the\n * port seam (the queue is already filtered by the time the primitive\n * iterates).\n *\n * Long-lived streaming CDC primitives (SFDC Pub-Sub gRPC, Debezium/Kafka,\n * Postgres logical replication) are deferred to `#226-8` — they need a\n * fundamentally different lifecycle (`subscribe(onChange, onError)`,\n * server-paced backpressure, ack-on-yield) and shouldn't be retrofitted\n * into either this primitive or the poll primitive.\n */\n\nimport type { DetectionConfig } from \"./detection-config.schema\";\nimport type {\n\tChange,\n\tIChangeSource,\n\tIntegrationSubscriptionView,\n} from \"./integration-change-source.protocol\";\nimport type {\n\tChangeIterator,\n\tChangeMiddleware,\n} from \"./integration-middleware.protocol\";\n\n// ============================================================================\n// Cursor + queue callback shapes\n// ============================================================================\n\n/**\n * Opaque webhook cursor shape. Webhook mode typically has a cursor of\n * `{ ts: ISO-string }` (last drained staging-row timestamp) but the\n * primitive treats it as opaque. Consumer-owned queue iterators interpret\n * it however the staging schema needs.\n */\nexport type WebhookCursor = unknown;\n\n/**\n * Context the primitive forwards to the queue iterator. Locked to exactly\n * two fields per the same Q5 reasoning that locks `PollFetchContext` — no\n * `userId` / `tenantId`.\n */\nexport interface WebhookFetchContext {\n\treadonly subscription: IntegrationSubscriptionView;\n\treadonly cursor: WebhookCursor | null;\n}\n\n/**\n * Consumer-supplied queue iterator. Returns an async iterable of\n * `{ record, eventId?, cursor? }` tuples — the consumer drains the inbound\n * staging queue and emits already-mapped canonical records `T`. The primitive\n * stamps `source: 'webhook'` and derives `dedupKey` with this precedence:\n *\n * 1. the yielded `eventId` (vendor delivery metadata — the queue is the\n * right channel for it: a vendor's event id should never need a field\n * on the vendor-neutral canonical record);\n * 2. else the record field named by `webhook.eventIdField`, when configured;\n * 3. else `undefined`.\n *\n * Yielding `eventId` is the safe channel when one canonical record identity\n * (the `external_id`) can recur across distinct vendor events in a single\n * drain batch — e.g. a message create and its later edit share an\n * `external_id` but are different events. Reading dedup identity off the\n * record (`eventIdField`) collapses those into one `dedupKey`; the yielded\n * `eventId` keeps them distinct. The consumer is the one who decided when a\n * staging row is \"ready\" to drain.\n *\n * Webhook mode has no per-record cursor advance — the staging-row drain\n * order is consumer policy (FIFO by ingestion timestamp, by event id, etc.)\n * and is opaque to the primitive. The orchestrator's last-yielded cursor\n * is whatever the consumer chooses to surface, if anything.\n */\nexport type WebhookFetchCallback<T> = (\n\tctx: WebhookFetchContext,\n) => AsyncIterable<{ record: T; eventId?: string; cursor?: WebhookCursor }>;\n\n// ============================================================================\n// Constructor options\n// ============================================================================\n\nexport interface WebhookChangeSourceOptions<T> {\n\t/** Consumer-supplied inbound queue iterator. */\n\treadonly queue: WebhookFetchCallback<T>;\n\t/**\n\t * Parsed detection config. MUST be `mode: 'webhook'`; the constructor\n\t * throws if a poll config is supplied. Codegen-emitted factories call\n\t * `DetectionConfigSchema.parse(...)` upstream so this is a safety net,\n\t * not the primary validation point.\n\t */\n\treadonly config: DetectionConfig;\n\t/**\n\t * Optional middleware chain. Same shape and composition rules as\n\t * `PollChangeSource` — first element is the outermost layer.\n\t */\n\treadonly middlewares?: ReadonlyArray<ChangeMiddleware<T>>;\n\t/**\n\t * Optional human label for run logs (e.g. `'stripe-webhook-charge'`).\n\t * Defaults to a derived label based on the mapping at construction.\n\t */\n\treadonly label?: string;\n}\n\n// ============================================================================\n// WebhookChangeSource<T>\n// ============================================================================\n\nexport class WebhookChangeSource<T> implements IChangeSource<T> {\n\tpublic readonly label: string;\n\n\tprivate readonly queue: WebhookFetchCallback<T>;\n\tprivate readonly externalIdSourceField: string;\n\t/**\n\t * Record field carrying the event id, when `webhook.eventIdField` is\n\t * configured. Used only as the fallback when the queue iterator does NOT\n\t * yield an `eventId` — see {@link WebhookFetchCallback} for the precedence.\n\t */\n\tprivate readonly eventIdSourceField: string | undefined;\n\tprivate readonly composed: ChangeIterator<T>;\n\n\tconstructor(opts: WebhookChangeSourceOptions<T>) {\n\t\tif (opts.config.mode !== \"webhook\") {\n\t\t\tthrow new Error(\n\t\t\t\t`WebhookChangeSource requires DetectionConfig.mode === 'webhook'; got '${(opts.config as { mode: string }).mode}'`,\n\t\t\t);\n\t\t}\n\t\tconst config = opts.config;\n\n\t\t// Field mapping: locate the entry whose canonical `target` is `external_id`\n\t\t// — mirrors the poll primitive's contract. Adapters emit records\n\t\t// already-mapped; the primitive needs to know which key on T carries the\n\t\t// external id so it can stamp `Change.externalId`. That key is the\n\t\t// mapping's `source` (the field on the emitted record), NOT its `target`\n\t\t// (the canonical column) — they differ whenever the canonical record is\n\t\t// vendor-neutral camelCase (e.g. `source: 'externalId'` → `target: 'external_id'`).\n\t\tconst externalIdMapping = config.mapping.find(\n\t\t\t(m) => m.target === \"external_id\",\n\t\t);\n\t\tif (!externalIdMapping) {\n\t\t\tthrow new Error(\n\t\t\t\t\"WebhookChangeSource: DetectionConfig.mapping must include an entry with target 'external_id' so emitted Change<T>.externalId can be populated\",\n\t\t\t);\n\t\t}\n\t\tthis.externalIdSourceField = externalIdMapping.source;\n\t\tthis.eventIdSourceField = config.webhook.eventIdField;\n\t\t// `eventIdField` is optional (a callback that always yields `eventId` need\n\t\t// not declare one); `undefined` here just disables the fallback extraction.\n\n\t\tthis.queue = opts.queue;\n\n\t\tthis.label =\n\t\t\topts.label ?? `webhook-change-source:${externalIdMapping.source}`;\n\n\t\t// Compose middleware chain — same shape as PollChangeSource.\n\t\tconst inner: ChangeIterator<T> = (sub, cur) => this.fetch(sub, cur);\n\t\tconst middlewares = opts.middlewares ?? [];\n\t\tthis.composed = middlewares.reduceRight<ChangeIterator<T>>(\n\t\t\t(next, mw) => mw(next),\n\t\t\tinner,\n\t\t);\n\t}\n\n\tlistChanges(\n\t\tsubscription: IntegrationSubscriptionView,\n\t\tcursor: unknown | null,\n\t): AsyncIterable<Change<T>> {\n\t\treturn this.composed(subscription, cursor);\n\t}\n\n\tprivate async *fetch(\n\t\tsubscription: IntegrationSubscriptionView,\n\t\tcursor: unknown | null,\n\t): AsyncIterable<Change<T>> {\n\t\tconst ctx: WebhookFetchContext = {\n\t\t\tsubscription,\n\t\t\tcursor: cursor as WebhookCursor | null,\n\t\t};\n\n\t\tfor await (const {\n\t\t\trecord,\n\t\t\teventId: yieldedEventId,\n\t\t\tcursor: nextCursor,\n\t\t} of this.queue(ctx)) {\n\t\t\tconst externalIdRaw = (record as Record<string, unknown>)[\n\t\t\t\tthis.externalIdSourceField\n\t\t\t];\n\t\t\tif (typeof externalIdRaw !== \"string\" || externalIdRaw.length === 0) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`WebhookChangeSource: record missing string '${this.externalIdSourceField}' — emitted records MUST carry the canonical external id keyed by the mapping source`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// dedupKey precedence: yielded `eventId` > `eventIdField` record\n\t\t\t// extraction > undefined. The yielded id is vendor delivery metadata\n\t\t\t// (the right channel for it), and keeps distinct vendor events for the\n\t\t\t// same `external_id` (e.g. a message and its edit) from collapsing to\n\t\t\t// one dedupKey — which a record-field extraction would do.\n\t\t\tconst dedupKey = this.deriveDedupKey(yieldedEventId, record);\n\n\t\t\tconst change: Change<T> = {\n\t\t\t\texternalId: externalIdRaw,\n\t\t\t\t// Webhook mode cannot distinguish create vs. update vs. delete on\n\t\t\t\t// its own — the orchestrator's diff stage handles classification.\n\t\t\t\t// Tombstone / soft-delete detection is consumer-driven (same as\n\t\t\t\t// poll mode — see ADR-033).\n\t\t\t\toperation: \"updated\",\n\t\t\t\trecord,\n\t\t\t\tcursor: nextCursor ?? null,\n\t\t\t\tsource: \"webhook\",\n\t\t\t\tdedupKey,\n\t\t\t};\n\t\t\tyield change;\n\t\t}\n\t}\n\n\t/**\n\t * Resolve `Change<T>.dedupKey` with the precedence: yielded `eventId` >\n\t * `webhook.eventIdField` record extraction > `undefined`. A non-empty\n\t * yielded `eventId` always wins; otherwise the configured field is read off\n\t * the record (and must be a non-empty string when the field is configured);\n\t * with neither, `dedupKey` is `undefined` (the orchestrator then has no\n\t * delivery-level dedup signal for this change).\n\t */\n\tprivate deriveDedupKey(\n\t\tyieldedEventId: string | undefined,\n\t\trecord: T,\n\t): string | undefined {\n\t\tif (yieldedEventId !== undefined && yieldedEventId.length > 0) {\n\t\t\treturn yieldedEventId;\n\t\t}\n\t\tif (this.eventIdSourceField === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst eventIdRaw = (record as Record<string, unknown>)[\n\t\t\tthis.eventIdSourceField\n\t\t];\n\t\tif (typeof eventIdRaw !== \"string\" || eventIdRaw.length === 0) {\n\t\t\tthrow new Error(\n\t\t\t\t`WebhookChangeSource: record missing string '${this.eventIdSourceField}' — a webhook record MUST carry the event id (DetectionConfig.webhook.eventIdField) so Change<T>.dedupKey can be populated, unless the queue iterator yields an 'eventId' alongside the record`,\n\t\t\t);\n\t\t}\n\t\treturn eventIdRaw;\n\t}\n}\n"],"mappings":";AAsIO,IAAM,sBAAN,MAAyD;AAAA,EAC/C;AAAA,EAEC;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA,EACA;AAAA,EAEjB,YAAY,MAAqC;AAChD,QAAI,KAAK,OAAO,SAAS,WAAW;AACnC,YAAM,IAAI;AAAA,QACT,yEAA0E,KAAK,OAA4B,IAAI;AAAA,MAChH;AAAA,IACD;AACA,UAAM,SAAS,KAAK;AASpB,UAAM,oBAAoB,OAAO,QAAQ;AAAA,MACxC,CAAC,MAAM,EAAE,WAAW;AAAA,IACrB;AACA,QAAI,CAAC,mBAAmB;AACvB,YAAM,IAAI;AAAA,QACT;AAAA,MACD;AAAA,IACD;AACA,SAAK,wBAAwB,kBAAkB;AAC/C,SAAK,qBAAqB,OAAO,QAAQ;AAIzC,SAAK,QAAQ,KAAK;AAElB,SAAK,QACJ,KAAK,SAAS,yBAAyB,kBAAkB,MAAM;AAGhE,UAAM,QAA2B,CAAC,KAAK,QAAQ,KAAK,MAAM,KAAK,GAAG;AAClE,UAAM,cAAc,KAAK,eAAe,CAAC;AACzC,SAAK,WAAW,YAAY;AAAA,MAC3B,CAAC,MAAM,OAAO,GAAG,IAAI;AAAA,MACrB;AAAA,IACD;AAAA,EACD;AAAA,EAEA,YACC,cACA,QAC2B;AAC3B,WAAO,KAAK,SAAS,cAAc,MAAM;AAAA,EAC1C;AAAA,EAEA,OAAe,MACd,cACA,QAC2B;AAC3B,UAAM,MAA2B;AAAA,MAChC;AAAA,MACA;AAAA,IACD;AAEA,qBAAiB;AAAA,MAChB;AAAA,MACA,SAAS;AAAA,MACT,QAAQ;AAAA,IACT,KAAK,KAAK,MAAM,GAAG,GAAG;AACrB,YAAM,gBAAiB,OACtB,KAAK,qBACN;AACA,UAAI,OAAO,kBAAkB,YAAY,cAAc,WAAW,GAAG;AACpE,cAAM,IAAI;AAAA,UACT,+CAA+C,KAAK,qBAAqB;AAAA,QAC1E;AAAA,MACD;AAOA,YAAM,WAAW,KAAK,eAAe,gBAAgB,MAAM;AAE3D,YAAM,SAAoB;AAAA,QACzB,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,QAKZ,WAAW;AAAA,QACX;AAAA,QACA,QAAQ,cAAc;AAAA,QACtB,QAAQ;AAAA,QACR;AAAA,MACD;AACA,YAAM;AAAA,IACP;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,eACP,gBACA,QACqB;AACrB,QAAI,mBAAmB,UAAa,eAAe,SAAS,GAAG;AAC9D,aAAO;AAAA,IACR;AACA,QAAI,KAAK,uBAAuB,QAAW;AAC1C,aAAO;AAAA,IACR;AACA,UAAM,aAAc,OACnB,KAAK,kBACN;AACA,QAAI,OAAO,eAAe,YAAY,WAAW,WAAW,GAAG;AAC9D,YAAM,IAAI;AAAA,QACT,+CAA+C,KAAK,kBAAkB;AAAA,MACvE;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACD;","names":[]}