diff --git a/.changeset/27448-connector-webhooks-refusal-prose.md b/.changeset/27448-connector-webhooks-refusal-prose.md new file mode 100644 index 00000000000..9b67babf83d --- /dev/null +++ b/.changeset/27448-connector-webhooks-refusal-prose.md @@ -0,0 +1,14 @@ +--- +'@objectstack/spec': patch +--- + +fix(spec): the retired `connector.webhooks` refusal describes declared webhooks as they work now: read from the metadata registry, with no `sys_webhook` row + +Clause-②: no +Metadata-protocol: none + +An author who writes `webhooks` on a connector is refused, and the refusal tells them to declare the webhook in the stack's top-level `webhooks:` collection. It used to add that such a webhook "is materialized into `sys_webhook`". That is no longer how the platform works (ADR-0131 D2): a declared webhook is read from the metadata registry and delivered on record events, and no `sys_webhook` row is written for it. `sys_webhook` holds only the webhooks an organization authors at runtime. An author following the old text looked for a row that never appears. + +The refusal now says this. The same correction is made in the protocol-18 upgrade note for the connector resilience family (its replacement and reason) and in the generated connector reference. + +Prose only. The refusal itself, its error code, its path and what the connector schema accepts are unchanged. diff --git a/content/docs/references/integration/connector.mdx b/content/docs/references/integration/connector.mdx index 29ada7d1831..37f86ab2c86 100644 --- a/content/docs/references/integration/connector.mdx +++ b/content/docs/references/integration/connector.mdx @@ -193,7 +193,7 @@ const result = ConnectorSchema.parse(data); | **triggers** | `never` | optional | [REMOVED] `connector.triggers` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a connector trigger never started anything: `AutomationEngine.registerConnector` registers a connector's actions only, no polling loop read `intervalSeconds` (or the `interval` spelling it was renamed from), and no receiver was driven by a `webhook` trigger. Delete the key; the `ConnectorTrigger` shape leaves with it. To start work from an external system, write a flow that calls the connector's action in a `connector_action` node: for an external event, an `api` flow that the event's sender calls; for a scheduled pull, a `schedule` flow. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. Its pull runs when a `job` drives it — a `job` whose `pull: { mapping }` names that mapping; the binding alone moves no rows. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **fieldMappings** | `never` | optional | [REMOVED] `connector.fieldMappings` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever moved a value through a connector field mapping: nothing read `source`, `target`, `defaultValue`, `dataType`, `required` or `syncMode`. Delete the key; the `ConnectorFieldMapping` shape leaves with it. Map fields on the sync's TARGET instead: a `mapping`'s `fieldMapping` (`source` → `target`, with a `transform` the import path executes), which its `connectorSource` pulls through. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | -| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so it was never materialized into `sys_webhook` and never delivered, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on record events — note that doing so STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so the webhook dispatcher never read it and never delivered it, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection. The platform reads a declared webhook from the metadata registry and delivers it on record events; it writes no `sys_webhook` row for it (`sys_webhook` holds only the webhooks an organization authors at runtime). Note that declaring it STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. This edit left `os migrate meta` with the protocol-16.x conversions: apply it by hand, or run the tool on a release that still carries the conversion — https://objectstack.ai/docs/protocol-upgrade. | | **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration | | **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | @@ -490,7 +490,7 @@ Connector type | **triggers** | `never` | optional | [REMOVED] `connector.triggers` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a connector trigger never started anything: `AutomationEngine.registerConnector` registers a connector's actions only, no polling loop read `intervalSeconds` (or the `interval` spelling it was renamed from), and no receiver was driven by a `webhook` trigger. Delete the key; the `ConnectorTrigger` shape leaves with it. To start work from an external system, write a flow that calls the connector's action in a `connector_action` node: for an external event, an `api` flow that the event's sender calls; for a scheduled pull, a `schedule` flow. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. Its pull runs when a `job` drives it — a `job` whose `pull: { mapping }` names that mapping; the binding alone moves no rows. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **fieldMappings** | `never` | optional | [REMOVED] `connector.fieldMappings` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever moved a value through a connector field mapping: nothing read `source`, `target`, `defaultValue`, `dataType`, `required` or `syncMode`. Delete the key; the `ConnectorFieldMapping` shape leaves with it. Map fields on the sync's TARGET instead: a `mapping`'s `fieldMapping` (`source` → `target`, with a `transform` the import path executes), which its `connectorSource` pulls through. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | -| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so it was never materialized into `sys_webhook` and never delivered, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on record events — note that doing so STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so the webhook dispatcher never read it and never delivered it, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection. The platform reads a declared webhook from the metadata registry and delivers it on record events; it writes no `sys_webhook` row for it (`sys_webhook` holds only the webhooks an organization authors at runtime). Note that declaring it STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. This edit left `os migrate meta` with the protocol-16.x conversions: apply it by hand, or run the tool on a release that still carries the conversion — https://objectstack.ai/docs/protocol-upgrade. | | **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration | | **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | diff --git a/packages/spec/scripts/liveness/proof-registry.mts b/packages/spec/scripts/liveness/proof-registry.mts index 4eb04dee8ef..4f205badecd 100644 --- a/packages/spec/scripts/liveness/proof-registry.mts +++ b/packages/spec/scripts/liveness/proof-registry.mts @@ -164,14 +164,14 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ id: 'webhook-materialization', label: 'Webhook materialization', summary: - 'a stack/connector-authored webhook is materialized into a dispatchable sys_webhook row on boot (#3461/#3489) — the authored value crosses manifest-decomposition → the ObjectQL registry (type `webhook`) → the bridge → engine.insert, and the bridge was first gated BEHIND the realtime/messaging dispatch guard, silently materializing nothing on a realtime-less boot despite green unit tests.', + 'a webhook declared in a stack\'s top-level `webhooks:` collection is dispatched from the metadata registry and writes no sys_webhook row (ADR-0131 D2; the boot seeder of #3461/#3489 is retired) — the authored value crosses manifest-decomposition → the ObjectQL registry (type `webhook`) → the `declaredWebhooks` reader the plugin binds on the data engine alone → the dispatcher, which delivers a matching record event with `object`→`object_name` applied. The proof boots WITHOUT realtime because the first bridge was gated BEHIND the realtime/messaging dispatch guard and silently bound nothing on a realtime-less boot despite green unit tests.', proofId: 'webhook-materialization', proofRef: 'packages/qa/dogfood/test/webhook-materialization.dogfood.test.ts#webhook-materialization', bound: true, // spec `object` → runtime `object_name` is the representative remap for the // whole authoring→dispatcher pipeline: its `live` status is only true - // because the materializer runs (engine-only, independent of realtime), and - // the proof boots WITHOUT realtime to pin exactly that. + // because the declared-webhook reader is bound (engine-only, independent of + // realtime), and the proof boots WITHOUT realtime to pin exactly that. ledgerBindings: [{ type: 'webhook', path: 'object' }], }, { diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index c0e24cc323b..86792adcbdd 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -10294,8 +10294,8 @@ const dashboardRefreshIntervalToRefreshIntervalSeconds: MetadataConversion = { * had no engine — nothing polled, counted consecutive failures or tripped a * breaker; `status` was read by nothing (the runtime publishes a COMPUTED * `state`, and participation is `enabled`); and a webhook nested in a - * connector was never registered as a `webhook` item, so it was never - * materialized into `sys_webhook` or delivered. + * connector was never registered as a `webhook` item, so the webhook + * dispatcher never read it or delivered it. * * A pure lossless delete, one notice per stripped key: none of the three ever * had an effect to preserve. In particular the nested `webhooks` are STRIPPED, diff --git a/packages/spec/src/integration/connector-resilience-keys-retirement.test.ts b/packages/spec/src/integration/connector-resilience-keys-retirement.test.ts index 75b4db70535..1a93d03f49e 100644 --- a/packages/spec/src/integration/connector-resilience-keys-retirement.test.ts +++ b/packages/spec/src/integration/connector-resilience-keys-retirement.test.ts @@ -116,6 +116,23 @@ describe('connector resilience family retirement — the tombstones', () => { }); } + it('the `webhooks` prescription teaches the declared-webhook model: read from the registry, no `sys_webhook` row', () => { + // ADR-0131 D2: a declared top-level webhook is code. The dispatcher reads it + // from the metadata registry and no `sys_webhook` row is written for it, so + // a prescription that says it "is materialized into `sys_webhook`" sends the + // author looking for a row that never appears. + const issue = issueAt(ConnectorSchema.safeParse({ ...WELL_FORMED, webhooks: AUTHORED.webhooks }), 'webhooks'); + expect(issue!.message).toContain('reads a declared webhook from the metadata registry'); + expect(issue!.message).toContain('it writes no `sys_webhook` row for it'); + expect(issue!.message).not.toMatch(/materiali[sz]ed into `sys_webhook`/); + expect(issue!.message).not.toMatch(/materiali[sz]/); + // The D3 entry an upgrading author reads says the same. + const entry = MIGRATIONS_BY_MAJOR[18]!.semantic.find((s) => s.id === 'connector-resilience-keys-retired'); + expect(entry!.replacement).toContain('reads from the metadata registry'); + expect(entry!.replacement).toContain('writes no `sys_webhook` row'); + for (const text of [entry!.replacement, entry!.reason]) expect(text).not.toMatch(/materiali[sz]/); + }); + it('the second carrier, the /meta door and `stack.connectors[]` refuse all three — with controls', () => { const door = getMetadataTypeSchema('connector'); expect(door, 'no schema bound for `connector`').toBeDefined(); diff --git a/packages/spec/src/integration/connector.zod.ts b/packages/spec/src/integration/connector.zod.ts index 43663c9e629..cefad52bda7 100644 --- a/packages/spec/src/integration/connector.zod.ts +++ b/packages/spec/src/integration/connector.zod.ts @@ -256,10 +256,10 @@ const FIELD_MAPPINGS_RETIRED = // through `ConnectorSchema.webhooks`. They left whole with that key: a webhook // nested inside a connector was never registered as a `webhook` metadata item // (the stack decomposition registers a connector entry WHOLE), so -// `@objectstack/plugin-webhooks` never materialized it into `sys_webhook` and -// nothing ever delivered it — and no code path emits a connector lifecycle -// event (`sync.completed`, `auth.expired`, …) for `events` to have subscribed -// to. The three defs are declared in `RETIRED_DEFS_BY_MAJOR[18]`; the carrier +// `@objectstack/plugin-webhooks`' dispatcher, which reads declared `webhook` +// items from the metadata registry, never read it and nothing ever delivered +// it — and no code path emits a connector lifecycle event (`sync.completed`, +// `auth.expired`, …) for `events` to have subscribed to. The three defs are declared in `RETIRED_DEFS_BY_MAJOR[18]`; the carrier // key is the `webhooks` tombstone on `ConnectorBaseSchema` below, and the // section "REMOVED: `health`, `status` and the nested `webhooks`" records the // measurement. A webhook that is actually delivered is declared in the stack's @@ -574,9 +574,9 @@ const CONNECTOR_RETIRED_KEY_RESIDUE = { // and nothing read it back; those writes were deleted with it. // - `webhooks` (the nested `WebhookConfig[]`): zero reads of a connector's own // array. The stack decomposition registers a connector entry WHOLE, so a -// webhook nested in it never became a `webhook` item, never reached -// `@objectstack/plugin-webhooks`' materializer and was never delivered — the -// top-level `webhooks:` collection is the delivered one. +// webhook nested in it never became a `webhook` item, was never read by +// `@objectstack/plugin-webhooks`' declared-webhook reader and was never +// delivered — the top-level `webhooks:` collection is the delivered one. // // `ConnectorSchema` is NOT `.strict()`, so a plain delete would be a silent // strip (ADR-0104): each carrier key is a `retiredKey()` tombstone below, @@ -646,13 +646,15 @@ const STATUS_RETIRED = */ const WEBHOOKS_RETIRED = '`connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — ' - + 'a webhook nested inside a connector was never registered as a `webhook` item, so it was ' - + 'never materialized into `sys_webhook` and never delivered, and nothing emits the connector ' + + 'a webhook nested inside a connector was never registered as a `webhook` item, so the ' + + 'webhook dispatcher never read it and never delivered it, and nothing emits the connector ' + 'events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete ' + 'the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, ' + '`WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack\'s ' - + 'top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on ' - + 'record events — note that doing so STARTS deliveries this connector never made. ' + + 'top-level `webhooks:` collection. The platform reads a declared webhook from the metadata ' + + 'registry and delivers it on record events; it writes no `sys_webhook` row for it ' + + '(`sys_webhook` holds only the webhooks an organization authors at runtime). Note that ' + + 'declaring it STARTS deliveries this connector never made. ' + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; // ============================================================================ @@ -942,11 +944,12 @@ const ConnectorBaseSchema = lazySchema(() => z.object({ /** * `webhooks` — RETIRED (ADR-0049 enforce-or-remove). A webhook nested in a - * connector was never registered as a `webhook` item, so it was never - * materialized into `sys_webhook` and never delivered; the delivered surface - * is the stack's top-level `webhooks:` collection. `ConnectorSchema` is NOT - * `.strict()`, so a plain delete would be a silent strip (ADR-0104); the - * tombstone makes the removal audible in `tsc` and at parse. See "REMOVED: + * connector was never registered as a `webhook` item, so the webhook + * dispatcher never read it and never delivered it; the delivered surface is + * the stack's top-level `webhooks:` collection, read from the metadata + * registry (no `sys_webhook` row is written for a declared webhook). + * `ConnectorSchema` is NOT `.strict()`, so a plain delete would be a silent + * strip (ADR-0104); the tombstone makes the removal audible in `tsc` and at parse. See "REMOVED: * `health`, `status` and the nested `webhooks`" above. */ webhooks: retiredKey(WEBHOOKS_RETIRED), diff --git a/packages/spec/src/migrations/entries/retired-defs/18.integration__WebhookConfig.ts b/packages/spec/src/migrations/entries/retired-defs/18.integration__WebhookConfig.ts index f0139df0778..e427b711a8d 100644 --- a/packages/spec/src/migrations/entries/retired-defs/18.integration__WebhookConfig.ts +++ b/packages/spec/src/migrations/entries/retired-defs/18.integration__WebhookConfig.ts @@ -3,8 +3,9 @@ // `integration/WebhookConfig` — the canonical `webhook` shape `.extend()`ed with // `events` and `signatureAlgorithm` — leaves with its only carrier, // `ConnectorSchema.webhooks`, tombstoned in this same major under ADR-0049 -// enforce-or-remove. A webhook nested in a connector was never registered, -// materialized or delivered; the delivered shape is `automation/Webhook`, which -// is unaffected. See `retired-keys/18.integration__Connector__webhooks.ts` for -// the retirement record. +// enforce-or-remove. A webhook nested in a connector was never registered as a +// `webhook` item, read by the webhook dispatcher or delivered; the delivered +// shape is `automation/Webhook`, which is unaffected. See +// `retired-keys/18.integration__Connector__webhooks.ts` for the retirement +// record. export const entry = 'integration/WebhookConfig'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__webhooks.ts b/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__webhooks.ts index 10b50936bd7..af30868ba56 100644 --- a/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__webhooks.ts +++ b/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__webhooks.ts @@ -5,10 +5,11 @@ // A connector's NESTED webhook array is not the collection anything delivers: // the stack decomposition registers a `connectors:` entry WHOLE, so a webhook // nested in it never becomes a `webhook` metadata item, and -// `@objectstack/plugin-webhooks` materializes `sys_webhook` rows only from those -// items (the top-level `webhooks:` collection). Measured on `origin/main`: zero -// reads of a connector's own `webhooks` outside `packages/spec`, while -// `stack.webhooks` — the lit control, same scan — is read five times; the one +// `@objectstack/plugin-webhooks` reads declared webhooks only from those items +// (the top-level `webhooks:` collection), from the metadata registry, and +// writes no `sys_webhook` row for them (ADR-0131 D2). Measured on +// `origin/main`: zero reads of a connector's own `webhooks` outside +// `packages/spec`, while `stack.webhooks` — the lit control, same scan — is read five times; the one // test that authors a nested array // (`declared-webhooks.connector-nested.test.ts`, renamed from the retired // seeder's `bootstrap-declared-webhooks.connector-nested.test.ts`) exists to pin that it diff --git a/packages/spec/src/migrations/entries/semantic/18.connector-resilience-keys-retired.ts b/packages/spec/src/migrations/entries/semantic/18.connector-resilience-keys-retired.ts index 79c6d810275..776fd5d5b51 100644 --- a/packages/spec/src/migrations/entries/semantic/18.connector-resilience-keys-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.connector-resilience-keys-retired.ts @@ -22,14 +22,16 @@ export const entry: SemanticMigration = { + 'Participation is `enabled` (and `provider` on a declarative instance); whether a registered ' + 'connector can be dispatched is the computed `state` (`ready` / `degraded`) on ' + '`GET /api/v1/automation/connectors`; a webhook that is actually delivered is declared in ' - + 'the top-level `webhooks:` collection; probes and circuit breaking belong in the connector ' - + 'provider or an upstream gateway.', + + 'the top-level `webhooks:` collection, which the platform reads from the metadata registry ' + + '(it writes no `sys_webhook` row for a declared webhook); probes and circuit breaking belong ' + + 'in the connector provider or an upstream gateway.', reason: 'The D2 conversion `connector-resilience-keys-removed` deletes `health`, `status` and ' + '`webhooks` from every connector, stack entry and stored connector row, one notice per key, ' + 'and the delete is lossless: no loop ever polled a connector endpoint, counted failures or ' + 'tripped a breaker, no code read an authored status, and a webhook nested in a connector ' - + 'was never registered, materialized or delivered. Three judgements remain. First, a probe ' - + 'or breaker the author believed was protecting a flaky upstream never was — if that ' + + 'was never registered as a `webhook` item, read by the webhook dispatcher or delivered. ' + + 'Three judgements remain. First, a probe or breaker the author believed was protecting a ' + + 'flaky upstream never was — if that ' + 'protection matters, it has to be built where calls are made (the connector provider) or ' + 'in front of the upstream (a gateway). Second, `status` values like `active` or `error` ' + 'gated nothing; an author who used `status` to switch a connector off needs `enabled: ' diff --git a/packages/spec/src/migrations/registry.ts.template b/packages/spec/src/migrations/registry.ts.template index 79baa7494d0..0f5f00d284a 100644 --- a/packages/spec/src/migrations/registry.ts.template +++ b/packages/spec/src/migrations/registry.ts.template @@ -1436,9 +1436,9 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'reader outside the spec package. No loop ever polled a connector endpoint or tripped a ' + 'breaker; nothing read an authored `status` (the runtime publishes a computed `state`, and ' + 'participation is `enabled`); and a webhook nested in a connector was never registered as a ' - + '`webhook` item, so it was never materialized or delivered — the top-level `webhooks:` ' - + 'collection is the delivered one. The three carrier keys are retiredKey tombstones on ' - + '`ConnectorBaseSchema`, registered under both carrier defs; `status`, defaulted ' + + '`webhook` item, so the webhook dispatcher never read it or delivered it — the top-level ' + + '`webhooks:` collection is the delivered one. The three carrier keys are retiredKey ' + + 'tombstones on `ConnectorBaseSchema`, registered under both carrier defs; `status`, defaulted ' + '`\'inactive\'`, joins `connectionTimeoutMs` in the retired-default residue stage, because ' + 'every 17.x parse emitted it into every connector. Seven defs leave whole — ' + '`ConnectorHealth`, `HealthCheckConfig`, `CircuitBreakerConfig`, `ConnectorStatus`, '