diff --git a/.changeset/webhook-dual-source-api-side.md b/.changeset/webhook-dual-source-api-side.md new file mode 100644 index 0000000000..e77882ae0e --- /dev/null +++ b/.changeset/webhook-dual-source-api-side.md @@ -0,0 +1,44 @@ +--- +"@objectstack/spec": major +--- + +feat(spec)!: `@objectstack/spec/api` no longer exports the bare names `WebhookConfig` / `WebhookEvent` — they belong to `./integration` alone (#4572) + +The names `WebhookConfig(Schema)` / `WebhookEvent(Schema)` resolved to **two +different declarations** depending on the import path (`./api` vs +`./integration`) — the #4411 dual-source trap, and a cross-form one: +`./api`'s `WebhookEventSchema` was a `z.object` (an OpenAPI 3.1 webhook +*definition* descriptor: `name`/`description`/`method`/`payloadSchema`/ +`security`) while `./integration`'s is a `z.enum` of connector event types +(`'record.created'` … `'rate_limit.exceeded'`). Auto-importing the wrong side +compiled and validated the wrong contract. Resolution (three-repo, +import-statement-level consumer scan: framework, cloud, objectui): + +- **Removed** `WebhookConfigSchema` / `WebhookConfig` from + `@objectstack/spec/api`. This pair was dead: wired into nothing — not even + `RestServerConfigSchema` — with zero import-level consumers in all three + repos, and no runtime ever read a REST-server webhook config. + - FROM `import { WebhookConfig } from '@objectstack/spec/api'` → + TO: no replacement exists for a REST-server webhook config (it never had a + runtime). For a real outbound webhook use `Webhook` from + `@objectstack/spec/automation`; for a connector webhook use + `WebhookConfig` from `@objectstack/spec/integration` (a **different + shape**: it extends the canonical automation `WebhookSchema` with + `events` / `signatureAlgorithm`, and has no `deliveryConfig` / + `registrationEndpoint` / `enabled`). +- **Renamed** `WebhookEventSchema` / `WebhookEvent` in `@objectstack/spec/api` + → `OpenApiWebhookEventSchema` / `OpenApiWebhookEvent` (same shape, rename + only; joins the existing `OpenApi*` family). `OpenApi31ExtensionsSchema.webhooks` + now references the renamed schema — its parsed/authored shape is unchanged. + - FROM `import { WebhookEvent } from '@objectstack/spec/api'` → + TO `import { OpenApiWebhookEvent } from '@objectstack/spec/api'` (if you + meant the OpenAPI 3.1 webhook descriptor), or + `import { WebhookEvent } from '@objectstack/spec/integration'` (if you + meant the connector event enum — check which shape you actually consume: + object vs string enum). +- `@objectstack/spec/integration`'s `WebhookConfig(Schema)` / + `WebhookEvent(Schema)` are **unchanged** and are now the sole owners of the + bare names. Imports from `./integration` need no migration. + +`dual-source-exports.baseline.json` shrinks by exactly these 4 rows (35 → 31, +#4535 C1). diff --git a/content/docs/references/api/connector.mdx b/content/docs/references/api/connector.mdx deleted file mode 100644 index aac0b373f2..0000000000 --- a/content/docs/references/api/connector.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Connector -description: Connector protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { WebhookConfig, WebhookEvent } from '@objectstack/spec/api'; -import type { WebhookConfig, WebhookEvent } from '@objectstack/spec/api'; - -// Validate data -const result = WebhookConfig.parse(data); -``` - ---- - -## WebhookConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | Enable webhook support | -| **events** | `{ name: string; description: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; payloadSchema: string; … }[]` | ✅ | Registered webhook events | -| **deliveryConfig** | `{ maxRetries: integer; retryIntervalMs: integer; timeoutMs: integer; signatureHeader: string }` | ✅ | Webhook delivery configuration | -| **registrationEndpoint** | `string` | ✅ | URL path for webhook registration | - - ---- - -## WebhookEvent - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Webhook event identifier (snake_case) | -| **description** | `string` | ✅ | Human-readable event description | -| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method for webhook delivery | -| **payloadSchema** | `string` | ✅ | JSON Schema $ref for the webhook payload | -| **headers** | `Record` | optional | Custom headers to include in webhook delivery | -| **security** | `Enum<'hmac_sha256' \| 'basic' \| 'bearer' \| 'api_key'>[]` | ✅ | Supported authentication methods for webhook verification | - - ---- - diff --git a/content/docs/references/api/meta.json b/content/docs/references/api/meta.json index 8ca6cd93ea..13b3509ecb 100644 --- a/content/docs/references/api/meta.json +++ b/content/docs/references/api/meta.json @@ -27,7 +27,6 @@ "auth", "auth-endpoints", "automation-api", - "connector", "core-services", "events", "export", diff --git a/content/docs/references/api/rest-server.mdx b/content/docs/references/api/rest-server.mdx index 585a824f62..c3eb6783de 100644 --- a/content/docs/references/api/rest-server.mdx +++ b/content/docs/references/api/rest-server.mdx @@ -38,8 +38,8 @@ Architecture alignment: ## TypeScript Usage ```typescript -import { BatchEndpointsConfig, Callback, CrudEndpointPattern, CrudEndpointsConfig, CrudOperation, EndpointRegistry, GeneratedEndpoint, MetadataEndpointsConfig, OpenApi31Extensions, RestApiConfig, RestServerConfig, RouteGenerationConfig } from '@objectstack/spec/api'; -import type { BatchEndpointsConfig, Callback, CrudEndpointPattern, CrudEndpointsConfig, CrudOperation, EndpointRegistry, GeneratedEndpoint, MetadataEndpointsConfig, OpenApi31Extensions, RestApiConfig, RestServerConfig, RouteGenerationConfig } from '@objectstack/spec/api'; +import { BatchEndpointsConfig, Callback, CrudEndpointPattern, CrudEndpointsConfig, CrudOperation, EndpointRegistry, GeneratedEndpoint, MetadataEndpointsConfig, OpenApi31Extensions, OpenApiWebhookEvent, RestApiConfig, RestServerConfig, RouteGenerationConfig } from '@objectstack/spec/api'; +import type { BatchEndpointsConfig, Callback, CrudEndpointPattern, CrudEndpointsConfig, CrudOperation, EndpointRegistry, GeneratedEndpoint, MetadataEndpointsConfig, OpenApi31Extensions, OpenApiWebhookEvent, RestApiConfig, RestServerConfig, RouteGenerationConfig } from '@objectstack/spec/api'; // Validate data const result = BatchEndpointsConfig.parse(data); @@ -173,6 +173,22 @@ const result = BatchEndpointsConfig.parse(data); | **pathItemReferences** | `boolean` | ✅ | Allow $ref in path items (OpenAPI 3.1 feature) | +--- + +## OpenApiWebhookEvent + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Webhook event identifier (snake_case) | +| **description** | `string` | ✅ | Human-readable event description | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method for webhook delivery | +| **payloadSchema** | `string` | ✅ | JSON Schema $ref for the webhook payload | +| **headers** | `Record` | optional | Custom headers to include in webhook delivery | +| **security** | `Enum<'hmac_sha256' \| 'basic' \| 'bearer' \| 'api_key'>[]` | ✅ | Supported authentication methods for webhook verification | + + --- ## RestApiConfig diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index a6718b2a93..83dc5ec0ff 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -2939,6 +2939,8 @@ "OpenApiServerSchema (const)", "OpenApiSpec (type)", "OpenApiSpecSchema (const)", + "OpenApiWebhookEvent (type)", + "OpenApiWebhookEventSchema (const)", "OperatorMapping (type)", "OperatorMappingSchema (const)", "PackageApiContracts (const)", @@ -3195,10 +3197,6 @@ "WebSocketPresenceStatus (type)", "WebSocketServerConfig (type)", "WebSocketServerConfigSchema (const)", - "WebhookConfig (type)", - "WebhookConfigSchema (const)", - "WebhookEvent (type)", - "WebhookEventSchema (const)", "WellKnownCapabilities (type)", "WellKnownCapabilitiesSchema (const)", "envelopeViolations (function)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 5272fb7ca2..3af6b1785c 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -1625,6 +1625,12 @@ "api/OpenApiSpec:security", "api/OpenApiSpec:servers", "api/OpenApiSpec:tags", + "api/OpenApiWebhookEvent:description", + "api/OpenApiWebhookEvent:headers", + "api/OpenApiWebhookEvent:method", + "api/OpenApiWebhookEvent:name", + "api/OpenApiWebhookEvent:payloadSchema", + "api/OpenApiWebhookEvent:security", "api/OperatorMapping:odata", "api/OperatorMapping:operator", "api/OperatorMapping:rest", @@ -2098,16 +2104,6 @@ "api/WebSocketServerConfig:path", "api/WebSocketServerConfig:presence", "api/WebSocketServerConfig:reconnectAttempts", - "api/WebhookConfig:deliveryConfig", - "api/WebhookConfig:enabled", - "api/WebhookConfig:events", - "api/WebhookConfig:registrationEndpoint", - "api/WebhookEvent:description", - "api/WebhookEvent:headers", - "api/WebhookEvent:method", - "api/WebhookEvent:name", - "api/WebhookEvent:payloadSchema", - "api/WebhookEvent:security", "api/WellKnownCapabilities:automation", "api/WellKnownCapabilities:chunkedUpload", "api/WellKnownCapabilities:comments", diff --git a/packages/spec/dual-source-exports.baseline.json b/packages/spec/dual-source-exports.baseline.json index 530bba8eaa..4658d64d2b 100644 --- a/packages/spec/dual-source-exports.baseline.json +++ b/packages/spec/dual-source-exports.baseline.json @@ -31,10 +31,6 @@ "Session — [./api (type)] ≠ [./identity (type)]", "SessionSchema — [./api (const)] ≠ [./identity (const)]", "TenantPlan — [./cloud (type)] ≠ [./system (type)]", - "TenantPlanSchema — [./cloud (const)] ≠ [./system (const)]", - "WebhookConfig — [./api (type)] ≠ [./integration (type)]", - "WebhookConfigSchema — [./api (const)] ≠ [./integration (const)]", - "WebhookEvent — [./api (type)] ≠ [./integration (type)]", - "WebhookEventSchema — [./api (const)] ≠ [./integration (const)]" + "TenantPlanSchema — [./cloud (const)] ≠ [./system (const)]" ] } diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 363ba5df49..3717c980ad 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -366,6 +366,7 @@ "api/OpenApiSecurityScheme", "api/OpenApiServer", "api/OpenApiSpec", + "api/OpenApiWebhookEvent", "api/OperatorMapping", "api/PackageApiErrorCode", "api/PackageInstallRequest", @@ -492,8 +493,6 @@ "api/WebSocketMessageType", "api/WebSocketPresenceStatus", "api/WebSocketServerConfig", - "api/WebhookConfig", - "api/WebhookEvent", "api/WellKnownCapabilities", "automation/ActionCategory", "automation/ActionDescriptor", diff --git a/packages/spec/src/api/rest-server.test.ts b/packages/spec/src/api/rest-server.test.ts index b0b1c10feb..4c383b5486 100644 --- a/packages/spec/src/api/rest-server.test.ts +++ b/packages/spec/src/api/rest-server.test.ts @@ -12,8 +12,7 @@ import { EndpointRegistrySchema, RestApiConfig, RestServerConfig, - WebhookEventSchema, - WebhookConfigSchema, + OpenApiWebhookEventSchema, CallbackSchema, OpenApi31ExtensionsSchema, type RestApiConfig as RestApiConfigType, @@ -656,9 +655,9 @@ describe('Integration Tests', () => { // OpenAPI 3.1 Webhooks & Callbacks Tests // ========================================== -describe('WebhookEventSchema', () => { +describe('OpenApiWebhookEventSchema', () => { it('should accept valid webhook event', () => { - const event = WebhookEventSchema.parse({ + const event = OpenApiWebhookEventSchema.parse({ name: 'record_created', description: 'Fired when a record is created', payloadSchema: '#/components/schemas/RecordCreated', @@ -671,7 +670,7 @@ describe('WebhookEventSchema', () => { }); it('should enforce snake_case name', () => { - expect(() => WebhookEventSchema.parse({ + expect(() => OpenApiWebhookEventSchema.parse({ name: 'RecordCreated', description: 'Bad name', payloadSchema: '#/ref', @@ -680,7 +679,7 @@ describe('WebhookEventSchema', () => { }); it('should accept event with custom headers', () => { - const event = WebhookEventSchema.parse({ + const event = OpenApiWebhookEventSchema.parse({ name: 'sync_completed', description: 'Sync finished', payloadSchema: '#/ref', @@ -693,7 +692,7 @@ describe('WebhookEventSchema', () => { it('should accept all security methods', () => { const methods = ['hmac_sha256', 'basic', 'bearer', 'api_key'] as const; - const event = WebhookEventSchema.parse({ + const event = OpenApiWebhookEventSchema.parse({ name: 'test_event', description: 'Test', payloadSchema: '#/ref', @@ -701,44 +700,27 @@ describe('WebhookEventSchema', () => { }); expect(event.security).toHaveLength(4); }); -}); - -describe('WebhookConfigSchema', () => { - it('should accept config with defaults', () => { - const config = WebhookConfigSchema.parse({ - events: [ - { - name: 'record_created', - description: 'Record created', - payloadSchema: '#/ref', - security: ['hmac_sha256'], - }, - ], - deliveryConfig: {}, - }); - - expect(config.enabled).toBe(false); - expect(config.deliveryConfig.maxRetries).toBe(3); - expect(config.deliveryConfig.timeoutMs).toBe(30000); - expect(config.deliveryConfig.signatureHeader).toBe('X-Signature-256'); - expect(config.registrationEndpoint).toBe('/webhooks'); - }); - - it('should accept full delivery config', () => { - const config = WebhookConfigSchema.parse({ - enabled: true, - events: [], - deliveryConfig: { - maxRetries: 5, - retryIntervalMs: 10000, - timeoutMs: 60000, - signatureHeader: 'X-Hub-Signature', - }, - registrationEndpoint: '/hooks', - }); - expect(config.deliveryConfig.maxRetries).toBe(5); - expect(config.registrationEndpoint).toBe('/hooks'); + // v17 dual-source cleanup (#4572): the bare names WebhookEvent(Schema) / + // WebhookConfig(Schema) now belong to @objectstack/spec/integration alone + // (connector event enum + connector webhook config). The ./api pair was the + // #4411-style trap: same names, different concepts, different forms + // (z.object here vs z.enum there). WebhookConfig(Schema) on ./api was dead + // — wired into nothing (not even RestServerConfigSchema) — and was removed + // rather than renamed. Pin: this module no longer declares the bare names. + // The pin is compile-time (typeof import is type-level only — no runtime + // barrel load): if either bare name is re-added here, the conditional type + // flips to `true` and the `false` assignment fails `tsc --noEmit`. + it('does not re-expose the bare WebhookEvent/WebhookConfig names from ./api', () => { + type RestServerModule = typeof import('./rest-server.zod'); + const hasBareEventSchema: 'WebhookEventSchema' extends keyof RestServerModule + ? true + : false = false; + const hasBareConfigSchema: 'WebhookConfigSchema' extends keyof RestServerModule + ? true + : false = false; + expect(hasBareEventSchema).toBe(false); + expect(hasBareConfigSchema).toBe(false); }); }); diff --git a/packages/spec/src/api/rest-server.zod.ts b/packages/spec/src/api/rest-server.zod.ts index 713cbf859d..510fbca41a 100644 --- a/packages/spec/src/api/rest-server.zod.ts +++ b/packages/spec/src/api/rest-server.zod.ts @@ -399,10 +399,16 @@ export type RouteGenerationConfigInput = z.input z.object({ +export const OpenApiWebhookEventSchema = lazySchema(() => z.object({ name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Webhook event identifier (snake_case)'), description: z.string().describe('Human-readable event description'), method: HttpMethod.default('POST').describe('HTTP method for webhook delivery'), @@ -413,25 +419,7 @@ export const WebhookEventSchema = lazySchema(() => z.object({ ).describe('Supported authentication methods for webhook verification'), })); -export type WebhookEvent = z.infer; - -/** - * Webhook Configuration Schema - * Top-level webhook configuration for the REST API - */ -export const WebhookConfigSchema = lazySchema(() => z.object({ - enabled: z.boolean().default(false).describe('Enable webhook support'), - events: z.array(WebhookEventSchema).describe('Registered webhook events'), - deliveryConfig: z.object({ - maxRetries: z.number().int().default(3).describe('Maximum delivery retry attempts'), - retryIntervalMs: z.number().int().default(5000).describe('Milliseconds between retry attempts'), - timeoutMs: z.number().int().default(30000).describe('Delivery request timeout in milliseconds'), - signatureHeader: z.string().default('X-Signature-256').describe('Header name for webhook signature'), - }).describe('Webhook delivery configuration'), - registrationEndpoint: z.string().default('/webhooks').describe('URL path for webhook registration'), -})); - -export type WebhookConfig = z.infer; +export type OpenApiWebhookEvent = z.infer; /** * Callback Schema @@ -451,7 +439,7 @@ export type Callback = z.infer; * Extensions specific to OpenAPI 3.1 specification */ export const OpenApi31ExtensionsSchema = lazySchema(() => z.object({ - webhooks: z.record(z.string(), WebhookEventSchema).optional() + webhooks: z.record(z.string(), OpenApiWebhookEventSchema).optional() .describe('OpenAPI 3.1 webhooks (top-level webhook definitions)'), callbacks: z.record(z.string(), z.array(CallbackSchema)).optional() .describe('OpenAPI 3.1 callbacks (async response definitions)'),