diff --git a/.changeset/datasource-config-driver-contract.md b/.changeset/datasource-config-driver-contract.md
new file mode 100644
index 0000000000..f912af836a
--- /dev/null
+++ b/.changeset/datasource-config-driver-contract.md
@@ -0,0 +1,77 @@
+---
+'@objectstack/spec': minor
+'@objectstack/service-datasource': minor
+---
+
+`datasource.config` is now validated against its driver's contract (#4410)
+
+`config` was the one authorable slot on a datasource with no gate at all. The
+schema's own comment claimed "the driver's own `configSchema` is what validates
+it" — nothing did: both bundled driver specs set `configSchema: {}`, no code read
+the field, and the per-driver zod schemas were not even exported from the
+package. So `config: { hostname: 'db.internal' }` (the key is `host`) was
+accepted in silence and the datasource connected to `localhost` while the parse,
+the save and the connection probe all reported success.
+
+`DatasourceSchema` now parses `config` — and each `readReplicas` entry — against
+the contract for the declared driver, and `DatasourceAdminService`
+(create/update/test, the Setup wizard's path) applies the same check. Both read
+one registry in `@objectstack/spec/data`, which also projects each contract to
+JSON Schema for `DriverDefinitionSchema.configSchema` and the Studio connection
+form, so the form offers exactly the fields the validator accepts.
+
+New exports from `@objectstack/spec/data`: `PostgresConfigSchema`,
+`MysqlConfigSchema`, `SqliteConfigSchema`, `SqliteWasmConfigSchema`,
+`MongoConfigSchema`, `MemoryConfigSchema`, plus `resolveDriverId`,
+`getDriverConfigSchema`, `getDriverConfigJsonSchemaById` and
+`validateDriverConfig`. A driver the platform ships no contract for (a plugin's
+`com.vendor.snowflake`) keeps an unvalidated `config`.
+
+**Migration.** A config that was silently ignored now fails with the correction
+in the message. The renames:
+
+| Wrote | Write instead | Driver |
+| --- | --- | --- |
+| `user` | `username` | postgres, mysql, mongo |
+| `connectionString` / `dsn` | `url` | postgres, mysql, mongo |
+| `uri` | `url` | mongo |
+| `file` / `path` / `database` | `filename` | sqlite, sqlite-wasm |
+| `hostname` | `host` | postgres, mysql, mongo |
+| `searchPath` | `schema` | postgres |
+
+And the relocations — keys that were never driver config:
+
+| Wrote in `config` | Write instead |
+| --- | --- |
+| `min` / `max` / `idleTimeoutMillis` / `connectionTimeoutMillis` | the datasource's own `pool` block |
+| `schemaMode` | next to `driver`, on the datasource |
+| `readOnly` | `capabilities: { readOnly: true }` |
+| `ssl: { ca, cert, key, rejectUnauthorized }` | the datasource's own `ssl` block — inside `config`, `ssl` is the on/off boolean shorthand |
+
+Two memory-driver keys are **removed**: `indexes` and `maxRecordsPerObject`.
+`InMemoryDriverConfig` has no field for either — the driver keeps no indexes and
+evicts nothing — so both were inert. Drop them; for real indexing use a driver
+that indexes.
+
+A postgres, mysql or mongo datasource must now name a connection target
+(`database`, or a `url` that carries it). An empty `config` used to mean "the
+client's own localhost default", which is the same defect in its most complete
+form.
+
+**Also fixed, because the contract can only be enforced where it is honoured.**
+These keys were declared and read by nothing; they now reach the driver:
+
+- `datasource.pool` is honoured by every SQL driver (it was declared, carried
+ into the connection spec, then overwritten with a hardcoded `{ min: 0, max: 5 }`),
+ and maps onto the Mongo client's `minPoolSize` / `maxPoolSize`.
+- `datasource.schemaMode` reaches the driver. It was dropped between the
+ datasource record and the connection spec, so a `schemaMode: 'external'`
+ database — one ObjectStack must never run DDL against — was constructed as
+ `managed`.
+- `datasource.ssl` reaches the SQL clients, certificates and all. It stopped at
+ the record — nothing put it on the connection spec — so a TLS block configured
+ nothing, which is exactly what its own schema comment warns about ("a TLS
+ setting that never took effect looked identical to one that did").
+- postgres `schema` (knex `searchPath`), `applicationName` and `statementTimeout`.
+- mongo `password`, `authSource` and `options`. A mongo datasource carrying a
+ `config.password` previously composed its URL with an **empty** password.
diff --git a/content/docs/data-modeling/drivers.mdx b/content/docs/data-modeling/drivers.mdx
index 5d8de57740..ff1f22191f 100644
--- a/content/docs/data-modeling/drivers.mdx
+++ b/content/docs/data-modeling/drivers.mdx
@@ -75,6 +75,56 @@ actually connect to Turso.
> Knex client name (`pg` / `mysql2` / `better-sqlite3`) when you instantiate
> `SqlDriver`.
+## `config` is validated per driver
+
+A datasource's `config` is driver-specific — a SQLite `filename` and a Postgres
+`host` share no shape — so the datasource schema keeps that slot open at the top
+level and parses it against the contract for the driver you named. Each built-in
+driver ships that contract as a zod schema, exported from `@objectstack/spec/data`:
+
+| `driver` | Contract | Keys |
+| :--- | :--- | :--- |
+| `postgres` \| `postgresql` \| `pg` | `PostgresConfigSchema` | `url`, `host`, `port`, `database`, `username`, `password`, `ssl`, `schema`, `applicationName`, `statementTimeout`, `autoMigrate` |
+| `mysql` \| `mysql2` \| `mariadb` | `MysqlConfigSchema` | `url`, `host`, `port`, `database`, `username`, `password`, `ssl`, `autoMigrate` |
+| `sqlite` \| `sqlite3` | `SqliteConfigSchema` | `filename`, `autoMigrate` |
+| `sqlite-wasm` \| `wasm-sqlite` | `SqliteWasmConfigSchema` | `filename`, `persist` |
+| `mongo` \| `mongodb` | `MongoConfigSchema` | `url`, `host`, `port`, `database`, `username`, `password`, `authSource`, `options` |
+| `memory` \| `in-memory` | `MemoryConfigSchema` | `initialData`, `strictMode`, `persistence` |
+
+An unrecognised key is rejected with its correction, at authoring time and in the
+Setup → Datasources wizard alike:
+
+```text
+Unrecognized key(s) on this postgres datasource's config: `hostname`.
+Did you mean `hostname` → `host`?
+```
+
+This matters more than a typical typo check, because the failure it replaces was
+silent: a misspelled key was dropped, the driver fell back to its own defaults,
+and the datasource connected to `localhost` while every signal — the parse, the
+save, the connection probe — reported success.
+
+Two things live **outside** `config`, because they are not driver-specific:
+
+- **Pool sizing** — the `pool` block on the datasource (`min`, `max`,
+ `idleTimeoutMillis`, `connectionTimeoutMillis`), honoured for every SQL driver
+ and mapped onto the Mongo client's `minPoolSize` / `maxPoolSize`.
+- **TLS certificates** — the `ssl` block on the datasource (`enabled`,
+ `rejectUnauthorized`, `ca`, `cert`, `key`). Inside `config`, `ssl` is the
+ on/off boolean shorthand.
+- **`schemaMode`** — the ADR-0015 ownership mode, declared next to `driver`.
+
+A plugin-contributed driver (`com.vendor.snowflake`) has no contract in this
+repo, so its `config` is left unvalidated rather than judged against a shape the
+platform does not have.
+
+
+The same schemas are projected to JSON Schema for
+`DriverDefinitionSchema.configSchema` and for `GET /api/v1/datasources/drivers`,
+which the Studio connection form renders — so the form offers exactly the fields
+the validator accepts.
+
+
## Startup: a driver that cannot connect aborts the boot
`ObjectQLEngine.init()` connects every registered driver during kernel
diff --git a/content/docs/data-modeling/external-datasources.mdx b/content/docs/data-modeling/external-datasources.mdx
index d2cdc4ae38..0d40289fbe 100644
--- a/content/docs/data-modeling/external-datasources.mdx
+++ b/content/docs/data-modeling/external-datasources.mdx
@@ -35,7 +35,7 @@ export const Warehouse = defineDatasource({
label: 'Analytics Warehouse (Postgres)',
driver: 'postgres',
schemaMode: 'external', // ObjectStack never runs DDL here
- config: { host: 'db.internal', port: 5432, database: 'analytics', user: 'readonly' },
+ config: { host: 'db.internal', port: 5432, database: 'analytics', username: 'readonly' },
external: {
allowWrites: false, // read-only (the default)
credentialsRef: 'sys_secret:9f2c…', // opaque handle minted by the secret store
diff --git a/content/docs/references/api/connector.mdx b/content/docs/references/api/connector.mdx
index 13d4911417..aac0b373f2 100644
--- a/content/docs/references/api/connector.mdx
+++ b/content/docs/references/api/connector.mdx
@@ -5,10 +5,6 @@ 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/. */}
-
-**Source:** `packages/spec/src/api/connector.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/api/core-services.mdx b/content/docs/references/api/core-services.mdx
index 1583948a79..5f88cac2aa 100644
--- a/content/docs/references/api/core-services.mdx
+++ b/content/docs/references/api/core-services.mdx
@@ -5,10 +5,6 @@ description: Core Services protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/api/core-services.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/api/http.mdx b/content/docs/references/api/http.mdx
index bbbd7be6ef..6eb5ac2296 100644
--- a/content/docs/references/api/http.mdx
+++ b/content/docs/references/api/http.mdx
@@ -5,10 +5,6 @@ description: Http protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/api/http.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/api/identity.mdx b/content/docs/references/api/identity.mdx
index f57cb9f9af..042270686d 100644
--- a/content/docs/references/api/identity.mdx
+++ b/content/docs/references/api/identity.mdx
@@ -5,10 +5,6 @@ description: Identity protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/api/identity.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/api/metadata-plugin.mdx b/content/docs/references/api/metadata-plugin.mdx
index dc3fa4e956..75edc8ead2 100644
--- a/content/docs/references/api/metadata-plugin.mdx
+++ b/content/docs/references/api/metadata-plugin.mdx
@@ -5,10 +5,6 @@ description: Metadata Plugin protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/api/metadata-plugin.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/api/notification.mdx b/content/docs/references/api/notification.mdx
index 063d3d6f18..d792d419f8 100644
--- a/content/docs/references/api/notification.mdx
+++ b/content/docs/references/api/notification.mdx
@@ -5,10 +5,6 @@ description: Notification protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/api/notification.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/api/package-registry.mdx b/content/docs/references/api/package-registry.mdx
index e48585dbdc..52f810ab06 100644
--- a/content/docs/references/api/package-registry.mdx
+++ b/content/docs/references/api/package-registry.mdx
@@ -5,10 +5,6 @@ description: Package Registry protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/api/package-registry.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/automation/connector.mdx b/content/docs/references/automation/connector.mdx
index f8baede8f5..c86ead52b8 100644
--- a/content/docs/references/automation/connector.mdx
+++ b/content/docs/references/automation/connector.mdx
@@ -5,10 +5,6 @@ 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/. */}
-
-**Source:** `packages/spec/src/automation/connector.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/automation/events-core.mdx b/content/docs/references/automation/events-core.mdx
new file mode 100644
index 0000000000..43d096bc0c
--- /dev/null
+++ b/content/docs/references/automation/events-core.mdx
@@ -0,0 +1,31 @@
+---
+title: Events Core
+description: Events Core 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 { Event } from '@objectstack/spec/automation';
+import type { Event } from '@objectstack/spec/automation';
+
+// Validate data
+const result = Event.parse(data);
+```
+
+---
+
+## Event
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Event Type (e.g. "APPROVE", "REJECT", "Submit") |
+| **schema** | `Record` | optional | Expected event payload structure |
+
+
+---
+
diff --git a/content/docs/references/automation/job.mdx b/content/docs/references/automation/job.mdx
index 8f79ea972b..d3e8b86a7e 100644
--- a/content/docs/references/automation/job.mdx
+++ b/content/docs/references/automation/job.mdx
@@ -5,10 +5,6 @@ description: Job protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/automation/job.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/automation/meta.json b/content/docs/references/automation/meta.json
index 30a557be1d..63900061b1 100644
--- a/content/docs/references/automation/meta.json
+++ b/content/docs/references/automation/meta.json
@@ -21,6 +21,7 @@
"job",
"---More---",
"builtin-node-config",
+ "events-core",
"flow-function",
"io-node-config",
"schemaless-node-config"
diff --git a/content/docs/references/automation/offline.mdx b/content/docs/references/automation/offline.mdx
index 00fb908d98..f6bfbe5d6d 100644
--- a/content/docs/references/automation/offline.mdx
+++ b/content/docs/references/automation/offline.mdx
@@ -5,10 +5,6 @@ description: Offline protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/automation/offline.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/automation/state-machine.mdx b/content/docs/references/automation/state-machine.mdx
index 6ecf8900b4..59601ec4cd 100644
--- a/content/docs/references/automation/state-machine.mdx
+++ b/content/docs/references/automation/state-machine.mdx
@@ -18,8 +18,8 @@ Prevent AI "hallucinations" by enforcing valid valid transitions.
## TypeScript Usage
```typescript
-import { ActionRef, Event, GuardRef, StateMachine, StateNode, Transition } from '@objectstack/spec/automation';
-import type { ActionRef, Event, GuardRef, StateMachine, StateNode, Transition } from '@objectstack/spec/automation';
+import { ActionRef, GuardRef, StateMachine, StateNode, Transition } from '@objectstack/spec/automation';
+import type { ActionRef, GuardRef, StateMachine, StateNode, Transition } from '@objectstack/spec/automation';
// Validate data
const result = ActionRef.parse(data);
@@ -53,18 +53,6 @@ Type: `string`
---
----
-
-## Event
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **type** | `string` | ✅ | Event Type (e.g. "APPROVE", "REJECT", "Submit") |
-| **schema** | `Record` | optional | Expected event payload structure |
-
-
---
## GuardRef
diff --git a/content/docs/references/cloud/plugin-security.mdx b/content/docs/references/cloud/plugin-security.mdx
index 877edc19be..a6501678f8 100644
--- a/content/docs/references/cloud/plugin-security.mdx
+++ b/content/docs/references/cloud/plugin-security.mdx
@@ -5,10 +5,6 @@ description: Plugin Security protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/cloud/plugin-security.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/cloud/provisioning.mdx b/content/docs/references/cloud/provisioning.mdx
index 126b63717c..bbcf0d592a 100644
--- a/content/docs/references/cloud/provisioning.mdx
+++ b/content/docs/references/cloud/provisioning.mdx
@@ -5,10 +5,6 @@ description: Provisioning protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/cloud/provisioning.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/data/driver-common.mdx b/content/docs/references/data/driver-common.mdx
new file mode 100644
index 0000000000..db66f93b5d
--- /dev/null
+++ b/content/docs/references/data/driver-common.mdx
@@ -0,0 +1,62 @@
+---
+title: Driver Common
+description: Driver Common protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+Shared building blocks for the per-driver `datasource.config` shapes (#4410).
+
+Every schema under `data/driver/` describes ONE driver's `config` slot — the
+
+keys an author may write and the platform actually reads. They are the
+
+enforcement half of the `config` escape hatch `datasource.zod.ts` opens: the
+
+slot stays `z.record` at the top of `DatasourceSchema` because a sqlite
+
+`filename` and a postgres `host` share no shape, and `DatasourceSchema`'s
+
+refinement then parses it against the schema for the declared driver.
+
+The rule these files are written to: **a key is declared here only if some
+
+code path reads it.** A config key that no driver and no factory consumes is
+
+the same silent-strip defect one level down (#4001, ADR-0078), so an unread
+
+key is either wired or rejected with a prescription — never left in the
+
+contract to look supported.
+
+
+**Source:** `packages/spec/src/data/driver/common.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { DriverSslToggle, SqlAutoMigrate } from '@objectstack/spec/data';
+import type { DriverSslToggle, SqlAutoMigrate } from '@objectstack/spec/data';
+
+// Validate data
+const result = DriverSslToggle.parse(data);
+```
+
+---
+
+
+---
+
+## SqlAutoMigrate
+
+Dev-only non-destructive schema self-heal (#2186)
+
+### Allowed Values
+
+* `off`
+* `safe`
+
+
+---
+
diff --git a/content/docs/references/data/driver-memory.mdx b/content/docs/references/data/driver-memory.mdx
new file mode 100644
index 0000000000..58e37f2916
--- /dev/null
+++ b/content/docs/references/data/driver-memory.mdx
@@ -0,0 +1,101 @@
+---
+title: Driver Memory
+description: Driver Memory protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+Memory Driver Configuration Schema
+
+Defines the configuration options for the in-memory driver.
+
+Reference: objectql/packages/drivers/memory (Mingo-powered production-ready driver)
+
+The memory driver is ideal for:
+
+- Unit testing (no database setup required)
+
+- Development & prototyping
+
+- Edge/Worker environments (Cloudflare Workers, Deno Deploy)
+
+- Client-side state management
+
+- Temporary data caching
+
+- CI/CD pipelines
+
+
+**Source:** `packages/spec/src/data/driver/memory.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { AutoPersistenceConfig, FilePersistenceConfig, LocalStoragePersistenceConfig, PersistenceType } from '@objectstack/spec/data';
+import type { AutoPersistenceConfig, FilePersistenceConfig, LocalStoragePersistenceConfig, PersistenceType } from '@objectstack/spec/data';
+
+// Validate data
+const result = AutoPersistenceConfig.parse(data);
+```
+
+---
+
+## AutoPersistenceConfig
+
+Auto-detect persistence configuration
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `'auto'` | ✅ | |
+| **path** | `string` | optional | File path override for Node.js environments |
+| **autoSaveInterval** | `number` | optional | Auto-save interval override for Node.js environments |
+| **key** | `string` | optional | localStorage key override for browser environments |
+
+
+---
+
+## FilePersistenceConfig
+
+File-system persistence configuration
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `'file'` | ✅ | |
+| **path** | `string` | optional | File path to persist data |
+| **autoSaveInterval** | `number` | ✅ | Auto-save interval in ms |
+
+
+---
+
+## LocalStoragePersistenceConfig
+
+localStorage persistence configuration
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `'local'` | ✅ | |
+| **key** | `string` | optional | localStorage key for persisted data |
+
+
+---
+
+## PersistenceType
+
+Persistence backend type
+
+### Allowed Values
+
+* `file`
+* `local`
+* `auto`
+
+
+---
+
diff --git a/content/docs/references/data/driver-mongo.mdx b/content/docs/references/data/driver-mongo.mdx
new file mode 100644
index 0000000000..102f346f06
--- /dev/null
+++ b/content/docs/references/data/driver-mongo.mdx
@@ -0,0 +1,61 @@
+---
+title: Driver Mongo
+description: Driver Mongo protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+MongoDB Standard Driver Protocol
+
+Describes the MongoDB connection settings and capabilities.
+
+ENFORCED as of #4410. This block used to claim it was "used by the Platform
+
+to validate `datasource.config` when `driver: 'mongo'`", which was false: the
+
+config slot was a bare `z.record` and this schema had no consumer at all —
+
+not even an export, since `data/driver/` was reachable only from its own
+
+tests. It is now what `DatasourceSchema` parses `config` against for a mongo
+
+datasource, and the same schema is projected onto
+
+`MongoDriverSpec`.configSchema for the connection form.
+
+
+**Source:** `packages/spec/src/data/driver/mongo.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { MongoConfig } from '@objectstack/spec/data';
+import type { MongoConfig } from '@objectstack/spec/data';
+
+// Validate data
+const result = MongoConfig.parse(data);
+```
+
+---
+
+## MongoConfig
+
+MongoDB Connection Configuration
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **url** | `string` | optional | Connection URI (supersedes the discrete fields) |
+| **database** | `string` | optional | Database name |
+| **host** | `string` | ✅ | Host address |
+| **port** | `integer` | ✅ | Port number |
+| **username** | `string` | optional | Authentication user |
+| **password** | `string` | optional | Authentication password (prefer external.credentialsRef) |
+| **authSource** | `string` | optional | Authentication database |
+| **options** | `Record` | optional | Extra MongoClient options (replicaSet, tls, timeouts, …) |
+
+
+---
+
diff --git a/content/docs/references/data/driver-mysql.mdx b/content/docs/references/data/driver-mysql.mdx
new file mode 100644
index 0000000000..418dced513
--- /dev/null
+++ b/content/docs/references/data/driver-mysql.mdx
@@ -0,0 +1,63 @@
+---
+title: Driver Mysql
+description: Driver Mysql protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+MySQL / MariaDB driver configuration — the `config` slot of a `datasource`
+
+whose `driver` resolves to `mysql` (`mysql2`).
+
+The driver id was offered by the connection form and buildable by the shared
+
+factory long before #4410, but had no config shape at all in `packages/spec`
+
+— postgres, mongo and memory each had one and mysql did not, so its `config`
+
+was the one slot with neither a gate nor a documented shape.
+
+Every key here is read by `createDefaultDatasourceDriverFactory`
+
+(→ `SqlDriver`, knex `mysql2`). Postgres-only knobs are deliberately absent:
+
+`mysql2` has no `application_name` and no `statement_timeout`, so declaring
+
+them would advertise settings the client drops.
+
+
+**Source:** `packages/spec/src/data/driver/mysql.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { MysqlConfig } from '@objectstack/spec/data';
+import type { MysqlConfig } from '@objectstack/spec/data';
+
+// Validate data
+const result = MysqlConfig.parse(data);
+```
+
+---
+
+## MysqlConfig
+
+MySQL / MariaDB connection configuration
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **url** | `string` | optional | Connection URI (supersedes the discrete fields) |
+| **host** | `string` | ✅ | Host address |
+| **port** | `integer` | ✅ | Port number |
+| **database** | `string` | optional | Database name |
+| **username** | `string` | optional | Authentication user |
+| **password** | `string` | optional | Authentication password (prefer external.credentialsRef) |
+| **ssl** | `boolean` | optional | Enable TLS. Certificates go in the datasource-level `ssl` block. |
+| **autoMigrate** | `Enum<'off' \| 'safe'>` | optional | Dev-only non-destructive schema self-heal (#2186) |
+
+
+---
+
diff --git a/content/docs/references/data/driver-postgres.mdx b/content/docs/references/data/driver-postgres.mdx
new file mode 100644
index 0000000000..66bf74e48f
--- /dev/null
+++ b/content/docs/references/data/driver-postgres.mdx
@@ -0,0 +1,62 @@
+---
+title: Driver Postgres
+description: Driver Postgres protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+PostgreSQL driver configuration — the `config` slot of a `datasource` whose
+
+`driver` resolves to `postgres` (`pg` / `postgresql`).
+
+ENFORCED as of #4410: `DatasourceSchema` parses `config` against this schema,
+
+so a misspelled connection key fails at authoring time instead of leaving the
+
+datasource on the client's localhost defaults. Every key here is read by
+
+`createDefaultDatasourceDriverFactory` (→ `SqlDriver`, knex `pg`).
+
+Pool sizing is NOT here: it lives in the driver-agnostic `datasource.pool`
+
+block, which the factory now honours for every SQL driver.
+
+
+**Source:** `packages/spec/src/data/driver/postgres.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { PostgresConfig } from '@objectstack/spec/data';
+import type { PostgresConfig } from '@objectstack/spec/data';
+
+// Validate data
+const result = PostgresConfig.parse(data);
+```
+
+---
+
+## PostgresConfig
+
+PostgreSQL connection configuration
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **url** | `string` | optional | Connection URI (supersedes the discrete fields) |
+| **host** | `string` | ✅ | Host address |
+| **port** | `integer` | ✅ | Port number |
+| **database** | `string` | optional | Database name |
+| **username** | `string` | optional | Authentication user |
+| **password** | `string` | optional | Authentication password (prefer external.credentialsRef) |
+| **ssl** | `boolean` | optional | Enable TLS. Certificates go in the datasource-level `ssl` block. |
+| **schema** | `string` | ✅ | Default schema (knex searchPath) |
+| **applicationName** | `string` | optional | Postgres application_name |
+| **statementTimeout** | `integer` | optional | Abort statements running longer than this (ms) |
+| **autoMigrate** | `Enum<'off' \| 'safe'>` | optional | Dev-only non-destructive schema self-heal (#2186) |
+
+
+---
+
diff --git a/content/docs/references/data/driver-sqlite.mdx b/content/docs/references/data/driver-sqlite.mdx
new file mode 100644
index 0000000000..ba941d67bf
--- /dev/null
+++ b/content/docs/references/data/driver-sqlite.mdx
@@ -0,0 +1,106 @@
+---
+title: Driver Sqlite
+description: Driver Sqlite protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+SQLite driver configuration — the `config` slot of a `datasource` whose
+
+`driver` resolves to `sqlite` (native `better-sqlite3`, with the dev-only
+
+step-down to wasm then in-memory, #2229) or to `sqlite-wasm` (pure-JS).
+
+The one key that matters is `filename`, and it is exactly the key the silent
+
+strip used to hide: an author who wrote `path:` got no error, the connection
+
+fell back to `:memory:`, and their data vanished on restart with every signal
+
+saying the datasource was configured.
+
+`file` and `database` are a different case — the factory reads them as
+
+undeclared `??` fallbacks, so they happened to work while being documented
+
+nowhere. They are named as renames here rather than blessed: one strict
+
+contract beats a spelling that works only because a reader is lenient
+
+(AGENTS.md Prime Directive #12). The factory keeps its tolerance for records
+
+already persisted that way; no new one can be authored.
+
+
+**Source:** `packages/spec/src/data/driver/sqlite.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { SqliteConfig, SqliteWasmConfig, SqliteWasmPersistMode } from '@objectstack/spec/data';
+import type { SqliteConfig, SqliteWasmConfig, SqliteWasmPersistMode } from '@objectstack/spec/data';
+
+// Validate data
+const result = SqliteConfig.parse(data);
+```
+
+---
+
+## SqliteConfig
+
+SQLite connection configuration
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **filename** | `string` | ✅ | Database file path, or ":memory:" for an ephemeral database |
+| **autoMigrate** | `Enum<'off' \| 'safe'>` | optional | Dev-only non-destructive schema self-heal (#2186) |
+
+
+---
+
+## SqliteWasmConfig
+
+SQLite (WASM) connection configuration
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **filename** | `string` | ✅ | Database file path, or ":memory:" for an ephemeral database |
+| **persist** | `'on-disconnect' \| 'on-write' \| string` | optional | When to flush a file-backed wasm database to disk |
+
+
+---
+
+## SqliteWasmPersistMode
+
+When to flush a file-backed wasm database to disk
+
+### Union Options
+
+This schema accepts one of the following structures:
+
+#### Option 1
+
+Type: `'on-disconnect'`
+
+---
+
+#### Option 2
+
+Type: `'on-write'`
+
+---
+
+#### Option 3
+
+Type: `string`
+
+---
+
+
+---
+
diff --git a/content/docs/references/data/index.mdx b/content/docs/references/data/index.mdx
index 207d74e17e..ce71a76e1c 100644
--- a/content/docs/references/data/index.mdx
+++ b/content/docs/references/data/index.mdx
@@ -13,8 +13,14 @@ This section contains all protocol schemas for the data layer of ObjectStack.
+
+
+
+
+
+
diff --git a/content/docs/references/data/meta.json b/content/docs/references/data/meta.json
index e8e922b97e..d53d6530bc 100644
--- a/content/docs/references/data/meta.json
+++ b/content/docs/references/data/meta.json
@@ -28,6 +28,12 @@
"seed-loader",
"---More---",
"context-tokens",
+ "driver-common",
+ "driver-memory",
+ "driver-mongo",
+ "driver-mysql",
+ "driver-postgres",
+ "driver-sqlite",
"field-value"
]
}
\ No newline at end of file
diff --git a/content/docs/references/integration/connector-auth.mdx b/content/docs/references/integration/connector-auth.mdx
index ac2912aaae..c74b8dfe55 100644
--- a/content/docs/references/integration/connector-auth.mdx
+++ b/content/docs/references/integration/connector-auth.mdx
@@ -5,10 +5,6 @@ description: Connector Auth protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/integration/connector-auth.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/integration/connector-database.mdx b/content/docs/references/integration/connector-database.mdx
new file mode 100644
index 0000000000..b550393cf7
--- /dev/null
+++ b/content/docs/references/integration/connector-database.mdx
@@ -0,0 +1,139 @@
+---
+title: Connector Database
+description: Connector Database protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+Database Connector Protocol Template
+
+Specialized connector for database systems (PostgreSQL, MySQL, SQL Server, etc.)
+
+Extends the base connector with database-specific features like schema discovery,
+
+CDC (Change Data Capture), and connection pooling.
+
+
+**Source:** `packages/spec/src/integration/connector/database.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { CdcConfig, DatabaseConnector, DatabasePoolConfig, DatabaseTable, SslConfig } from '@objectstack/spec/integration';
+import type { CdcConfig, DatabaseConnector, DatabasePoolConfig, DatabaseTable, SslConfig } from '@objectstack/spec/integration';
+
+// Validate data
+const result = CdcConfig.parse(data);
+```
+
+---
+
+## CdcConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **enabled** | `boolean` | ✅ | Enable CDC |
+| **method** | `Enum<'log_based' \| 'trigger_based' \| 'query_based' \| 'custom'>` | ✅ | CDC method |
+| **slotName** | `string` | optional | Replication slot name (for log-based CDC) |
+| **publicationName** | `string` | optional | Publication name (for PostgreSQL) |
+| **startPosition** | `string` | optional | Starting position/LSN for CDC stream |
+| **batchSize** | `number` | ✅ | CDC batch size |
+| **pollIntervalMs** | `number` | ✅ | CDC polling interval in ms |
+
+
+---
+
+## DatabaseConnector
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Unique connector identifier |
+| **label** | `string` | ✅ | Display label |
+| **type** | `'database'` | ✅ | |
+| **description** | `string` | optional | Connector description |
+| **icon** | `string` | optional | Icon identifier |
+| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
+| **provider** | `Enum<'postgresql' \| 'mysql' \| 'mariadb' \| 'mssql' \| 'oracle' \| 'mongodb' \| 'redis' \| 'cassandra' \| 'snowflake' \| 'bigquery' \| 'redshift' \| 'custom'>` | ✅ | Database provider type |
+| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
+| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
+| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
+| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
+| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
+| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
+| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
+| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
+| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
+| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
+| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
+| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
+| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
+| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
+| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
+| **metadata** | `Record` | optional | Custom connector metadata |
+| **connectionConfig** | `{ host: string; port: number; database: string; username: string; … }` | ✅ | Database connection configuration |
+| **poolConfig** | `{ min?: number; max?: number; idleTimeoutMs?: number; connectionTimeoutMs?: number; … }` | optional | Connection pool configuration |
+| **sslConfig** | `{ enabled?: boolean; rejectUnauthorized?: boolean; ca?: string; cert?: string; … }` | optional | SSL/TLS configuration |
+| **tables** | `{ name: string; label: string; schema?: string; tableName: string; … }[]` | ✅ | Tables to sync |
+| **cdcConfig** | `{ enabled?: boolean; method: Enum<'log_based' \| 'trigger_based' \| 'query_based' \| 'custom'>; slotName?: string; publicationName?: string; … }` | optional | CDC configuration |
+| **readReplicaConfig** | `{ enabled?: boolean; hosts: { host: string; port: number; weight?: number }[] }` | optional | Read replica configuration |
+| **queryTimeoutMs** | `number` | optional | Query timeout in ms |
+| **enableQueryLogging** | `boolean` | optional | Enable SQL query logging |
+
+
+---
+
+## DatabasePoolConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **min** | `number` | ✅ | Minimum connections in pool |
+| **max** | `number` | ✅ | Maximum connections in pool |
+| **idleTimeoutMs** | `number` | ✅ | Idle connection timeout in ms |
+| **connectionTimeoutMs** | `number` | ✅ | Connection establishment timeout in ms |
+| **acquireTimeoutMs** | `number` | ✅ | Connection acquisition timeout in ms |
+| **evictionRunIntervalMs** | `number` | ✅ | Connection eviction check interval in ms |
+| **testOnBorrow** | `boolean` | ✅ | Test connection before use |
+
+
+---
+
+## DatabaseTable
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Table name in ObjectStack (snake_case) |
+| **label** | `string` | ✅ | Display label |
+| **schema** | `string` | optional | Database schema name |
+| **tableName** | `string` | ✅ | Actual table name in database |
+| **primaryKey** | `string` | ✅ | Primary key column |
+| **enabled** | `boolean` | optional | Enable sync for this table |
+| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Table-specific field mappings |
+| **whereClause** | `string` | optional | SQL WHERE clause for filtering |
+
+
+---
+
+## SslConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **enabled** | `boolean` | ✅ | Enable SSL/TLS |
+| **rejectUnauthorized** | `boolean` | ✅ | Reject unauthorized certificates |
+| **ca** | `string` | optional | Certificate Authority certificate |
+| **cert** | `string` | optional | Client certificate |
+| **key** | `string` | optional | Client private key |
+
+
+---
+
diff --git a/content/docs/references/integration/connector-file-storage.mdx b/content/docs/references/integration/connector-file-storage.mdx
new file mode 100644
index 0000000000..5235de8e66
--- /dev/null
+++ b/content/docs/references/integration/connector-file-storage.mdx
@@ -0,0 +1,170 @@
+---
+title: Connector File Storage
+description: Connector File Storage protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+File Storage Connector Protocol Template
+
+Specialized connector for file storage systems (S3, Azure Blob, Google Cloud Storage, etc.)
+
+Extends the base connector with file-specific features like multipart uploads,
+
+versioning, and metadata extraction.
+
+
+**Source:** `packages/spec/src/integration/connector/file-storage.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { FileAccessPattern, FileFilterConfig, FileMetadataConfig, FileStorageConnector, FileStorageProvider, FileVersioningConfig, StorageBucket } from '@objectstack/spec/integration';
+import type { FileAccessPattern, FileFilterConfig, FileMetadataConfig, FileStorageConnector, FileStorageProvider, FileVersioningConfig, StorageBucket } from '@objectstack/spec/integration';
+
+// Validate data
+const result = FileAccessPattern.parse(data);
+```
+
+---
+
+## FileAccessPattern
+
+File access pattern
+
+### Allowed Values
+
+* `public_read`
+* `private`
+* `authenticated_read`
+* `bucket_owner_read`
+* `bucket_owner_full`
+
+
+---
+
+## FileFilterConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **includePatterns** | `string[]` | optional | File patterns to include (glob) |
+| **excludePatterns** | `string[]` | optional | File patterns to exclude (glob) |
+| **minFileSize** | `number` | optional | Minimum file size in bytes |
+| **maxFileSize** | `number` | optional | Maximum file size in bytes |
+| **allowedExtensions** | `string[]` | optional | Allowed file extensions |
+| **blockedExtensions** | `string[]` | optional | Blocked file extensions |
+
+
+---
+
+## FileMetadataConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **extractMetadata** | `boolean` | ✅ | Extract file metadata |
+| **metadataFields** | `Enum<'content_type' \| 'file_size' \| 'last_modified' \| 'etag' \| 'checksum' \| 'creator' \| 'created_at' \| 'custom'>[]` | optional | Metadata fields to extract |
+| **customMetadata** | `Record` | optional | Custom metadata key-value pairs |
+
+
+---
+
+## FileStorageConnector
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Unique connector identifier |
+| **label** | `string` | ✅ | Display label |
+| **type** | `'file_storage'` | ✅ | |
+| **description** | `string` | optional | Connector description |
+| **icon** | `string` | optional | Icon identifier |
+| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
+| **provider** | `Enum<'s3' \| 'azure_blob' \| 'gcs' \| 'dropbox' \| 'box' \| 'onedrive' \| 'google_drive' \| 'sharepoint' \| 'ftp' \| 'local' \| 'custom'>` | ✅ | File storage provider type |
+| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
+| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
+| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
+| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
+| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
+| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
+| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
+| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
+| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
+| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
+| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
+| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
+| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
+| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
+| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
+| **metadata** | `Record` | optional | Custom connector metadata |
+| **storageConfig** | `{ endpoint?: string; region?: string; pathStyle?: boolean }` | optional | Storage configuration |
+| **buckets** | `{ name: string; label: string; bucketName: string; region?: string; … }[]` | ✅ | Buckets/containers to sync |
+| **metadataConfig** | `{ extractMetadata?: boolean; metadataFields?: Enum<'content_type' \| 'file_size' \| 'last_modified' \| 'etag' \| 'checksum' \| 'creator' \| 'created_at' \| 'custom'>[]; customMetadata?: Record }` | optional | Metadata extraction configuration |
+| **multipartConfig** | `{ enabled?: boolean; partSize?: number; maxConcurrentParts?: number; threshold?: number }` | optional | Multipart upload configuration |
+| **versioningConfig** | `{ enabled?: boolean; maxVersions?: number; retentionDays?: number }` | optional | File versioning configuration |
+| **encryption** | `{ enabled?: boolean; algorithm?: Enum<'AES256' \| 'aws:kms' \| 'custom'>; kmsKeyId?: string }` | optional | Encryption configuration |
+| **lifecyclePolicy** | `{ enabled?: boolean; deleteAfterDays?: number; archiveAfterDays?: number }` | optional | Lifecycle policy |
+| **contentProcessing** | `{ extractText?: boolean; generateThumbnails?: boolean; thumbnailSizes?: { width: number; height: number }[]; virusScan?: boolean }` | optional | Content processing configuration |
+| **bufferSize** | `number` | optional | Buffer size in bytes |
+| **transferAcceleration** | `boolean` | optional | Enable transfer acceleration |
+
+
+---
+
+## FileStorageProvider
+
+File storage provider type
+
+### Allowed Values
+
+* `s3`
+* `azure_blob`
+* `gcs`
+* `dropbox`
+* `box`
+* `onedrive`
+* `google_drive`
+* `sharepoint`
+* `ftp`
+* `local`
+* `custom`
+
+
+---
+
+## FileVersioningConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **enabled** | `boolean` | ✅ | Enable file versioning |
+| **maxVersions** | `number` | optional | Maximum versions to retain |
+| **retentionDays** | `number` | optional | Version retention period in days |
+
+
+---
+
+## StorageBucket
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Bucket identifier in ObjectStack (snake_case) |
+| **label** | `string` | ✅ | Display label |
+| **bucketName** | `string` | ✅ | Actual bucket/container name in storage system |
+| **region** | `string` | optional | Storage region |
+| **enabled** | `boolean` | ✅ | Enable sync for this bucket |
+| **prefix** | `string` | optional | Prefix/path within bucket |
+| **accessPattern** | `Enum<'public_read' \| 'private' \| 'authenticated_read' \| 'bucket_owner_read' \| 'bucket_owner_full'>` | optional | Access pattern |
+| **fileFilters** | `{ includePatterns?: string[]; excludePatterns?: string[]; minFileSize?: number; maxFileSize?: number; … }` | optional | File filter configuration |
+
+
+---
+
diff --git a/content/docs/references/integration/connector-github.mdx b/content/docs/references/integration/connector-github.mdx
new file mode 100644
index 0000000000..df78fc7bbb
--- /dev/null
+++ b/content/docs/references/integration/connector-github.mdx
@@ -0,0 +1,246 @@
+---
+title: Connector Github
+description: Connector Github protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+GitHub Connector Protocol
+
+Specialized connector for GitHub integration enabling automated
+
+version control operations, CI/CD workflows, and release management.
+
+Use Cases:
+
+- Automated code commits and pull requests
+
+- GitHub Actions workflow management
+
+- Issue and project tracking
+
+- Release and tag management
+
+- Repository administration
+
+@example
+
+```typescript
+
+import \{ GitHubConnector \} from '@objectstack/spec/integration';
+
+const githubConnector: GitHubConnector = \{
+
+name: 'github_enterprise',
+
+label: 'GitHub Enterprise',
+
+type: 'saas',
+
+provider: 'github',
+
+baseUrl: 'https://api.github.com',
+
+authentication: \{
+
+type: 'oauth2',
+
+clientId: '$\{GITHUB_CLIENT_ID\}',
+
+clientSecret: '$\{GITHUB_CLIENT_SECRET\}',
+
+authorizationUrl: 'https://github.com/login/oauth/authorize',
+
+tokenUrl: 'https://github.com/login/oauth/access_token',
+
+grantType: 'authorization_code',
+
+scopes: ['repo', 'workflow', 'admin:org'],
+
+\},
+
+repositories: [
+
+\{
+
+owner: 'objectstack-ai',
+
+name: 'spec',
+
+defaultBranch: 'main',
+
+autoMerge: false,
+
+\},
+
+],
+
+\};
+
+```
+
+
+**Source:** `packages/spec/src/integration/connector/github.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { GitHubActionsWorkflow, GitHubCommitConfig, GitHubConnector, GitHubIssueTracking, GitHubProvider, GitHubPullRequestConfig, GitHubReleaseConfig, GitHubRepository } from '@objectstack/spec/integration';
+import type { GitHubActionsWorkflow, GitHubCommitConfig, GitHubConnector, GitHubIssueTracking, GitHubProvider, GitHubPullRequestConfig, GitHubReleaseConfig, GitHubRepository } from '@objectstack/spec/integration';
+
+// Validate data
+const result = GitHubActionsWorkflow.parse(data);
+```
+
+---
+
+## GitHubActionsWorkflow
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Workflow name |
+| **path** | `string` | ✅ | Workflow file path (e.g., .github/workflows/ci.yml) |
+| **enabled** | `boolean` | ✅ | Enable workflow |
+| **triggers** | `Enum<'push' \| 'pull_request' \| 'release' \| 'schedule' \| 'workflow_dispatch' \| 'repository_dispatch'>[]` | optional | Workflow triggers |
+| **env** | `Record` | optional | Environment variables |
+| **secrets** | `string[]` | optional | Required secrets |
+
+
+---
+
+## GitHubCommitConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **authorName** | `string` | optional | Commit author name |
+| **authorEmail** | `string` | optional | Commit author email |
+| **signCommits** | `boolean` | ✅ | Sign commits with GPG |
+| **messageTemplate** | `string` | optional | Commit message template |
+| **useConventionalCommits** | `boolean` | ✅ | Use conventional commits format |
+
+
+---
+
+## GitHubConnector
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Unique connector identifier |
+| **label** | `string` | ✅ | Display label |
+| **type** | `'saas'` | ✅ | |
+| **description** | `string` | optional | Connector description |
+| **icon** | `string` | optional | Icon identifier |
+| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
+| **provider** | `Enum<'github' \| 'github_enterprise'>` | ✅ | GitHub provider |
+| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
+| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
+| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
+| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
+| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
+| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
+| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
+| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
+| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
+| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
+| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
+| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
+| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
+| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
+| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
+| **metadata** | `Record` | optional | Custom connector metadata |
+| **baseUrl** | `string` | optional | GitHub API base URL |
+| **repositories** | `{ owner: string; name: string; defaultBranch?: string; autoMerge?: boolean; … }[]` | ✅ | Repositories to manage |
+| **commitConfig** | `{ authorName?: string; authorEmail?: string; signCommits?: boolean; messageTemplate?: string; … }` | optional | Commit configuration |
+| **pullRequestConfig** | `{ titleTemplate?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; bodyTemplate?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; defaultReviewers?: string[]; defaultAssignees?: string[]; … }` | optional | Pull request configuration |
+| **workflows** | `{ name: string; path: string; enabled?: boolean; triggers?: Enum<'push' \| 'pull_request' \| 'release' \| 'schedule' \| 'workflow_dispatch' \| 'repository_dispatch'>[]; … }[]` | optional | GitHub Actions workflows |
+| **releaseConfig** | `{ tagPattern?: string; semanticVersioning?: boolean; autoReleaseNotes?: boolean; releaseNameTemplate?: string; … }` | optional | Release configuration |
+| **issueTracking** | `{ enabled?: boolean; defaultLabels?: string[]; templatePaths?: string[]; autoAssign?: boolean; … }` | optional | Issue tracking configuration |
+| **enableWebhooks** | `boolean` | optional | Enable GitHub webhooks |
+| **webhookEvents** | `Enum<'push' \| 'pull_request' \| 'issues' \| 'issue_comment' \| 'release' \| 'workflow_run' \| 'deployment' \| 'deployment_status' \| 'check_run' \| 'check_suite' \| 'status'>[]` | optional | Webhook events to subscribe to |
+
+
+---
+
+## GitHubIssueTracking
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **enabled** | `boolean` | ✅ | Enable issue tracking |
+| **defaultLabels** | `string[]` | optional | Default issue labels |
+| **templatePaths** | `string[]` | optional | Issue template paths |
+| **autoAssign** | `boolean` | ✅ | Auto-assign issues |
+| **autoCloseStale** | `{ enabled: boolean; daysBeforeStale: integer; daysBeforeClose: integer; staleLabel: string }` | optional | Auto-close stale issues configuration |
+
+
+---
+
+## GitHubProvider
+
+GitHub provider type
+
+### Allowed Values
+
+* `github`
+* `github_enterprise`
+
+
+---
+
+## GitHubPullRequestConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **titleTemplate** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | PR title template — supports `{{var}`} interpolation |
+| **bodyTemplate** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | PR body template — supports `{{var}`} interpolation |
+| **defaultReviewers** | `string[]` | optional | Default reviewers (usernames) |
+| **defaultAssignees** | `string[]` | optional | Default assignees (usernames) |
+| **defaultLabels** | `string[]` | optional | Default labels |
+| **draftByDefault** | `boolean` | optional | Create draft PRs by default |
+| **deleteHeadBranch** | `boolean` | optional | Delete head branch after merge |
+
+
+---
+
+## GitHubReleaseConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **tagPattern** | `string` | ✅ | Tag name pattern (e.g., v*, release/*) |
+| **semanticVersioning** | `boolean` | ✅ | Use semantic versioning |
+| **autoReleaseNotes** | `boolean` | ✅ | Generate release notes automatically |
+| **releaseNameTemplate** | `string` | optional | Release name template |
+| **preReleasePattern** | `string` | optional | Pre-release pattern (e.g., *-alpha, *-beta) |
+| **draftByDefault** | `boolean` | ✅ | Create draft releases by default |
+
+
+---
+
+## GitHubRepository
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **owner** | `string` | ✅ | Repository owner (organization or username) |
+| **name** | `string` | ✅ | Repository name |
+| **defaultBranch** | `string` | ✅ | Default branch name |
+| **autoMerge** | `boolean` | ✅ | Enable auto-merge for pull requests |
+| **branchProtection** | `{ requiredReviewers: integer; requireStatusChecks: boolean; enforceAdmins: boolean; allowForcePushes: boolean; … }` | optional | Branch protection configuration |
+| **topics** | `string[]` | optional | Repository topics |
+
+
+---
+
diff --git a/content/docs/references/integration/connector-message-queue.mdx b/content/docs/references/integration/connector-message-queue.mdx
new file mode 100644
index 0000000000..3af90419ab
--- /dev/null
+++ b/content/docs/references/integration/connector-message-queue.mdx
@@ -0,0 +1,171 @@
+---
+title: Connector Message Queue
+description: Connector Message Queue protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+Message Queue Connector Protocol Template
+
+Specialized connector for message queue systems (RabbitMQ, Kafka, SQS, etc.)
+
+Extends the base connector with message queue-specific features like topics,
+
+consumer groups, and message acknowledgment patterns.
+
+
+**Source:** `packages/spec/src/integration/connector/message-queue.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { AckMode, DeliveryGuarantee, DlqConfig, MessageFormat, MessageQueueConnector, ProducerConfig, TopicQueue } from '@objectstack/spec/integration';
+import type { AckMode, DeliveryGuarantee, DlqConfig, MessageFormat, MessageQueueConnector, ProducerConfig, TopicQueue } from '@objectstack/spec/integration';
+
+// Validate data
+const result = AckMode.parse(data);
+```
+
+---
+
+## AckMode
+
+Message acknowledgment mode
+
+### Allowed Values
+
+* `auto`
+* `manual`
+* `client`
+
+
+---
+
+## DeliveryGuarantee
+
+Message delivery guarantee
+
+### Allowed Values
+
+* `at_most_once`
+* `at_least_once`
+* `exactly_once`
+
+
+---
+
+## DlqConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **enabled** | `boolean` | ✅ | Enable DLQ |
+| **queueName** | `string` | ✅ | Dead letter queue/topic name |
+| **maxRetries** | `number` | ✅ | Max retries before DLQ |
+| **retryDelayMs** | `number` | ✅ | Retry delay in ms |
+
+
+---
+
+## MessageFormat
+
+Message format/serialization
+
+### Allowed Values
+
+* `json`
+* `xml`
+* `protobuf`
+* `avro`
+* `text`
+* `binary`
+
+
+---
+
+## MessageQueueConnector
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Unique connector identifier |
+| **label** | `string` | ✅ | Display label |
+| **type** | `'message_queue'` | ✅ | |
+| **description** | `string` | optional | Connector description |
+| **icon** | `string` | optional | Icon identifier |
+| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
+| **provider** | `Enum<'rabbitmq' \| 'kafka' \| 'redis_pubsub' \| 'redis_streams' \| 'aws_sqs' \| 'aws_sns' \| 'google_pubsub' \| 'azure_service_bus' \| 'azure_event_hubs' \| 'nats' \| 'pulsar' \| 'activemq' \| 'custom'>` | ✅ | Message queue provider type |
+| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
+| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
+| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
+| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
+| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
+| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
+| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
+| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
+| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
+| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
+| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
+| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
+| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
+| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
+| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
+| **metadata** | `Record` | optional | Custom connector metadata |
+| **brokerConfig** | `{ brokers: string[]; clientId?: string; connectionTimeoutMs?: number; requestTimeoutMs?: number }` | ✅ | Broker connection configuration |
+| **topics** | `{ name: string; label: string; topicName: string; enabled?: boolean; … }[]` | ✅ | Topics/queues to sync |
+| **deliveryGuarantee** | `Enum<'at_most_once' \| 'at_least_once' \| 'exactly_once'>` | optional | Message delivery guarantee |
+| **sslConfig** | `{ enabled?: boolean; rejectUnauthorized?: boolean; ca?: string; cert?: string; … }` | optional | SSL/TLS configuration |
+| **saslConfig** | `{ mechanism: Enum<'plain' \| 'scram-sha-256' \| 'scram-sha-512' \| 'aws'>; username?: string; password?: string }` | optional | SASL authentication configuration |
+| **schemaRegistry** | `{ url: string; auth?: object }` | optional | Schema registry configuration |
+| **preserveOrder** | `boolean` | optional | Preserve message ordering |
+| **enableMetrics** | `boolean` | optional | Enable message queue metrics |
+| **enableTracing** | `boolean` | optional | Enable distributed tracing |
+
+
+---
+
+## ProducerConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **enabled** | `boolean` | ✅ | Enable producer |
+| **acks** | `Enum<'0' \| '1' \| 'all'>` | ✅ | Acknowledgment level |
+| **compressionType** | `Enum<'none' \| 'gzip' \| 'snappy' \| 'lz4' \| 'zstd'>` | ✅ | Compression type |
+| **batchSize** | `number` | ✅ | Batch size in bytes |
+| **lingerMs** | `number` | ✅ | Linger time in ms |
+| **maxInFlightRequests** | `number` | ✅ | Max in-flight requests |
+| **idempotence** | `boolean` | ✅ | Enable idempotent producer |
+| **transactional** | `boolean` | ✅ | Enable transactional producer |
+| **transactionTimeoutMs** | `number` | optional | Transaction timeout in ms |
+
+
+---
+
+## TopicQueue
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Topic/queue identifier in ObjectStack (snake_case) |
+| **label** | `string` | ✅ | Display label |
+| **topicName** | `string` | ✅ | Actual topic/queue name in message queue system |
+| **enabled** | `boolean` | ✅ | Enable sync for this topic/queue |
+| **mode** | `Enum<'consumer' \| 'producer' \| 'both'>` | ✅ | Consumer, producer, or both |
+| **messageFormat** | `Enum<'json' \| 'xml' \| 'protobuf' \| 'avro' \| 'text' \| 'binary'>` | ✅ | Message format/serialization |
+| **partitions** | `number` | optional | Number of partitions (for Kafka) |
+| **replicationFactor** | `number` | optional | Replication factor (for Kafka) |
+| **consumerConfig** | `{ enabled: boolean; consumerGroup?: string; concurrency: number; prefetchCount: number; … }` | optional | Consumer-specific configuration |
+| **producerConfig** | `{ enabled: boolean; acks: Enum<'0' \| '1' \| 'all'>; compressionType: Enum<'none' \| 'gzip' \| 'snappy' \| 'lz4' \| 'zstd'>; batchSize: number; … }` | optional | Producer-specific configuration |
+| **dlqConfig** | `{ enabled: boolean; queueName: string; maxRetries: number; retryDelayMs: number }` | optional | Dead letter queue configuration |
+| **routingKey** | `string` | optional | Routing key pattern |
+| **messageFilter** | `{ headers?: Record; attributes?: Record }` | optional | Message filter criteria |
+
+
+---
+
diff --git a/content/docs/references/integration/connector-saas.mdx b/content/docs/references/integration/connector-saas.mdx
new file mode 100644
index 0000000000..2cf29adfae
--- /dev/null
+++ b/content/docs/references/integration/connector-saas.mdx
@@ -0,0 +1,125 @@
+---
+title: Connector Saas
+description: Connector Saas protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+SaaS Connector Protocol Template
+
+Specialized connector for SaaS applications (Salesforce, HubSpot, Stripe, etc.)
+
+Extends the base connector with SaaS-specific features like OAuth flows,
+
+object type discovery, and API version management.
+
+
+**Source:** `packages/spec/src/integration/connector/saas.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { ApiVersionConfig, SaasConnector, SaasObjectType, SaasProvider } from '@objectstack/spec/integration';
+import type { ApiVersionConfig, SaasConnector, SaasObjectType, SaasProvider } from '@objectstack/spec/integration';
+
+// Validate data
+const result = ApiVersionConfig.parse(data);
+```
+
+---
+
+## ApiVersionConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **version** | `string` | ✅ | API version (e.g., "v2", "2023-10-01") |
+| **isDefault** | `boolean` | ✅ | Is this the default version |
+| **deprecationDate** | `string` | optional | API version deprecation date (ISO 8601) |
+| **sunsetDate** | `string` | optional | API version sunset date (ISO 8601) |
+
+
+---
+
+## SaasConnector
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Unique connector identifier |
+| **label** | `string` | ✅ | Display label |
+| **type** | `'saas'` | ✅ | |
+| **description** | `string` | optional | Connector description |
+| **icon** | `string` | optional | Icon identifier |
+| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
+| **provider** | `Enum<'salesforce' \| 'hubspot' \| 'stripe' \| 'shopify' \| 'zendesk' \| 'intercom' \| 'mailchimp' \| 'slack' \| 'microsoft_dynamics' \| 'servicenow' \| 'netsuite' \| 'custom'>` | ✅ | SaaS provider type |
+| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
+| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
+| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
+| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
+| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
+| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
+| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
+| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
+| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
+| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
+| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
+| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
+| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
+| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
+| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
+| **metadata** | `Record` | optional | Custom connector metadata |
+| **baseUrl** | `string` | ✅ | API base URL |
+| **apiVersion** | `{ version: string; isDefault?: boolean; deprecationDate?: string; sunsetDate?: string }` | optional | API version configuration |
+| **objectTypes** | `{ name: string; label: string; apiName: string; enabled?: boolean; … }[]` | ✅ | Syncable object types |
+| **oauthSettings** | `{ scopes: string[]; refreshTokenUrl?: string; revokeTokenUrl?: string; autoRefresh?: boolean }` | optional | OAuth-specific configuration |
+| **paginationConfig** | `{ type?: Enum<'cursor' \| 'offset' \| 'page'>; defaultPageSize?: number; maxPageSize?: number }` | optional | Pagination configuration |
+| **sandboxConfig** | `{ enabled?: boolean; baseUrl?: string }` | optional | Sandbox environment configuration |
+| **customHeaders** | `Record` | optional | Custom HTTP headers for all requests |
+
+
+---
+
+## SaasObjectType
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Object type name (snake_case) |
+| **label** | `string` | ✅ | Display label |
+| **apiName** | `string` | ✅ | API name in external system |
+| **enabled** | `boolean` | optional | Enable sync for this object |
+| **supportsCreate** | `boolean` | optional | Supports record creation |
+| **supportsUpdate** | `boolean` | optional | Supports record updates |
+| **supportsDelete** | `boolean` | optional | Supports record deletion |
+| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Object-specific field mappings |
+
+
+---
+
+## SaasProvider
+
+SaaS provider type
+
+### Allowed Values
+
+* `salesforce`
+* `hubspot`
+* `stripe`
+* `shopify`
+* `zendesk`
+* `intercom`
+* `mailchimp`
+* `slack`
+* `microsoft_dynamics`
+* `servicenow`
+* `netsuite`
+* `custom`
+
+
+---
+
diff --git a/content/docs/references/integration/connector-vercel.mdx b/content/docs/references/integration/connector-vercel.mdx
new file mode 100644
index 0000000000..43e1b0fd4e
--- /dev/null
+++ b/content/docs/references/integration/connector-vercel.mdx
@@ -0,0 +1,300 @@
+---
+title: Connector Vercel
+description: Connector Vercel protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+Vercel Connector Protocol
+
+Specialized connector for Vercel deployment platform enabling automated
+
+deployments, preview environments, and production releases.
+
+Use Cases:
+
+- Automated deployments from Git
+
+- Preview deployments for pull requests
+
+- Production releases
+
+- Environment variable management
+
+- Domain and SSL configuration
+
+- Edge function deployment
+
+@example
+
+```typescript
+
+import \{ VercelConnector \} from '@objectstack/spec/integration';
+
+const vercelConnector: VercelConnector = \{
+
+name: 'vercel_production',
+
+label: 'Vercel Production',
+
+type: 'saas',
+
+provider: 'vercel',
+
+baseUrl: 'https://api.vercel.com',
+
+authentication: \{
+
+type: 'bearer',
+
+token: '$\{VERCEL_TOKEN\}',
+
+\},
+
+projects: [
+
+\{
+
+name: 'objectstack-app',
+
+framework: 'nextjs',
+
+gitRepository: \{
+
+type: 'github',
+
+repo: 'objectstack-ai/app',
+
+\},
+
+\},
+
+],
+
+\};
+
+```
+
+
+**Source:** `packages/spec/src/integration/connector/vercel.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { BuildConfig, DeploymentConfig, DomainConfig, EdgeFunctionConfig, EnvironmentVariables, GitRepositoryConfig, VercelConnector, VercelFramework, VercelMonitoring, VercelProject, VercelProvider, VercelTeam } from '@objectstack/spec/integration';
+import type { BuildConfig, DeploymentConfig, DomainConfig, EdgeFunctionConfig, EnvironmentVariables, GitRepositoryConfig, VercelConnector, VercelFramework, VercelMonitoring, VercelProject, VercelProvider, VercelTeam } from '@objectstack/spec/integration';
+
+// Validate data
+const result = BuildConfig.parse(data);
+```
+
+---
+
+## BuildConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **buildCommand** | `string` | optional | Build command (e.g., npm run build) |
+| **outputDirectory** | `string` | optional | Output directory (e.g., .next, dist) |
+| **installCommand** | `string` | optional | Install command (e.g., npm install, pnpm install) |
+| **devCommand** | `string` | optional | Development command (e.g., npm run dev) |
+| **nodeVersion** | `string` | optional | Node.js version (e.g., 18.x, 20.x) |
+| **env** | `Record` | optional | Build environment variables |
+
+
+---
+
+## DeploymentConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **autoDeployment** | `boolean` | ✅ | Enable automatic deployments |
+| **regions** | `Enum<'iad1' \| 'sfo1' \| 'gru1' \| 'lhr1' \| 'fra1' \| 'sin1' \| 'syd1' \| 'hnd1' \| 'icn1'>[]` | optional | Deployment regions |
+| **enablePreview** | `boolean` | ✅ | Enable preview deployments |
+| **previewComments** | `boolean` | ✅ | Post preview URLs in PR comments |
+| **productionProtection** | `boolean` | ✅ | Require approval for production deployments |
+| **deployHooks** | `{ name: string; url: string; branch?: string }[]` | optional | Deploy hooks |
+
+
+---
+
+## DomainConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **domain** | `string` | ✅ | Domain name (e.g., app.example.com) |
+| **httpsRedirect** | `boolean` | ✅ | Redirect HTTP to HTTPS |
+| **customCertificate** | `{ cert: string; key: string; ca?: string }` | optional | Custom SSL certificate |
+| **gitBranch** | `string` | optional | Git branch to deploy to this domain |
+
+
+---
+
+## EdgeFunctionConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Edge function name |
+| **path** | `string` | ✅ | Function path (e.g., /api/*) |
+| **regions** | `string[]` | optional | Specific regions for this function |
+| **memoryLimit** | `integer` | ✅ | Memory limit in MB |
+| **timeout** | `integer` | ✅ | Timeout in seconds |
+
+
+---
+
+## EnvironmentVariables
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **key** | `string` | ✅ | Environment variable name |
+| **value** | `string` | ✅ | Environment variable value |
+| **target** | `Enum<'production' \| 'preview' \| 'development'>[]` | ✅ | Target environments |
+| **isSecret** | `boolean` | ✅ | Encrypt this variable |
+| **gitBranch** | `string` | optional | Specific git branch |
+
+
+---
+
+## GitRepositoryConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `Enum<'github' \| 'gitlab' \| 'bitbucket'>` | ✅ | Git provider |
+| **repo** | `string` | ✅ | Repository identifier (e.g., owner/repo) |
+| **productionBranch** | `string` | ✅ | Production branch name |
+| **autoDeployProduction** | `boolean` | ✅ | Auto-deploy production branch |
+| **autoDeployPreview** | `boolean` | ✅ | Auto-deploy preview branches |
+
+
+---
+
+## VercelConnector
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Unique connector identifier |
+| **label** | `string` | ✅ | Display label |
+| **type** | `'saas'` | ✅ | |
+| **description** | `string` | optional | Connector description |
+| **icon** | `string` | optional | Icon identifier |
+| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
+| **provider** | `Enum<'vercel'>` | ✅ | Vercel provider |
+| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
+| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
+| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
+| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
+| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
+| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
+| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
+| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
+| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
+| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
+| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
+| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
+| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
+| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
+| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
+| **metadata** | `Record` | optional | Custom connector metadata |
+| **baseUrl** | `string` | optional | Vercel API base URL |
+| **team** | `{ teamId?: string; teamName?: string }` | optional | Vercel team configuration |
+| **projects** | `{ name: string; framework?: Enum<'nextjs' \| 'react' \| 'vue' \| 'nuxtjs' \| 'gatsby' \| 'remix' \| 'astro' \| 'sveltekit' \| 'solid' \| 'angular' \| 'static' \| 'other'>; gitRepository?: object; buildConfig?: object; … }[]` | ✅ | Vercel projects |
+| **monitoring** | `{ enableWebAnalytics?: boolean; enableSpeedInsights?: boolean; logDrains?: { name: string; url: string; headers?: Record; sources?: Enum<'static' \| 'lambda' \| 'edge'>[] }[] }` | optional | Monitoring configuration |
+| **enableWebhooks** | `boolean` | optional | Enable Vercel webhooks |
+| **webhookEvents** | `Enum<'deployment.created' \| 'deployment.succeeded' \| 'deployment.failed' \| 'deployment.ready' \| 'deployment.error' \| 'deployment.canceled' \| 'deployment-checks-completed' \| 'deployment-prepared' \| 'project.created' \| 'project.removed'>[]` | optional | Webhook events to subscribe to |
+
+
+---
+
+## VercelFramework
+
+Frontend framework
+
+### Allowed Values
+
+* `nextjs`
+* `react`
+* `vue`
+* `nuxtjs`
+* `gatsby`
+* `remix`
+* `astro`
+* `sveltekit`
+* `solid`
+* `angular`
+* `static`
+* `other`
+
+
+---
+
+## VercelMonitoring
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **enableWebAnalytics** | `boolean` | ✅ | Enable Vercel Web Analytics |
+| **enableSpeedInsights** | `boolean` | ✅ | Enable Vercel Speed Insights |
+| **logDrains** | `{ name: string; url: string; headers?: Record; sources?: Enum<'static' \| 'lambda' \| 'edge'>[] }[]` | optional | Log drains configuration |
+
+
+---
+
+## VercelProject
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Vercel project name |
+| **framework** | `Enum<'nextjs' \| 'react' \| 'vue' \| 'nuxtjs' \| 'gatsby' \| 'remix' \| 'astro' \| 'sveltekit' \| 'solid' \| 'angular' \| 'static' \| 'other'>` | optional | Frontend framework |
+| **gitRepository** | `{ type: Enum<'github' \| 'gitlab' \| 'bitbucket'>; repo: string; productionBranch: string; autoDeployProduction: boolean; … }` | optional | Git repository configuration |
+| **buildConfig** | `{ buildCommand?: string; outputDirectory?: string; installCommand?: string; devCommand?: string; … }` | optional | Build configuration |
+| **deploymentConfig** | `{ autoDeployment: boolean; regions?: Enum<'iad1' \| 'sfo1' \| 'gru1' \| 'lhr1' \| 'fra1' \| 'sin1' \| 'syd1' \| 'hnd1' \| 'icn1'>[]; enablePreview: boolean; previewComments: boolean; … }` | optional | Deployment configuration |
+| **domains** | `{ domain: string; httpsRedirect: boolean; customCertificate?: object; gitBranch?: string }[]` | optional | Custom domains |
+| **environmentVariables** | `{ key: string; value: string; target: Enum<'production' \| 'preview' \| 'development'>[]; isSecret: boolean; … }[]` | optional | Environment variables |
+| **edgeFunctions** | `{ name: string; path: string; regions?: string[]; memoryLimit: integer; … }[]` | optional | Edge functions |
+| **rootDirectory** | `string` | optional | Root directory (for monorepos) |
+
+
+---
+
+## VercelProvider
+
+Vercel provider type
+
+### Allowed Values
+
+* `vercel`
+
+
+---
+
+## VercelTeam
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **teamId** | `string` | optional | Team ID or slug |
+| **teamName** | `string` | optional | Team name |
+
+
+---
+
diff --git a/content/docs/references/integration/http.mdx b/content/docs/references/integration/http.mdx
index 17663c9578..3ad0fbc34a 100644
--- a/content/docs/references/integration/http.mdx
+++ b/content/docs/references/integration/http.mdx
@@ -5,10 +5,6 @@ description: Http protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/integration/http.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/integration/index.mdx b/content/docs/references/integration/index.mdx
index f4cbea5ec3..2ac0bbf02b 100644
--- a/content/docs/references/integration/index.mdx
+++ b/content/docs/references/integration/index.mdx
@@ -7,4 +7,10 @@ This section contains all protocol schemas for the integration layer of ObjectSt
+
+
+
+
+
+
diff --git a/content/docs/references/integration/mapping.mdx b/content/docs/references/integration/mapping.mdx
index b82d0245a4..6ec881b433 100644
--- a/content/docs/references/integration/mapping.mdx
+++ b/content/docs/references/integration/mapping.mdx
@@ -5,10 +5,6 @@ description: Mapping protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/integration/mapping.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/integration/message-queue.mdx b/content/docs/references/integration/message-queue.mdx
index 4e9df76ec0..8f8aae5f92 100644
--- a/content/docs/references/integration/message-queue.mdx
+++ b/content/docs/references/integration/message-queue.mdx
@@ -5,10 +5,6 @@ description: Message Queue protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/integration/message-queue.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/integration/meta.json b/content/docs/references/integration/meta.json
index d672bdfa95..ab617bca90 100644
--- a/content/docs/references/integration/meta.json
+++ b/content/docs/references/integration/meta.json
@@ -11,7 +11,13 @@
"object-storage",
"offline",
"---Tenancy---",
- "misc",
- "tenant"
+ "tenant",
+ "---More---",
+ "connector-database",
+ "connector-file-storage",
+ "connector-github",
+ "connector-message-queue",
+ "connector-saas",
+ "connector-vercel"
]
}
\ No newline at end of file
diff --git a/content/docs/references/integration/misc.mdx b/content/docs/references/integration/misc.mdx
deleted file mode 100644
index cdc46335fc..0000000000
--- a/content/docs/references/integration/misc.mdx
+++ /dev/null
@@ -1,860 +0,0 @@
----
-title: Misc
-description: Misc protocol schemas
----
-
-{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-
-**Source:** `packages/spec/src/integration/misc.zod.ts`
-
-
-## TypeScript Usage
-
-```typescript
-import { AckMode, ApiVersionConfig, BuildConfig, CdcConfig, DatabaseConnector, DatabasePoolConfig, DatabaseTable, DeliveryGuarantee, DeploymentConfig, DlqConfig, DomainConfig, EdgeFunctionConfig, EnvironmentVariables, FileAccessPattern, FileFilterConfig, FileMetadataConfig, FileStorageConnector, FileStorageProvider, FileVersioningConfig, GitHubActionsWorkflow, GitHubCommitConfig, GitHubConnector, GitHubIssueTracking, GitHubProvider, GitHubPullRequestConfig, GitHubReleaseConfig, GitHubRepository, GitRepositoryConfig, MessageFormat, MessageQueueConnector, ProducerConfig, SaasConnector, SaasObjectType, SaasProvider, SslConfig, StorageBucket, TopicQueue, VercelConnector, VercelFramework, VercelMonitoring, VercelProject, VercelProvider, VercelTeam } from '@objectstack/spec/integration';
-import type { AckMode, ApiVersionConfig, BuildConfig, CdcConfig, DatabaseConnector, DatabasePoolConfig, DatabaseTable, DeliveryGuarantee, DeploymentConfig, DlqConfig, DomainConfig, EdgeFunctionConfig, EnvironmentVariables, FileAccessPattern, FileFilterConfig, FileMetadataConfig, FileStorageConnector, FileStorageProvider, FileVersioningConfig, GitHubActionsWorkflow, GitHubCommitConfig, GitHubConnector, GitHubIssueTracking, GitHubProvider, GitHubPullRequestConfig, GitHubReleaseConfig, GitHubRepository, GitRepositoryConfig, MessageFormat, MessageQueueConnector, ProducerConfig, SaasConnector, SaasObjectType, SaasProvider, SslConfig, StorageBucket, TopicQueue, VercelConnector, VercelFramework, VercelMonitoring, VercelProject, VercelProvider, VercelTeam } from '@objectstack/spec/integration';
-
-// Validate data
-const result = AckMode.parse(data);
-```
-
----
-
-## AckMode
-
-Message acknowledgment mode
-
-### Allowed Values
-
-* `auto`
-* `manual`
-* `client`
-
-
----
-
-## ApiVersionConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **version** | `string` | ✅ | API version (e.g., "v2", "2023-10-01") |
-| **isDefault** | `boolean` | ✅ | Is this the default version |
-| **deprecationDate** | `string` | optional | API version deprecation date (ISO 8601) |
-| **sunsetDate** | `string` | optional | API version sunset date (ISO 8601) |
-
-
----
-
-## BuildConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **buildCommand** | `string` | optional | Build command (e.g., npm run build) |
-| **outputDirectory** | `string` | optional | Output directory (e.g., .next, dist) |
-| **installCommand** | `string` | optional | Install command (e.g., npm install, pnpm install) |
-| **devCommand** | `string` | optional | Development command (e.g., npm run dev) |
-| **nodeVersion** | `string` | optional | Node.js version (e.g., 18.x, 20.x) |
-| **env** | `Record` | optional | Build environment variables |
-
-
----
-
-## CdcConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **enabled** | `boolean` | ✅ | Enable CDC |
-| **method** | `Enum<'log_based' \| 'trigger_based' \| 'query_based' \| 'custom'>` | ✅ | CDC method |
-| **slotName** | `string` | optional | Replication slot name (for log-based CDC) |
-| **publicationName** | `string` | optional | Publication name (for PostgreSQL) |
-| **startPosition** | `string` | optional | Starting position/LSN for CDC stream |
-| **batchSize** | `number` | ✅ | CDC batch size |
-| **pollIntervalMs** | `number` | ✅ | CDC polling interval in ms |
-
-
----
-
-## DatabaseConnector
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Unique connector identifier |
-| **label** | `string` | ✅ | Display label |
-| **type** | `'database'` | ✅ | |
-| **description** | `string` | optional | Connector description |
-| **icon** | `string` | optional | Icon identifier |
-| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
-| **provider** | `Enum<'postgresql' \| 'mysql' \| 'mariadb' \| 'mssql' \| 'oracle' \| 'mongodb' \| 'redis' \| 'cassandra' \| 'snowflake' \| 'bigquery' \| 'redshift' \| 'custom'>` | ✅ | Database provider type |
-| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
-| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
-| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
-| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
-| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
-| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
-| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
-| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
-| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
-| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
-| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
-| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
-| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
-| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
-| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
-| **metadata** | `Record` | optional | Custom connector metadata |
-| **connectionConfig** | `{ host: string; port: number; database: string; username: string; … }` | ✅ | Database connection configuration |
-| **poolConfig** | `{ min?: number; max?: number; idleTimeoutMs?: number; connectionTimeoutMs?: number; … }` | optional | Connection pool configuration |
-| **sslConfig** | `{ enabled?: boolean; rejectUnauthorized?: boolean; ca?: string; cert?: string; … }` | optional | SSL/TLS configuration |
-| **tables** | `{ name: string; label: string; schema?: string; tableName: string; … }[]` | ✅ | Tables to sync |
-| **cdcConfig** | `{ enabled?: boolean; method: Enum<'log_based' \| 'trigger_based' \| 'query_based' \| 'custom'>; slotName?: string; publicationName?: string; … }` | optional | CDC configuration |
-| **readReplicaConfig** | `{ enabled?: boolean; hosts: { host: string; port: number; weight?: number }[] }` | optional | Read replica configuration |
-| **queryTimeoutMs** | `number` | optional | Query timeout in ms |
-| **enableQueryLogging** | `boolean` | optional | Enable SQL query logging |
-
-
----
-
-## DatabasePoolConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **min** | `number` | ✅ | Minimum connections in pool |
-| **max** | `number` | ✅ | Maximum connections in pool |
-| **idleTimeoutMs** | `number` | ✅ | Idle connection timeout in ms |
-| **connectionTimeoutMs** | `number` | ✅ | Connection establishment timeout in ms |
-| **acquireTimeoutMs** | `number` | ✅ | Connection acquisition timeout in ms |
-| **evictionRunIntervalMs** | `number` | ✅ | Connection eviction check interval in ms |
-| **testOnBorrow** | `boolean` | ✅ | Test connection before use |
-
-
----
-
-## DatabaseTable
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Table name in ObjectStack (snake_case) |
-| **label** | `string` | ✅ | Display label |
-| **schema** | `string` | optional | Database schema name |
-| **tableName** | `string` | ✅ | Actual table name in database |
-| **primaryKey** | `string` | ✅ | Primary key column |
-| **enabled** | `boolean` | optional | Enable sync for this table |
-| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Table-specific field mappings |
-| **whereClause** | `string` | optional | SQL WHERE clause for filtering |
-
-
----
-
-## DeliveryGuarantee
-
-Message delivery guarantee
-
-### Allowed Values
-
-* `at_most_once`
-* `at_least_once`
-* `exactly_once`
-
-
----
-
-## DeploymentConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **autoDeployment** | `boolean` | ✅ | Enable automatic deployments |
-| **regions** | `Enum<'iad1' \| 'sfo1' \| 'gru1' \| 'lhr1' \| 'fra1' \| 'sin1' \| 'syd1' \| 'hnd1' \| 'icn1'>[]` | optional | Deployment regions |
-| **enablePreview** | `boolean` | ✅ | Enable preview deployments |
-| **previewComments** | `boolean` | ✅ | Post preview URLs in PR comments |
-| **productionProtection** | `boolean` | ✅ | Require approval for production deployments |
-| **deployHooks** | `{ name: string; url: string; branch?: string }[]` | optional | Deploy hooks |
-
-
----
-
-## DlqConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **enabled** | `boolean` | ✅ | Enable DLQ |
-| **queueName** | `string` | ✅ | Dead letter queue/topic name |
-| **maxRetries** | `number` | ✅ | Max retries before DLQ |
-| **retryDelayMs** | `number` | ✅ | Retry delay in ms |
-
-
----
-
-## DomainConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **domain** | `string` | ✅ | Domain name (e.g., app.example.com) |
-| **httpsRedirect** | `boolean` | ✅ | Redirect HTTP to HTTPS |
-| **customCertificate** | `{ cert: string; key: string; ca?: string }` | optional | Custom SSL certificate |
-| **gitBranch** | `string` | optional | Git branch to deploy to this domain |
-
-
----
-
-## EdgeFunctionConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Edge function name |
-| **path** | `string` | ✅ | Function path (e.g., /api/*) |
-| **regions** | `string[]` | optional | Specific regions for this function |
-| **memoryLimit** | `integer` | ✅ | Memory limit in MB |
-| **timeout** | `integer` | ✅ | Timeout in seconds |
-
-
----
-
-## EnvironmentVariables
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **key** | `string` | ✅ | Environment variable name |
-| **value** | `string` | ✅ | Environment variable value |
-| **target** | `Enum<'production' \| 'preview' \| 'development'>[]` | ✅ | Target environments |
-| **isSecret** | `boolean` | ✅ | Encrypt this variable |
-| **gitBranch** | `string` | optional | Specific git branch |
-
-
----
-
-## FileAccessPattern
-
-File access pattern
-
-### Allowed Values
-
-* `public_read`
-* `private`
-* `authenticated_read`
-* `bucket_owner_read`
-* `bucket_owner_full`
-
-
----
-
-## FileFilterConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **includePatterns** | `string[]` | optional | File patterns to include (glob) |
-| **excludePatterns** | `string[]` | optional | File patterns to exclude (glob) |
-| **minFileSize** | `number` | optional | Minimum file size in bytes |
-| **maxFileSize** | `number` | optional | Maximum file size in bytes |
-| **allowedExtensions** | `string[]` | optional | Allowed file extensions |
-| **blockedExtensions** | `string[]` | optional | Blocked file extensions |
-
-
----
-
-## FileMetadataConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **extractMetadata** | `boolean` | ✅ | Extract file metadata |
-| **metadataFields** | `Enum<'content_type' \| 'file_size' \| 'last_modified' \| 'etag' \| 'checksum' \| 'creator' \| 'created_at' \| 'custom'>[]` | optional | Metadata fields to extract |
-| **customMetadata** | `Record` | optional | Custom metadata key-value pairs |
-
-
----
-
-## FileStorageConnector
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Unique connector identifier |
-| **label** | `string` | ✅ | Display label |
-| **type** | `'file_storage'` | ✅ | |
-| **description** | `string` | optional | Connector description |
-| **icon** | `string` | optional | Icon identifier |
-| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
-| **provider** | `Enum<'s3' \| 'azure_blob' \| 'gcs' \| 'dropbox' \| 'box' \| 'onedrive' \| 'google_drive' \| 'sharepoint' \| 'ftp' \| 'local' \| 'custom'>` | ✅ | File storage provider type |
-| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
-| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
-| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
-| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
-| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
-| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
-| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
-| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
-| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
-| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
-| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
-| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
-| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
-| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
-| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
-| **metadata** | `Record` | optional | Custom connector metadata |
-| **storageConfig** | `{ endpoint?: string; region?: string; pathStyle?: boolean }` | optional | Storage configuration |
-| **buckets** | `{ name: string; label: string; bucketName: string; region?: string; … }[]` | ✅ | Buckets/containers to sync |
-| **metadataConfig** | `{ extractMetadata?: boolean; metadataFields?: Enum<'content_type' \| 'file_size' \| 'last_modified' \| 'etag' \| 'checksum' \| 'creator' \| 'created_at' \| 'custom'>[]; customMetadata?: Record }` | optional | Metadata extraction configuration |
-| **multipartConfig** | `{ enabled?: boolean; partSize?: number; maxConcurrentParts?: number; threshold?: number }` | optional | Multipart upload configuration |
-| **versioningConfig** | `{ enabled?: boolean; maxVersions?: number; retentionDays?: number }` | optional | File versioning configuration |
-| **encryption** | `{ enabled?: boolean; algorithm?: Enum<'AES256' \| 'aws:kms' \| 'custom'>; kmsKeyId?: string }` | optional | Encryption configuration |
-| **lifecyclePolicy** | `{ enabled?: boolean; deleteAfterDays?: number; archiveAfterDays?: number }` | optional | Lifecycle policy |
-| **contentProcessing** | `{ extractText?: boolean; generateThumbnails?: boolean; thumbnailSizes?: { width: number; height: number }[]; virusScan?: boolean }` | optional | Content processing configuration |
-| **bufferSize** | `number` | optional | Buffer size in bytes |
-| **transferAcceleration** | `boolean` | optional | Enable transfer acceleration |
-
-
----
-
-## FileStorageProvider
-
-File storage provider type
-
-### Allowed Values
-
-* `s3`
-* `azure_blob`
-* `gcs`
-* `dropbox`
-* `box`
-* `onedrive`
-* `google_drive`
-* `sharepoint`
-* `ftp`
-* `local`
-* `custom`
-
-
----
-
-## FileVersioningConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **enabled** | `boolean` | ✅ | Enable file versioning |
-| **maxVersions** | `number` | optional | Maximum versions to retain |
-| **retentionDays** | `number` | optional | Version retention period in days |
-
-
----
-
-## GitHubActionsWorkflow
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Workflow name |
-| **path** | `string` | ✅ | Workflow file path (e.g., .github/workflows/ci.yml) |
-| **enabled** | `boolean` | ✅ | Enable workflow |
-| **triggers** | `Enum<'push' \| 'pull_request' \| 'release' \| 'schedule' \| 'workflow_dispatch' \| 'repository_dispatch'>[]` | optional | Workflow triggers |
-| **env** | `Record` | optional | Environment variables |
-| **secrets** | `string[]` | optional | Required secrets |
-
-
----
-
-## GitHubCommitConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **authorName** | `string` | optional | Commit author name |
-| **authorEmail** | `string` | optional | Commit author email |
-| **signCommits** | `boolean` | ✅ | Sign commits with GPG |
-| **messageTemplate** | `string` | optional | Commit message template |
-| **useConventionalCommits** | `boolean` | ✅ | Use conventional commits format |
-
-
----
-
-## GitHubConnector
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Unique connector identifier |
-| **label** | `string` | ✅ | Display label |
-| **type** | `'saas'` | ✅ | |
-| **description** | `string` | optional | Connector description |
-| **icon** | `string` | optional | Icon identifier |
-| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
-| **provider** | `Enum<'github' \| 'github_enterprise'>` | ✅ | GitHub provider |
-| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
-| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
-| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
-| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
-| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
-| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
-| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
-| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
-| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
-| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
-| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
-| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
-| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
-| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
-| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
-| **metadata** | `Record` | optional | Custom connector metadata |
-| **baseUrl** | `string` | optional | GitHub API base URL |
-| **repositories** | `{ owner: string; name: string; defaultBranch?: string; autoMerge?: boolean; … }[]` | ✅ | Repositories to manage |
-| **commitConfig** | `{ authorName?: string; authorEmail?: string; signCommits?: boolean; messageTemplate?: string; … }` | optional | Commit configuration |
-| **pullRequestConfig** | `{ titleTemplate?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; bodyTemplate?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; defaultReviewers?: string[]; defaultAssignees?: string[]; … }` | optional | Pull request configuration |
-| **workflows** | `{ name: string; path: string; enabled?: boolean; triggers?: Enum<'push' \| 'pull_request' \| 'release' \| 'schedule' \| 'workflow_dispatch' \| 'repository_dispatch'>[]; … }[]` | optional | GitHub Actions workflows |
-| **releaseConfig** | `{ tagPattern?: string; semanticVersioning?: boolean; autoReleaseNotes?: boolean; releaseNameTemplate?: string; … }` | optional | Release configuration |
-| **issueTracking** | `{ enabled?: boolean; defaultLabels?: string[]; templatePaths?: string[]; autoAssign?: boolean; … }` | optional | Issue tracking configuration |
-| **enableWebhooks** | `boolean` | optional | Enable GitHub webhooks |
-| **webhookEvents** | `Enum<'push' \| 'pull_request' \| 'issues' \| 'issue_comment' \| 'release' \| 'workflow_run' \| 'deployment' \| 'deployment_status' \| 'check_run' \| 'check_suite' \| 'status'>[]` | optional | Webhook events to subscribe to |
-
-
----
-
-## GitHubIssueTracking
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **enabled** | `boolean` | ✅ | Enable issue tracking |
-| **defaultLabels** | `string[]` | optional | Default issue labels |
-| **templatePaths** | `string[]` | optional | Issue template paths |
-| **autoAssign** | `boolean` | ✅ | Auto-assign issues |
-| **autoCloseStale** | `{ enabled: boolean; daysBeforeStale: integer; daysBeforeClose: integer; staleLabel: string }` | optional | Auto-close stale issues configuration |
-
-
----
-
-## GitHubProvider
-
-GitHub provider type
-
-### Allowed Values
-
-* `github`
-* `github_enterprise`
-
-
----
-
-## GitHubPullRequestConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **titleTemplate** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | PR title template — supports `{{var}`} interpolation |
-| **bodyTemplate** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | PR body template — supports `{{var}`} interpolation |
-| **defaultReviewers** | `string[]` | optional | Default reviewers (usernames) |
-| **defaultAssignees** | `string[]` | optional | Default assignees (usernames) |
-| **defaultLabels** | `string[]` | optional | Default labels |
-| **draftByDefault** | `boolean` | optional | Create draft PRs by default |
-| **deleteHeadBranch** | `boolean` | optional | Delete head branch after merge |
-
-
----
-
-## GitHubReleaseConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **tagPattern** | `string` | ✅ | Tag name pattern (e.g., v*, release/*) |
-| **semanticVersioning** | `boolean` | ✅ | Use semantic versioning |
-| **autoReleaseNotes** | `boolean` | ✅ | Generate release notes automatically |
-| **releaseNameTemplate** | `string` | optional | Release name template |
-| **preReleasePattern** | `string` | optional | Pre-release pattern (e.g., *-alpha, *-beta) |
-| **draftByDefault** | `boolean` | ✅ | Create draft releases by default |
-
-
----
-
-## GitHubRepository
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **owner** | `string` | ✅ | Repository owner (organization or username) |
-| **name** | `string` | ✅ | Repository name |
-| **defaultBranch** | `string` | ✅ | Default branch name |
-| **autoMerge** | `boolean` | ✅ | Enable auto-merge for pull requests |
-| **branchProtection** | `{ requiredReviewers: integer; requireStatusChecks: boolean; enforceAdmins: boolean; allowForcePushes: boolean; … }` | optional | Branch protection configuration |
-| **topics** | `string[]` | optional | Repository topics |
-
-
----
-
-## GitRepositoryConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **type** | `Enum<'github' \| 'gitlab' \| 'bitbucket'>` | ✅ | Git provider |
-| **repo** | `string` | ✅ | Repository identifier (e.g., owner/repo) |
-| **productionBranch** | `string` | ✅ | Production branch name |
-| **autoDeployProduction** | `boolean` | ✅ | Auto-deploy production branch |
-| **autoDeployPreview** | `boolean` | ✅ | Auto-deploy preview branches |
-
-
----
-
-## MessageFormat
-
-Message format/serialization
-
-### Allowed Values
-
-* `json`
-* `xml`
-* `protobuf`
-* `avro`
-* `text`
-* `binary`
-
-
----
-
-## MessageQueueConnector
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Unique connector identifier |
-| **label** | `string` | ✅ | Display label |
-| **type** | `'message_queue'` | ✅ | |
-| **description** | `string` | optional | Connector description |
-| **icon** | `string` | optional | Icon identifier |
-| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
-| **provider** | `Enum<'rabbitmq' \| 'kafka' \| 'redis_pubsub' \| 'redis_streams' \| 'aws_sqs' \| 'aws_sns' \| 'google_pubsub' \| 'azure_service_bus' \| 'azure_event_hubs' \| 'nats' \| 'pulsar' \| 'activemq' \| 'custom'>` | ✅ | Message queue provider type |
-| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
-| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
-| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
-| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
-| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
-| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
-| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
-| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
-| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
-| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
-| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
-| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
-| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
-| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
-| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
-| **metadata** | `Record` | optional | Custom connector metadata |
-| **brokerConfig** | `{ brokers: string[]; clientId?: string; connectionTimeoutMs?: number; requestTimeoutMs?: number }` | ✅ | Broker connection configuration |
-| **topics** | `{ name: string; label: string; topicName: string; enabled?: boolean; … }[]` | ✅ | Topics/queues to sync |
-| **deliveryGuarantee** | `Enum<'at_most_once' \| 'at_least_once' \| 'exactly_once'>` | optional | Message delivery guarantee |
-| **sslConfig** | `{ enabled?: boolean; rejectUnauthorized?: boolean; ca?: string; cert?: string; … }` | optional | SSL/TLS configuration |
-| **saslConfig** | `{ mechanism: Enum<'plain' \| 'scram-sha-256' \| 'scram-sha-512' \| 'aws'>; username?: string; password?: string }` | optional | SASL authentication configuration |
-| **schemaRegistry** | `{ url: string; auth?: object }` | optional | Schema registry configuration |
-| **preserveOrder** | `boolean` | optional | Preserve message ordering |
-| **enableMetrics** | `boolean` | optional | Enable message queue metrics |
-| **enableTracing** | `boolean` | optional | Enable distributed tracing |
-
-
----
-
-## ProducerConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **enabled** | `boolean` | ✅ | Enable producer |
-| **acks** | `Enum<'0' \| '1' \| 'all'>` | ✅ | Acknowledgment level |
-| **compressionType** | `Enum<'none' \| 'gzip' \| 'snappy' \| 'lz4' \| 'zstd'>` | ✅ | Compression type |
-| **batchSize** | `number` | ✅ | Batch size in bytes |
-| **lingerMs** | `number` | ✅ | Linger time in ms |
-| **maxInFlightRequests** | `number` | ✅ | Max in-flight requests |
-| **idempotence** | `boolean` | ✅ | Enable idempotent producer |
-| **transactional** | `boolean` | ✅ | Enable transactional producer |
-| **transactionTimeoutMs** | `number` | optional | Transaction timeout in ms |
-
-
----
-
-## SaasConnector
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Unique connector identifier |
-| **label** | `string` | ✅ | Display label |
-| **type** | `'saas'` | ✅ | |
-| **description** | `string` | optional | Connector description |
-| **icon** | `string` | optional | Icon identifier |
-| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
-| **provider** | `Enum<'salesforce' \| 'hubspot' \| 'stripe' \| 'shopify' \| 'zendesk' \| 'intercom' \| 'mailchimp' \| 'slack' \| 'microsoft_dynamics' \| 'servicenow' \| 'netsuite' \| 'custom'>` | ✅ | SaaS provider type |
-| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
-| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
-| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
-| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
-| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
-| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
-| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
-| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
-| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
-| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
-| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
-| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
-| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
-| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
-| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
-| **metadata** | `Record` | optional | Custom connector metadata |
-| **baseUrl** | `string` | ✅ | API base URL |
-| **apiVersion** | `{ version: string; isDefault?: boolean; deprecationDate?: string; sunsetDate?: string }` | optional | API version configuration |
-| **objectTypes** | `{ name: string; label: string; apiName: string; enabled?: boolean; … }[]` | ✅ | Syncable object types |
-| **oauthSettings** | `{ scopes: string[]; refreshTokenUrl?: string; revokeTokenUrl?: string; autoRefresh?: boolean }` | optional | OAuth-specific configuration |
-| **paginationConfig** | `{ type?: Enum<'cursor' \| 'offset' \| 'page'>; defaultPageSize?: number; maxPageSize?: number }` | optional | Pagination configuration |
-| **sandboxConfig** | `{ enabled?: boolean; baseUrl?: string }` | optional | Sandbox environment configuration |
-| **customHeaders** | `Record` | optional | Custom HTTP headers for all requests |
-
-
----
-
-## SaasObjectType
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Object type name (snake_case) |
-| **label** | `string` | ✅ | Display label |
-| **apiName** | `string` | ✅ | API name in external system |
-| **enabled** | `boolean` | optional | Enable sync for this object |
-| **supportsCreate** | `boolean` | optional | Supports record creation |
-| **supportsUpdate** | `boolean` | optional | Supports record updates |
-| **supportsDelete** | `boolean` | optional | Supports record deletion |
-| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Object-specific field mappings |
-
-
----
-
-## SaasProvider
-
-SaaS provider type
-
-### Allowed Values
-
-* `salesforce`
-* `hubspot`
-* `stripe`
-* `shopify`
-* `zendesk`
-* `intercom`
-* `mailchimp`
-* `slack`
-* `microsoft_dynamics`
-* `servicenow`
-* `netsuite`
-* `custom`
-
-
----
-
-## SslConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **enabled** | `boolean` | ✅ | Enable SSL/TLS |
-| **rejectUnauthorized** | `boolean` | ✅ | Reject unauthorized certificates |
-| **ca** | `string` | optional | Certificate Authority certificate |
-| **cert** | `string` | optional | Client certificate |
-| **key** | `string` | optional | Client private key |
-
-
----
-
-## StorageBucket
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Bucket identifier in ObjectStack (snake_case) |
-| **label** | `string` | ✅ | Display label |
-| **bucketName** | `string` | ✅ | Actual bucket/container name in storage system |
-| **region** | `string` | optional | Storage region |
-| **enabled** | `boolean` | ✅ | Enable sync for this bucket |
-| **prefix** | `string` | optional | Prefix/path within bucket |
-| **accessPattern** | `Enum<'public_read' \| 'private' \| 'authenticated_read' \| 'bucket_owner_read' \| 'bucket_owner_full'>` | optional | Access pattern |
-| **fileFilters** | `{ includePatterns?: string[]; excludePatterns?: string[]; minFileSize?: number; maxFileSize?: number; … }` | optional | File filter configuration |
-
-
----
-
-## TopicQueue
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Topic/queue identifier in ObjectStack (snake_case) |
-| **label** | `string` | ✅ | Display label |
-| **topicName** | `string` | ✅ | Actual topic/queue name in message queue system |
-| **enabled** | `boolean` | ✅ | Enable sync for this topic/queue |
-| **mode** | `Enum<'consumer' \| 'producer' \| 'both'>` | ✅ | Consumer, producer, or both |
-| **messageFormat** | `Enum<'json' \| 'xml' \| 'protobuf' \| 'avro' \| 'text' \| 'binary'>` | ✅ | Message format/serialization |
-| **partitions** | `number` | optional | Number of partitions (for Kafka) |
-| **replicationFactor** | `number` | optional | Replication factor (for Kafka) |
-| **consumerConfig** | `{ enabled: boolean; consumerGroup?: string; concurrency: number; prefetchCount: number; … }` | optional | Consumer-specific configuration |
-| **producerConfig** | `{ enabled: boolean; acks: Enum<'0' \| '1' \| 'all'>; compressionType: Enum<'none' \| 'gzip' \| 'snappy' \| 'lz4' \| 'zstd'>; batchSize: number; … }` | optional | Producer-specific configuration |
-| **dlqConfig** | `{ enabled: boolean; queueName: string; maxRetries: number; retryDelayMs: number }` | optional | Dead letter queue configuration |
-| **routingKey** | `string` | optional | Routing key pattern |
-| **messageFilter** | `{ headers?: Record; attributes?: Record }` | optional | Message filter criteria |
-
-
----
-
-## VercelConnector
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Unique connector identifier |
-| **label** | `string` | ✅ | Display label |
-| **type** | `'saas'` | ✅ | |
-| **description** | `string` | optional | Connector description |
-| **icon** | `string` | optional | Icon identifier |
-| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
-| **provider** | `Enum<'vercel'>` | ✅ | Vercel provider |
-| **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
-| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
-| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | |
-| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | Trigger definitions (not yet enforced — never read at registration; see #3197) |
-| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
-| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules |
-| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) |
-| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
-| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
-| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
-| **requestTimeoutMs** | `number` | optional | Request timeout in ms |
-| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional | Connector status |
-| **enabled** | `boolean` | optional | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor (#2612). |
-| **errorMapping** | `{ rules: { sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; … }[]; defaultCategory?: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'timeout' \| 'server_error' \| 'integration_error'>; unmappedBehavior: Enum<'passthrough' \| 'generic_error' \| 'throw'>; logUnmapped?: boolean }` | optional | Error mapping configuration |
-| **health** | `{ healthCheck?: object; circuitBreaker?: object }` | optional | Health and resilience configuration |
-| **metadata** | `Record` | optional | Custom connector metadata |
-| **baseUrl** | `string` | optional | Vercel API base URL |
-| **team** | `{ teamId?: string; teamName?: string }` | optional | Vercel team configuration |
-| **projects** | `{ name: string; framework?: Enum<'nextjs' \| 'react' \| 'vue' \| 'nuxtjs' \| 'gatsby' \| 'remix' \| 'astro' \| 'sveltekit' \| 'solid' \| 'angular' \| 'static' \| 'other'>; gitRepository?: object; buildConfig?: object; … }[]` | ✅ | Vercel projects |
-| **monitoring** | `{ enableWebAnalytics?: boolean; enableSpeedInsights?: boolean; logDrains?: { name: string; url: string; headers?: Record; sources?: Enum<'static' \| 'lambda' \| 'edge'>[] }[] }` | optional | Monitoring configuration |
-| **enableWebhooks** | `boolean` | optional | Enable Vercel webhooks |
-| **webhookEvents** | `Enum<'deployment.created' \| 'deployment.succeeded' \| 'deployment.failed' \| 'deployment.ready' \| 'deployment.error' \| 'deployment.canceled' \| 'deployment-checks-completed' \| 'deployment-prepared' \| 'project.created' \| 'project.removed'>[]` | optional | Webhook events to subscribe to |
-
-
----
-
-## VercelFramework
-
-Frontend framework
-
-### Allowed Values
-
-* `nextjs`
-* `react`
-* `vue`
-* `nuxtjs`
-* `gatsby`
-* `remix`
-* `astro`
-* `sveltekit`
-* `solid`
-* `angular`
-* `static`
-* `other`
-
-
----
-
-## VercelMonitoring
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **enableWebAnalytics** | `boolean` | ✅ | Enable Vercel Web Analytics |
-| **enableSpeedInsights** | `boolean` | ✅ | Enable Vercel Speed Insights |
-| **logDrains** | `{ name: string; url: string; headers?: Record; sources?: Enum<'static' \| 'lambda' \| 'edge'>[] }[]` | optional | Log drains configuration |
-
-
----
-
-## VercelProject
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Vercel project name |
-| **framework** | `Enum<'nextjs' \| 'react' \| 'vue' \| 'nuxtjs' \| 'gatsby' \| 'remix' \| 'astro' \| 'sveltekit' \| 'solid' \| 'angular' \| 'static' \| 'other'>` | optional | Frontend framework |
-| **gitRepository** | `{ type: Enum<'github' \| 'gitlab' \| 'bitbucket'>; repo: string; productionBranch: string; autoDeployProduction: boolean; … }` | optional | Git repository configuration |
-| **buildConfig** | `{ buildCommand?: string; outputDirectory?: string; installCommand?: string; devCommand?: string; … }` | optional | Build configuration |
-| **deploymentConfig** | `{ autoDeployment: boolean; regions?: Enum<'iad1' \| 'sfo1' \| 'gru1' \| 'lhr1' \| 'fra1' \| 'sin1' \| 'syd1' \| 'hnd1' \| 'icn1'>[]; enablePreview: boolean; previewComments: boolean; … }` | optional | Deployment configuration |
-| **domains** | `{ domain: string; httpsRedirect: boolean; customCertificate?: object; gitBranch?: string }[]` | optional | Custom domains |
-| **environmentVariables** | `{ key: string; value: string; target: Enum<'production' \| 'preview' \| 'development'>[]; isSecret: boolean; … }[]` | optional | Environment variables |
-| **edgeFunctions** | `{ name: string; path: string; regions?: string[]; memoryLimit: integer; … }[]` | optional | Edge functions |
-| **rootDirectory** | `string` | optional | Root directory (for monorepos) |
-
-
----
-
-## VercelProvider
-
-Vercel provider type
-
-### Allowed Values
-
-* `vercel`
-
-
----
-
-## VercelTeam
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **teamId** | `string` | optional | Team ID or slug |
-| **teamName** | `string` | optional | Team name |
-
-
----
-
diff --git a/content/docs/references/integration/object-storage.mdx b/content/docs/references/integration/object-storage.mdx
index c2f2e95aca..8c5c882c5e 100644
--- a/content/docs/references/integration/object-storage.mdx
+++ b/content/docs/references/integration/object-storage.mdx
@@ -5,10 +5,6 @@ description: Object Storage protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/integration/object-storage.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/integration/offline.mdx b/content/docs/references/integration/offline.mdx
index 75736d3b07..5ddceaf3ee 100644
--- a/content/docs/references/integration/offline.mdx
+++ b/content/docs/references/integration/offline.mdx
@@ -5,10 +5,6 @@ description: Offline protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/integration/offline.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/integration/tenant.mdx b/content/docs/references/integration/tenant.mdx
index d7a420d1ff..e0624b519d 100644
--- a/content/docs/references/integration/tenant.mdx
+++ b/content/docs/references/integration/tenant.mdx
@@ -5,10 +5,6 @@ description: Tenant protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/integration/tenant.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/kernel/events-bus.mdx b/content/docs/references/kernel/events-bus.mdx
new file mode 100644
index 0000000000..872fd0a2e0
--- /dev/null
+++ b/content/docs/references/kernel/events-bus.mdx
@@ -0,0 +1,64 @@
+---
+title: Events Bus
+description: Events Bus protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+Event Bus Configuration Schema
+
+Complete configuration for the event bus system
+
+@example
+
+\{
+
+"persistence": \{ "enabled": true, "retention": 365 \},
+
+"queue": \{ "concurrency": 20 \},
+
+"eventSourcing": \{ "enabled": true \},
+
+"webhooks": [],
+
+"messageQueue": \{ "provider": "kafka", "topic": "events" \},
+
+"realtime": \{ "enabled": true, "protocol": "websocket" \}
+
+\}
+
+
+**Source:** `packages/spec/src/kernel/events/bus.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { EventBusConfig } from '@objectstack/spec/kernel';
+import type { EventBusConfig } from '@objectstack/spec/kernel';
+
+// Validate data
+const result = EventBusConfig.parse(data);
+```
+
+---
+
+## EventBusConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **persistence** | `{ enabled: boolean; retention: integer; filter?: any; storage: Enum<'database' \| 'file' \| 's3' \| 'custom'> }` | optional | Event persistence configuration |
+| **queue** | `{ name: string; concurrency: integer; retryPolicy?: object; deadLetterQueue?: string; … }` | optional | Event queue configuration |
+| **eventSourcing** | `{ enabled: boolean; snapshotInterval: integer; snapshotRetention: integer; retention: integer; … }` | optional | Event sourcing configuration |
+| **replay** | `{ enabled: boolean }` | optional | Event replay configuration |
+| **webhooks** | `{ id?: string; eventPattern: string; url: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH'>; … }[]` | optional | Webhook configurations |
+| **messageQueue** | `{ provider: Enum<'kafka' \| 'rabbitmq' \| 'aws-sqs' \| 'redis-pubsub' \| 'google-pubsub' \| 'azure-service-bus'>; topic: string; eventPattern: string; partitionKey?: string; … }` | optional | Message queue integration |
+| **realtime** | `{ enabled: boolean; protocol: Enum<'websocket' \| 'sse' \| 'long-polling'>; eventPattern: string; userFilter: boolean; … }` | optional | Real-time notification configuration |
+| **eventTypes** | `{ name: string; version: string; schema?: any; description?: string; … }[]` | optional | Event type definitions |
+| **handlers** | `{ id?: string; eventName: string; handler: any; priority: integer; … }[]` | optional | Global event handlers |
+
+
+---
+
diff --git a/content/docs/references/kernel/events-core.mdx b/content/docs/references/kernel/events-core.mdx
new file mode 100644
index 0000000000..83478875fb
--- /dev/null
+++ b/content/docs/references/kernel/events-core.mdx
@@ -0,0 +1,90 @@
+---
+title: Events Core
+description: Events Core protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+Event Priority Enum
+
+Priority levels for event processing
+
+Lower numbers = higher priority
+
+
+**Source:** `packages/spec/src/kernel/events/core.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { Event, EventMetadata, EventPriority, EventTypeDefinition } from '@objectstack/spec/kernel';
+import type { Event, EventMetadata, EventPriority, EventTypeDefinition } from '@objectstack/spec/kernel';
+
+// Validate data
+const result = Event.parse(data);
+```
+
+---
+
+## Event
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | optional | Unique event identifier |
+| **name** | `string` | ✅ | Event name (lowercase with dots, e.g., user.created, order.paid) |
+| **payload** | `any` | ✅ | Event payload schema |
+| **metadata** | `{ source: string; timestamp: string; userId?: string; tenantId?: string; … }` | ✅ | Event metadata |
+
+
+---
+
+## EventMetadata
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **source** | `string` | ✅ | Event source (e.g., plugin name, system component) |
+| **timestamp** | `string` | ✅ | ISO 8601 datetime when event was created |
+| **userId** | `string` | optional | User who triggered the event |
+| **tenantId** | `string` | optional | Tenant identifier for multi-tenant systems |
+| **correlationId** | `string` | optional | Correlation ID for event tracing |
+| **causationId** | `string` | optional | ID of the event that caused this event |
+| **priority** | `Enum<'critical' \| 'high' \| 'normal' \| 'low' \| 'background'>` | ✅ | Event priority |
+| **cluster** | `{ scope: Enum<'local' \| 'cluster' \| 'tenant'>; deliverySemantics?: Enum<'best-effort' \| 'at-least-once' \| 'exactly-once'>; partitionKey?: string }` | optional | Per-emit cluster routing & delivery options. See cluster-semantics.mdx §4. |
+
+
+---
+
+## EventPriority
+
+### Allowed Values
+
+* `critical`
+* `high`
+* `normal`
+* `low`
+* `background`
+
+
+---
+
+## EventTypeDefinition
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Event type name (lowercase with dots) |
+| **version** | `string` | ✅ | Event schema version |
+| **schema** | `any` | optional | JSON Schema for event payload validation |
+| **description** | `string` | optional | Event type description |
+| **deprecated** | `boolean` | ✅ | Whether this event type is deprecated |
+| **tags** | `string[]` | optional | Event type tags |
+
+
+---
+
diff --git a/content/docs/references/kernel/events-dlq.mdx b/content/docs/references/kernel/events-dlq.mdx
new file mode 100644
index 0000000000..3c8affddee
--- /dev/null
+++ b/content/docs/references/kernel/events-dlq.mdx
@@ -0,0 +1,61 @@
+---
+title: Events Dlq
+description: Events Dlq protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+Dead Letter Queue Entry Schema
+
+Represents a failed event in the dead letter queue
+
+
+**Source:** `packages/spec/src/kernel/events/dlq.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { DeadLetterQueueEntry, EventLogEntry } from '@objectstack/spec/kernel';
+import type { DeadLetterQueueEntry, EventLogEntry } from '@objectstack/spec/kernel';
+
+// Validate data
+const result = DeadLetterQueueEntry.parse(data);
+```
+
+---
+
+## DeadLetterQueueEntry
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Unique entry identifier |
+| **event** | `{ id?: string; name: string; payload: any; metadata: object }` | ✅ | Original event |
+| **error** | `{ message: string; stack?: string; code?: string }` | ✅ | Failure details |
+| **retries** | `integer` | ✅ | Number of retry attempts |
+| **firstFailedAt** | `string` | ✅ | When event first failed |
+| **lastFailedAt** | `string` | ✅ | When event last failed |
+| **failedHandler** | `string` | optional | Handler ID that failed |
+
+
+---
+
+## EventLogEntry
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Unique log entry identifier |
+| **event** | `{ id?: string; name: string; payload: any; metadata: object }` | ✅ | The event |
+| **status** | `Enum<'pending' \| 'processing' \| 'completed' \| 'failed'>` | ✅ | Processing status |
+| **handlersExecuted** | `{ handlerId: string; status: Enum<'success' \| 'failed' \| 'timeout'>; durationMs?: integer; error?: string }[]` | optional | Handlers that processed this event |
+| **receivedAt** | `string` | ✅ | When event was received |
+| **processedAt** | `string` | optional | When event was processed |
+| **totalDurationMs** | `integer` | optional | Total processing time |
+
+
+---
+
diff --git a/content/docs/references/kernel/events-handlers.mdx b/content/docs/references/kernel/events-handlers.mdx
new file mode 100644
index 0000000000..bd3f67001f
--- /dev/null
+++ b/content/docs/references/kernel/events-handlers.mdx
@@ -0,0 +1,72 @@
+---
+title: Events Handlers
+description: Events Handlers protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+Event Handler Schema
+
+Defines how to handle a specific event
+
+
+**Source:** `packages/spec/src/kernel/events/handlers.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { EventHandler, EventPersistence, EventRoute } from '@objectstack/spec/kernel';
+import type { EventHandler, EventPersistence, EventRoute } from '@objectstack/spec/kernel';
+
+// Validate data
+const result = EventHandler.parse(data);
+```
+
+---
+
+## EventHandler
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | optional | Unique handler identifier |
+| **eventName** | `string` | ✅ | Name of event to handle (supports wildcards like user.*) |
+| **handler** | `any` | ✅ | Handler function |
+| **priority** | `integer` | ✅ | Execution priority (lower numbers execute first) |
+| **async** | `boolean` | ✅ | Execute in background (true) or block (false) |
+| **retry** | `{ maxRetries: integer; backoffMs: integer; backoffMultiplier: number }` | optional | Retry policy for failed handlers |
+| **timeoutMs** | `integer` | optional | Handler timeout in milliseconds |
+| **filter** | `any` | optional | Optional filter to determine if handler should execute |
+
+
+---
+
+## EventPersistence
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **enabled** | `boolean` | ✅ | Enable event persistence |
+| **retention** | `integer` | ✅ | Days to retain persisted events |
+| **filter** | `any` | optional | Optional filter function to select which events to persist |
+| **storage** | `Enum<'database' \| 'file' \| 's3' \| 'custom'>` | ✅ | Storage backend for persisted events |
+
+
+---
+
+## EventRoute
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **from** | `string` | ✅ | Source event pattern (supports wildcards, e.g., user.* or *.created) |
+| **to** | `string[]` | ✅ | Target event names to route to |
+| **transform** | `any` | optional | Optional function to transform payload |
+
+
+---
+
diff --git a/content/docs/references/kernel/events-integrations.mdx b/content/docs/references/kernel/events-integrations.mdx
new file mode 100644
index 0000000000..8757af711e
--- /dev/null
+++ b/content/docs/references/kernel/events-integrations.mdx
@@ -0,0 +1,97 @@
+---
+title: Events Integrations
+description: Events Integrations protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+Event Webhook Configuration Schema
+
+Configuration for sending events to webhooks
+
+@example
+
+\{
+
+"eventPattern": "order.*",
+
+"url": "https://api.example.com/webhooks/orders",
+
+"method": "POST",
+
+"headers": \{ "Authorization": "Bearer token" \}
+
+\}
+
+
+**Source:** `packages/spec/src/kernel/events/integrations.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { EventMessageQueueConfig, EventWebhookConfig, RealTimeNotificationConfig } from '@objectstack/spec/kernel';
+import type { EventMessageQueueConfig, EventWebhookConfig, RealTimeNotificationConfig } from '@objectstack/spec/kernel';
+
+// Validate data
+const result = EventMessageQueueConfig.parse(data);
+```
+
+---
+
+## EventMessageQueueConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **provider** | `Enum<'kafka' \| 'rabbitmq' \| 'aws-sqs' \| 'redis-pubsub' \| 'google-pubsub' \| 'azure-service-bus'>` | ✅ | Message queue provider |
+| **topic** | `string` | ✅ | Topic or queue name |
+| **eventPattern** | `string` | ✅ | Event name pattern to publish (supports wildcards) |
+| **partitionKey** | `string` | optional | JSON path for partition key (e.g., "metadata.tenantId") |
+| **format** | `Enum<'json' \| 'avro' \| 'protobuf'>` | ✅ | Message serialization format |
+| **includeMetadata** | `boolean` | ✅ | Include event metadata in message |
+| **compression** | `Enum<'none' \| 'gzip' \| 'snappy' \| 'lz4'>` | ✅ | Message compression |
+| **batchSize** | `integer` | ✅ | Batch size for publishing |
+| **flushIntervalMs** | `integer` | ✅ | Flush interval for batching |
+
+
+---
+
+## EventWebhookConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | optional | Unique webhook identifier |
+| **eventPattern** | `string` | ✅ | Event name pattern (supports wildcards) |
+| **url** | `string` | ✅ | Webhook endpoint URL |
+| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH'>` | ✅ | HTTP method |
+| **headers** | `Record` | optional | HTTP headers |
+| **authentication** | `{ type: Enum<'none' \| 'bearer' \| 'basic' \| 'api-key'>; credentials?: Record }` | optional | Authentication configuration |
+| **retryPolicy** | `{ maxRetries: integer; backoffStrategy: Enum<'fixed' \| 'linear' \| 'exponential'>; initialDelayMs: integer; maxDelayMs: integer }` | optional | Retry policy |
+| **timeoutMs** | `integer` | ✅ | Request timeout in milliseconds |
+| **transform** | `any` | optional | Transform event before sending |
+| **enabled** | `boolean` | ✅ | Whether webhook is enabled |
+
+
+---
+
+## RealTimeNotificationConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **enabled** | `boolean` | ✅ | Enable real-time notifications |
+| **protocol** | `Enum<'websocket' \| 'sse' \| 'long-polling'>` | ✅ | Real-time protocol |
+| **eventPattern** | `string` | ✅ | Event pattern to broadcast |
+| **userFilter** | `boolean` | ✅ | Filter events by user |
+| **tenantFilter** | `boolean` | ✅ | Filter events by tenant |
+| **channels** | `{ name: string; eventPattern: string; filter?: any }[]` | optional | Named channels for event broadcasting |
+| **rateLimit** | `{ maxEventsPerSecond: integer; windowMs: integer }` | optional | Rate limiting configuration |
+
+
+---
+
diff --git a/content/docs/references/kernel/events-queue.mdx b/content/docs/references/kernel/events-queue.mdx
new file mode 100644
index 0000000000..d262c5db13
--- /dev/null
+++ b/content/docs/references/kernel/events-queue.mdx
@@ -0,0 +1,92 @@
+---
+title: Events Queue
+description: Events Queue protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+Event Queue Configuration Schema
+
+Configuration for async event processing queue
+
+@example
+
+\{
+
+"name": "event_queue",
+
+"concurrency": 10,
+
+"retryPolicy": \{
+
+"maxRetries": 3,
+
+"backoffStrategy": "exponential"
+
+\}
+
+\}
+
+
+**Source:** `packages/spec/src/kernel/events/queue.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { EventQueueConfig, EventReplayConfig, EventSourcingConfig } from '@objectstack/spec/kernel';
+import type { EventQueueConfig, EventReplayConfig, EventSourcingConfig } from '@objectstack/spec/kernel';
+
+// Validate data
+const result = EventQueueConfig.parse(data);
+```
+
+---
+
+## EventQueueConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | Event queue name |
+| **concurrency** | `integer` | ✅ | Max concurrent event handlers |
+| **retryPolicy** | `{ maxRetries: integer; backoffStrategy: Enum<'fixed' \| 'linear' \| 'exponential'>; initialDelayMs: integer; maxDelayMs: integer }` | optional | Default retry policy for events |
+| **deadLetterQueue** | `string` | optional | Dead letter queue name for failed events |
+| **priorityEnabled** | `boolean` | ✅ | Process events based on priority |
+
+
+---
+
+## EventReplayConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **fromTimestamp** | `string` | ✅ | Start timestamp for replay (ISO 8601) |
+| **toTimestamp** | `string` | optional | End timestamp for replay (ISO 8601) |
+| **eventTypes** | `string[]` | optional | Event types to replay (empty = all) |
+| **filters** | `Record` | optional | Additional filters for event selection |
+| **speed** | `number` | ✅ | Replay speed multiplier (1 = real-time) |
+| **targetHandlers** | `string[]` | optional | Handler IDs to execute (empty = all) |
+
+
+---
+
+## EventSourcingConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **enabled** | `boolean` | ✅ | Enable event sourcing |
+| **snapshotInterval** | `integer` | ✅ | Create snapshot every N events |
+| **snapshotRetention** | `integer` | ✅ | Number of snapshots to retain |
+| **retention** | `integer` | ✅ | Days to retain events |
+| **aggregateTypes** | `string[]` | optional | Aggregate types to enable event sourcing for |
+| **storage** | `{ type: Enum<'database' \| 'file' \| 's3' \| 'eventstore'>; options?: Record }` | optional | Event store configuration |
+
+
+---
+
diff --git a/content/docs/references/kernel/index.mdx b/content/docs/references/kernel/index.mdx
index c4d3f3c9a0..5eb897a29f 100644
--- a/content/docs/references/kernel/index.mdx
+++ b/content/docs/references/kernel/index.mdx
@@ -10,6 +10,12 @@ This section contains all protocol schemas for the kernel layer of ObjectStack.
+
+
+
+
+
+
diff --git a/content/docs/references/kernel/meta.json b/content/docs/references/kernel/meta.json
index a44766a69c..83c2af0c5a 100644
--- a/content/docs/references/kernel/meta.json
+++ b/content/docs/references/kernel/meta.json
@@ -30,9 +30,14 @@
"metadata-persistence",
"metadata-plugin",
"metadata-protection",
- "misc",
"service-registry",
"startup-orchestrator",
- "state-machine"
+ "---More---",
+ "events-bus",
+ "events-core",
+ "events-dlq",
+ "events-handlers",
+ "events-integrations",
+ "events-queue"
]
}
\ No newline at end of file
diff --git a/content/docs/references/kernel/metadata-persistence.mdx b/content/docs/references/kernel/metadata-persistence.mdx
index 9c6c423978..89b9cb20a8 100644
--- a/content/docs/references/kernel/metadata-persistence.mdx
+++ b/content/docs/references/kernel/metadata-persistence.mdx
@@ -5,10 +5,6 @@ description: Metadata Persistence protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/kernel/metadata-persistence.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/kernel/misc.mdx b/content/docs/references/kernel/misc.mdx
deleted file mode 100644
index 29792ea516..0000000000
--- a/content/docs/references/kernel/misc.mdx
+++ /dev/null
@@ -1,271 +0,0 @@
----
-title: Misc
-description: Misc protocol schemas
----
-
-{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-
-**Source:** `packages/spec/src/kernel/misc.zod.ts`
-
-
-## TypeScript Usage
-
-```typescript
-import { DeadLetterQueueEntry, EventBusConfig, EventHandler, EventLogEntry, EventMessageQueueConfig, EventMetadata, EventPersistence, EventPriority, EventQueueConfig, EventReplayConfig, EventRoute, EventSourcingConfig, EventTypeDefinition, EventWebhookConfig, RealTimeNotificationConfig } from '@objectstack/spec/kernel';
-import type { DeadLetterQueueEntry, EventBusConfig, EventHandler, EventLogEntry, EventMessageQueueConfig, EventMetadata, EventPersistence, EventPriority, EventQueueConfig, EventReplayConfig, EventRoute, EventSourcingConfig, EventTypeDefinition, EventWebhookConfig, RealTimeNotificationConfig } from '@objectstack/spec/kernel';
-
-// Validate data
-const result = DeadLetterQueueEntry.parse(data);
-```
-
----
-
-## DeadLetterQueueEntry
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **id** | `string` | ✅ | Unique entry identifier |
-| **event** | `{ id?: string; name: string; payload: any; metadata: object }` | ✅ | Original event |
-| **error** | `{ message: string; stack?: string; code?: string }` | ✅ | Failure details |
-| **retries** | `integer` | ✅ | Number of retry attempts |
-| **firstFailedAt** | `string` | ✅ | When event first failed |
-| **lastFailedAt** | `string` | ✅ | When event last failed |
-| **failedHandler** | `string` | optional | Handler ID that failed |
-
-
----
-
-## EventBusConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **persistence** | `{ enabled: boolean; retention: integer; filter?: any; storage: Enum<'database' \| 'file' \| 's3' \| 'custom'> }` | optional | Event persistence configuration |
-| **queue** | `{ name: string; concurrency: integer; retryPolicy?: object; deadLetterQueue?: string; … }` | optional | Event queue configuration |
-| **eventSourcing** | `{ enabled: boolean; snapshotInterval: integer; snapshotRetention: integer; retention: integer; … }` | optional | Event sourcing configuration |
-| **replay** | `{ enabled: boolean }` | optional | Event replay configuration |
-| **webhooks** | `{ id?: string; eventPattern: string; url: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH'>; … }[]` | optional | Webhook configurations |
-| **messageQueue** | `{ provider: Enum<'kafka' \| 'rabbitmq' \| 'aws-sqs' \| 'redis-pubsub' \| 'google-pubsub' \| 'azure-service-bus'>; topic: string; eventPattern: string; partitionKey?: string; … }` | optional | Message queue integration |
-| **realtime** | `{ enabled: boolean; protocol: Enum<'websocket' \| 'sse' \| 'long-polling'>; eventPattern: string; userFilter: boolean; … }` | optional | Real-time notification configuration |
-| **eventTypes** | `{ name: string; version: string; schema?: any; description?: string; … }[]` | optional | Event type definitions |
-| **handlers** | `{ id?: string; eventName: string; handler: any; priority: integer; … }[]` | optional | Global event handlers |
-
-
----
-
-## EventHandler
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **id** | `string` | optional | Unique handler identifier |
-| **eventName** | `string` | ✅ | Name of event to handle (supports wildcards like user.*) |
-| **handler** | `any` | ✅ | Handler function |
-| **priority** | `integer` | ✅ | Execution priority (lower numbers execute first) |
-| **async** | `boolean` | ✅ | Execute in background (true) or block (false) |
-| **retry** | `{ maxRetries: integer; backoffMs: integer; backoffMultiplier: number }` | optional | Retry policy for failed handlers |
-| **timeoutMs** | `integer` | optional | Handler timeout in milliseconds |
-| **filter** | `any` | optional | Optional filter to determine if handler should execute |
-
-
----
-
-## EventLogEntry
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **id** | `string` | ✅ | Unique log entry identifier |
-| **event** | `{ id?: string; name: string; payload: any; metadata: object }` | ✅ | The event |
-| **status** | `Enum<'pending' \| 'processing' \| 'completed' \| 'failed'>` | ✅ | Processing status |
-| **handlersExecuted** | `{ handlerId: string; status: Enum<'success' \| 'failed' \| 'timeout'>; durationMs?: integer; error?: string }[]` | optional | Handlers that processed this event |
-| **receivedAt** | `string` | ✅ | When event was received |
-| **processedAt** | `string` | optional | When event was processed |
-| **totalDurationMs** | `integer` | optional | Total processing time |
-
-
----
-
-## EventMessageQueueConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **provider** | `Enum<'kafka' \| 'rabbitmq' \| 'aws-sqs' \| 'redis-pubsub' \| 'google-pubsub' \| 'azure-service-bus'>` | ✅ | Message queue provider |
-| **topic** | `string` | ✅ | Topic or queue name |
-| **eventPattern** | `string` | ✅ | Event name pattern to publish (supports wildcards) |
-| **partitionKey** | `string` | optional | JSON path for partition key (e.g., "metadata.tenantId") |
-| **format** | `Enum<'json' \| 'avro' \| 'protobuf'>` | ✅ | Message serialization format |
-| **includeMetadata** | `boolean` | ✅ | Include event metadata in message |
-| **compression** | `Enum<'none' \| 'gzip' \| 'snappy' \| 'lz4'>` | ✅ | Message compression |
-| **batchSize** | `integer` | ✅ | Batch size for publishing |
-| **flushIntervalMs** | `integer` | ✅ | Flush interval for batching |
-
-
----
-
-## EventMetadata
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **source** | `string` | ✅ | Event source (e.g., plugin name, system component) |
-| **timestamp** | `string` | ✅ | ISO 8601 datetime when event was created |
-| **userId** | `string` | optional | User who triggered the event |
-| **tenantId** | `string` | optional | Tenant identifier for multi-tenant systems |
-| **correlationId** | `string` | optional | Correlation ID for event tracing |
-| **causationId** | `string` | optional | ID of the event that caused this event |
-| **priority** | `Enum<'critical' \| 'high' \| 'normal' \| 'low' \| 'background'>` | ✅ | Event priority |
-| **cluster** | `{ scope: Enum<'local' \| 'cluster' \| 'tenant'>; deliverySemantics?: Enum<'best-effort' \| 'at-least-once' \| 'exactly-once'>; partitionKey?: string }` | optional | Per-emit cluster routing & delivery options. See cluster-semantics.mdx §4. |
-
-
----
-
-## EventPersistence
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **enabled** | `boolean` | ✅ | Enable event persistence |
-| **retention** | `integer` | ✅ | Days to retain persisted events |
-| **filter** | `any` | optional | Optional filter function to select which events to persist |
-| **storage** | `Enum<'database' \| 'file' \| 's3' \| 'custom'>` | ✅ | Storage backend for persisted events |
-
-
----
-
-## EventPriority
-
-### Allowed Values
-
-* `critical`
-* `high`
-* `normal`
-* `low`
-* `background`
-
-
----
-
-## EventQueueConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Event queue name |
-| **concurrency** | `integer` | ✅ | Max concurrent event handlers |
-| **retryPolicy** | `{ maxRetries: integer; backoffStrategy: Enum<'fixed' \| 'linear' \| 'exponential'>; initialDelayMs: integer; maxDelayMs: integer }` | optional | Default retry policy for events |
-| **deadLetterQueue** | `string` | optional | Dead letter queue name for failed events |
-| **priorityEnabled** | `boolean` | ✅ | Process events based on priority |
-
-
----
-
-## EventReplayConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **fromTimestamp** | `string` | ✅ | Start timestamp for replay (ISO 8601) |
-| **toTimestamp** | `string` | optional | End timestamp for replay (ISO 8601) |
-| **eventTypes** | `string[]` | optional | Event types to replay (empty = all) |
-| **filters** | `Record` | optional | Additional filters for event selection |
-| **speed** | `number` | ✅ | Replay speed multiplier (1 = real-time) |
-| **targetHandlers** | `string[]` | optional | Handler IDs to execute (empty = all) |
-
-
----
-
-## EventRoute
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **from** | `string` | ✅ | Source event pattern (supports wildcards, e.g., user.* or *.created) |
-| **to** | `string[]` | ✅ | Target event names to route to |
-| **transform** | `any` | optional | Optional function to transform payload |
-
-
----
-
-## EventSourcingConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **enabled** | `boolean` | ✅ | Enable event sourcing |
-| **snapshotInterval** | `integer` | ✅ | Create snapshot every N events |
-| **snapshotRetention** | `integer` | ✅ | Number of snapshots to retain |
-| **retention** | `integer` | ✅ | Days to retain events |
-| **aggregateTypes** | `string[]` | optional | Aggregate types to enable event sourcing for |
-| **storage** | `{ type: Enum<'database' \| 'file' \| 's3' \| 'eventstore'>; options?: Record }` | optional | Event store configuration |
-
-
----
-
-## EventTypeDefinition
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Event type name (lowercase with dots) |
-| **version** | `string` | ✅ | Event schema version |
-| **schema** | `any` | optional | JSON Schema for event payload validation |
-| **description** | `string` | optional | Event type description |
-| **deprecated** | `boolean` | ✅ | Whether this event type is deprecated |
-| **tags** | `string[]` | optional | Event type tags |
-
-
----
-
-## EventWebhookConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **id** | `string` | optional | Unique webhook identifier |
-| **eventPattern** | `string` | ✅ | Event name pattern (supports wildcards) |
-| **url** | `string` | ✅ | Webhook endpoint URL |
-| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH'>` | ✅ | HTTP method |
-| **headers** | `Record` | optional | HTTP headers |
-| **authentication** | `{ type: Enum<'none' \| 'bearer' \| 'basic' \| 'api-key'>; credentials?: Record }` | optional | Authentication configuration |
-| **retryPolicy** | `{ maxRetries: integer; backoffStrategy: Enum<'fixed' \| 'linear' \| 'exponential'>; initialDelayMs: integer; maxDelayMs: integer }` | optional | Retry policy |
-| **timeoutMs** | `integer` | ✅ | Request timeout in milliseconds |
-| **transform** | `any` | optional | Transform event before sending |
-| **enabled** | `boolean` | ✅ | Whether webhook is enabled |
-
-
----
-
-## RealTimeNotificationConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **enabled** | `boolean` | ✅ | Enable real-time notifications |
-| **protocol** | `Enum<'websocket' \| 'sse' \| 'long-polling'>` | ✅ | Real-time protocol |
-| **eventPattern** | `string` | ✅ | Event pattern to broadcast |
-| **userFilter** | `boolean` | ✅ | Filter events by user |
-| **tenantFilter** | `boolean` | ✅ | Filter events by tenant |
-| **channels** | `{ name: string; eventPattern: string; filter?: any }[]` | optional | Named channels for event broadcasting |
-| **rateLimit** | `{ maxEventsPerSecond: integer; windowMs: integer }` | optional | Rate limiting configuration |
-
-
----
-
diff --git a/content/docs/references/kernel/state-machine.mdx b/content/docs/references/kernel/state-machine.mdx
deleted file mode 100644
index a0d9a5e15c..0000000000
--- a/content/docs/references/kernel/state-machine.mdx
+++ /dev/null
@@ -1,37 +0,0 @@
----
-title: State Machine
-description: State Machine protocol schemas
----
-
-{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-
-**Source:** `packages/spec/src/kernel/state-machine.zod.ts`
-
-
-## TypeScript Usage
-
-```typescript
-import { Event } from '@objectstack/spec/kernel';
-import type { Event } from '@objectstack/spec/kernel';
-
-// Validate data
-const result = Event.parse(data);
-```
-
----
-
-## Event
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **id** | `string` | optional | Unique event identifier |
-| **name** | `string` | ✅ | Event name (lowercase with dots, e.g., user.created, order.paid) |
-| **payload** | `any` | ✅ | Event payload schema |
-| **metadata** | `{ source: string; timestamp: string; userId?: string; tenantId?: string; … }` | ✅ | Event metadata |
-
-
----
-
diff --git a/content/docs/references/security/misc.mdx b/content/docs/references/security/misc.mdx
index af3e3fd36d..e2fdb6bad1 100644
--- a/content/docs/references/security/misc.mdx
+++ b/content/docs/references/security/misc.mdx
@@ -5,10 +5,6 @@ description: Misc protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/security/misc.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/shared/metadata-persistence.mdx b/content/docs/references/shared/metadata-persistence.mdx
index 184a882589..30669611cf 100644
--- a/content/docs/references/shared/metadata-persistence.mdx
+++ b/content/docs/references/shared/metadata-persistence.mdx
@@ -5,10 +5,6 @@ description: Metadata Persistence protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/shared/metadata-persistence.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/studio/action.mdx b/content/docs/references/studio/action.mdx
index 492761a89f..7f76880e66 100644
--- a/content/docs/references/studio/action.mdx
+++ b/content/docs/references/studio/action.mdx
@@ -5,10 +5,6 @@ description: Action protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/studio/action.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/system/metadata-loader.mdx b/content/docs/references/system/metadata-loader.mdx
index 43bc2a5c7d..534e801be6 100644
--- a/content/docs/references/system/metadata-loader.mdx
+++ b/content/docs/references/system/metadata-loader.mdx
@@ -5,10 +5,6 @@ description: Metadata Loader protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/system/metadata-loader.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/content/docs/references/ui/http.mdx b/content/docs/references/ui/http.mdx
index 5d5b81ce53..dab575e0eb 100644
--- a/content/docs/references/ui/http.mdx
+++ b/content/docs/references/ui/http.mdx
@@ -5,10 +5,6 @@ description: Http protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-**Source:** `packages/spec/src/ui/http.zod.ts`
-
-
## TypeScript Usage
```typescript
diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.md b/docs/audits/2026-07-unknown-key-strictness-ledger.md
index 7e3e3c46a0..5e6549f7e8 100644
--- a/docs/audits/2026-07-unknown-key-strictness-ledger.md
+++ b/docs/audits/2026-07-unknown-key-strictness-ledger.md
@@ -110,9 +110,24 @@ dropped at parse, and nothing failed.
now catches it was directed, with the platform's authority, at a slot where
the same mistake is silent again: `config: { hostname: … }` is stripped and
the datasource connects on localhost — #4001's original bug verbatim, one
- level down. Corrected to name the per-driver shape instead of promising a
- gate; enforcement is #4410. **A wrong instruction is worse than none**, and
- worst for an AI author, whose only signal is whether the parse complained.
+ level down. First corrected to name the per-driver shape instead of promising
+ a gate; **#4410 then built the gate**, so the prescription makes a validation
+ claim again and the claim is true. **A wrong instruction is worse than
+ none**, and worst for an AI author, whose only signal is whether the parse
+ complained.
+
+ Two things #4410 had to fix before the sentence was safe to write, both
+ instructive beyond this schema. The prescription has to name the key the
+ contract *lands on*, not the one the author typed — pointing a misplaced
+ `user` at `config: { user: … }` when postgres spells it `username` would
+ swap a one-step correction for a two-step one. And a gate over `config`
+ means every key inside it now claims to be honoured, which forced a per-key
+ audit against the code that reads them: `indexes` / `maxRecordsPerObject`
+ (memory) were removed as inert, while `datasource.pool`, `schemaMode`,
+ postgres `schema` / `applicationName` / `statementTimeout` and mongo
+ `password` / `authSource` / `options` were **wired**, having been declared
+ and dropped on the floor. Enforcing a contract and honouring it are the same
+ task from two directions.
This is the empirical argument for the ratchet: the inference "no metadata in
the repo carries unknown keys" was **false three times over**, and only the
@@ -158,7 +173,7 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts).
| `notification.zod.ts` / `offline.zod.ts` / `report.zod.ts` | 3 ea | authorable (p) | |
| `sharing.zod.ts` | 2 | authorable (p) | public-sharing config |
-### `data/` — 158 sites
+### `data/` — 160 sites
| File | Sites | Class | Note |
|---|---|---|---|
@@ -169,8 +184,9 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts).
| `field.zod.ts` | 11 | authorable | partially strict |
| `filter.zod.ts` / `query.zod.ts` | 11+5 | open | query dialect — user data flows through; validated semantically elsewhere. `query.zod.ts` dropped one site in #4196: `FieldNodeSchema`'s nested-select object form was declared-but-inert and narrowed to `z.string()`, so the union's second member is gone. Four more left in #4286 with the `joins`/`windowFunctions` removals: `JoinNodeBaseSchema`, `WindowFunctionNodeSchema`, and `WindowSpecSchema`'s two blocks (outer + `frame`) were deleted with their clusters. Class unchanged |
| `driver-nosql.zod.ts` / `driver.zod.ts` / `driver-sql.zod.ts` | 10+9+2 | wire | driver capability contracts |
-| `datasource.zod.ts` | 9 | authorable | **strict as of #4001 data step** — all 9: `DatasourceSchema` (+ `pool` / `healthCheck` / `ssl` / `retryPolicy`), `ExternalDatasourceSettingsSchema` (+ `validation`), `DatasourceCapabilities`, `DriverDefinitionSchema`. `config` + `readReplicas` stay `z.record` by construction (per-driver shapes — see the `driver/` row below). This row used to add "the driver's own `configSchema` validates them"; **it does not, and never did** — corrected, and the gap is #4410. Which is precisely why the top level had to close: a connection key written one level too high was stripped, and the datasource then connected on driver defaults instead of failing |
-| `driver/memory.zod.ts` / `driver/mongo.zod.ts` / `driver/postgres.zod.ts` | 6+1+2 | authorable | The per-driver shapes for the `config` slot — what an author actually writes under `datasource.config` (`host`, `port`, `filename`, pool sizes). **Undeclared here until the coverage walk went recursive** (see below): a subdirectory was invisible to the gate, so these nine sites sat outside the map while the map reported full coverage. Authorable by the rule, but they are **contract-only exports today** — nothing parses `datasource.config` against them and both `*DriverSpec.configSchema` literals are `{}` (#4410). Strictness here would therefore enforce nothing; this row is blocked on #4410 giving it a parse site, not on a verification pass |
+| `datasource.zod.ts` | 9 | authorable | **strict as of #4001 data step** — all 9: `DatasourceSchema` (+ `pool` / `healthCheck` / `ssl` / `retryPolicy`), `ExternalDatasourceSettingsSchema` (+ `validation`), `DatasourceCapabilities`, `DriverDefinitionSchema`. `config` + `readReplicas` stay `z.record` **at this level** by construction (per-driver shapes), but are no longer unchecked: **#4410** made `DatasourceSchema`'s refinement parse both against the contract for the declared driver (`driver/config-registry.zod.ts`), so the openness here is a shape this level cannot express rather than the absence of one. This row used to add "the driver's own `configSchema` validates them", which was false until #4410 landed the parse site it names |
+| `driver/memory.zod.ts` / `driver/mongo.zod.ts` / `driver/postgres.zod.ts` | 6+1+1 | authorable | The per-driver shapes for the `config` slot — what an author actually writes under `datasource.config` (`host`, `port`, `filename`). **Undeclared here until the coverage walk went recursive** (see below): a subdirectory was invisible to the gate, so these sites sat outside the map while the map reported full coverage. **Strict as of #4410**, which is also what unblocked them: this row previously read "strictness here would enforce nothing" because nothing parsed `datasource.config` against these schemas and both `*DriverSpec.configSchema` literals were `{}`. Now `DatasourceSchema` parses `config` (and each `readReplicas` entry) against them, and the same schemas project onto `configSchema` and onto the Studio connection form. `postgres.zod.ts` drops a site: its `ssl` was a `boolean | {ca, cert, key, …}` union, and the object arm is gone — certificates now live in the datasource-level `ssl` block (declared, strict, and until #4410 read by nobody), leaving `config.ssl` as the on/off shorthand. That narrowing is forced by the same projection: the Studio form renders anything that is not boolean/enum/number as a TEXT INPUT, so a union here would have produced a wizard whose every `ssl` value the new gate rejects. `memory.zod.ts` keeps 6 but loses two KEYS — `indexes` / `maxRecordsPerObject`, which `InMemoryDriverConfig` has no field for, removed under ADR-0049 rather than blessed by the new gate |
+| `driver/mysql.zod.ts` / `driver/sqlite.zod.ts` | 1+2 | authorable | The rest of the `config` contract, added by #4410. `mysql.zod.ts` and `sqlite.zod.ts` (sqlite + sqlite-wasm) are shapes that **never existed** — both driver ids were offered by the connection form and buildable by the shared factory, with no config contract anywhere, so `driver: 'sqlite'` + a misspelled `filename` was an ephemeral `:memory:` database reported as configured. All three sites strict, same error factory as the rest of the campaign. (Their sibling `driver/common.zod.ts` holds shared enums and prescription strings and has no `z.object(` site, so the coverage gate skips it) |
| `analytics.zod.ts` | 8 | mixed (p) | |
| `document.zod.ts` | 8 | wire (p) | |
| `hook.zod.ts` / `hook-body.zod.ts` | 6+2 | mixed | **strict as of #4001 data step** for the AUTHORING shapes: `HookSchema` (+ `retryPolicy`) and both body branches (`ExpressionBodySchema` / `ScriptBodySchema`). `HookContextSchema` and its `session` / `provenance` / `user` blocks are the RUNTIME shape the engine hands a handler — they stay tolerant, and must: strictness there would make an engine-internal enrichment (as `provenance` was in #3712) a breaking change for anyone parsing a context they were given. The file's old blanket `authorable (p)` was too wide — verification split it |
diff --git a/examples/app-crm/src/datasources/crm.datasource.ts b/examples/app-crm/src/datasources/crm.datasource.ts
index 09c210a7f0..14b5b58253 100644
--- a/examples/app-crm/src/datasources/crm.datasource.ts
+++ b/examples/app-crm/src/datasources/crm.datasource.ts
@@ -22,6 +22,10 @@ export const CrmDatasource = defineDatasource({
/**
* Read-replica for analytics queries — demonstrates datasource routing.
+ *
+ * `readOnly` is a datasource CAPABILITY, not sqlite config. It sat inside
+ * `config` here until #4410 gave that slot a gate — a key no driver read, so
+ * the "read replica" was writable while every signal said it was not.
*/
export const CrmAnalyticsDatasource = defineDatasource({
name: 'crm_analytics',
@@ -29,6 +33,8 @@ export const CrmAnalyticsDatasource = defineDatasource({
driver: 'sqlite',
config: {
filename: ':memory:',
+ },
+ capabilities: {
readOnly: true,
},
active: true,
diff --git a/examples/app-showcase/src/system/datasources/showcase-external.datasource.ts b/examples/app-showcase/src/system/datasources/showcase-external.datasource.ts
index a2c3d95ab8..71a8625fbb 100644
--- a/examples/app-showcase/src/system/datasources/showcase-external.datasource.ts
+++ b/examples/app-showcase/src/system/datasources/showcase-external.datasource.ts
@@ -52,7 +52,7 @@ export const ShowcaseExternalDatasource = defineDatasource({
// label: 'Analytics Warehouse (Postgres)',
// driver: 'postgres',
// schemaMode: 'external',
-// config: { host: 'localhost', port: 5432, database: 'analytics', user: 'readonly' },
+// config: { host: 'localhost', port: 5432, database: 'analytics', username: 'readonly' },
// external: {
// allowWrites: false,
// credentialsRef: 'secret:warehouse/password',
diff --git a/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts b/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts
index 49122259b5..af1911ed0b 100644
--- a/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts
+++ b/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts
@@ -73,7 +73,7 @@ describe('DatasourceAdminServicePlugin: probe', () => {
driverFactory: fakeFactory(),
});
const res = await service.testConnection(
- { name: 'reporting', driver: 'postgres', config: { host: 'db' } },
+ { name: 'reporting', driver: 'postgres', config: { host: 'db', database: 'analytics' } },
{ value: 's3cret' },
);
expect(res.ok).toBe(true);
@@ -91,7 +91,7 @@ describe('DatasourceAdminServicePlugin: probe', () => {
it('returns ok:false when no factory is registered at all', async () => {
const { service } = await boot();
- const res = await service.testConnection({ name: 'x', driver: 'postgres', config: {} });
+ const res = await service.testConnection({ name: 'x', driver: 'postgres', config: { database: 'analytics' } });
expect(res.ok).toBe(false);
expect(res.error).toMatch(/no driver factory is registered/i);
});
@@ -101,7 +101,10 @@ describe('DatasourceAdminServicePlugin: secret fail-closed', () => {
it('refuses to create a secret-bearing datasource without a secret binder', async () => {
const { service, registry } = await boot({ driverFactory: fakeFactory() });
await expect(
- service.createDatasource({ name: 'reporting', driver: 'postgres', config: {} }, { value: 'pw' }),
+ service.createDatasource(
+ { name: 'reporting', driver: 'postgres', config: { database: 'analytics' } },
+ { value: 'pw' },
+ ),
).rejects.toThrow(/no secret store configured/i);
// nothing persisted
expect(registry.get('datasource')?.size ?? 0).toBe(0);
@@ -118,7 +121,10 @@ describe('DatasourceAdminServicePlugin: secret fail-closed', () => {
},
},
});
- await service.createDatasource({ name: 'reporting', driver: 'postgres', config: {} }, { value: 'pw' });
+ await service.createDatasource(
+ { name: 'reporting', driver: 'postgres', config: { database: 'analytics' } },
+ { value: 'pw' },
+ );
const rec = registry.get('datasource')?.get('reporting') as any;
expect(rec.origin).toBe('runtime');
expect(rec.external?.credentialsRef).toBe('sys_secret://datasource/reporting#1');
@@ -181,7 +187,7 @@ describe('DatasourceAdminServicePlugin: boot rehydration', () => {
driver: 'postgres',
origin: 'runtime',
active: true,
- config: { host: 'db' },
+ config: { host: 'db', database: 'analytics' },
external: { credentialsRef: 'sys_secret:abc' },
},
],
@@ -220,7 +226,7 @@ describe('DatasourceAdminServicePlugin: persistence + bound count', () => {
// seed an object bound to a runtime datasource
registry.set('object', new Map([['lead', { name: 'lead', datasource: 'reporting' }]]));
- await service.createDatasource({ name: 'reporting', driver: 'postgres', config: {} });
+ await service.createDatasource({ name: 'reporting', driver: 'postgres', config: { database: 'analytics' } });
const list = await service.listDatasources();
expect(list.find((d) => d.name === 'crm_primary')?.origin).toBe('code');
diff --git a/packages/services/service-datasource/src/__tests__/datasource-admin-service.test.ts b/packages/services/service-datasource/src/__tests__/datasource-admin-service.test.ts
index 454f3209eb..28e72bbbbe 100644
--- a/packages/services/service-datasource/src/__tests__/datasource-admin-service.test.ts
+++ b/packages/services/service-datasource/src/__tests__/datasource-admin-service.test.ts
@@ -115,7 +115,7 @@ describe('testConnection', () => {
it('probes with the cleartext secret without persisting anything', async () => {
const { service, store, probed } = makeHarness();
const res = await service.testConnection(
- { name: 'tmp', driver: 'postgres', config: { host: 'db.internal' } },
+ { name: 'tmp', driver: 'postgres', config: { host: 'db.internal', database: 'analytics' } },
{ value: 's3cret' },
);
expect(res.ok).toBe(true);
@@ -136,10 +136,43 @@ describe('testConnection', () => {
throw new Error('ECONNREFUSED');
},
});
- const res = await service.testConnection({ name: 'x', driver: 'postgres' });
+ const res = await service.testConnection({
+ name: 'x',
+ driver: 'postgres',
+ config: { database: 'app' },
+ });
expect(res.ok).toBe(false);
expect(res.error).toMatch(/ECONNREFUSED/);
});
+
+ // #4410. A probe is the wizard's evidence that a connection works, so it must
+ // not run against a config the driver would silently discard: `hostname` is
+ // dropped, `pg` opens its own localhost default, and a green "Connection
+ // successful" is reported for a datasource pointing somewhere else.
+ it('refuses to probe a config the driver would silently ignore', async () => {
+ const { service, probed } = makeHarness();
+ const res = await service.testConnection({
+ name: 'x',
+ driver: 'postgres',
+ config: { hostname: 'db.internal', database: 'app' },
+ });
+
+ expect(res.ok).toBe(false);
+ expect(res.error).toContain('`hostname` → `host`');
+ expect(probed).toHaveLength(0);
+ });
+
+ it('probes a driver the platform ships no contract for, unchanged', async () => {
+ const { service, probed } = makeHarness();
+ const res = await service.testConnection({
+ name: 'x',
+ driver: 'com.vendor.snowflake',
+ config: { account: 'xy12345' },
+ });
+
+ expect(res.ok).toBe(true);
+ expect(probed).toHaveLength(1);
+ });
});
describe('createDatasource', () => {
@@ -168,16 +201,39 @@ describe('createDatasource', () => {
it('hot-registers the pool after create', async () => {
const { service, registered } = makeHarness();
- await service.createDatasource({ name: 'reporting', driver: 'postgres' });
+ await service.createDatasource({
+ name: 'reporting',
+ driver: 'postgres',
+ config: { database: 'analytics' },
+ });
expect(registered).toContain('reporting');
});
+ // The wizard is the OTHER authoring door: `createDatasource` writes through
+ // `metadata.register`, whose validation is a structural name/label check, so
+ // a bad config reached the store even after DatasourceSchema's gate landed.
+ it('rejects a config its driver would not honour (#4410)', async () => {
+ const { service, store } = makeHarness();
+ await expect(
+ service.createDatasource({
+ name: 'reporting',
+ driver: 'postgres',
+ config: { hostname: 'db.internal', database: 'analytics' },
+ }),
+ ).rejects.toThrow(/`hostname` → `host`/);
+ expect(store.size).toBe(0);
+ });
+
it('rejects a name owned by a code-defined datasource', async () => {
const { service } = makeHarness({
seed: [{ name: 'crm_primary', driver: 'sqlite', origin: 'code' }],
});
await expect(
- service.createDatasource({ name: 'crm_primary', driver: 'postgres' }),
+ service.createDatasource({
+ name: 'crm_primary',
+ driver: 'postgres',
+ config: { database: 'analytics' },
+ }),
).rejects.toThrow(/code-defined/i);
});
@@ -186,14 +242,22 @@ describe('createDatasource', () => {
seed: [{ name: 'reporting', driver: 'postgres', origin: 'runtime' }],
});
await expect(
- service.createDatasource({ name: 'reporting', driver: 'postgres' }),
+ service.createDatasource({
+ name: 'reporting',
+ driver: 'postgres',
+ config: { database: 'analytics' },
+ }),
).rejects.toThrow(/already exists/i);
});
it('rejects an invalid name', async () => {
const { service } = makeHarness();
await expect(
- service.createDatasource({ name: 'Bad-Name', driver: 'postgres' }),
+ service.createDatasource({
+ name: 'Bad-Name',
+ driver: 'postgres',
+ config: { database: 'analytics' },
+ }),
).rejects.toThrow(/must match/i);
});
});
diff --git a/packages/services/service-datasource/src/__tests__/default-datasource-driver-factory.test.ts b/packages/services/service-datasource/src/__tests__/default-datasource-driver-factory.test.ts
index bb310013f9..c10a0150bd 100644
--- a/packages/services/service-datasource/src/__tests__/default-datasource-driver-factory.test.ts
+++ b/packages/services/service-datasource/src/__tests__/default-datasource-driver-factory.test.ts
@@ -169,3 +169,97 @@ describe('createDefaultDatasourceDriverFactory — memory construction (#4083)',
await explicit.handle.disconnect?.();
});
});
+
+// #4410 — the keys that were DECLARED and dropped on the floor. Each of these
+// was authorable, strict, documented and read by nothing, so a datasource that
+// set it behaved exactly like one that did not. The gate over `datasource.config`
+// is only honest if the contract it enforces is one the factory honours, which
+// is what these pin.
+describe('createDefaultDatasourceDriverFactory — declared keys reach the driver (#4410)', () => {
+ /** The knex config a constructed SqlDriver was built from. */
+ function knexConfigOf(driver: any): any {
+ return driver?.config ?? driver?.knexConfig ?? driver?.options ?? {};
+ }
+
+ it('honours the datasource `pool` block instead of the hardcoded min0/max5', async () => {
+ const handle: any = await factory().create({
+ driver: 'postgres',
+ config: { host: 'db.internal', database: 'analytics' },
+ pool: { min: 2, max: 20, idleTimeoutMillis: 45_000 },
+ });
+ const cfg = knexConfigOf(handle.driver ?? handle);
+ expect(cfg.pool).toMatchObject({ min: 2, max: 20, idleTimeoutMillis: 45_000 });
+ try { await handle.disconnect?.(); } catch { /* pool never opened */ }
+ });
+
+ it('keeps the previous defaults when no pool is declared', async () => {
+ const handle: any = await factory().create({
+ driver: 'postgres',
+ config: { host: 'db.internal', database: 'analytics' },
+ });
+ expect(knexConfigOf(handle.driver ?? handle).pool).toMatchObject({ min: 0, max: 5 });
+ try { await handle.disconnect?.(); } catch { /* pool never opened */ }
+ });
+
+ it('carries postgres schema / applicationName / statementTimeout onto the connection', async () => {
+ const handle: any = await factory().create({
+ driver: 'postgres',
+ config: {
+ host: 'db.internal',
+ database: 'analytics',
+ schema: 'reporting',
+ applicationName: 'objectstack',
+ statementTimeout: 30_000,
+ },
+ });
+ const cfg = knexConfigOf(handle.driver ?? handle);
+ expect(cfg.searchPath).toBe('reporting');
+ expect(cfg.connection).toMatchObject({
+ application_name: 'objectstack',
+ statement_timeout: 30_000,
+ });
+ try { await handle.disconnect?.(); } catch { /* pool never opened */ }
+ });
+
+ it('carries the datasource `ssl` block onto the connection, certificates and all', async () => {
+ const handle: any = await factory().create({
+ driver: 'postgres',
+ config: { host: 'db.internal', database: 'analytics' },
+ ssl: { enabled: true, rejectUnauthorized: false, ca: 'CA-PEM' },
+ });
+ expect(knexConfigOf(handle.driver ?? handle).connection).toMatchObject({
+ ssl: { rejectUnauthorized: false, ca: 'CA-PEM' },
+ });
+ try { await handle.disconnect?.(); } catch { /* pool never opened */ }
+ });
+
+ it('reads `ssl: false` on the block as TLS off, not as absent', async () => {
+ const handle: any = await factory().create({
+ driver: 'postgres',
+ config: { host: 'db.internal', database: 'analytics', ssl: true },
+ ssl: { enabled: false },
+ });
+ expect(knexConfigOf(handle.driver ?? handle).connection).toMatchObject({ ssl: false });
+ try { await handle.disconnect?.(); } catch { /* pool never opened */ }
+ });
+
+ it('falls back to the per-driver boolean shorthand when no block is declared', async () => {
+ const handle: any = await factory().create({
+ driver: 'postgres',
+ config: { host: 'db.internal', database: 'analytics', ssl: true },
+ });
+ expect(knexConfigOf(handle.driver ?? handle).connection).toMatchObject({ ssl: true });
+ try { await handle.disconnect?.(); } catch { /* pool never opened */ }
+ });
+
+ it("applies the datasource's own schemaMode, which never reached the driver before", async () => {
+ const handle: any = await factory().create({
+ driver: 'postgres',
+ config: { host: 'db.internal', database: 'analytics' },
+ schemaMode: 'external',
+ });
+ const driver: any = handle.driver ?? handle;
+ expect(knexConfigOf(driver).schemaMode ?? driver.schemaMode).toBe('external');
+ try { await handle.disconnect?.(); } catch { /* pool never opened */ }
+ });
+});
diff --git a/packages/services/service-datasource/src/__tests__/driver-catalog.test.ts b/packages/services/service-datasource/src/__tests__/driver-catalog.test.ts
new file mode 100644
index 0000000000..4452074515
--- /dev/null
+++ b/packages/services/service-datasource/src/__tests__/driver-catalog.test.ts
@@ -0,0 +1,79 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * The connection form and the config gate must describe ONE shape (#4410).
+ *
+ * Before this, the catalog carried hand-written JSON-Schema literals while
+ * `packages/spec` carried zod schemas for the same drivers — two descriptions,
+ * neither checked against the other and neither validating anything, so the
+ * drift was invisible. Now that the zod side is the gate `DatasourceSchema` and
+ * `DatasourceAdminService` both parse `config` against, a divergence stops being
+ * cosmetic: a form field the gate rejects is a Save that cannot succeed, and a
+ * gate key the form omits is a setting only hand-written JSON can reach.
+ *
+ * The catalog is derived rather than reconciled, so these are proofs that the
+ * derivation is real — the failure they exist to catch is someone "simplifying"
+ * it back into literals.
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ getDriverConfigJsonSchemaById,
+ validateDriverConfig,
+ type BuiltinDriverId,
+} from '@objectstack/spec/data';
+
+import { DRIVER_CATALOG } from '../driver-catalog.js';
+
+describe('DRIVER_CATALOG', () => {
+ it('serves the spec projection for every offered driver', () => {
+ expect(DRIVER_CATALOG.length).toBeGreaterThan(0);
+ for (const entry of DRIVER_CATALOG) {
+ expect(entry.configSchema, entry.id)
+ .toBe(getDriverConfigJsonSchemaById(entry.id as BuiltinDriverId));
+ }
+ });
+
+ it('offers only drivers the platform can build and validate', () => {
+ for (const entry of DRIVER_CATALOG) {
+ expect(validateDriverConfig(entry.id, {}).known, entry.id).toBe(true);
+ }
+ });
+
+ it('keeps its curation — label, description and icon per entry', () => {
+ for (const entry of DRIVER_CATALOG) {
+ expect(entry.label, entry.id).toBeTruthy();
+ expect(entry.description, entry.id).toBeTruthy();
+ expect(entry.icon, entry.id).toBeTruthy();
+ }
+ expect(DRIVER_CATALOG.map((d) => d.id)).toEqual(['memory', 'sqlite', 'postgres', 'mysql', 'mongo']);
+ });
+
+ /**
+ * The form renders the AUTHOR-facing shape, so a field carrying a default
+ * must not be marked required — an input-mode projection, not output-mode.
+ * Getting this backwards would make the wizard demand a `host` the driver
+ * already defaults.
+ */
+ it('projects the input shape, so defaulted fields stay optional', () => {
+ const postgres = DRIVER_CATALOG.find((d) => d.id === 'postgres')!;
+ const schema = postgres.configSchema as { required?: string[]; properties: Record };
+
+ expect(Object.keys(schema.properties)).toContain('host');
+ expect(schema.required ?? []).not.toContain('host');
+ });
+
+ it('every field the form offers is a field the gate accepts', () => {
+ for (const entry of DRIVER_CATALOG) {
+ const schema = entry.configSchema as { properties: Record };
+ for (const key of Object.keys(schema.properties)) {
+ const result = validateDriverConfig(entry.id, { [key]: undefined });
+ expect(result.known, `${entry.id}.${key}`).toBe(true);
+ const issues = (result as { issues: Array<{ message: string }> }).issues;
+ const unknownKey = issues.some((i) => i.message.includes('Unrecognized key'));
+ expect(unknownKey, `${entry.id}.${key} is offered by the form but rejected by the gate`)
+ .toBe(false);
+ }
+ }
+ });
+});
diff --git a/packages/services/service-datasource/src/contracts/datasource-driver-factory.ts b/packages/services/service-datasource/src/contracts/datasource-driver-factory.ts
index cc83273d75..500e496218 100644
--- a/packages/services/service-datasource/src/contracts/datasource-driver-factory.ts
+++ b/packages/services/service-datasource/src/contracts/datasource-driver-factory.ts
@@ -29,12 +29,34 @@ export interface DatasourceConnectionSpec {
driver: string;
/** Driver-specific connection config (host, port, database, …). No secrets. */
config: Record;
+ /**
+ * Schema ownership mode (ADR-0015) — whether ObjectStack owns this schema or
+ * is a guest in a database it must never run DDL against.
+ *
+ * Carried here since #4410. The factory used to look for it on `external`
+ * (the federation-settings block, which has no such key) and then inside
+ * `config` (which nothing ever wrote), so a datasource's own declared
+ * `schemaMode` never reached the driver: an `external` database was
+ * constructed as `managed`, with DDL ungated at the driver level.
+ */
+ schemaMode?: 'managed' | 'external' | 'validate-only';
/** Cleartext secret (password / DSN) injected for this connection only. */
secret?: string;
/** External federation settings (timeouts, allowed schemas, …). */
external?: Record;
/** Connection pool settings. */
pool?: Record;
+ /**
+ * Datasource-level TLS block (`enabled`, `rejectUnauthorized`, `ca`, `cert`,
+ * `key`).
+ *
+ * Carried here since #4410. It was declared on the datasource, strict,
+ * documented — and never reached a driver, because it stopped at the record:
+ * nothing put it on this spec. So a TLS configuration that never took effect
+ * looked exactly like one that did, which is the failure `datasource.ssl`'s
+ * own schema comment warns about.
+ */
+ ssl?: Record;
}
/**
diff --git a/packages/services/service-datasource/src/datasource-admin-service.ts b/packages/services/service-datasource/src/datasource-admin-service.ts
index bf80f3a348..09a12a64a1 100644
--- a/packages/services/service-datasource/src/datasource-admin-service.ts
+++ b/packages/services/service-datasource/src/datasource-admin-service.ts
@@ -21,6 +21,7 @@
* - Removal is refused while objects are still bound to the datasource.
*/
+import { validateDriverConfig } from '@objectstack/spec/data';
import type {
IDatasourceAdminService,
DatasourceDraft,
@@ -207,6 +208,15 @@ export class DatasourceAdminService implements IDatasourceAdminService {
if (!input?.driver) {
return { ok: false, error: 'A driver is required to test a connection.' };
}
+ // Checked BEFORE the probe: a misspelled key makes the driver fall back to
+ // its own defaults, so the probe would open a connection to localhost and
+ // report a green "Connection successful" for a datasource that points
+ // somewhere else entirely — the wizard's version of #4410's core bug.
+ try {
+ this.assertValidConfig(input.driver, input.config);
+ } catch (err) {
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
+ }
const queryTimeoutMs = (input.external as { queryTimeoutMs?: number } | undefined)?.queryTimeoutMs;
try {
return await this.config.probe({
@@ -224,6 +234,7 @@ export class DatasourceAdminService implements IDatasourceAdminService {
async createDatasource(input: DatasourceDraft, secret?: SecretInput): Promise {
this.assertValidName(input?.name);
if (!input.driver) throw new Error('A driver is required to create a datasource.');
+ this.assertValidConfig(input.driver, input.config);
const existing = await this.config.getDatasourceRecord(input.name);
if (existing) {
@@ -278,6 +289,16 @@ export class DatasourceAdminService implements IDatasourceAdminService {
merged.external = { ...patch.external, credentialsRef: existing.external?.credentialsRef };
}
+ // Judged on the MERGED record, but only when this write actually touches
+ // the pairing: a new `config`, or a new `driver` that reinterprets the
+ // stored one. An edit that renames a datasource or flips `active` must not
+ // be blocked by a config it is not touching — a record written before
+ // #4410 would otherwise become uneditable, including the `active: false`
+ // that takes a misconfigured datasource out of service.
+ if (patch.config !== undefined || patch.driver !== undefined) {
+ this.assertValidConfig(merged.driver, merged.config);
+ }
+
if (secret) {
const prevRef = existing.external?.credentialsRef;
const credentialsRef = await this.config.writeSecret(secret, { name });
@@ -311,6 +332,28 @@ export class DatasourceAdminService implements IDatasourceAdminService {
// --- internals -----------------------------------------------------------
+ /**
+ * Reject a `config` that does not satisfy its driver's contract (#4410).
+ *
+ * The wizard is the OTHER authoring surface for a datasource, and it does not
+ * reach `DatasourceSchema`: `createDatasource` writes through
+ * `metadata.register`, whose validation is a structural `name`/`label` check,
+ * not a zod parse. So a `config` typed into the Setup form was accepted here
+ * even after the spec gate landed — the same silent acceptance, one door
+ * along. Both doors now consult the same registry.
+ *
+ * A driver the platform ships no contract for passes untouched, matching the
+ * spec gate's boundary rather than inventing a stricter one for the UI.
+ */
+ private assertValidConfig(driver: string, config: unknown): void {
+ const result = validateDriverConfig(driver, config);
+ if (!result.known || result.issues.length === 0) return;
+ const detail = result.issues
+ .map((issue) => (issue.path.length ? `config.${issue.path.join('.')}: ${issue.message}` : issue.message))
+ .join('\n');
+ throw new Error(`Invalid configuration for driver '${driver}'.\n${detail}`);
+ }
+
private assertValidName(name: string | undefined): void {
if (!name || !NAME_RE.test(name)) {
throw new Error(
diff --git a/packages/services/service-datasource/src/datasource-connection-service.ts b/packages/services/service-datasource/src/datasource-connection-service.ts
index b9ae5bddd5..36a5914f41 100644
--- a/packages/services/service-datasource/src/datasource-connection-service.ts
+++ b/packages/services/service-datasource/src/datasource-connection-service.ts
@@ -53,6 +53,8 @@ export interface ConnectableDatasource {
validation?: { onMismatch?: 'fail' | 'warn' | 'ignore' };
}) | undefined;
pool?: Record;
+ /** Datasource-level TLS block — carried to the driver since #4410. */
+ ssl?: Record;
active?: boolean;
origin?: 'code' | 'runtime';
/**
@@ -673,8 +675,12 @@ function toSpec(record: ConnectableDatasource): DatasourceConnectionSpec {
name: record.name,
driver: record.driver,
config: record.config ?? {},
+ // #4410: dropped here before, which is why the factory went looking for
+ // `schemaMode` in two places that could never hold it.
+ ...(record.schemaMode ? { schemaMode: record.schemaMode } : {}),
external: record.external,
pool: record.pool,
+ ssl: record.ssl,
};
}
diff --git a/packages/services/service-datasource/src/default-datasource-driver-factory.ts b/packages/services/service-datasource/src/default-datasource-driver-factory.ts
index 72c5ec14b5..a2e1c0671b 100644
--- a/packages/services/service-datasource/src/default-datasource-driver-factory.ts
+++ b/packages/services/service-datasource/src/default-datasource-driver-factory.ts
@@ -15,10 +15,13 @@
* - `sqlite` / `sqlite3` → `@objectstack/driver-sql` (better-sqlite3)
* - `sqlite-wasm` / `wasm-sqlite` → `@objectstack/driver-sqlite-wasm` (pure-JS)
* - `mysql` / `mysql2` → `@objectstack/driver-sql` (client `mysql2`)
- * - `mongodb` / `mongo` → `@objectstack/driver-mongodb` (peer dep)
- * - `memory` / `inmemory` → `@objectstack/driver-memory` (ephemeral,
+ * - `mongo` / `mongodb` → `@objectstack/driver-mongodb` (peer dep)
+ * - `memory` / `inmemory` → `@objectstack/driver-memory` (ephemeral,
* per-datasource — see {@link buildMemoryConfig})
*
+ * The full alias table lives in `@objectstack/spec` (`resolveDriverId`), which
+ * is also what selects each driver's config contract — see {@link resolveKind}.
+ *
* `sqlite-wasm` joined for ADR-0062 D1 (#3826): the standalone stack's
* `default` datasource is a *declared definition* connected through the shared
* `DatasourceConnectionService`, and its CI-safe wasm default must therefore be
@@ -32,34 +35,28 @@
*/
import { join } from 'node:path';
+import { resolveDriverId, type BuiltinDriverId } from '@objectstack/spec/data';
import type {
IDatasourceDriverFactory,
DatasourceConnectionSpec,
DatasourceDriverHandle,
} from './contracts/index.js';
-type ResolvedKind = 'postgres' | 'sqlite' | 'sqlite-wasm' | 'mysql' | 'mongodb' | 'memory';
-
-const DRIVER_ID_ALIASES: Record = {
- postgres: 'postgres',
- postgresql: 'postgres',
- pg: 'postgres',
- sqlite: 'sqlite',
- sqlite3: 'sqlite',
- 'better-sqlite3': 'sqlite',
- 'sqlite-wasm': 'sqlite-wasm',
- 'wasm-sqlite': 'sqlite-wasm',
- mysql: 'mysql',
- mysql2: 'mysql',
- mongodb: 'mongodb',
- mongo: 'mongodb',
- memory: 'memory',
- inmemory: 'memory',
- 'in-memory': 'memory',
-};
+/**
+ * Driver-id resolution comes from the spec since #4410 — this file used to keep
+ * its own copy of the alias table.
+ *
+ * Two tables meant the id that selects a DRIVER and the id that selects that
+ * driver's CONFIG CONTRACT could disagree: a spelling only this table knew
+ * would be built while its config was validated against nothing — the exact
+ * silent acceptance the config gate exists to end, reintroduced as a lookup
+ * miss. One table, so "buildable" and "has a contract" are the same set by
+ * construction.
+ */
+type ResolvedKind = BuiltinDriverId;
function resolveKind(driverId: string): ResolvedKind | undefined {
- return DRIVER_ID_ALIASES[String(driverId ?? '').toLowerCase()];
+ return resolveDriverId(driverId);
}
/**
@@ -78,11 +75,64 @@ function toHandle(driver: any, serverVersion?: () => Promise
};
}
+/**
+ * Postgres connection options that are neither the target nor the credentials —
+ * declared on `PostgresConfigSchema` and carried onto every connection shape
+ * (DSN or discrete fields alike), since `pg` accepts them next to a
+ * `connectionString`.
+ *
+ * These were declared in the spec and read by nothing until #4410. Giving
+ * `config` a gate means every key inside it now claims to be honoured, so each
+ * one is either wired (here) or removed from the contract — a declared key that
+ * silently does nothing is the defect this whole campaign is about.
+ */
+function pgConnectionExtras(cfg: Record): Record {
+ return {
+ ...(cfg.applicationName ? { application_name: cfg.applicationName } : {}),
+ ...(cfg.statementTimeout != null ? { statement_timeout: cfg.statementTimeout } : {}),
+ };
+}
+
+/**
+ * The `ssl` value to hand a SQL client, from the datasource's TLS block or the
+ * per-driver on/off shorthand.
+ *
+ * `datasource.ssl` is declared, strict, documented — and until #4410 stopped at
+ * the record: nothing put it on the connection spec, so a TLS block with a CA
+ * certificate in it configured precisely nothing, which is the failure its own
+ * schema comment warns about ("a TLS setting that never took effect looked
+ * identical to one that did"). The block wins when present because it is the
+ * more specific statement; `config.ssl` remains the boolean shorthand.
+ */
+function resolveSslOption(spec: DatasourceConnectionSpec): unknown {
+ const block = spec.ssl as
+ | { enabled?: boolean; rejectUnauthorized?: boolean; ca?: string; cert?: string; key?: string }
+ | undefined;
+ if (block) {
+ if (block.enabled === false) return false;
+ const options = {
+ ...(block.rejectUnauthorized !== undefined ? { rejectUnauthorized: block.rejectUnauthorized } : {}),
+ ...(block.ca ? { ca: block.ca } : {}),
+ ...(block.cert ? { cert: block.cert } : {}),
+ ...(block.key ? { key: block.key } : {}),
+ };
+ // `ssl: {}` would read as "TLS with default options" to `pg`, which is what
+ // `enabled: true` with nothing else means anyway — but an empty object is
+ // an odd thing to hand a client, so collapse it to the boolean.
+ return Object.keys(options).length > 0 ? options : true;
+ }
+ const shorthand = (spec.config ?? {}).ssl;
+ return shorthand == null ? undefined : shorthand;
+}
+
/** Build the Knex `connection` for a SQL driver from a spec's config + secret. */
function buildSqlConnection(spec: DatasourceConnectionSpec, client: 'pg' | 'better-sqlite3'): unknown {
const cfg = (spec.config ?? {}) as Record;
if (client === 'better-sqlite3') {
+ // `file` / `database` are pre-#4410 tolerance for shapes already persisted
+ // by the runtime store. Authoring rejects both with a rename hint
+ // (`SqliteConfigSchema`), so nothing new can arrive spelled this way.
const filename =
(cfg.filename as string | undefined) ??
(cfg.file as string | undefined) ??
@@ -93,10 +143,18 @@ function buildSqlConnection(spec: DatasourceConnectionSpec, client: 'pg' | 'bett
// pg — accept either a connection string (`url`/`connectionString`) or
// discrete fields. The secret is the password and is never part of `config`.
+ const ssl = resolveSslOption(spec);
const url = (cfg.url as string | undefined) ?? (cfg.connectionString as string | undefined);
if (url) {
// For a DSN, a separately-supplied secret overrides the embedded password.
- return spec.secret ? { connectionString: url, password: spec.secret } : { connectionString: url };
+ // TLS still applies: `sslmode` in a DSN and the `ssl` option are separate
+ // channels to `pg`, and a datasource that declares one should get it.
+ return {
+ connectionString: url,
+ ...(spec.secret ? { password: spec.secret } : {}),
+ ...(ssl !== undefined ? { ssl } : {}),
+ ...pgConnectionExtras(cfg),
+ };
}
return {
host: cfg.host,
@@ -104,7 +162,29 @@ function buildSqlConnection(spec: DatasourceConnectionSpec, client: 'pg' | 'bett
database: cfg.database,
user: cfg.user ?? cfg.username,
...(spec.secret ? { password: spec.secret } : cfg.password ? { password: cfg.password } : {}),
- ...(cfg.ssl != null ? { ssl: cfg.ssl } : {}),
+ ...(ssl !== undefined ? { ssl } : {}),
+ ...pgConnectionExtras(cfg),
+ };
+}
+
+/**
+ * Knex pool options for a SQL driver, from the datasource's own `pool` block.
+ *
+ * `datasource.pool` is declared, strict, documented and — until #4410 — read by
+ * nobody: `toSpec` carried it into the connection spec and this factory then
+ * hardcoded `{ min: 0, max: 5 }` over the top, so an author who sized their pool
+ * got the defaults and no indication. Those defaults are preserved for the
+ * unspecified case, so nothing that did not set `pool` changes behaviour.
+ */
+function buildSqlPool(spec: DatasourceConnectionSpec): Record {
+ const pool = (spec.pool ?? {}) as Record;
+ return {
+ min: typeof pool.min === 'number' ? pool.min : 0,
+ max: typeof pool.max === 'number' ? pool.max : 5,
+ ...(typeof pool.idleTimeoutMillis === 'number' ? { idleTimeoutMillis: pool.idleTimeoutMillis } : {}),
+ ...(typeof pool.connectionTimeoutMillis === 'number'
+ ? { acquireTimeoutMillis: pool.connectionTimeoutMillis }
+ : {}),
};
}
@@ -116,6 +196,7 @@ function buildSqlConnection(spec: DatasourceConnectionSpec, client: 'pg' | 'bett
*/
function buildMysqlConnection(spec: DatasourceConnectionSpec): unknown {
const cfg = (spec.config ?? {}) as Record;
+ const mysqlSsl = resolveSslOption(spec);
const url = (cfg.url as string | undefined) ?? (cfg.connectionString as string | undefined);
if (url) return url;
return {
@@ -124,7 +205,7 @@ function buildMysqlConnection(spec: DatasourceConnectionSpec): unknown {
database: cfg.database,
user: cfg.user ?? cfg.username,
...(spec.secret ? { password: spec.secret } : cfg.password ? { password: cfg.password } : {}),
- ...(cfg.ssl != null ? { ssl: cfg.ssl } : {}),
+ ...(mysqlSsl !== undefined ? { ssl: mysqlSsl } : {}),
};
}
@@ -200,17 +281,31 @@ function buildMemoryConfig(spec: DatasourceConnectionSpec): Record;
+ // `uri` is pre-#4410 tolerance for already-persisted shapes; authoring
+ // rejects it with a rename hint to `url` (`MongoConfigSchema`).
const explicit = (cfg.url as string | undefined) ?? (cfg.uri as string | undefined);
if (explicit) return explicit;
const host = (cfg.host as string | undefined) ?? 'localhost';
const port = (cfg.port as number | string | undefined) ?? 27017;
const db = (cfg.database as string | undefined) ?? '';
const user = (cfg.user as string | undefined) ?? (cfg.username as string | undefined);
- const auth = user ? `${encodeURIComponent(user)}:${encodeURIComponent(spec.secret ?? '')}@` : '';
- return `mongodb://${auth}${host}:${port}/${db}`;
+ const password = spec.secret ?? (cfg.password as string | undefined) ?? '';
+ const auth = user ? `${encodeURIComponent(user)}:${encodeURIComponent(password)}@` : '';
+ const authSource = cfg.authSource as string | undefined;
+ const query = authSource ? `?authSource=${encodeURIComponent(authSource)}` : '';
+ return `mongodb://${auth}${host}:${port}/${db}${query}`;
}
/**
@@ -242,7 +337,15 @@ export function createDefaultDatasourceDriverFactory(
throw new Error(`Unsupported driver id '${spec.driver}'.`);
}
- const schemaMode = (spec.external as { schemaMode?: string } | undefined)?.schemaMode
+ // ADR-0015's ownership mode. `spec.schemaMode` — the datasource's own
+ // declared key — is FIRST since #4410; before that the first two arms
+ // were all there was, and neither could ever hold it: `external` is the
+ // federation-settings block (no `schemaMode` key), and nothing wrote the
+ // `config` copy. So `schemaMode: 'external'` on a datasource reached the
+ // driver as `undefined` and a database ObjectStack is a guest in was
+ // treated as managed — DDL ungated at the driver.
+ const schemaMode = spec.schemaMode
+ ?? (spec.external as { schemaMode?: string } | undefined)?.schemaMode
?? ((spec.config as Record | undefined)?.schemaMode as string | undefined);
// Host-composition passthroughs (#3826): the CLI's declared `default`
// definition carries the dev loosen-only self-heal (#2186) and the wasm
@@ -254,10 +357,15 @@ export function createDefaultDatasourceDriverFactory(
if (kind === 'postgres') {
const { SqlDriver } = await import('@objectstack/driver-sql');
+ // `searchPath` is knex's own key for postgres' default schema — the
+ // landing site for `config.schema`, declared since the protocol's first
+ // postgres shape and read by nothing until #4410.
+ const searchPath = cfg.schema as string | undefined;
const driver = new SqlDriver({
client: 'pg',
connection: buildSqlConnection(spec, 'pg') as any,
- pool: { min: 0, max: 5 },
+ pool: buildSqlPool(spec),
+ ...(searchPath ? { searchPath } : {}),
...(schemaMode ? { schemaMode: schemaMode as any } : {}),
...(autoMigrate ? { autoMigrate } : {}),
} as any);
@@ -310,14 +418,14 @@ export function createDefaultDatasourceDriverFactory(
const driver = new SqlDriver({
client: 'mysql2',
connection: buildMysqlConnection(spec) as any,
- pool: { min: 0, max: 5 },
+ pool: buildSqlPool(spec),
...(schemaMode ? { schemaMode: schemaMode as any } : {}),
...(autoMigrate ? { autoMigrate } : {}),
} as any);
return toHandle(driver);
}
- if (kind === 'mongodb') {
+ if (kind === 'mongo') {
let MongoDBDriver: any;
try {
({ MongoDBDriver } = await import('@objectstack/driver-mongodb' as any));
@@ -326,7 +434,17 @@ export function createDefaultDatasourceDriverFactory(
`mongodb driver requested but @objectstack/driver-mongodb is not installed (${err?.message ?? err}).`,
);
}
- const driver = new MongoDBDriver({ url: buildMongoUrl(spec) });
+ // `options` (the MongoClient passthrough) and the datasource's `pool`
+ // block reach the client since #4410 — the driver has always read
+ // `options` / `minPoolSize` / `maxPoolSize`; only `url` was ever passed.
+ const pool = (spec.pool ?? {}) as Record;
+ const driver = new MongoDBDriver({
+ url: buildMongoUrl(spec),
+ ...(cfg.database ? { database: cfg.database } : {}),
+ ...(cfg.options && typeof cfg.options === 'object' ? { options: cfg.options } : {}),
+ ...(typeof pool.min === 'number' ? { minPoolSize: pool.min } : {}),
+ ...(typeof pool.max === 'number' ? { maxPoolSize: pool.max } : {}),
+ });
return toHandle(driver);
}
diff --git a/packages/services/service-datasource/src/driver-catalog.ts b/packages/services/service-datasource/src/driver-catalog.ts
index 0d275717fc..ba0ba7f359 100644
--- a/packages/services/service-datasource/src/driver-catalog.ts
+++ b/packages/services/service-datasource/src/driver-catalog.ts
@@ -11,8 +11,26 @@
* Served by `GET /api/v1/datasources/drivers`. This is the curated set of
* connection drivers the connection form offers; a future runtime driver
* registry can supersede this list without changing the route contract.
+ *
+ * ## The schemas are PROJECTED, not written here (#4410)
+ *
+ * They used to be JSON-Schema literals maintained in this file, in parallel
+ * with `packages/spec`'s per-driver zod schemas — two descriptions of one shape,
+ * neither checked against the other, and neither validating anything. #4410
+ * made the zod side the gate `DatasourceSchema` parses `config` against, which
+ * turns that duplication from untidy into dangerous: a form offering a field the
+ * gate rejects is a Setup wizard whose Save cannot succeed, with the platform's
+ * own form as the thing at fault.
+ *
+ * So the form renders the projection of the same schema that judges the save.
+ * What stays local is CURATION — which drivers the form offers, and their
+ * label/description/icon. `sqlite-wasm` is deliberately absent: it is
+ * constructible and has a config contract, but it exists for CI and
+ * no-native-build environments rather than as something an admin picks here.
*/
+import { getDriverConfigJsonSchemaById, type BuiltinDriverId } from '@objectstack/spec/data';
+
export interface DriverCatalogEntry {
/** Unique driver identifier used as `datasource.driver`. */
id: string;
@@ -26,88 +44,46 @@ export interface DriverCatalogEntry {
configSchema: Record;
}
-const SSL_PROP = {
- ssl: { type: 'boolean', title: 'Use SSL/TLS', default: false },
-} as const;
-
-export const DRIVER_CATALOG: DriverCatalogEntry[] = [
+/** The curated part — everything except the shape, which comes from the spec. */
+const CURATED: ReadonlyArray<{
+ id: BuiltinDriverId;
+ label: string;
+ description: string;
+ icon: string;
+}> = [
{
id: 'memory',
label: 'In-Memory',
description: 'Ephemeral in-memory driver for dev, tests, and prototyping. No connection settings.',
icon: 'memory-stick',
- configSchema: { type: 'object', properties: {}, additionalProperties: false },
},
{
id: 'sqlite',
label: 'SQLite',
description: 'File-backed (or in-memory) SQL database. Great for local dev and small deployments.',
icon: 'database',
- configSchema: {
- type: 'object',
- properties: {
- filename: {
- type: 'string',
- title: 'Filename',
- description: 'Database file path, or ":memory:" for an ephemeral in-memory database.',
- default: ':memory:',
- },
- },
- required: ['filename'],
- additionalProperties: false,
- },
},
{
id: 'postgres',
label: 'PostgreSQL',
description: 'PostgreSQL connection. Supply host/port/database or a connection URL.',
icon: 'database',
- configSchema: {
- type: 'object',
- properties: {
- url: { type: 'string', title: 'Connection URL', description: 'postgres://user:pass@host:5432/db (overrides the fields below when set).' },
- host: { type: 'string', title: 'Host', default: 'localhost' },
- port: { type: 'number', title: 'Port', default: 5432 },
- database: { type: 'string', title: 'Database' },
- username: { type: 'string', title: 'User' },
- password: { type: 'string', title: 'Password', format: 'password' },
- schema: { type: 'string', title: 'Schema', default: 'public' },
- ...SSL_PROP,
- },
- additionalProperties: true,
- },
},
{
id: 'mysql',
label: 'MySQL / MariaDB',
description: 'MySQL or MariaDB connection.',
icon: 'database',
- configSchema: {
- type: 'object',
- properties: {
- host: { type: 'string', title: 'Host', default: 'localhost' },
- port: { type: 'number', title: 'Port', default: 3306 },
- database: { type: 'string', title: 'Database' },
- username: { type: 'string', title: 'User' },
- password: { type: 'string', title: 'Password', format: 'password' },
- ...SSL_PROP,
- },
- additionalProperties: true,
- },
},
{
id: 'mongo',
label: 'MongoDB',
description: 'MongoDB connection via a connection URI.',
icon: 'database',
- configSchema: {
- type: 'object',
- properties: {
- url: { type: 'string', title: 'Connection URI', description: 'mongodb://host:27017' },
- database: { type: 'string', title: 'Database' },
- },
- required: ['url'],
- additionalProperties: true,
- },
},
];
+
+export const DRIVER_CATALOG: DriverCatalogEntry[] = CURATED.map((entry) => ({
+ ...entry,
+ configSchema: getDriverConfigJsonSchemaById(entry.id),
+}));
diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json
index c1abceb0dd..0177e4c033 100644
--- a/packages/spec/api-surface.json
+++ b/packages/spec/api-surface.json
@@ -201,11 +201,15 @@
"ApiPrimitive (type)",
"AuditProvenanceField (type)",
"AuthoringKeySurface (type)",
+ "AutoPersistenceConfig (type)",
+ "AutoPersistenceConfigSchema (const)",
"AutonumberToken (type)",
"BOOLEAN_VALUE_TYPES (const)",
+ "BUILTIN_DRIVER_IDS (const)",
"BaseEngineOptions (type)",
"BaseEngineOptionsSchema (const)",
"BaseValidationRuleShape (interface)",
+ "BuiltinDriverId (type)",
"CALENDAR_DATE_TYPES (const)",
"CLOCK_TIME_TYPES (const)",
"COMPUTED_VALUE_TYPES (const)",
@@ -237,6 +241,8 @@
"CurrencyConfigSchema (const)",
"CurrencyValue (type)",
"CurrencyValueSchema (const)",
+ "CustomPersistenceConfig (type)",
+ "CustomPersistenceConfigSchema (const)",
"DATA_ACTION_TO_API_OPERATION (const)",
"DATE_MACRO_ALIAS_TOKENS (const)",
"DATE_MACRO_DESCRIPTIONS (const)",
@@ -246,6 +252,8 @@
"DATE_MACRO_TOKENS (const)",
"DATE_MACRO_UNITS (const)",
"DATE_MACRO_WRAPPED_RE (const)",
+ "DRIVER_CONFIG_SCHEMAS (const)",
+ "DRIVER_ID_ALIASES (const)",
"DataEngineAggregateOptions (type)",
"DataEngineAggregateOptionsSchema (const)",
"DataEngineAggregateRequestSchema (const)",
@@ -305,12 +313,15 @@
"DriverCapabilities (type)",
"DriverCapabilitiesSchema (const)",
"DriverConfig (type)",
+ "DriverConfigIssue (interface)",
"DriverConfigSchema (const)",
+ "DriverDefinition (type)",
"DriverDefinitionSchema (const)",
"DriverInterface (type)",
"DriverInterfaceSchema (const)",
"DriverOptions (type)",
"DriverOptionsSchema (const)",
+ "DriverSslToggleSchema (const)",
"DriverType (const)",
"DroppedFieldsEvent (type)",
"DroppedFieldsEventSchema (const)",
@@ -369,6 +380,8 @@
"FieldSchema (const)",
"FieldType (type)",
"FileLikeValueSchema (const)",
+ "FilePersistenceConfig (type)",
+ "FilePersistenceConfigSchema (const)",
"FileReferenceIdValueSchema (const)",
"FileValueSchema (const)",
"Filter (type)",
@@ -409,6 +422,8 @@
"LifecycleClass (type)",
"LifecycleClassSchema (const)",
"LifecycleSchema (const)",
+ "LocalStoragePersistenceConfig (type)",
+ "LocalStoragePersistenceConfigSchema (const)",
"LocationCoordinates (type)",
"LocationCoordinatesSchema (const)",
"LocationValueSchema (const)",
@@ -419,8 +434,18 @@
"Mapping (type)",
"MappingInput (type)",
"MappingSchema (const)",
+ "MemoryConfig (type)",
+ "MemoryConfigSchema (const)",
+ "MemoryDriverSpec (const)",
+ "MemoryPersistenceConfig (type)",
+ "MemoryPersistenceConfigSchema (const)",
"Metric (type)",
"MetricSchema (const)",
+ "MongoConfig (type)",
+ "MongoConfigSchema (const)",
+ "MongoDriverSpec (const)",
+ "MysqlConfig (type)",
+ "MysqlConfigSchema (const)",
"NUMERIC_VALUE_TYPES (const)",
"NoSQLDataTypeMapping (type)",
"NoSQLDataTypeMappingSchema (const)",
@@ -473,8 +498,14 @@
"PaginationConformanceRow (interface)",
"PerOperationRequiredPermissions (type)",
"PerOperationRequiredPermissionsSchema (const)",
+ "PersistenceAdapter (type)",
+ "PersistenceAdapterSchema (const)",
+ "PersistenceType (type)",
+ "PersistenceTypeSchema (const)",
"PoolConfig (type)",
"PoolConfigSchema (const)",
+ "PostgresConfig (type)",
+ "PostgresConfigSchema (const)",
"ProvisionPrimaryOptions (interface)",
"QUERY_CURSOR_REMOVED (const)",
"QUERY_DISTINCT_REMOVED (const)",
@@ -486,6 +517,7 @@
"QueryInput (type)",
"QuerySchema (const)",
"RAW_FILE_VALUES_CONTEXT_KEY (const)",
+ "READ_ONLY_BELONGS_ON_DATASOURCE (const)",
"RECORD_SURFACE_PAGE_THRESHOLD (const)",
"REFERENCE_VALUE_TYPES (const)",
"RPC_QUERY_ALIAS_SLOTS (const)",
@@ -512,6 +544,7 @@
"RowCrudActionOverrideInput (type)",
"RowCrudActionOverrideSchema (const)",
"RowCrudPredicates (interface)",
+ "SCHEMA_MODE_BELONGS_ON_DATASOURCE (const)",
"SEARCHABLE_ENUM_TYPES (const)",
"SEARCHABLE_TEXTUAL_TYPES (const)",
"SEARCH_AUTO_EXCLUDED_FIELDS (const)",
@@ -525,6 +558,7 @@
"SQLiteDataTypeMappingDefaults (const)",
"SSLConfig (type)",
"SSLConfigSchema (const)",
+ "SSL_DETAIL_BELONGS_ON_DATASOURCE (const)",
"STACK_KEY_GUIDANCE (const)",
"STACK_RUNTIME_MEMBERS (const)",
"STRING_VALUE_TYPES (const)",
@@ -565,7 +599,15 @@
"SortNode (type)",
"SortNodeSchema (const)",
"SpecialOperatorSchema (const)",
+ "SqlAutoMigrate (type)",
+ "SqlAutoMigrateSchema (const)",
"SqlDialect (type)",
+ "SqliteConfig (type)",
+ "SqliteConfigSchema (const)",
+ "SqliteWasmConfig (type)",
+ "SqliteWasmConfigSchema (const)",
+ "SqliteWasmPersistMode (type)",
+ "SqliteWasmPersistModeSchema (const)",
"StateMachineValidation (type)",
"StateMachineValidationSchema (const)",
"StringOperatorSchema (const)",
@@ -611,10 +653,19 @@
"deriveFieldGroupLayout (function)",
"deriveRecordFlowSurface (function)",
"deriveRecordSurface (function)",
+ "driverConfigJsonSchema (function)",
"effectiveOperationsArray (function)",
"fieldForm (const)",
"foldQueryAliasSlots (function)",
"formatUnknownAuthoringKey (function)",
+ "getDriverConfigJsonSchemaById (function)",
+ "getDriverConfigSchema (function)",
+ "getMemoryConfigJsonSchema (const)",
+ "getMongoConfigJsonSchema (const)",
+ "getMysqlConfigJsonSchema (const)",
+ "getPostgresConfigJsonSchema (const)",
+ "getSqliteConfigJsonSchema (const)",
+ "getSqliteWasmConfigJsonSchema (const)",
"hasDynamicTokens (function)",
"hookForm (const)",
"isApiOperationAllowed (function)",
@@ -647,6 +698,7 @@
"renderAutonumber (function)",
"resolveCrudAffordances (function)",
"resolveDisplayField (function)",
+ "resolveDriverId (function)",
"resolveEffectiveApiMethods (function)",
"resolveRecordDisplayName (function)",
"resolveSearchFieldResolution (function)",
@@ -655,6 +707,7 @@
"stripLegacyApiMethods (function)",
"suggestFieldType (function)",
"utcInstantMs (function)",
+ "validateDriverConfig (function)",
"valueSchemaFor (function)"
],
"./system": [
diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json
index fb905f2dd6..be9a921172 100644
--- a/packages/spec/authorable-surface.json
+++ b/packages/spec/authorable-surface.json
@@ -3184,6 +3184,10 @@
"data/AnalyticsQuery:timeDimensions",
"data/AnalyticsQuery:timezone",
"data/AnalyticsQuery:where",
+ "data/AutoPersistenceConfig:autoSaveInterval",
+ "data/AutoPersistenceConfig:key",
+ "data/AutoPersistenceConfig:path",
+ "data/AutoPersistenceConfig:type",
"data/BaseEngineOptions:context",
"data/ConditionalValidation:active",
"data/ConditionalValidation:description",
@@ -3560,6 +3564,9 @@
"data/FieldMapping:target",
"data/FieldMapping:transform",
"data/FieldReference:$field",
+ "data/FilePersistenceConfig:autoSaveInterval",
+ "data/FilePersistenceConfig:path",
+ "data/FilePersistenceConfig:type",
"data/FileValue:alt",
"data/FileValue:duration",
"data/FileValue:mimeType",
@@ -3622,6 +3629,8 @@
"data/Lifecycle:retention",
"data/Lifecycle:storage",
"data/Lifecycle:ttl",
+ "data/LocalStoragePersistenceConfig:key",
+ "data/LocalStoragePersistenceConfig:type",
"data/LocationCoordinates:accuracy",
"data/LocationCoordinates:altitude",
"data/LocationCoordinates:latitude",
@@ -3647,6 +3656,22 @@
"data/Metric:name",
"data/Metric:sql",
"data/Metric:type",
+ "data/MongoConfig:authSource",
+ "data/MongoConfig:database",
+ "data/MongoConfig:host",
+ "data/MongoConfig:options",
+ "data/MongoConfig:password",
+ "data/MongoConfig:port",
+ "data/MongoConfig:url",
+ "data/MongoConfig:username",
+ "data/MysqlConfig:autoMigrate",
+ "data/MysqlConfig:database",
+ "data/MysqlConfig:host",
+ "data/MysqlConfig:password",
+ "data/MysqlConfig:port",
+ "data/MysqlConfig:ssl",
+ "data/MysqlConfig:url",
+ "data/MysqlConfig:username",
"data/NoSQLDataTypeMapping:array",
"data/NoSQLDataTypeMapping:binary",
"data/NoSQLDataTypeMapping:boolean",
@@ -3782,6 +3807,17 @@
"data/PoolConfig:idleTimeoutMillis",
"data/PoolConfig:max",
"data/PoolConfig:min",
+ "data/PostgresConfig:applicationName",
+ "data/PostgresConfig:autoMigrate",
+ "data/PostgresConfig:database",
+ "data/PostgresConfig:host",
+ "data/PostgresConfig:password",
+ "data/PostgresConfig:port",
+ "data/PostgresConfig:schema",
+ "data/PostgresConfig:ssl",
+ "data/PostgresConfig:statementTimeout",
+ "data/PostgresConfig:url",
+ "data/PostgresConfig:username",
"data/Query:aggregations",
"data/Query:cursor [RETIRED]",
"data/Query:distinct [RETIRED]",
@@ -3899,6 +3935,10 @@
"data/SortNode:order",
"data/SpecialOperator:$exists",
"data/SpecialOperator:$null",
+ "data/SqliteConfig:autoMigrate",
+ "data/SqliteConfig:filename",
+ "data/SqliteWasmConfig:filename",
+ "data/SqliteWasmConfig:persist",
"data/StateMachineValidation:active",
"data/StateMachineValidation:description",
"data/StateMachineValidation:events",
diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json
index 4022e4eb8d..991c7e8330 100644
--- a/packages/spec/json-schema.manifest.json
+++ b/packages/spec/json-schema.manifest.json
@@ -703,6 +703,7 @@
"data/AnalyticsQuery",
"data/ApiMethod",
"data/ApiOperation",
+ "data/AutoPersistenceConfig",
"data/BaseEngineOptions",
"data/CalendarDateValue",
"data/ClockTimeValue",
@@ -750,6 +751,7 @@
"data/DriverConfig",
"data/DriverDefinition",
"data/DriverOptions",
+ "data/DriverSslToggle",
"data/DriverType",
"data/DroppedFieldsEvent",
"data/ESignatureConfig",
@@ -775,6 +777,7 @@
"data/FieldReference",
"data/FieldType",
"data/FileLikeValue",
+ "data/FilePersistenceConfig",
"data/FileReferenceIdValue",
"data/FileValue",
"data/FilterCondition",
@@ -790,11 +793,14 @@
"data/JSONValidation",
"data/Lifecycle",
"data/LifecycleClass",
+ "data/LocalStoragePersistenceConfig",
"data/LocationCoordinates",
"data/LocationValue",
"data/Mapping",
"data/Metric",
"data/ModeSchema",
+ "data/MongoConfig",
+ "data/MysqlConfig",
"data/NoSQLDataTypeMapping",
"data/NoSQLDatabaseType",
"data/NoSQLDriverConfig",
@@ -814,7 +820,9 @@
"data/ObjectOwnershipEnum",
"data/ObjectRequiredPermissions",
"data/PerOperationRequiredPermissions",
+ "data/PersistenceType",
"data/PoolConfig",
+ "data/PostgresConfig",
"data/Query",
"data/QueryFilter",
"data/ReferenceIdValue",
@@ -839,6 +847,10 @@
"data/ShardingConfig",
"data/SortNode",
"data/SpecialOperator",
+ "data/SqlAutoMigrate",
+ "data/SqliteConfig",
+ "data/SqliteWasmConfig",
+ "data/SqliteWasmPersistMode",
"data/StateMachineValidation",
"data/StringOperator",
"data/TenancyConfig",
diff --git a/packages/spec/scripts/build-docs.ts b/packages/spec/scripts/build-docs.ts
index ba8c70b401..bfc50b8e1b 100644
--- a/packages/spec/scripts/build-docs.ts
+++ b/packages/spec/scripts/build-docs.ts
@@ -61,6 +61,41 @@ const schemaZodFileMap = new Map();
const categoryZodFiles = new Map>();
// Track Zod File collisions
const zodFileCounts = new Map();
+/**
+ * Page slug -> its real path under `packages/spec/src//`.
+ *
+ * A page named after a NESTED file (`driver-postgres`) does not sit at
+ * `/driver-postgres.zod.ts`, so the "Source:" line has to be looked up
+ * rather than reassembled from the slug. Naming a file that does not exist is
+ * the same defect as a schema that does not validate: a reader following it
+ * finds nothing and has no way to tell the pointer was invented.
+ */
+const zodFileSourceRel = new Map();
+
+/**
+ * `.zod.ts` files under a category, RECURSIVELY, keyed by the slug their page
+ * takes (`driver/postgres.zod.ts` → `driver-postgres`).
+ *
+ * The walk used to be one level deep, which made every schema under
+ * `data/driver/` invisible: those twelve landed in the catch-all `misc` bucket
+ * the moment they were exported (#4410), on a page whose "Source" line named
+ * `data/misc.zod.ts` — a file that does not exist. Same one-level-deep bug the
+ * strictness ledger's own coverage gate had, and the same lesson: a generator
+ * that under-reports produces confident output about surface it never saw.
+ */
+function collectZodFiles(dir: string, prefix = ''): Array<{ slug: string; rel: string }> {
+ const out: Array<{ slug: string; rel: string }> = [];
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ if (entry.isDirectory()) {
+ out.push(...collectZodFiles(path.join(dir, entry.name), `${prefix}${entry.name}/`));
+ continue;
+ }
+ if (!entry.name.endsWith('.zod.ts')) continue;
+ const rel = `${prefix}${entry.name}`;
+ out.push({ slug: rel.replace(/\.zod\.ts$/, '').replace(/\//g, '-'), rel });
+ }
+ return out;
+}
// Scan source files to build maps
function scanCategories() {
@@ -69,33 +104,42 @@ function scanCategories() {
if (!fs.existsSync(dir)) return;
const zodFiles = new Set();
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.zod.ts'));
-
- for (const file of files) {
- const zodFileName = file.replace('.zod.ts', '');
- zodFiles.add(zodFileName);
-
- const count = zodFileCounts.get(zodFileName) || 0;
- zodFileCounts.set(zodFileName, count + 1);
-
- const content = fs.readFileSync(path.join(dir, file), 'utf-8');
-
+
+ for (const { slug, rel } of collectZodFiles(dir)) {
+ zodFiles.add(slug);
+ zodFileSourceRel.set(`${category}/${slug}`, rel);
+
+ const count = zodFileCounts.get(slug) || 0;
+ zodFileCounts.set(slug, count + 1);
+
+ const content = fs.readFileSync(path.join(dir, rel), 'utf-8');
+
// Match export const Name = ... OR export const Name: Type = ...
const regex = /export const (\w+)\s*(?:[:=])/g;
-
+
let match;
while ((match = regex.exec(content)) !== null) {
const rawName = match[1];
const finalName = rawName.endsWith('Schema') ? rawName.replace('Schema', '') : rawName;
schemaCategoryMap.set(finalName, category);
- schemaZodFileMap.set(finalName, zodFileName);
+ schemaZodFileMap.set(finalName, slug);
}
}
-
+
categoryZodFiles.set(category, zodFiles);
});
}
+/**
+ * Repo-relative source path for a page slug, or `undefined` when the slug has
+ * no file behind it (the `misc` catch-all bucket). Callers must omit the
+ * "Source:" pointer in that case rather than print a plausible-looking path.
+ */
+function sourcePathFor(category: string, zodFile: string): string | undefined {
+ const rel = zodFileSourceRel.get(`${category}/${zodFile}`);
+ return rel ? `packages/spec/src/${category}/${rel}` : undefined;
+}
+
scanCategories();
/**
@@ -387,9 +431,10 @@ function generateZodFileMarkdown(zodFile: string, schemas: Array<{name: string,
const zodTitle = zodFile.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
// Get source description
- const sourcePath = path.join(SRC_DIR, category, `${zodFile}.zod.ts`);
+ const sourceRel = sourcePathFor(category, zodFile);
+ const sourcePath = sourceRel ? path.join(REPO_ROOT, sourceRel) : undefined;
let fileDesc = '';
- if (fs.existsSync(sourcePath)) {
+ if (sourcePath && fs.existsSync(sourcePath)) {
fileDesc = getFileDescription(fs.readFileSync(sourcePath, 'utf-8'));
}
@@ -403,9 +448,13 @@ function generateZodFileMarkdown(zodFile: string, schemas: Array<{name: string,
md += `${fileDesc}\n\n`;
}
- md += `\n`;
- md += `**Source:** \`packages/spec/src/${category}/${zodFile}.zod.ts\`\n`;
- md += `\n\n`;
+ // Only when there IS one — the `misc` catch-all has no file behind it, and a
+ // reassembled `packages/spec/src//misc.zod.ts` points at nothing.
+ if (sourceRel) {
+ md += `\n`;
+ md += `**Source:** \`${sourceRel}\`\n`;
+ md += `\n\n`;
+ }
// Add TypeScript usage example
const schemaNames = schemas.map(s => s.name).join(', ');
@@ -634,8 +683,9 @@ Object.entries(CATEGORIES).forEach(([category, title]) => {
// written and the stale files are still lying around.)
if (!wasEmitted(path.join(DOCS_ROOT, category, `${zodFile}.mdx`))) return;
const fileTitle = zodFile.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
+ const cardSource = sourcePathFor(category, zodFile);
// Link relative to the category folder (where index.mdx lives)
- mdx += ` \n`;
+ mdx += ` \n`;
});
mdx += `\n`;
diff --git a/packages/spec/src/data/datasource.test.ts b/packages/spec/src/data/datasource.test.ts
index 72d1265d4f..5ef3d93492 100644
--- a/packages/spec/src/data/datasource.test.ts
+++ b/packages/spec/src/data/datasource.test.ts
@@ -190,19 +190,19 @@ describe('DatasourceSchema', () => {
expect(() => DatasourceSchema.parse({
name: 'valid_datasource_name',
driver: 'postgres',
- config: {},
+ config: { database: 'mydb' },
})).not.toThrow();
expect(() => DatasourceSchema.parse({
name: 'InvalidDatasource',
driver: 'postgres',
- config: {},
+ config: { database: 'mydb' },
})).toThrow();
expect(() => DatasourceSchema.parse({
name: 'invalid-datasource',
driver: 'postgres',
- config: {},
+ config: { database: 'mydb' },
})).toThrow();
});
@@ -210,7 +210,7 @@ describe('DatasourceSchema', () => {
const datasource = DatasourceSchema.parse({
name: 'test_db',
driver: 'postgres',
- config: {},
+ config: { database: 'mydb' },
});
expect(datasource.active).toBe(true);
@@ -278,13 +278,29 @@ describe('DatasourceSchema', () => {
name: 'mongo_db',
driver: 'mongo',
config: {
- connectionString: 'mongodb://localhost:27017/mydb',
+ url: 'mongodb://localhost:27017/mydb',
},
});
expect(datasource.driver).toBe('mongo');
});
+ // This fixture used to spell the URI `connectionString`, a key the mongo
+ // builder never read — so the datasource it described would have connected to
+ // mongodb://localhost:27017 with no database, and the test asserting it was
+ // "accepted" was asserting the silence #4410 removed.
+ it('rejects a mongo config that spells the URI `connectionString`', () => {
+ const result = DatasourceSchema.safeParse({
+ name: 'mongo_db',
+ driver: 'mongo',
+ config: { connectionString: 'mongodb://localhost:27017/mydb' },
+ });
+
+ expect(result.success).toBe(false);
+ expect(result.error!.issues[0]!.path).toEqual(['config']);
+ expect(result.error!.issues[0]!.message).toContain('`connectionString` → `url`');
+ });
+
it('should accept Redis datasource', () => {
const datasource = DatasourceSchema.parse({
name: 'redis_cache',
@@ -341,7 +357,7 @@ describe('DatasourceSchema', () => {
const datasource = DatasourceSchema.parse({
name: 'disabled_db',
driver: 'postgres',
- config: {},
+ config: { database: 'mydb' },
active: false,
});
@@ -352,7 +368,7 @@ describe('DatasourceSchema', () => {
const datasource = DatasourceSchema.parse({
name: 'custom_db',
driver: 'postgres',
- config: {},
+ config: { database: 'mydb' },
capabilities: {
queryWindowFunctions: false,
querySubqueries: false,
@@ -386,26 +402,45 @@ describe('DatasourceSchema', () => {
host: 'localhost',
port: 5432,
database: 'mydb',
- pool: {
- min: 2,
- max: 10,
- idleTimeoutMillis: 30000,
- },
- ssl: {
- rejectUnauthorized: false,
- ca: 'certificate_content',
- },
+ ssl: true,
+ },
+ pool: {
+ min: 2,
+ max: 10,
+ idleTimeoutMillis: 30000,
+ },
+ ssl: {
+ enabled: true,
+ rejectUnauthorized: false,
+ ca: 'certificate_content',
},
});
- expect(datasource.config.pool).toBeDefined();
- expect(datasource.config.ssl).toBeDefined();
+ expect(datasource.pool).toBeDefined();
+ expect(datasource.config.ssl).toBe(true);
+ // Certificates belong to the datasource-level block, which the factory now
+ // carries down to the client (#4410). Inside `config`, `ssl` is on/off.
+ expect(datasource.ssl?.ca).toBe('certificate_content');
+ });
+
+ // The fixture above used to nest `pool` INSIDE `config`, where no driver
+ // reads it — so it asserted a pooled datasource that was running on the
+ // factory's hardcoded defaults. The rejection now carries the relocation.
+ it('rejects pool sizing nested inside `config`', () => {
+ const result = DatasourceSchema.safeParse({
+ name: 'complex_db',
+ driver: 'postgres',
+ config: { database: 'mydb', pool: { min: 2, max: 10 } },
+ });
+
+ expect(result.success).toBe(false);
+ expect(result.error!.issues[0]!.message).toContain('`pool: { max: … }`');
});
it('should reject datasource without required fields', () => {
expect(() => DatasourceSchema.parse({
driver: 'postgres',
- config: {},
+ config: { database: 'mydb' },
})).toThrow();
expect(() => DatasourceSchema.parse({
@@ -429,7 +464,7 @@ describe('DatasourceSchema - ssl', () => {
const result = DatasourceSchema.parse({
name: 'secure_db',
driver: 'postgres',
- config: { host: 'db.example.com', port: 5432 },
+ config: { host: 'db.example.com', port: 5432, database: 'mydb' },
ssl: {
enabled: true,
rejectUnauthorized: true,
@@ -445,7 +480,7 @@ describe('DatasourceSchema - ssl', () => {
const result = DatasourceSchema.parse({
name: 'mtls_db',
driver: 'postgres',
- config: { host: 'db.secure.com' },
+ config: { host: 'db.secure.com', database: 'mydb' },
ssl: {
enabled: true,
ca: '/certs/ca.pem',
@@ -461,7 +496,7 @@ describe('DatasourceSchema - ssl', () => {
const result = DatasourceSchema.parse({
name: 'ssl_db',
driver: 'mysql',
- config: {},
+ config: { database: 'mydb' },
ssl: { enabled: true },
});
expect(result.ssl?.rejectUnauthorized).toBe(true);
@@ -471,7 +506,7 @@ describe('DatasourceSchema - ssl', () => {
const result = DatasourceSchema.parse({
name: 'local_db',
driver: 'sqlite',
- config: { path: './data.db' },
+ config: { filename: './data.db' },
});
expect(result.ssl).toBeUndefined();
});
@@ -482,7 +517,7 @@ describe('SchemaMode & External Federation (ADR-0015)', () => {
const ds = DatasourceSchema.parse({
name: 'default',
driver: 'postgres',
- config: {},
+ config: { database: 'mydb' },
});
expect(ds.schemaMode).toBe('managed');
expect(ds.external).toBeUndefined();
@@ -502,7 +537,7 @@ describe('SchemaMode & External Federation (ADR-0015)', () => {
const ds = DatasourceSchema.parse({
name: 'warehouse',
driver: 'postgres',
- config: { connectionString: 'postgres://...' },
+ config: { url: 'postgres://user@warehouse.internal/analytics' },
schemaMode: 'external',
external: { label: 'Analytics Warehouse' },
});
@@ -518,7 +553,7 @@ describe('SchemaMode & External Federation (ADR-0015)', () => {
const result = DatasourceSchema.safeParse({
name: 'warehouse',
driver: 'postgres',
- config: {},
+ config: { database: 'mydb' },
schemaMode: 'external',
});
expect(result.success).toBe(false);
@@ -531,7 +566,7 @@ describe('SchemaMode & External Federation (ADR-0015)', () => {
const result = DatasourceSchema.safeParse({
name: 'default',
driver: 'postgres',
- config: {},
+ config: { database: 'mydb' },
schemaMode: 'managed',
external: { allowWrites: true },
});
@@ -545,7 +580,7 @@ describe('SchemaMode & External Federation (ADR-0015)', () => {
const result = DatasourceSchema.safeParse({
name: 'warehouse',
driver: 'postgres',
- config: {},
+ config: { database: 'mydb' },
schemaMode: 'validate-only',
});
expect(result.success).toBe(false);
diff --git a/packages/spec/src/data/datasource.zod.ts b/packages/spec/src/data/datasource.zod.ts
index f21e5c4018..c60f5e1c5c 100644
--- a/packages/spec/src/data/datasource.zod.ts
+++ b/packages/spec/src/data/datasource.zod.ts
@@ -9,28 +9,34 @@ import { z } from 'zod';
*/
import { lazySchema } from '../shared/lazy-schema';
import { strictUnknownKeyError } from '../shared/suggestions.zod';
+import { validateDriverConfig } from './driver/config-registry.zod';
/*
- * ── Unknown-key strictness (#4001 data step) ────────────────────────────────
+ * ── Unknown-key strictness (#4001 data step, closed out by #4410) ───────────
*
* Every AUTHORING shape in this module is `.strict()`. `datasource` is a
* registered metadata type (BUILTIN_METADATA_TYPE_SCHEMAS), so one shape backs
* `defineDatasource()`, `defineStack({ datasources })`, the
* `/api/v1/meta/datasource` endpoint, and the Setup → Datasources form.
*
- * TWO ESCAPE HATCHES STAY OPEN, and must:
- * - `config` is per-driver by construction (a sqlite `filename` and a
- * postgres `host`/`port` share no shape), so it stays `z.record`.
- * NOTHING VALIDATES INSIDE IT TODAY — see {@link belongsInConfig}, which
- * used to claim otherwise. Tracked as #4410.
- * - `readReplicas` carries the same per-driver config objects.
+ * `config` and `readReplicas` stay `z.record` HERE, because they are per-driver
+ * by construction: a sqlite `filename` and a postgres `host`/`port` share no
+ * shape. What they are no longer is unchecked. Since #4410 the refinement on
+ * {@link DatasourceSchema} parses both against the contract for the declared
+ * driver (`data/driver/config-registry.zod.ts`), so the openness at this level
+ * is a shape this level cannot express — not the absence of one.
*
- * That openness is exactly why the TOP level had to close. Before this, a
+ * That openness is exactly why the TOP level had to close first. Before #4001, a
* connection key written one level too high — `host` next to `driver` instead
* of inside `config` — was stripped in silence, and the datasource then
* connected on driver defaults (localhost, default port) rather than failing.
* A misplaced `password` is the same bug wearing a worse hat, which is why it
* is prescribed toward `external.credentialsRef` rather than merely relocated.
+ *
+ * A driver the platform ships no contract for (a plugin's
+ * `com.vendor.snowflake`) keeps an unvalidated `config`. That is the honest
+ * boundary, not a leftover hole — see the registry's own note on why inventing
+ * a verdict against a shape we do not have would be worse than the silence.
*/
/** Keys {@link DriverDefinitionSchema} declares (drift-guarded by datasource.test.ts). */
@@ -67,28 +73,31 @@ const DATASOURCE_RETRY_POLICY_KEYS = ['maxRetries', 'baseDelayMs', 'maxDelayMs',
/**
* A connection detail written one level too high — it belongs inside `config`.
*
- * This prescription stops at *where to put it* and deliberately does NOT promise
- * that the move gets validated. It used to: the sentence read "the driver's own
- * configSchema validates it there", and that was false twice over —
- * {@link DriverDefinitionSchema}'s `configSchema` is a `z.record` that both
- * bundled driver specs set to `{}`, and nothing in this repo reads it (#4410).
- *
- * Which made this the worst line in the module: it took an author who had made a
- * recoverable mistake at a place that now catches it, and pointed them — with the
- * platform's authority — at a slot where the same mistake is silent again.
- * `config: { hostname: … }` is stripped in silence and the datasource connects on
- * localhost, which is #4001's original bug verbatim, one level down. A wrong
- * instruction is worse than none, and worst of all for an AI author, whose only
- * check on "did that work?" is whether the parse complained.
+ * This prescription makes a validation claim again, and #4410 is what made the
+ * claim true. Between #4001 and #4410 it did not: the sentence read "the
+ * driver's own configSchema validates it there", which was false twice over —
+ * {@link DriverDefinitionSchema}'s `configSchema` was a `z.record` both bundled
+ * driver specs set to `{}`, and nothing read it. That made this the worst line
+ * in the module: it took an author who had made a *recoverable* mistake at a
+ * place that catches it, and pointed them — with the platform's authority — at a
+ * slot where the same mistake was silent again. `config: { hostname: … }` was
+ * stripped in silence and the datasource connected on localhost, which is
+ * #4001's original bug verbatim, one level down. A wrong instruction is worse
+ * than none, and worst of all for an AI author, whose only check on "did that
+ * work?" is whether the parse complained.
*
- * Naming the per-driver schema is the honest form: it is the shape to write
- * against, and a reader can check themselves against it even while nothing
- * enforces it. Restore a validation claim here only when #4410 makes one true.
+ * Note the SECOND thing #4410 had to fix for this line to be safe: the target
+ * must be the key the driver contract actually declares. Prescribing
+ * `config: { user: … }` when the postgres contract spells it `username` would
+ * have swapped a one-step correction for a two-step one — reject at the top,
+ * reject again inside — so `canonical` names the landing key, not the one the
+ * author happened to type.
*/
-const belongsInConfig = (key: string) =>
+const belongsInConfig = (key: string, canonical: string = key) =>
`\`${key}\` is a driver connection detail — it belongs inside \`config\`, not at the top `
- + `level. Move it to \`config: { ${key}: … }\`, matching your driver's config shape `
- + `(\`PostgresConfigSchema\` / \`MongoConfigSchema\` / \`MemoryConfigSchema\` in \`data/driver/\`).`;
+ + `level. Move it to \`config: { ${canonical}: … }\`, which is parsed against your driver's `
+ + `config contract (\`PostgresConfigSchema\` / \`MysqlConfigSchema\` / \`SqliteConfigSchema\` / `
+ + `\`MongoConfigSchema\` / \`MemoryConfigSchema\`, exported from \`@objectstack/spec/data\`).`;
const driverDefinitionUnknownKeyError = strictUnknownKeyError({
surface: 'this driver definition',
@@ -166,11 +175,11 @@ const datasourceUnknownKeyError = strictUnknownKeyError({
host: belongsInConfig('host'),
port: belongsInConfig('port'),
database: belongsInConfig('database'),
- user: belongsInConfig('user'),
+ user: belongsInConfig('user', 'username'),
username: belongsInConfig('username'),
filename: belongsInConfig('filename'),
url: belongsInConfig('url'),
- connectionString: belongsInConfig('connectionString'),
+ connectionString: belongsInConfig('connectionString', 'url'),
password:
'`password` must never be inlined on a datasource. Interpolate it from the environment '
+ 'inside `config`, or for an external datasource reference the secrets store via '
@@ -300,11 +309,21 @@ export const DriverDefinitionSchema = lazySchema(() => z.object({
/**
* Configuration Schema (JSON Schema)
- * Describes the structure of the `config` object needed for this driver.
- * Used by the UI to generate the connection form.
+ *
+ * The structure of the `config` object this driver needs — rendered by the
+ * Studio connection form (`GET /api/v1/datasources/drivers`) and, for the
+ * built-in drivers, the JSON-Schema projection of the very zod schema
+ * `DatasourceSchema` parses `config` against. Form and gate therefore describe
+ * one shape by construction.
+ *
+ * Both bundled driver specs used to set this to `{}`, one of them with a
+ * comment promising it would be "populated at runtime" by code that did not
+ * exist; nothing read the field either (#4410). Fill it from a real schema —
+ * an empty object here means the connection form has nothing to render and
+ * says so, which is the loud version of the same absence.
*/
configSchema: z.record(z.string(), z.unknown()).describe('JSON Schema for connection configuration'),
-
+
/**
* Default Capabilities
* What this driver supports out-of-the-box.
@@ -312,6 +331,9 @@ export const DriverDefinitionSchema = lazySchema(() => z.object({
capabilities: z.lazy(() => DatasourceCapabilities).optional(),
}, { error: driverDefinitionUnknownKeyError }).strict());
+/** A driver definition — {@link DriverDefinitionSchema}'s parsed shape. */
+export type DriverDefinition = z.infer;
+
/**
* Datasource Capabilities Schema
* Declares what this datasource naturally supports.
@@ -416,6 +438,30 @@ export const ExternalDatasourceSettingsSchema = z.object({
export type ExternalDatasourceSettings = z.infer;
+/**
+ * Replay a driver-config parse onto the datasource's own issue list (#4410).
+ *
+ * A no-op for a driver the platform ships no contract for — `known: false` is
+ * the registry saying "nothing to check against", which is deliberately NOT the
+ * same answer as "checked and clean".
+ */
+function reportDriverConfigIssues(
+ ctx: z.RefinementCtx,
+ driver: unknown,
+ config: unknown,
+ basePath: (string | number)[],
+): void {
+ const result = validateDriverConfig(driver, config);
+ if (!result.known) return;
+ for (const issue of result.issues) {
+ ctx.addIssue({
+ code: 'custom',
+ path: [...basePath, ...issue.path],
+ message: issue.message,
+ });
+ }
+}
+
/**
* Datasource Schema
* Represents a connection to an external data store.
@@ -531,6 +577,17 @@ export const DatasourceSchema = lazySchema(() => z.object({
origin: z.enum(['code', 'runtime']).default('code')
.describe('Datasource provenance (server-managed, read-only)'),
}, { error: datasourceUnknownKeyError }).strict().superRefine((ds, ctx) => {
+ // The `config` gate (#4410). `config` and each `readReplicas` entry carry the
+ // same per-driver shape, so both are parsed against the contract for the
+ // declared driver and every issue is re-pathed under the slot it came from —
+ // the author sees `config.hostname`, not a detached message.
+ reportDriverConfigIssues(ctx, ds.driver, ds.config, ['config']);
+ if (Array.isArray(ds.readReplicas)) {
+ ds.readReplicas.forEach((replica, index) => {
+ reportDriverConfigIssues(ctx, ds.driver, replica, ['readReplicas', index]);
+ });
+ }
+
if (ds.schemaMode !== 'managed' && !ds.external) {
ctx.addIssue({
code: 'custom',
diff --git a/packages/spec/src/data/driver/common.zod.ts b/packages/spec/src/data/driver/common.zod.ts
new file mode 100644
index 0000000000..d026221e45
--- /dev/null
+++ b/packages/spec/src/data/driver/common.zod.ts
@@ -0,0 +1,100 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import { z } from 'zod';
+
+/**
+ * Shared building blocks for the per-driver `datasource.config` shapes (#4410).
+ *
+ * Every schema under `data/driver/` describes ONE driver's `config` slot — the
+ * keys an author may write and the platform actually reads. They are the
+ * enforcement half of the `config` escape hatch `datasource.zod.ts` opens: the
+ * slot stays `z.record` at the top of `DatasourceSchema` because a sqlite
+ * `filename` and a postgres `host` share no shape, and `DatasourceSchema`'s
+ * refinement then parses it against the schema for the declared driver.
+ *
+ * The rule these files are written to: **a key is declared here only if some
+ * code path reads it.** A config key that no driver and no factory consumes is
+ * the same silent-strip defect one level down (#4001, ADR-0078), so an unread
+ * key is either wired or rejected with a prescription — never left in the
+ * contract to look supported.
+ */
+
+/**
+ * Dev-only, loosen-only schema self-heal (#2186), honoured by the SQL drivers.
+ *
+ * Read by `createDefaultDatasourceDriverFactory` and passed to
+ * `SqlDriver.autoMigrate`; force-disabled under `NODE_ENV=production`. `'safe'`
+ * applies only non-destructive alters (relax NOT NULL, widen varchar).
+ */
+export const SqlAutoMigrateSchema = z.enum(['off', 'safe'])
+ .describe('Dev-only non-destructive schema self-heal (#2186)');
+
+export type SqlAutoMigrate = z.infer;
+
+/**
+ * `schemaMode` written inside `config`. Shared by every SQL driver: the factory
+ * used to look for it there because the datasource-level key was dropped
+ * between the record and the connection spec, so the nested copy was the only
+ * spelling that reached a driver. #4410 carries the declared key down instead.
+ */
+export const SCHEMA_MODE_BELONGS_ON_DATASOURCE =
+ '`schemaMode` is a datasource-level key, not driver config. Write it next to `driver` '
+ + "(`schemaMode: 'external'`) — the connection service now carries it down to the driver, so "
+ + 'the copy inside `config` is gone rather than duplicated.';
+
+/** `readOnly` written inside `config`. Shared by every driver. */
+export const READ_ONLY_BELONGS_ON_DATASOURCE =
+ '`readOnly` is not driver config. Use `capabilities: { readOnly: true }` on the datasource to '
+ + 'declare the connection read-only, or `external.allowWrites: false` for a federated database.';
+
+/**
+ * TLS on/off for a SQL driver — the shorthand, and deliberately ONLY the
+ * shorthand.
+ *
+ * Certificates live in the datasource's own `ssl` block (`enabled`,
+ * `rejectUnauthorized`, `ca`, `cert`, `key`), which #4410 wired through to the
+ * client; before that it was declared, strict, documented and read by nobody,
+ * so the only TLS setting that did anything was this per-driver one. Two slots
+ * for the same setting is one too many, and this is the one that has to stay
+ * narrow: it is what the Studio connection form renders from, and the form
+ * turns anything that is not a boolean / enum / number into a TEXT INPUT. A
+ * `boolean | object` union here would hand the wizard a text box whose every
+ * value the gate then rejects — a form that cannot produce a saveable record.
+ */
+export const DriverSslToggleSchema = z.boolean()
+ .describe('Enable TLS. Certificates go in the datasource-level `ssl` block.');
+
+/** Where the certificate-bearing form of TLS lives. */
+export const SSL_DETAIL_BELONGS_ON_DATASOURCE =
+ 'Certificates and verification live in the datasource-level `ssl` block, not in driver config: '
+ + '`ssl: { enabled: true, rejectUnauthorized: false, ca: … }` next to `driver`. Inside `config`, '
+ + '`ssl` is the on/off shorthand only.';
+
+/** Options every driver-config JSON-Schema projection is built with. */
+const TO_JSON_SCHEMA = {
+ target: 'draft-2020-12',
+ // The AUTHOR-facing shape: a key with a `.default()` is optional to write.
+ io: 'input',
+ // The memory driver's `persistence` accepts a custom adapter — an object of
+ // functions, which has no JSON-Schema form. Emitting `{}` for it keeps the
+ // connection form renderable instead of throwing at boot (#3746 hazard).
+ unrepresentable: 'any',
+} as const;
+
+/**
+ * Memoized JSON-Schema projection of a driver-config schema.
+ *
+ * One projection per schema, computed on first use and cached: this is what
+ * `DriverDefinitionSchema.configSchema` publishes and what the Studio
+ * connection form renders, so the form and the parse gate cannot describe
+ * different shapes — they are the same zod object seen twice.
+ */
+export function driverConfigJsonSchema(schema: z.ZodType): () => Record {
+ let cached: Record | undefined;
+ return () => {
+ if (cached === undefined) {
+ cached = z.toJSONSchema(schema, TO_JSON_SCHEMA) as Record;
+ }
+ return cached;
+ };
+}
diff --git a/packages/spec/src/data/driver/config-registry.test.ts b/packages/spec/src/data/driver/config-registry.test.ts
new file mode 100644
index 0000000000..a6c0700a20
--- /dev/null
+++ b/packages/spec/src/data/driver/config-registry.test.ts
@@ -0,0 +1,158 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import { describe, it, expect } from 'vitest';
+
+import { DatasourceSchema } from '../datasource.zod';
+import {
+ BUILTIN_DRIVER_IDS,
+ DRIVER_CONFIG_SCHEMAS,
+ DRIVER_ID_ALIASES,
+ getDriverConfigJsonSchemaById,
+ getDriverConfigSchema,
+ resolveDriverId,
+ validateDriverConfig,
+} from './config-registry.zod';
+
+describe('driver config registry', () => {
+ it('ships a schema and a JSON-Schema projection for every canonical id', () => {
+ for (const id of BUILTIN_DRIVER_IDS) {
+ expect(DRIVER_CONFIG_SCHEMAS[id], id).toBeTruthy();
+ const json = getDriverConfigJsonSchemaById(id) as { type?: string; properties?: object };
+ expect(json.type, id).toBe('object');
+ expect(json.properties, id).toBeTruthy();
+ }
+ });
+
+ it('memoizes each projection so the form and the gate share one object', () => {
+ expect(getDriverConfigJsonSchemaById('postgres')).toBe(getDriverConfigJsonSchemaById('postgres'));
+ });
+
+ it('resolves every alias onto a canonical id that has a contract', () => {
+ for (const [alias, canonical] of Object.entries(DRIVER_ID_ALIASES)) {
+ expect(resolveDriverId(alias), alias).toBe(canonical);
+ expect(BUILTIN_DRIVER_IDS).toContain(canonical);
+ }
+ });
+
+ it('resolves case- and whitespace-insensitively', () => {
+ expect(resolveDriverId(' PostgreSQL ')).toBe('postgres');
+ expect(resolveDriverId('MongoDB')).toBe('mongo');
+ });
+
+ /**
+ * The distinction the whole gate rests on: "nothing to check against" is not
+ * the same answer as "checked and clean", and a caller that conflates them
+ * reintroduces the silence #4410 removed.
+ */
+ it('reports an unknown driver as unknown rather than as valid', () => {
+ expect(resolveDriverId('com.vendor.snowflake')).toBeUndefined();
+ expect(getDriverConfigSchema('com.vendor.snowflake')).toBeUndefined();
+ expect(validateDriverConfig('com.vendor.snowflake', { whatever: 1 })).toEqual({ known: false });
+ });
+
+ it('validates a known driver and returns path-relative issues', () => {
+ const result = validateDriverConfig('pg', { database: 'app', hostname: 'db.internal' });
+
+ expect(result.known).toBe(true);
+ expect(result).toHaveProperty('issues');
+ const issues = (result as { issues: Array<{ path: unknown[]; message: string }> }).issues;
+ expect(issues).toHaveLength(1);
+ expect(issues[0]!.message).toContain('`hostname` → `host`');
+ });
+});
+
+describe('DatasourceSchema × driver config (#4410)', () => {
+ const base = { name: 'warehouse', driver: 'postgres' };
+
+ /**
+ * The reported bug, verbatim: the correct key is `host`, `hostname` was
+ * accepted in silence, and the datasource then connected to localhost while
+ * reporting success — which for an AI author is indistinguishable from having
+ * configured it.
+ */
+ it('rejects a misspelled connection key inside config, pathed at the key', () => {
+ const result = DatasourceSchema.safeParse({
+ ...base,
+ config: { hostname: 'db.internal', database: 'analytics' },
+ });
+
+ expect(result.success).toBe(false);
+ expect(result.error!.issues[0]!.path).toEqual(['config']);
+ expect(result.error!.issues[0]!.message).toContain('`hostname` → `host`');
+ });
+
+ it('accepts the corrected config', () => {
+ const result = DatasourceSchema.safeParse({
+ ...base,
+ config: { host: 'db.internal', database: 'analytics' },
+ });
+
+ expect(result.success).toBe(true);
+ });
+
+ it('leaves a plugin-contributed driver`s config alone', () => {
+ const result = DatasourceSchema.safeParse({
+ name: 'warehouse',
+ driver: 'com.vendor.snowflake',
+ config: { account: 'xy12345', warehouse: 'COMPUTE_WH' },
+ });
+
+ expect(result.success).toBe(true);
+ });
+
+ it('validates every driver id alias the same way', () => {
+ for (const alias of ['pg', 'postgresql', 'POSTGRES']) {
+ const result = DatasourceSchema.safeParse({
+ name: 'warehouse',
+ driver: alias,
+ config: { hostname: 'db.internal', database: 'analytics' },
+ });
+ expect(result.success, alias).toBe(false);
+ }
+ });
+
+ /**
+ * `readReplicas` is the sibling escape hatch — same per-driver shape, same
+ * silence before #4410. Reported per index so the author is told WHICH
+ * replica is wrong.
+ */
+ it('validates each readReplicas entry and paths issues by index', () => {
+ const result = DatasourceSchema.safeParse({
+ ...base,
+ config: { host: 'db.internal', database: 'analytics' },
+ readReplicas: [
+ { host: 'replica-1.internal', database: 'analytics' },
+ { hostname: 'replica-2.internal', database: 'analytics' },
+ ],
+ });
+
+ expect(result.success).toBe(false);
+ expect(result.error!.issues).toHaveLength(1);
+ expect(result.error!.issues[0]!.path).toEqual(['readReplicas', 1]);
+ });
+
+ it('rejects a sqlite datasource whose filename is misspelled', () => {
+ const result = DatasourceSchema.safeParse({
+ name: 'local',
+ driver: 'sqlite',
+ config: { file: './data.db' },
+ });
+
+ expect(result.success).toBe(false);
+ expect(result.error!.issues[0]!.message).toContain('`file` → `filename`');
+ });
+
+ it('still reports the schemaMode/external coherence rule alongside a config problem', () => {
+ const result = DatasourceSchema.safeParse({
+ name: 'warehouse',
+ driver: 'postgres',
+ config: { hostname: 'db.internal', database: 'analytics' },
+ schemaMode: 'external',
+ });
+
+ expect(result.success).toBe(false);
+ const paths = result.error!.issues.map((i) => i.path.join('.'));
+ expect(paths).toContain('config');
+ expect(paths).toContain('external');
+ });
+});
diff --git a/packages/spec/src/data/driver/config-registry.zod.ts b/packages/spec/src/data/driver/config-registry.zod.ts
new file mode 100644
index 0000000000..b0babf21ff
--- /dev/null
+++ b/packages/spec/src/data/driver/config-registry.zod.ts
@@ -0,0 +1,174 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import type { z } from 'zod';
+
+import { getMemoryConfigJsonSchema, MemoryConfigSchema } from './memory.zod';
+import { getMongoConfigJsonSchema, MongoConfigSchema } from './mongo.zod';
+import { getMysqlConfigJsonSchema, MysqlConfigSchema } from './mysql.zod';
+import { getPostgresConfigJsonSchema, PostgresConfigSchema } from './postgres.zod';
+import {
+ getSqliteConfigJsonSchema,
+ getSqliteWasmConfigJsonSchema,
+ SqliteConfigSchema,
+ SqliteWasmConfigSchema,
+} from './sqlite.zod';
+
+/**
+ * The driver-id → `datasource.config` shape registry (#4410).
+ *
+ * ## Why this exists
+ *
+ * `DatasourceSchema` went `.strict()` in #4207 with `config` deliberately left
+ * open — per-driver by construction, a sqlite `filename` and a postgres `host`
+ * share no shape. The module comment justified the hole by saying "the driver's
+ * own `configSchema` is what validates it". Nothing did: the two bundled driver
+ * specs set `configSchema: {}`, no code read the field, and the three per-driver
+ * zod schemas were not even exported from the package. So the one slot an author
+ * writes by hand was the one slot with no gate, and `config: { hostname: … }`
+ * connected to localhost while reporting success.
+ *
+ * This registry closes that: it maps every driver id the platform can actually
+ * BUILD onto the schema for that driver's config, and `DatasourceSchema` parses
+ * `config` (and each `readReplicas` entry) against it.
+ *
+ * ## Where the boundary is
+ *
+ * An id this registry does not know stays unvalidated, and that is deliberate
+ * rather than a remaining hole: `driver` is an open namespace — a plugin ships
+ * `com.vendor.snowflake` with its own config shape, and rejecting keys against a
+ * shape we do not have would be worse than the silence it replaces. The honest
+ * line is "we validate what we can construct", and
+ * {@link BUILTIN_DRIVER_IDS} is exactly the set the shared
+ * `createDefaultDatasourceDriverFactory` builds.
+ *
+ * ## Why the alias table lives HERE
+ *
+ * The factory had its own copy. Two tables meant the id that selects a driver
+ * and the id that selects its config schema could disagree — validating a `pg`
+ * datasource against nothing while building it as postgres — so the factory now
+ * imports {@link resolveDriverId} instead of keeping a second list.
+ */
+
+/** Canonical driver ids the platform ships a config contract for. */
+export const BUILTIN_DRIVER_IDS = [
+ 'memory',
+ 'sqlite',
+ 'sqlite-wasm',
+ 'postgres',
+ 'mysql',
+ 'mongo',
+] as const;
+
+export type BuiltinDriverId = (typeof BUILTIN_DRIVER_IDS)[number];
+
+/**
+ * Accepted spellings of each canonical driver id, matched case-insensitively.
+ *
+ * These are DRIVER SELECTORS, not config keys: `driver: 'pg'` and
+ * `driver: 'postgres'` build the same driver, so they must resolve to the same
+ * config contract. (Unknown-key tolerance inside `config` is a different
+ * question, and the answer there is a rejection with a rename hint.)
+ */
+export const DRIVER_ID_ALIASES: Readonly> = {
+ memory: 'memory',
+ inmemory: 'memory',
+ 'in-memory': 'memory',
+ mingo: 'memory',
+ sqlite: 'sqlite',
+ sqlite3: 'sqlite',
+ 'better-sqlite3': 'sqlite',
+ 'sqlite-wasm': 'sqlite-wasm',
+ 'wasm-sqlite': 'sqlite-wasm',
+ postgres: 'postgres',
+ postgresql: 'postgres',
+ pg: 'postgres',
+ mysql: 'mysql',
+ mysql2: 'mysql',
+ mariadb: 'mysql',
+ mongo: 'mongo',
+ mongodb: 'mongo',
+};
+
+/**
+ * Resolve an authored `datasource.driver` onto its canonical id, or `undefined`
+ * when the platform ships no contract for it (a plugin-contributed driver).
+ */
+export function resolveDriverId(driver: unknown): BuiltinDriverId | undefined {
+ if (typeof driver !== 'string') return undefined;
+ return DRIVER_ID_ALIASES[driver.trim().toLowerCase()];
+}
+
+/** Canonical driver id → the schema its `datasource.config` must satisfy. */
+export const DRIVER_CONFIG_SCHEMAS: Readonly> = {
+ memory: MemoryConfigSchema,
+ sqlite: SqliteConfigSchema,
+ 'sqlite-wasm': SqliteWasmConfigSchema,
+ postgres: PostgresConfigSchema,
+ mysql: MysqlConfigSchema,
+ mongo: MongoConfigSchema,
+};
+
+/**
+ * The config schema for an authored `driver` value, following aliases.
+ * `undefined` means "no contract shipped" — the caller must leave the config
+ * alone rather than invent a verdict for it.
+ */
+export function getDriverConfigSchema(driver: unknown): z.ZodType | undefined {
+ const id = resolveDriverId(driver);
+ return id ? DRIVER_CONFIG_SCHEMAS[id] : undefined;
+}
+
+/** Canonical driver id → the memoized JSON-Schema projection of its config shape. */
+const DRIVER_CONFIG_JSON_SCHEMAS: Readonly Record>> = {
+ memory: getMemoryConfigJsonSchema,
+ sqlite: getSqliteConfigJsonSchema,
+ 'sqlite-wasm': getSqliteWasmConfigJsonSchema,
+ postgres: getPostgresConfigJsonSchema,
+ mysql: getMysqlConfigJsonSchema,
+ mongo: getMongoConfigJsonSchema,
+};
+
+/**
+ * JSON-Schema projection of a built-in driver's config contract — what
+ * `DriverDefinitionSchema.configSchema` publishes and what the Studio
+ * connection form renders.
+ *
+ * Takes a CANONICAL id (not an alias) so a caller enumerating drivers cannot
+ * quietly get `undefined` for a spelling it thought was covered; use
+ * {@link resolveDriverId} first when the id came from authored metadata.
+ */
+export function getDriverConfigJsonSchemaById(id: BuiltinDriverId): Record {
+ return DRIVER_CONFIG_JSON_SCHEMAS[id]();
+}
+
+/** One problem found in a `datasource.config`, path-relative to the config object. */
+export interface DriverConfigIssue {
+ /** Property path inside `config` (empty for a whole-object problem). */
+ path: (string | number)[];
+ message: string;
+}
+
+/**
+ * Validate a `datasource.config` against its driver's contract.
+ *
+ * Returns `{ known: false }` for a driver the platform ships no contract for,
+ * so callers can distinguish "checked and clean" from "nothing to check
+ * against" — a distinction the silent-strip failure mode depends on nobody
+ * making. Never throws.
+ */
+export function validateDriverConfig(
+ driver: unknown,
+ config: unknown,
+): { known: false } | { known: true; issues: DriverConfigIssue[] } {
+ const schema = getDriverConfigSchema(driver);
+ if (!schema) return { known: false };
+ const result = schema.safeParse(config ?? {});
+ if (result.success) return { known: true, issues: [] };
+ return {
+ known: true,
+ issues: result.error.issues.map((issue) => ({
+ path: [...issue.path] as (string | number)[],
+ message: issue.message,
+ })),
+ };
+}
diff --git a/packages/spec/src/data/driver/index.ts b/packages/spec/src/data/driver/index.ts
new file mode 100644
index 0000000000..16a8b904d0
--- /dev/null
+++ b/packages/spec/src/data/driver/index.ts
@@ -0,0 +1,20 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Per-driver `datasource.config` contracts (#4410).
+ *
+ * These shapes existed since the protocol's early days and were reachable from
+ * nothing — no barrel exported `data/driver/`, so the schemas `datasource.zod.ts`
+ * told authors to write against could not even be imported. They are exported
+ * here because they are now load-bearing: `DatasourceSchema` parses `config`
+ * against them, `DriverDefinitionSchema.configSchema` publishes their JSON-Schema
+ * projection, and the Studio connection form renders from that same projection.
+ */
+
+export * from './common.zod';
+export * from './config-registry.zod';
+export * from './memory.zod';
+export * from './mongo.zod';
+export * from './mysql.zod';
+export * from './postgres.zod';
+export * from './sqlite.zod';
diff --git a/packages/spec/src/data/driver/memory.test.ts b/packages/spec/src/data/driver/memory.test.ts
index b5098ea798..5a2c8a351c 100644
--- a/packages/spec/src/data/driver/memory.test.ts
+++ b/packages/spec/src/data/driver/memory.test.ts
@@ -25,8 +25,6 @@ describe('MemoryConfigSchema', () => {
expect(config.strictMode).toBe(false);
expect(config.initialData).toBeUndefined();
expect(config.persistence).toBe(false);
- expect(config.indexes).toBeUndefined();
- expect(config.maxRecordsPerObject).toBeUndefined();
});
it('still accepts "auto" as an explicit opt-in to the previous behaviour', () => {
@@ -179,25 +177,26 @@ describe('MemoryConfigSchema', () => {
expect(typeof p.adapter.flush).toBe('function');
});
- it('should accept config with indexes', () => {
- const config = MemoryConfigSchema.parse({
- indexes: {
- users: ['email', 'role'],
- posts: ['author_id', 'status'],
- },
+ // `indexes` and `maxRecordsPerObject` were declared here and read by nobody:
+ // `InMemoryDriverConfig` has no field for either, the driver keeps no indexes
+ // (every read is a linear Mingo scan) and evicts nothing. These tests used to
+ // assert they were "accepted" — which was true, and meant nothing. #4410 gave
+ // `config` a gate, so a key inside it now claims to be honoured; both were
+ // removed rather than blessed, and the rejection carries the reason.
+ it('rejects `indexes`, which the memory driver never kept', () => {
+ const result = MemoryConfigSchema.safeParse({
+ indexes: { users: ['email', 'role'] },
});
- expect(config.indexes).toBeDefined();
- expect(config.indexes!.users).toEqual(['email', 'role']);
- expect(config.indexes!.posts).toEqual(['author_id', 'status']);
+ expect(result.success).toBe(false);
+ expect(result.error!.issues[0]!.message).toContain('the memory driver keeps no indexes');
});
- it('should accept config with maxRecordsPerObject', () => {
- const config = MemoryConfigSchema.parse({
- maxRecordsPerObject: 10000,
- });
+ it('rejects `maxRecordsPerObject`, which the memory driver never enforced', () => {
+ const result = MemoryConfigSchema.safeParse({ maxRecordsPerObject: 10000 });
- expect(config.maxRecordsPerObject).toBe(10000);
+ expect(result.success).toBe(false);
+ expect(result.error!.issues[0]!.message).toContain('the memory driver evicts nothing');
});
it('should accept full config with all options', () => {
@@ -211,18 +210,12 @@ describe('MemoryConfigSchema', () => {
path: '/var/data/memory.json',
autoSaveInterval: 3000,
},
- indexes: {
- users: ['email'],
- },
- maxRecordsPerObject: 50000,
});
expect(config.strictMode).toBe(true);
expect(config.initialData!.users).toHaveLength(1);
const p = config.persistence as { type: 'file'; path?: string };
expect(p.path).toBe('/var/data/memory.json');
- expect(config.indexes!.users).toEqual(['email']);
- expect(config.maxRecordsPerObject).toBe(50000);
});
it('should reject file persistence with invalid autoSaveInterval', () => {
diff --git a/packages/spec/src/data/driver/memory.zod.ts b/packages/spec/src/data/driver/memory.zod.ts
index 9a732840f9..321fe233f5 100644
--- a/packages/spec/src/data/driver/memory.zod.ts
+++ b/packages/spec/src/data/driver/memory.zod.ts
@@ -1,11 +1,18 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
-import { DriverDefinitionSchema } from '../datasource.zod';
+
+import { strictUnknownKeyError } from '../../shared/suggestions.zod';
+import type { DriverDefinition } from '../datasource.zod';
+import {
+ driverConfigJsonSchema,
+ READ_ONLY_BELONGS_ON_DATASOURCE,
+ SCHEMA_MODE_BELONGS_ON_DATASOURCE,
+} from './common.zod';
/**
* Memory Driver Configuration Schema
- *
+ *
* Defines the configuration options for the in-memory driver.
* Reference: objectql/packages/drivers/memory (Mingo-powered production-ready driver)
*
@@ -158,6 +165,50 @@ export const MemoryPersistenceConfigSchema = lazySchema(() => z.union([
// 2. Connection Configuration
// ==========================================================================
+const MEMORY_CONFIG_KEYS = ['initialData', 'strictMode', 'persistence'] as const;
+
+/**
+ * Two keys were declared here and read by nobody: `indexes` and
+ * `maxRecordsPerObject`. `InMemoryDriverConfig` (`driver-memory`) has no field
+ * for either — the driver indexes nothing (its reads are a linear Mingo scan)
+ * and evicts nothing (there is no LRU) — so an author who bounded a store or
+ * asked for an index got a clean parse and no behaviour. #4410's enforce step
+ * is what surfaced them: giving `config` a gate means every key inside it now
+ * claims to be honoured, so a key that is not gets removed rather than blessed.
+ * Both are rejected with the prescription below (ADR-0049 enforce-or-remove).
+ */
+const memoryConfigUnknownKeyError = strictUnknownKeyError({
+ surface: "this memory datasource's config",
+ knownKeys: MEMORY_CONFIG_KEYS,
+ aliases: {
+ data: 'initialData',
+ seed: 'initialData',
+ seeddata: 'initialData',
+ strict: 'strictMode',
+ persist: 'persistence',
+ persistent: 'persistence',
+ },
+ guidance: {
+ indexes:
+ '`indexes` was declared but never read: the memory driver keeps no indexes — every read is '
+ + 'a linear Mingo scan — so it changed nothing. Drop it, or move the datasource to a '
+ + 'driver that indexes (`sqlite` / `postgres`), where object-level `indexes` apply.',
+ maxRecordsPerObject:
+ '`maxRecordsPerObject` was declared but never read: the memory driver evicts nothing, so a '
+ + 'bound here was never enforced and the store grew unbounded regardless. Drop it and bound '
+ + 'the data you load, or use a driver with real storage limits.',
+ filename:
+ '`filename` is a sqlite key. For a memory datasource that survives restarts set '
+ + "`persistence: 'file'` (the file is scoped per datasource); for a real file-backed SQL "
+ + "database set `driver: 'sqlite'`.",
+ schemaMode: SCHEMA_MODE_BELONGS_ON_DATASOURCE,
+ readOnly: READ_ONLY_BELONGS_ON_DATASOURCE,
+ },
+ history:
+ 'Until #4410 nothing validated `datasource.config` at all — an unrecognised key was accepted '
+ + 'in silence and the store came up on the driver defaults instead.',
+});
+
export const MemoryConfigSchema = lazySchema(() => z.object({
/**
* Initial data to pre-populate the in-memory store.
@@ -238,45 +289,41 @@ export const MemoryConfigSchema = lazySchema(() => z.object({
* so two pools that DO opt in still need it to avoid aliasing one file.
*/
persistence: MemoryPersistenceConfigSchema.or(z.literal(false)).default(false).describe('Persistence configuration (opt-in; defaults to pure in-memory)'),
+}, { error: memoryConfigUnknownKeyError }).strict()
+ .describe('Memory Driver Connection Configuration'));
- /**
- * Fields to index for faster lookups.
- * Maps object names to arrays of field names to index.
- *
- * @example
- * {
- * users: ['email', 'role'],
- * posts: ['author_id', 'status']
- * }
- */
- indexes: z.record(
- z.string(),
- z.array(z.string())
- ).optional().describe('Index configuration per object'),
-
- /**
- * Maximum number of records per object type.
- * When exceeded, oldest records may be evicted (LRU).
- * Useful for caching or bounded memory usage.
- */
- maxRecordsPerObject: z.number().min(1).optional().describe('Max records per object (memory bound)'),
-
-}).describe('Memory Driver Connection Configuration'));
+/**
+ * JSON-Schema projection of {@link MemoryConfigSchema}, memoized — what
+ * {@link MemoryDriverSpec} publishes as its `configSchema`.
+ *
+ * The custom-adapter branch of `persistence` is an object of functions, which
+ * has no JSON-Schema form; the shared projection emits `{}` for it rather than
+ * throwing, so the connection form stays renderable.
+ */
+export const getMemoryConfigJsonSchema = driverConfigJsonSchema(MemoryConfigSchema);
// ==========================================================================
// 3. Driver Definition (Metadata)
// ==========================================================================
/**
- * The static definition of the Memory driver's capabilities and default metadata.
- * Implements the `DriverDefinitionSchema` contract.
+ * The static definition of the Memory driver's capabilities and default
+ * metadata, satisfying the `DriverDefinitionSchema` contract (proved by
+ * `memory.test.ts`, which parses this constant).
+ *
+ * `configSchema` was `{}` here — a declared slot that nothing filled and
+ * nothing read (#4410). It now projects {@link MemoryConfigSchema}, lazily, so
+ * the shape the connection form renders and the shape `DatasourceSchema`
+ * enforces are the same object seen twice.
*/
-export const MemoryDriverSpec = DriverDefinitionSchema.parse({
+export const MemoryDriverSpec = {
id: 'memory',
label: 'In-Memory',
description: 'High-performance in-memory driver powered by Mingo (MongoDB-compatible query engine). Supports filtering, aggregation pipelines, sorting, projection.',
icon: 'memory',
- configSchema: {},
+ get configSchema() {
+ return getMemoryConfigJsonSchema();
+ },
capabilities: {
transactions: true,
// Query
@@ -290,10 +337,12 @@ export const MemoryDriverSpec = DriverDefinitionSchema.parse({
querySubqueries: false,
// No full-text search (linear scan)
fullTextSearch: false,
+ // Not read-only
+ readOnly: false,
// Dynamic schema (no DDL needed)
dynamicSchema: true,
},
-});
+} satisfies DriverDefinition;
// ==========================================================================
// 4. Derived Types
diff --git a/packages/spec/src/data/driver/mongo.zod.ts b/packages/spec/src/data/driver/mongo.zod.ts
index c41c6a00a6..a41922dbb2 100644
--- a/packages/spec/src/data/driver/mongo.zod.ts
+++ b/packages/spec/src/data/driver/mongo.zod.ts
@@ -1,78 +1,163 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
-import { DriverDefinitionSchema } from '../datasource.zod';
+
+import { lazySchema } from '../../shared/lazy-schema';
+import { strictUnknownKeyError } from '../../shared/suggestions.zod';
+import type { DriverDefinition } from '../datasource.zod';
+import {
+ driverConfigJsonSchema,
+ READ_ONLY_BELONGS_ON_DATASOURCE,
+ SCHEMA_MODE_BELONGS_ON_DATASOURCE,
+} from './common.zod';
/**
* MongoDB Standard Driver Protocol
*
* Describes the MongoDB connection settings and capabilities.
*
- * CONTRACT ONLY — nothing parses `datasource.config` against this. This block
- * used to claim it was "used by the Platform to validate `datasource.config`
- * when `driver: 'mongo'`", which was never true: the config slot is a `z.record`
- * and this schema has no consumer (#4410). It is the shape to author against,
- * not a gate that runs. Say "validates" here again only once #4410 lands.
+ * ENFORCED as of #4410. This block used to claim it was "used by the Platform
+ * to validate `datasource.config` when `driver: 'mongo'`", which was false: the
+ * config slot was a bare `z.record` and this schema had no consumer at all —
+ * not even an export, since `data/driver/` was reachable only from its own
+ * tests. It is now what `DatasourceSchema` parses `config` against for a mongo
+ * datasource, and the same schema is projected onto
+ * {@link MongoDriverSpec}.configSchema for the connection form.
*/
// ==========================================================================
// 1. Connection Configuration
// ==========================================================================
-import { lazySchema } from '../../shared/lazy-schema';
+const MONGO_CONFIG_KEYS = [
+ 'url', 'host', 'port', 'database', 'username', 'password', 'authSource', 'options',
+] as const;
+
+const mongoConfigUnknownKeyError = strictUnknownKeyError({
+ surface: "this mongo datasource's config",
+ knownKeys: MONGO_CONFIG_KEYS,
+ aliases: {
+ uri: 'url',
+ connectionstring: 'url',
+ dsn: 'url',
+ hostname: 'host',
+ server: 'host',
+ dbname: 'database',
+ db: 'database',
+ user: 'username',
+ passwd: 'password',
+ pwd: 'password',
+ authdb: 'authSource',
+ authdatabase: 'authSource',
+ replicaset: 'options',
+ },
+ guidance: {
+ pool:
+ '`pool` is not driver config — connection pooling is configured once for every driver in '
+ + "the datasource's own `pool` block, which the factory maps onto the Mongo client's "
+ + '`minPoolSize`/`maxPoolSize`. Move it next to `driver`.',
+ schemaMode: SCHEMA_MODE_BELONGS_ON_DATASOURCE,
+ readOnly: READ_ONLY_BELONGS_ON_DATASOURCE,
+ ssl:
+ '`ssl` is not a top-level mongo key. TLS is a connection-string concern here: put it in '
+ + '`url` (`?tls=true`) or in the `options` passthrough the Mongo client reads.',
+ },
+ history:
+ 'Until #4410 nothing validated `datasource.config` at all — an unrecognised connection key '
+ + 'was accepted in silence and the datasource then connected to mongodb://localhost:27017 '
+ + 'rather than failing.',
+});
+
export const MongoConfigSchema = lazySchema(() => z.object({
/**
- * Connection URI (Standard Connection String)
- * If provided, host/port/username/password fields may be ignored or merged depending on driver logic.
- * Format: mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]
+ * Connection URI (standard connection string). When present it supersedes
+ * `host`/`port`/`database`/`username`/`authSource` — those are only used to
+ * COMPOSE a URI when none is given.
+ * Format: `mongodb://[username:password@]host1[:port1][,…][/[db][?options]]`
*/
- url: z.string().describe('Connection URI').optional(),
+ url: z.string().optional().describe('Connection URI (supersedes the discrete fields)')
+ .meta({ title: 'Connection URI' }),
/**
- * Database Name (Required)
- * The logical database to store collections.
+ * Database name — the logical database holding the collections.
+ * Required unless `url` carries it.
*/
- database: z.string().min(1).describe('Database Name'),
+ database: z.string().min(1).optional().describe('Database name').meta({ title: 'Database' }),
+
+ /** Hostname. Used only when `url` is absent. */
+ host: z.string().default('localhost').describe('Host address').meta({ title: 'Host' }),
- /** Hostname (Optional if url is provided) */
- host: z.string().default('127.0.0.1').describe('Host address').optional(),
+ /** Port. Used only when `url` is absent. */
+ port: z.number().int().default(27017).describe('Port number').meta({ title: 'Port' }),
- /** Port (Optional, default 27017) */
- port: z.number().int().default(27017).describe('Port number').optional(),
+ /** Authentication user. Used only when `url` is absent. */
+ username: z.string().optional().describe('Authentication user').meta({ title: 'User' }),
- /** Username for authentication */
- username: z.string().describe('Authentication Username').optional(),
+ /**
+ * Authentication password. Prefer `external.credentialsRef` — a datasource
+ * secret always wins over this value.
+ */
+ password: z.string().optional()
+ .describe('Authentication password (prefer external.credentialsRef)')
+ .meta({ title: 'Password', format: 'password' }),
- /** Password for authentication */
- password: z.string().describe('Authentication Password').optional(),
-
- /** Authentication Database (Defaults to admin or database name) */
- authSource: z.string().describe('Authentication Database').optional(),
+ /** Authentication database, when it differs from `database`. */
+ authSource: z.string().optional().describe('Authentication database')
+ .meta({ title: 'Auth source' }),
/**
- * Connection Options
- * Passthrough options to the underlying MongoDB driver (e.g. valid certs, timeouts)
+ * Passthrough options handed to the MongoDB client verbatim
+ * (`replicaSet`, `tls`, timeouts, …).
*/
- options: z.record(z.string(), z.unknown()).describe('Extra driver options (ssl, poolSize, etc)').optional(),
-}).describe('MongoDB Connection Configuration'));
+ options: z.record(z.string(), z.unknown()).optional()
+ .describe('Extra MongoClient options (replicaSet, tls, timeouts, …)'),
+}, { error: mongoConfigUnknownKeyError }).strict()
+ .describe('MongoDB Connection Configuration')
+ .superRefine((cfg, ctx) => {
+ if (!cfg.url && !cfg.database) {
+ ctx.addIssue({
+ code: 'custom',
+ path: ['database'],
+ message:
+ 'A mongo datasource needs a connection target: set `database` (with `host`/`port`) or '
+ + 'a full `url`. Neither was given, so the connection would fall back to '
+ + 'mongodb://localhost:27017 with no database selected.',
+ });
+ }
+ }));
+
+/**
+ * JSON-Schema projection of {@link MongoConfigSchema}, memoized — what
+ * {@link MongoDriverSpec} publishes as its `configSchema`.
+ */
+export const getMongoConfigJsonSchema = driverConfigJsonSchema(MongoConfigSchema);
// ==========================================================================
// 2. Driver Definition (Metadata)
// ==========================================================================
/**
- * The static definition of the Mongo driver's capabilities and default metadata.
- * This implements the `DriverDefinitionSchema` contract.
+ * The static definition of the Mongo driver's capabilities and default
+ * metadata, satisfying the `DriverDefinitionSchema` contract (proved by
+ * `mongo.test.ts`, which parses this constant).
+ *
+ * `configSchema` is a getter so the JSON-Schema projection is computed on first
+ * read rather than at module load — the same deferral `lazySchema` exists for,
+ * and what lets this constant drop its runtime import of `DatasourceSchema`'s
+ * module (a `.parse()` at module scope would have made the config registry and
+ * this file a cycle). It used to be `{}` with a comment promising it would be
+ * "populated with a JSON Schema version of MongoConfigSchema at runtime"; no
+ * such code ever existed (#4410), so the promise is discharged here rather than
+ * described.
*/
-export const MongoDriverSpec = DriverDefinitionSchema.parse({
+export const MongoDriverSpec = {
id: 'mongo',
label: 'MongoDB',
description: 'Official MongoDB Driver for ObjectStack. Supports rich queries, aggregation, and atomic updates.',
icon: 'database',
- // Empty, and nothing fills it. This comment used to promise the field would be
- // "populated with a JSON Schema version of MongoConfigSchema at runtime" — no
- // such code exists here or in any consumer (#4410).
- configSchema: {},
+ get configSchema() {
+ return getMongoConfigJsonSchema();
+ },
capabilities: {
transactions: true,
// Query
@@ -80,11 +165,15 @@ export const MongoDriverSpec = DriverDefinitionSchema.parse({
queryAggregations: true,
querySorting: true,
queryPagination: true,
+ queryWindowFunctions: false,
+ querySubqueries: false,
+ joins: false,
fullTextSearch: true,
+ readOnly: false,
// Schema
dynamicSchema: true,
- }
-});
+ },
+} satisfies DriverDefinition;
/**
* Derived Types
diff --git a/packages/spec/src/data/driver/mysql.zod.ts b/packages/spec/src/data/driver/mysql.zod.ts
new file mode 100644
index 0000000000..9bb6f6fcb3
--- /dev/null
+++ b/packages/spec/src/data/driver/mysql.zod.ts
@@ -0,0 +1,125 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import { z } from 'zod';
+
+import { lazySchema } from '../../shared/lazy-schema';
+import { strictUnknownKeyError } from '../../shared/suggestions.zod';
+import {
+ driverConfigJsonSchema,
+ DriverSslToggleSchema,
+ READ_ONLY_BELONGS_ON_DATASOURCE,
+ SCHEMA_MODE_BELONGS_ON_DATASOURCE,
+ SqlAutoMigrateSchema,
+ SSL_DETAIL_BELONGS_ON_DATASOURCE,
+} from './common.zod';
+
+/**
+ * MySQL / MariaDB driver configuration — the `config` slot of a `datasource`
+ * whose `driver` resolves to `mysql` (`mysql2`).
+ *
+ * The driver id was offered by the connection form and buildable by the shared
+ * factory long before #4410, but had no config shape at all in `packages/spec`
+ * — postgres, mongo and memory each had one and mysql did not, so its `config`
+ * was the one slot with neither a gate nor a documented shape.
+ *
+ * Every key here is read by `createDefaultDatasourceDriverFactory`
+ * (→ `SqlDriver`, knex `mysql2`). Postgres-only knobs are deliberately absent:
+ * `mysql2` has no `application_name` and no `statement_timeout`, so declaring
+ * them would advertise settings the client drops.
+ */
+const MYSQL_CONFIG_KEYS = [
+ 'url', 'host', 'port', 'database', 'username', 'password', 'ssl', 'autoMigrate',
+] as const;
+
+const mysqlConfigUnknownKeyError = strictUnknownKeyError({
+ surface: "this mysql datasource's config",
+ knownKeys: MYSQL_CONFIG_KEYS,
+ aliases: {
+ hostname: 'host',
+ server: 'host',
+ dbname: 'database',
+ db: 'database',
+ schema: 'database',
+ user: 'username',
+ passwd: 'password',
+ pwd: 'password',
+ connectionstring: 'url',
+ dsn: 'url',
+ uri: 'url',
+ sslmode: 'ssl',
+ tls: 'ssl',
+ usessl: 'ssl',
+ },
+ guidance: {
+ pool:
+ '`pool` is not driver config — connection pooling is configured once for every driver in '
+ + "the datasource's own `pool` block. Move it next to `driver`.",
+ schemaMode: SCHEMA_MODE_BELONGS_ON_DATASOURCE,
+ readOnly: READ_ONLY_BELONGS_ON_DATASOURCE,
+ ca: SSL_DETAIL_BELONGS_ON_DATASOURCE,
+ cert: SSL_DETAIL_BELONGS_ON_DATASOURCE,
+ key: SSL_DETAIL_BELONGS_ON_DATASOURCE,
+ rejectUnauthorized: SSL_DETAIL_BELONGS_ON_DATASOURCE,
+ charset:
+ '`charset` is not honoured: the factory builds the mysql2 connection from the keys listed '
+ + 'here only. Put it in the `url` as a query parameter (`?charset=utf8mb4`) so the client '
+ + 'actually receives it.',
+ },
+ history:
+ 'Until #4410 nothing validated `datasource.config` at all — an unrecognised connection key '
+ + 'was accepted in silence and the datasource then connected on the client defaults '
+ + '(localhost:3306) rather than failing.',
+});
+
+export const MysqlConfigSchema = lazySchema(() => z.object({
+ /**
+ * Connection URI, passed to `mysql2` as-is when present.
+ * Format: `mysql://[user[:password]@][host][:port]/[dbname][?params]`
+ */
+ url: z.string().optional().describe('Connection URI (supersedes the discrete fields)')
+ .meta({ title: 'Connection URL' }),
+
+ /** Hostname or IP address. */
+ host: z.string().default('localhost').describe('Host address').meta({ title: 'Host' }),
+
+ /** Port number. */
+ port: z.number().int().default(3306).describe('Port number').meta({ title: 'Port' }),
+
+ /** Database (schema) name. Required unless `url` carries it. */
+ database: z.string().optional().describe('Database name').meta({ title: 'Database' }),
+
+ /** Authentication user. Passed to `mysql2` as `user`. */
+ username: z.string().optional().describe('Authentication user').meta({ title: 'User' }),
+
+ /**
+ * Authentication password. Prefer `external.credentialsRef`; a datasource
+ * secret always wins over this value.
+ */
+ password: z.string().optional()
+ .describe('Authentication password (prefer external.credentialsRef)')
+ .meta({ title: 'Password', format: 'password' }),
+
+ /** TLS settings, passed to `mysql2` verbatim. */
+ ssl: DriverSslToggleSchema.optional().meta({ title: 'Use SSL/TLS' }),
+
+ /** Dev-only, loosen-only schema self-heal (#2186). */
+ autoMigrate: SqlAutoMigrateSchema.optional(),
+}, { error: mysqlConfigUnknownKeyError }).strict()
+ .describe('MySQL / MariaDB connection configuration')
+ .superRefine((cfg, ctx) => {
+ if (!cfg.url && !cfg.database) {
+ ctx.addIssue({
+ code: 'custom',
+ path: ['database'],
+ message:
+ 'A mysql datasource needs a connection target: set `database` (with `host`/`port`) or '
+ + 'a full `url`. Neither was given, so the connection would fall back to the client '
+ + 'defaults and silently open a different database than the one intended.',
+ });
+ }
+ }));
+
+export type MysqlConfig = z.infer;
+
+/** JSON-Schema projection of {@link MysqlConfigSchema}, memoized. */
+export const getMysqlConfigJsonSchema = driverConfigJsonSchema(MysqlConfigSchema);
diff --git a/packages/spec/src/data/driver/postgres.test.ts b/packages/spec/src/data/driver/postgres.test.ts
index 8a52587abd..45315638ae 100644
--- a/packages/spec/src/data/driver/postgres.test.ts
+++ b/packages/spec/src/data/driver/postgres.test.ts
@@ -11,8 +11,6 @@ describe('PostgresConfigSchema', () => {
expect(config.host).toBe('localhost');
expect(config.port).toBe(5432);
expect(config.schema).toBe('public');
- expect(config.max).toBe(10);
- expect(config.min).toBe(0);
});
it('should accept config with connection URI', () => {
@@ -35,10 +33,6 @@ describe('PostgresConfigSchema', () => {
schema: 'app_schema',
ssl: true,
applicationName: 'objectstack',
- max: 50,
- min: 5,
- idleTimeoutMillis: 60000,
- connectionTimeoutMillis: 10000,
statementTimeout: 30000,
});
@@ -47,10 +41,6 @@ describe('PostgresConfigSchema', () => {
expect(config.schema).toBe('app_schema');
expect(config.ssl).toBe(true);
expect(config.applicationName).toBe('objectstack');
- expect(config.max).toBe(50);
- expect(config.min).toBe(5);
- expect(config.idleTimeoutMillis).toBe(60000);
- expect(config.connectionTimeoutMillis).toBe(10000);
expect(config.statementTimeout).toBe(30000);
});
@@ -62,15 +52,11 @@ describe('PostgresConfigSchema', () => {
expect(config.host).toBe('localhost');
expect(config.port).toBe(5432);
expect(config.schema).toBe('public');
- expect(config.max).toBe(10);
- expect(config.min).toBe(0);
expect(config.url).toBeUndefined();
expect(config.username).toBeUndefined();
expect(config.password).toBeUndefined();
expect(config.ssl).toBeUndefined();
expect(config.applicationName).toBeUndefined();
- expect(config.idleTimeoutMillis).toBeUndefined();
- expect(config.connectionTimeoutMillis).toBeUndefined();
expect(config.statementTimeout).toBeUndefined();
});
@@ -83,41 +69,51 @@ describe('PostgresConfigSchema', () => {
expect(config.ssl).toBe(false);
});
- it('should accept ssl as detailed object', () => {
- const config = PostgresConfigSchema.parse({
+ // `config.ssl` is the on/off shorthand; certificates live in the
+ // datasource-level `ssl` block, which #4410 wired through to the client (it
+ // was declared, strict and read by nobody before that). The narrowing is
+ // forced by the connection form: it renders anything that is not
+ // boolean/enum/number as a text input, so a `boolean | object` union here
+ // would produce a wizard whose every `ssl` value the gate rejects.
+ it('rejects the certificate-bearing object form, naming where it belongs', () => {
+ const result = PostgresConfigSchema.safeParse({
database: 'mydb',
ssl: {
rejectUnauthorized: false,
ca: '-----BEGIN CERTIFICATE-----\nMIIB...',
- key: '-----BEGIN PRIVATE KEY-----\nMIIE...',
- cert: '-----BEGIN CERTIFICATE-----\nMIIC...',
},
});
- expect(config.ssl).toBeDefined();
- expect(typeof config.ssl).toBe('object');
- const sslObj = config.ssl as { rejectUnauthorized?: boolean; ca?: string };
- expect(sslObj.rejectUnauthorized).toBe(false);
- expect(sslObj.ca).toBeDefined();
+ expect(result.success).toBe(false);
+ expect(result.error!.issues.some((i) => i.path.join('.') === 'ssl')).toBe(true);
});
- it('should accept ssl object with partial fields', () => {
- const config = PostgresConfigSchema.parse({
+ it('points a misplaced certificate key at the datasource-level block', () => {
+ const result = PostgresConfigSchema.safeParse({
database: 'mydb',
- ssl: {
- rejectUnauthorized: true,
- },
+ ca: '-----BEGIN CERTIFICATE-----\nMIIB...',
});
- const sslObj = config.ssl as { rejectUnauthorized?: boolean };
- expect(sslObj.rejectUnauthorized).toBe(true);
+ expect(result.success).toBe(false);
+ expect(result.error!.issues[0]!.message).toContain('datasource-level `ssl` block');
});
- it('should reject config without database', () => {
+ it('should reject a config with no connection target at all', () => {
+ // Neither `database` nor `url`: the pg client would then open its own
+ // default (localhost, the OS user's database), so an empty config is a
+ // datasource pointing somewhere nobody chose.
expect(() => PostgresConfigSchema.parse({})).toThrow();
expect(() => PostgresConfigSchema.parse({ host: 'localhost' })).toThrow();
});
+ it('accepts a `url` on its own as the connection target', () => {
+ const config = PostgresConfigSchema.parse({
+ url: 'postgresql://user@db.example.com:5432/analytics',
+ });
+
+ expect(config.database).toBeUndefined();
+ });
+
it('should reject config with invalid port type', () => {
expect(() => PostgresConfigSchema.parse({
database: 'mydb',
@@ -132,11 +128,37 @@ describe('PostgresConfigSchema', () => {
})).toThrow();
});
- it('should reject config with invalid max pool type', () => {
- expect(() => PostgresConfigSchema.parse({
+ it('rejects pool sizing, and says where it belongs', () => {
+ // `max` / `min` / `idleTimeoutMillis` / `connectionTimeoutMillis` were
+ // declared here and read by nothing: the factory hardcoded its own pool. The
+ // datasource-level `pool` block is the one the factory now honours, so the
+ // rejection relocates rather than merely refusing (#4410).
+ const result = PostgresConfigSchema.safeParse({
database: 'mydb',
- max: 'ten',
- })).toThrow();
+ max: 100,
+ min: 10,
+ });
+
+ expect(result.success).toBe(false);
+ expect(result.error!.issues[0]!.message).toContain('`pool: { max: … }`');
+ expect(result.error!.issues[0]!.message).toContain('`pool: { min: … }`');
+ });
+
+ it('rejects an unknown key with a rename suggestion', () => {
+ const result = PostgresConfigSchema.safeParse({
+ database: 'mydb',
+ hostname: 'db.internal',
+ });
+
+ expect(result.success).toBe(false);
+ expect(result.error!.issues[0]!.message).toContain('`hostname` → `host`');
+ });
+
+ it('rejects `user`, pointing at the canonical `username`', () => {
+ const result = PostgresConfigSchema.safeParse({ database: 'mydb', user: 'app' });
+
+ expect(result.success).toBe(false);
+ expect(result.error!.issues[0]!.message).toContain('`user` → `username`');
});
it('should accept config with environment variable patterns', () => {
@@ -151,25 +173,10 @@ describe('PostgresConfigSchema', () => {
expect(config.host).toBe('${DB_HOST}');
});
- it('should accept zero as min pool size', () => {
- const config = PostgresConfigSchema.parse({
- database: 'mydb',
- min: 0,
- });
-
- expect(config.min).toBe(0);
- });
-
- it('should accept custom pool configuration', () => {
- const config = PostgresConfigSchema.parse({
- database: 'mydb',
- max: 100,
- min: 10,
- idleTimeoutMillis: 120000,
- connectionTimeoutMillis: 5000,
- });
-
- expect(config.max).toBe(100);
- expect(config.min).toBe(10);
+ it('accepts the dev-only autoMigrate passthrough', () => {
+ expect(PostgresConfigSchema.parse({ database: 'mydb', autoMigrate: 'safe' }).autoMigrate)
+ .toBe('safe');
+ expect(() => PostgresConfigSchema.parse({ database: 'mydb', autoMigrate: 'destructive' }))
+ .toThrow();
});
});
diff --git a/packages/spec/src/data/driver/postgres.zod.ts b/packages/spec/src/data/driver/postgres.zod.ts
index 9954e3bd6d..6593cca1b0 100644
--- a/packages/spec/src/data/driver/postgres.zod.ts
+++ b/packages/spec/src/data/driver/postgres.zod.ts
@@ -2,103 +2,148 @@
import { z } from 'zod';
-/**
- * PostgreSQL Driver Configuration Schema
- * Defines the connection settings specific to PostgreSQL.
- */
import { lazySchema } from '../../shared/lazy-schema';
-export const PostgresConfigSchema = lazySchema(() => z.object({
- /**
- * Connection URI.
- * If provided, it takes precedence over host/port/database.
- * Format: postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]
- */
- url: z.string().optional().describe('Connection URI'),
-
- /**
- * Database Name.
- */
- database: z.string().describe('Database Name'),
-
- /**
- * Hostname or IP address.
- * Defaults to localhost.
- */
- host: z.string().default('localhost').describe('Host address'),
-
- /**
- * Port number.
- * Defaults to 5432.
- */
- port: z.number().default(5432).describe('Port number'),
-
- /**
- * Authentication Username.
- */
- username: z.string().optional().describe('Auth User'),
-
- /**
- * Authentication Password.
- */
- password: z.string().optional().describe('Auth Password'),
+import { strictUnknownKeyError } from '../../shared/suggestions.zod';
+import {
+ driverConfigJsonSchema,
+ DriverSslToggleSchema,
+ READ_ONLY_BELONGS_ON_DATASOURCE,
+ SCHEMA_MODE_BELONGS_ON_DATASOURCE,
+ SqlAutoMigrateSchema,
+ SSL_DETAIL_BELONGS_ON_DATASOURCE,
+} from './common.zod';
- /**
- * Default Schema.
- * The schema to use for tables that do not specify a schema.
- * Defaults to 'public'.
- */
- schema: z.string().default('public').describe('Default Schema'),
+/**
+ * PostgreSQL driver configuration — the `config` slot of a `datasource` whose
+ * `driver` resolves to `postgres` (`pg` / `postgresql`).
+ *
+ * ENFORCED as of #4410: `DatasourceSchema` parses `config` against this schema,
+ * so a misspelled connection key fails at authoring time instead of leaving the
+ * datasource on the client's localhost defaults. Every key here is read by
+ * `createDefaultDatasourceDriverFactory` (→ `SqlDriver`, knex `pg`).
+ *
+ * Pool sizing is NOT here: it lives in the driver-agnostic `datasource.pool`
+ * block, which the factory now honours for every SQL driver.
+ */
+const POSTGRES_CONFIG_KEYS = [
+ 'url', 'host', 'port', 'database', 'username', 'password', 'ssl',
+ 'schema', 'applicationName', 'statementTimeout', 'autoMigrate',
+] as const;
+
+/** Prescription for a pool knob written inside `config` instead of `pool`. */
+const poolBelongsOnDatasource = (key: string, canonical: string) =>
+ `\`${key}\` is not driver config — connection pooling is configured once for every driver in `
+ + `the datasource's own \`pool\` block. Move it to \`pool: { ${canonical}: … }\`. `
+ + `(It was declared here and read by nothing until #4410.)`;
+
+const postgresConfigUnknownKeyError = strictUnknownKeyError({
+ surface: "this postgres datasource's config",
+ knownKeys: POSTGRES_CONFIG_KEYS,
+ aliases: {
+ hostname: 'host',
+ server: 'host',
+ dbname: 'database',
+ db: 'database',
+ user: 'username',
+ passwd: 'password',
+ pwd: 'password',
+ connectionstring: 'url',
+ dsn: 'url',
+ uri: 'url',
+ searchpath: 'schema',
+ applicationname: 'applicationName',
+ statementtimeout: 'statementTimeout',
+ sslmode: 'ssl',
+ tls: 'ssl',
+ usessl: 'ssl',
+ },
+ guidance: {
+ pool: poolBelongsOnDatasource('pool', 'max'),
+ min: poolBelongsOnDatasource('min', 'min'),
+ max: poolBelongsOnDatasource('max', 'max'),
+ idleTimeoutMillis: poolBelongsOnDatasource('idleTimeoutMillis', 'idleTimeoutMillis'),
+ connectionTimeoutMillis: poolBelongsOnDatasource(
+ 'connectionTimeoutMillis',
+ 'connectionTimeoutMillis',
+ ),
+ schemaMode: SCHEMA_MODE_BELONGS_ON_DATASOURCE,
+ readOnly: READ_ONLY_BELONGS_ON_DATASOURCE,
+ ca: SSL_DETAIL_BELONGS_ON_DATASOURCE,
+ cert: SSL_DETAIL_BELONGS_ON_DATASOURCE,
+ key: SSL_DETAIL_BELONGS_ON_DATASOURCE,
+ rejectUnauthorized: SSL_DETAIL_BELONGS_ON_DATASOURCE,
+ },
+ history:
+ 'Until #4410 nothing validated `datasource.config` at all — an unrecognised connection key '
+ + 'was accepted in silence and the datasource then connected on the client defaults '
+ + "(localhost:5432), which is #4001's original bug one level down.",
+});
+export const PostgresConfigSchema = lazySchema(() => z.object({
/**
- * Enable SSL/TLS.
- * Can be a boolean or an object with specific SSL configuration (ca, cert, key, rejectUnauthorized).
+ * Connection URI. When present it supersedes `host`/`port`/`database`/
+ * `username`, and a datasource secret (`external.credentialsRef`) still
+ * overrides any password embedded in it.
+ * Format: `postgresql://[user[:password]@][host][:port][/dbname][?params]`
*/
- ssl: z.union([
- z.boolean(),
- z.object({
- rejectUnauthorized: z.boolean().optional(),
- ca: z.string().optional(),
- key: z.string().optional(),
- cert: z.string().optional(),
- })
- ]).optional().describe('Enable SSL'),
+ url: z.string().optional().describe('Connection URI (supersedes the discrete fields)')
+ .meta({ title: 'Connection URL' }),
- /**
- * Application Name.
- * Sets the application_name configuration parameter.
- */
- applicationName: z.string().optional().describe('Application Name'),
+ /** Hostname or IP address. */
+ host: z.string().default('localhost').describe('Host address').meta({ title: 'Host' }),
- /**
- * Connection Pool: Max Clients.
- * Maximum number of clients the pool should contain.
- */
- max: z.number().default(10).describe('Max Pool Size'),
+ /** Port number. */
+ port: z.number().int().default(5432).describe('Port number').meta({ title: 'Port' }),
- /**
- * Connection Pool: Min Clients.
- * Minimum number of clients to keep in the pool.
- */
- min: z.number().default(0).describe('Min Pool Size'),
+ /** Database name. Required unless `url` carries it. */
+ database: z.string().optional().describe('Database name').meta({ title: 'Database' }),
- /**
- * Idle Timeout (ms).
- * The number of milliseconds a client must sit idle in the pool and not be checked out
- * before it is disconnected from the backend and discarded.
- */
- idleTimeoutMillis: z.number().optional().describe('Idle Timeout (ms)'),
+ /** Authentication user. Passed to `pg` as `user`. */
+ username: z.string().optional().describe('Authentication user').meta({ title: 'User' }),
/**
- * Connection Timeout (ms).
- * The number of milliseconds to wait before timing out when connecting a new client.
- */
- connectionTimeoutMillis: z.number().optional().describe('Connection Timeout (ms)'),
-
- /**
- * Statement Timeout (ms).
- * Abort any statement that takes more than the specified number of milliseconds.
+ * Authentication password. Prefer `external.credentialsRef` — a secret-store
+ * reference — or an environment placeholder; a datasource secret always wins
+ * over this value.
*/
- statementTimeout: z.number().optional().describe('Statement Timeout (ms)'),
-}));
+ password: z.string().optional()
+ .describe('Authentication password (prefer external.credentialsRef)')
+ .meta({ title: 'Password', format: 'password' }),
+
+ /** TLS settings, passed to `pg` verbatim. */
+ ssl: DriverSslToggleSchema.optional().meta({ title: 'Use SSL/TLS' }),
+
+ /** Default schema for tables that do not name one — knex `searchPath`. */
+ schema: z.string().default('public').describe('Default schema (knex searchPath)')
+ .meta({ title: 'Schema' }),
+
+ /** `application_name` on the connection — how this stack shows up in `pg_stat_activity`. */
+ applicationName: z.string().optional().describe('Postgres application_name')
+ .meta({ title: 'Application name' }),
+
+ /** `statement_timeout` in milliseconds — aborts any statement that runs longer. */
+ statementTimeout: z.number().int().positive().optional()
+ .describe('Abort statements running longer than this (ms)')
+ .meta({ title: 'Statement timeout (ms)' }),
+
+ /** Dev-only, loosen-only schema self-heal (#2186). */
+ autoMigrate: SqlAutoMigrateSchema.optional(),
+}, { error: postgresConfigUnknownKeyError }).strict()
+ .describe('PostgreSQL connection configuration')
+ .superRefine((cfg, ctx) => {
+ if (!cfg.url && !cfg.database) {
+ ctx.addIssue({
+ code: 'custom',
+ path: ['database'],
+ message:
+ 'A postgres datasource needs a connection target: set `database` (with `host`/`port`) '
+ + 'or a full `url`. Neither was given, so the connection would fall back to the client '
+ + 'defaults and silently open a different database than the one intended.',
+ });
+ }
+ }));
export type PostgresConfig = z.infer;
+
+/** JSON-Schema projection of {@link PostgresConfigSchema}, memoized. */
+export const getPostgresConfigJsonSchema = driverConfigJsonSchema(PostgresConfigSchema);
diff --git a/packages/spec/src/data/driver/sqlite.zod.ts b/packages/spec/src/data/driver/sqlite.zod.ts
new file mode 100644
index 0000000000..97b27b4604
--- /dev/null
+++ b/packages/spec/src/data/driver/sqlite.zod.ts
@@ -0,0 +1,135 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import { z } from 'zod';
+
+import { lazySchema } from '../../shared/lazy-schema';
+import { strictUnknownKeyError } from '../../shared/suggestions.zod';
+import {
+ driverConfigJsonSchema,
+ READ_ONLY_BELONGS_ON_DATASOURCE,
+ SCHEMA_MODE_BELONGS_ON_DATASOURCE,
+ SqlAutoMigrateSchema,
+} from './common.zod';
+
+/**
+ * SQLite driver configuration — the `config` slot of a `datasource` whose
+ * `driver` resolves to `sqlite` (native `better-sqlite3`, with the dev-only
+ * step-down to wasm then in-memory, #2229) or to `sqlite-wasm` (pure-JS).
+ *
+ * The one key that matters is `filename`, and it is exactly the key the silent
+ * strip used to hide: an author who wrote `path:` got no error, the connection
+ * fell back to `:memory:`, and their data vanished on restart with every signal
+ * saying the datasource was configured.
+ *
+ * `file` and `database` are a different case — the factory reads them as
+ * undeclared `??` fallbacks, so they happened to work while being documented
+ * nowhere. They are named as renames here rather than blessed: one strict
+ * contract beats a spelling that works only because a reader is lenient
+ * (AGENTS.md Prime Directive #12). The factory keeps its tolerance for records
+ * already persisted that way; no new one can be authored.
+ */
+const SQLITE_CONFIG_KEYS = ['filename', 'autoMigrate'] as const;
+
+const FILENAME_ALIASES = {
+ file: 'filename',
+ filepath: 'filename',
+ path: 'filename',
+ database: 'filename',
+ db: 'filename',
+ url: 'filename',
+ connectionstring: 'filename',
+} as const;
+
+const sqliteHistory =
+ 'Until #4410 nothing validated `datasource.config` at all — a misspelled `filename` was '
+ + 'accepted in silence and the database silently became an ephemeral `:memory:` one, so the '
+ + 'data was gone on the next boot with nothing having reported a problem.';
+
+const IN_MEMORY_GUIDANCE =
+ '`memory` is not a sqlite key. An ephemeral database is `filename: \':memory:\'`; for the '
+ + 'mingo in-memory engine (a different driver entirely) set `driver: \'memory\'`.';
+
+const sqliteConfigUnknownKeyError = strictUnknownKeyError({
+ surface: "this sqlite datasource's config",
+ knownKeys: SQLITE_CONFIG_KEYS,
+ aliases: FILENAME_ALIASES,
+ guidance: {
+ schemaMode: SCHEMA_MODE_BELONGS_ON_DATASOURCE,
+ readOnly: READ_ONLY_BELONGS_ON_DATASOURCE,
+ memory: IN_MEMORY_GUIDANCE,
+ persist:
+ '`persist` is a `sqlite-wasm` key — the native sqlite driver writes through on every '
+ + "statement and has nothing to schedule. Set `driver: 'sqlite-wasm'` to use it.",
+ },
+ history: sqliteHistory,
+});
+
+export const SqliteConfigSchema = lazySchema(() => z.object({
+ /**
+ * Database file path, or `:memory:` for an ephemeral in-process database.
+ * A relative path resolves against the server's working directory.
+ */
+ filename: z.string().default(':memory:')
+ .describe('Database file path, or ":memory:" for an ephemeral database')
+ .meta({ title: 'Filename' }),
+
+ /** Dev-only, loosen-only schema self-heal (#2186). */
+ autoMigrate: SqlAutoMigrateSchema.optional(),
+}, { error: sqliteConfigUnknownKeyError }).strict()
+ .describe('SQLite connection configuration'));
+
+export type SqliteConfig = z.infer;
+
+/** JSON-Schema projection of {@link SqliteConfigSchema}, memoized. */
+export const getSqliteConfigJsonSchema = driverConfigJsonSchema(SqliteConfigSchema);
+
+/**
+ * When a file-backed wasm database is flushed back to disk. `debounced:`
+ * batches writes; `:memory:` databases ignore this entirely.
+ */
+export const SqliteWasmPersistModeSchema = z.union([
+ z.literal('on-disconnect'),
+ z.literal('on-write'),
+ z.string().regex(/^debounced:\d+$/, 'Expected `debounced:`'),
+]).describe('When to flush a file-backed wasm database to disk');
+
+export type SqliteWasmPersistMode = z.infer;
+
+const SQLITE_WASM_CONFIG_KEYS = ['filename', 'persist'] as const;
+
+const sqliteWasmConfigUnknownKeyError = strictUnknownKeyError({
+ surface: "this sqlite-wasm datasource's config",
+ knownKeys: SQLITE_WASM_CONFIG_KEYS,
+ aliases: FILENAME_ALIASES,
+ guidance: {
+ schemaMode: SCHEMA_MODE_BELONGS_ON_DATASOURCE,
+ readOnly: READ_ONLY_BELONGS_ON_DATASOURCE,
+ memory: IN_MEMORY_GUIDANCE,
+ autoMigrate:
+ '`autoMigrate` is honoured by the native sqlite / postgres / mysql drivers only — the '
+ + 'wasm driver is constructed without it, so writing it here would change nothing.',
+ },
+ history: sqliteHistory,
+});
+
+export const SqliteWasmConfigSchema = lazySchema(() => z.object({
+ /**
+ * Database file path, or `:memory:` for an ephemeral in-process database.
+ * A file-backed wasm database persists according to {@link SqliteWasmPersistModeSchema}.
+ */
+ filename: z.string().default(':memory:')
+ .describe('Database file path, or ":memory:" for an ephemeral database')
+ .meta({ title: 'Filename' }),
+
+ /**
+ * Flush policy for a file-backed database. Defaults to `on-write` when a
+ * filename is given; `:memory:` never persists.
+ */
+ persist: SqliteWasmPersistModeSchema.optional().meta({ title: 'Persist mode' }),
+}, { error: sqliteWasmConfigUnknownKeyError }).strict()
+ .describe('SQLite (WASM) connection configuration'));
+
+export type SqliteWasmConfig = z.infer;
+
+/** JSON-Schema projection of {@link SqliteWasmConfigSchema}, memoized. */
+export const getSqliteWasmConfigJsonSchema = driverConfigJsonSchema(SqliteWasmConfigSchema);
diff --git a/packages/spec/src/data/index.ts b/packages/spec/src/data/index.ts
index 08c39bc149..9880134882 100644
--- a/packages/spec/src/data/index.ts
+++ b/packages/spec/src/data/index.ts
@@ -60,6 +60,12 @@ export * from './document.zod';
export * from './external-lookup.zod';
export * from './datasource.zod';
+// Per-driver `datasource.config` contracts (#4410) — the enforcement half of
+// the `config` escape hatch DatasourceSchema leaves open at the top level.
+// Exported because they are now load-bearing; nothing could import them while
+// they were merely the shapes authors were TOLD to write against.
+export * from './driver/index';
+
// External Datasource Federation — SQL↔field type compatibility (ADR-0015)
export * from './type-compat';
export * from './external-catalog.zod';
diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md
index 4594076b10..3127aca64e 100644
--- a/skills/objectstack-data/references/_index.md
+++ b/skills/objectstack-data/references/_index.md
@@ -19,6 +19,13 @@ from `node_modules` — there is no local copy in the skill bundle.
## Transitive dependencies
+- `node_modules/@objectstack/spec/src/data/driver/common.zod.ts` — Shared building blocks for the per-driver `datasource.config` shapes (#4410).
+- `node_modules/@objectstack/spec/src/data/driver/config-registry.zod.ts` — The driver-id → `datasource.config` shape registry (#4410).
+- `node_modules/@objectstack/spec/src/data/driver/memory.zod.ts` — Memory Driver Configuration Schema
+- `node_modules/@objectstack/spec/src/data/driver/mongo.zod.ts` — MongoDB Standard Driver Protocol
+- `node_modules/@objectstack/spec/src/data/driver/mysql.zod.ts` — MySQL / MariaDB driver configuration — the `config` slot of a `datasource`
+- `node_modules/@objectstack/spec/src/data/driver/postgres.zod.ts` — PostgreSQL driver configuration — the `config` slot of a `datasource` whose
+- `node_modules/@objectstack/spec/src/data/driver/sqlite.zod.ts` — SQLite driver configuration — the `config` slot of a `datasource` whose
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Capability tokens a script body may request.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
diff --git a/skills/objectstack-platform/references/_index.md b/skills/objectstack-platform/references/_index.md
index bd86e85676..6946d58129 100644
--- a/skills/objectstack-platform/references/_index.md
+++ b/skills/objectstack-platform/references/_index.md
@@ -21,6 +21,13 @@ from `node_modules` — there is no local copy in the skill bundle.
## Transitive dependencies
+- `node_modules/@objectstack/spec/src/data/driver/common.zod.ts` — Shared building blocks for the per-driver `datasource.config` shapes (#4410).
+- `node_modules/@objectstack/spec/src/data/driver/config-registry.zod.ts` — The driver-id → `datasource.config` shape registry (#4410).
+- `node_modules/@objectstack/spec/src/data/driver/memory.zod.ts` — Memory Driver Configuration Schema
+- `node_modules/@objectstack/spec/src/data/driver/mongo.zod.ts` — MongoDB Standard Driver Protocol
+- `node_modules/@objectstack/spec/src/data/driver/mysql.zod.ts` — MySQL / MariaDB driver configuration — the `config` slot of a `datasource`
+- `node_modules/@objectstack/spec/src/data/driver/postgres.zod.ts` — PostgreSQL driver configuration — the `config` slot of a `datasource` whose
+- `node_modules/@objectstack/spec/src/data/driver/sqlite.zod.ts` — SQLite driver configuration — the `config` slot of a `datasource` whose
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Field Type Enum
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Capability tokens a script body may request.