Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .changeset/webhook-dual-source-api-side.md
Original file line number Diff line number Diff line change
@@ -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).
49 changes: 0 additions & 49 deletions content/docs/references/api/connector.mdx

This file was deleted.

1 change: 0 additions & 1 deletion content/docs/references/api/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"auth",
"auth-endpoints",
"automation-api",
"connector",
"core-services",
"events",
"export",
Expand Down
20 changes: 18 additions & 2 deletions content/docs/references/api/rest-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<string, string>` | optional | Custom headers to include in webhook delivery |
| **security** | `Enum<'hmac_sha256' \| 'basic' \| 'bearer' \| 'api_key'>[]` | ✅ | Supported authentication methods for webhook verification |


---

## RestApiConfig
Expand Down
6 changes: 2 additions & 4 deletions packages/spec/api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -2939,6 +2939,8 @@
"OpenApiServerSchema (const)",
"OpenApiSpec (type)",
"OpenApiSpecSchema (const)",
"OpenApiWebhookEvent (type)",
"OpenApiWebhookEventSchema (const)",
"OperatorMapping (type)",
"OperatorMappingSchema (const)",
"PackageApiContracts (const)",
Expand Down Expand Up @@ -3195,10 +3197,6 @@
"WebSocketPresenceStatus (type)",
"WebSocketServerConfig (type)",
"WebSocketServerConfigSchema (const)",
"WebhookConfig (type)",
"WebhookConfigSchema (const)",
"WebhookEvent (type)",
"WebhookEventSchema (const)",
"WellKnownCapabilities (type)",
"WellKnownCapabilitiesSchema (const)",
"envelopeViolations (function)",
Expand Down
16 changes: 6 additions & 10 deletions packages/spec/authorable-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 1 addition & 5 deletions packages/spec/dual-source-exports.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)]"
]
}
3 changes: 1 addition & 2 deletions packages/spec/json-schema.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@
"api/OpenApiSecurityScheme",
"api/OpenApiServer",
"api/OpenApiSpec",
"api/OpenApiWebhookEvent",
"api/OperatorMapping",
"api/PackageApiErrorCode",
"api/PackageInstallRequest",
Expand Down Expand Up @@ -492,8 +493,6 @@
"api/WebSocketMessageType",
"api/WebSocketPresenceStatus",
"api/WebSocketServerConfig",
"api/WebhookConfig",
"api/WebhookEvent",
"api/WellKnownCapabilities",
"automation/ActionCategory",
"automation/ActionDescriptor",
Expand Down
70 changes: 26 additions & 44 deletions packages/spec/src/api/rest-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
EndpointRegistrySchema,
RestApiConfig,
RestServerConfig,
WebhookEventSchema,
WebhookConfigSchema,
OpenApiWebhookEventSchema,
CallbackSchema,
OpenApi31ExtensionsSchema,
type RestApiConfig as RestApiConfigType,
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -693,52 +692,35 @@ 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',
security: [...methods],
});
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);
});
});

Expand Down
34 changes: 11 additions & 23 deletions packages/spec/src/api/rest-server.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,16 @@ export type RouteGenerationConfigInput = z.input<typeof RouteGenerationConfigSch
// ==========================================

/**
* Webhook Event Schema
* Defines an event that can trigger a webhook delivery
* OpenAPI Webhook Event Schema
* Defines a webhook event as declared in an OpenAPI 3.1 document's top-level
* `webhooks` section — an API-documentation descriptor, NOT a runtime
* subscription. (Renamed from `WebhookEventSchema` in v17 — that bare name
* collided with the connector event enum in `@objectstack/spec/integration`,
* a different concept AND a different form: z.object here vs z.enum there.
* The outbound webhook an author configures is `WebhookSchema` in
* `@objectstack/spec/automation`.)
*/
export const WebhookEventSchema = lazySchema(() => 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'),
Expand All @@ -413,25 +419,7 @@ export const WebhookEventSchema = lazySchema(() => z.object({
).describe('Supported authentication methods for webhook verification'),
}));

export type WebhookEvent = z.infer<typeof WebhookEventSchema>;

/**
* 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<typeof WebhookConfigSchema>;
export type OpenApiWebhookEvent = z.infer<typeof OpenApiWebhookEventSchema>;

/**
* Callback Schema
Expand All @@ -451,7 +439,7 @@ export type Callback = z.infer<typeof CallbackSchema>;
* 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)'),
Expand Down
Loading