diff --git a/content/docs/references/data/connector.mdx b/content/docs/references/data/connector.mdx
new file mode 100644
index 000000000..d911637f9
--- /dev/null
+++ b/content/docs/references/data/connector.mdx
@@ -0,0 +1,34 @@
+---
+title: Connector
+description: Connector protocol schemas
+---
+
+# Connector
+
+
+**Source:** `packages/spec/src/data/connector.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { FieldMappingSchema } from '@objectstack/spec/data';
+import type { FieldMapping } from '@objectstack/spec/data';
+
+// Validate data
+const result = FieldMappingSchema.parse(data);
+```
+
+---
+
+## FieldMapping
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **externalField** | `string` | ✅ | External field name |
+| **localField** | `string` | ✅ | Local field name |
+| **type** | `string` | ✅ | Field type |
+| **readonly** | `boolean` | optional | Read-only field |
+
diff --git a/content/docs/references/data/document.mdx b/content/docs/references/data/document.mdx
new file mode 100644
index 000000000..75dba04f4
--- /dev/null
+++ b/content/docs/references/data/document.mdx
@@ -0,0 +1,87 @@
+---
+title: Document
+description: Document protocol schemas
+---
+
+# Document
+
+
+**Source:** `packages/spec/src/data/document.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { DocumentSchema, DocumentTemplateSchema, DocumentVersionSchema, ESignatureConfigSchema } from '@objectstack/spec/data';
+import type { Document, DocumentTemplate, DocumentVersion, ESignatureConfig } from '@objectstack/spec/data';
+
+// Validate data
+const result = DocumentSchema.parse(data);
+```
+
+---
+
+## Document
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Document ID |
+| **name** | `string` | ✅ | Document name |
+| **description** | `string` | optional | Document description |
+| **fileType** | `string` | ✅ | File MIME type |
+| **fileSize** | `number` | ✅ | File size in bytes |
+| **category** | `string` | optional | Document category |
+| **tags** | `string[]` | optional | Document tags |
+| **versioning** | `object` | optional | Version control |
+| **template** | `object` | optional | Document template |
+| **eSignature** | `object` | optional | E-signature config |
+| **access** | `object` | optional | Access control |
+| **metadata** | `Record` | optional | Custom metadata |
+
+---
+
+## DocumentTemplate
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Template ID |
+| **name** | `string` | ✅ | Template name |
+| **description** | `string` | optional | Template description |
+| **fileUrl** | `string` | ✅ | Template file URL |
+| **fileType** | `string` | ✅ | File MIME type |
+| **placeholders** | `object[]` | ✅ | Template placeholders |
+
+---
+
+## DocumentVersion
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **versionNumber** | `number` | ✅ | Version number |
+| **createdAt** | `number` | ✅ | Creation timestamp |
+| **createdBy** | `string` | ✅ | Creator user ID |
+| **size** | `number` | ✅ | File size in bytes |
+| **checksum** | `string` | ✅ | File checksum |
+| **downloadUrl** | `string` | ✅ | Download URL |
+| **isLatest** | `boolean` | optional | Is latest version |
+
+---
+
+## ESignatureConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **provider** | `Enum<'docusign' \| 'adobe-sign' \| 'hellosign' \| 'custom'>` | ✅ | E-signature provider |
+| **enabled** | `boolean` | optional | E-signature enabled |
+| **signers** | `object[]` | ✅ | Document signers |
+| **expirationDays** | `number` | optional | Expiration days |
+| **reminderDays** | `number` | optional | Reminder interval days |
+
diff --git a/content/docs/references/data/external-lookup.mdx b/content/docs/references/data/external-lookup.mdx
new file mode 100644
index 000000000..eb65904dc
--- /dev/null
+++ b/content/docs/references/data/external-lookup.mdx
@@ -0,0 +1,51 @@
+---
+title: External Lookup
+description: External Lookup protocol schemas
+---
+
+# External Lookup
+
+
+**Source:** `packages/spec/src/data/external-lookup.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { ExternalDataSourceSchema, ExternalLookupSchema } from '@objectstack/spec/data';
+import type { ExternalDataSource, ExternalLookup } from '@objectstack/spec/data';
+
+// Validate data
+const result = ExternalDataSourceSchema.parse(data);
+```
+
+---
+
+## ExternalDataSource
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Data source ID |
+| **name** | `string` | ✅ | Data source name |
+| **type** | `Enum<'odata' \| 'rest-api' \| 'graphql' \| 'custom'>` | ✅ | Protocol type |
+| **endpoint** | `string` | ✅ | API endpoint URL |
+| **authentication** | `object` | ✅ | Authentication |
+
+---
+
+## ExternalLookup
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **fieldName** | `string` | ✅ | Field name |
+| **dataSource** | `object` | ✅ | External data source |
+| **query** | `object` | ✅ | Query configuration |
+| **fieldMappings** | `object[]` | ✅ | Field mappings |
+| **caching** | `object` | optional | Caching configuration |
+| **fallback** | `object` | optional | Fallback configuration |
+| **rateLimit** | `object` | optional | Rate limiting |
+
diff --git a/content/docs/references/data/index.mdx b/content/docs/references/data/index.mdx
index a91672d32..bd834f232 100644
--- a/content/docs/references/data/index.mdx
+++ b/content/docs/references/data/index.mdx
@@ -9,6 +9,8 @@ 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 6b613d272..661e79412 100644
--- a/content/docs/references/data/meta.json
+++ b/content/docs/references/data/meta.json
@@ -2,6 +2,8 @@
"title": "Data Protocol",
"pages": [
"dataset",
+ "document",
+ "external-lookup",
"field",
"filter",
"hook",
diff --git a/content/docs/references/system/change-management.mdx b/content/docs/references/system/change-management.mdx
new file mode 100644
index 000000000..aa19a4e25
--- /dev/null
+++ b/content/docs/references/system/change-management.mdx
@@ -0,0 +1,108 @@
+---
+title: Change Management
+description: Change Management protocol schemas
+---
+
+# Change Management
+
+
+**Source:** `packages/spec/src/system/change-management.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { ChangeImpactSchema, ChangePrioritySchema, ChangeRequestSchema, ChangeStatusSchema, ChangeTypeSchema, RollbackPlanSchema } from '@objectstack/spec/system';
+import type { ChangeImpact, ChangePriority, ChangeRequest, ChangeStatus, ChangeType, RollbackPlan } from '@objectstack/spec/system';
+
+// Validate data
+const result = ChangeImpactSchema.parse(data);
+```
+
+---
+
+## ChangeImpact
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **level** | `Enum<'low' \| 'medium' \| 'high' \| 'critical'>` | ✅ | Impact level |
+| **affectedSystems** | `string[]` | ✅ | Affected systems |
+| **affectedUsers** | `number` | optional | Affected user count |
+| **downtime** | `object` | optional | Downtime information |
+
+---
+
+## ChangePriority
+
+### Allowed Values
+
+* `critical`
+* `high`
+* `medium`
+* `low`
+
+---
+
+## ChangeRequest
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Change request ID |
+| **title** | `string` | ✅ | Change title |
+| **description** | `string` | ✅ | Change description |
+| **type** | `Enum<'standard' \| 'normal' \| 'emergency' \| 'major'>` | ✅ | Change type |
+| **priority** | `Enum<'critical' \| 'high' \| 'medium' \| 'low'>` | ✅ | Change priority |
+| **status** | `Enum<'draft' \| 'submitted' \| 'in-review' \| 'approved' \| 'scheduled' \| 'in-progress' \| 'completed' \| 'failed' \| 'rolled-back' \| 'cancelled'>` | ✅ | Change status |
+| **requestedBy** | `string` | ✅ | Requester user ID |
+| **requestedAt** | `number` | ✅ | Request timestamp |
+| **impact** | `object` | ✅ | Impact assessment |
+| **implementation** | `object` | ✅ | Implementation plan |
+| **rollbackPlan** | `object` | ✅ | Rollback plan |
+| **schedule** | `object` | optional | Schedule |
+| **approval** | `object` | optional | Approval workflow |
+| **attachments** | `object[]` | optional | Attachments |
+
+---
+
+## ChangeStatus
+
+### Allowed Values
+
+* `draft`
+* `submitted`
+* `in-review`
+* `approved`
+* `scheduled`
+* `in-progress`
+* `completed`
+* `failed`
+* `rolled-back`
+* `cancelled`
+
+---
+
+## ChangeType
+
+### Allowed Values
+
+* `standard`
+* `normal`
+* `emergency`
+* `major`
+
+---
+
+## RollbackPlan
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **description** | `string` | ✅ | Rollback description |
+| **steps** | `object[]` | ✅ | Rollback steps |
+| **testProcedure** | `string` | optional | Test procedure |
+
diff --git a/content/docs/references/system/index.mdx b/content/docs/references/system/index.mdx
index 654db1db1..569cf0fdc 100644
--- a/content/docs/references/system/index.mdx
+++ b/content/docs/references/system/index.mdx
@@ -10,6 +10,7 @@ This section contains all protocol schemas for the system layer of ObjectStack.
+
@@ -28,6 +29,7 @@ This section contains all protocol schemas for the system layer of ObjectStack.
+
diff --git a/content/docs/references/system/meta.json b/content/docs/references/system/meta.json
index 0321fd6b9..175581a12 100644
--- a/content/docs/references/system/meta.json
+++ b/content/docs/references/system/meta.json
@@ -3,6 +3,7 @@
"pages": [
"audit",
"cache",
+ "change-management",
"collaboration",
"compliance",
"context",
@@ -21,6 +22,7 @@
"masking",
"message-queue",
"metrics",
+ "notification",
"object-storage",
"plugin",
"plugin-capability",
diff --git a/content/docs/references/system/notification.mdx b/content/docs/references/system/notification.mdx
new file mode 100644
index 000000000..120e421cf
--- /dev/null
+++ b/content/docs/references/system/notification.mdx
@@ -0,0 +1,110 @@
+---
+title: Notification
+description: Notification protocol schemas
+---
+
+# Notification
+
+
+**Source:** `packages/spec/src/system/notification.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { EmailTemplateSchema, InAppNotificationSchema, NotificationChannelSchema, NotificationConfigSchema, PushNotificationSchema, SMSTemplateSchema } from '@objectstack/spec/system';
+import type { EmailTemplate, InAppNotification, NotificationChannel, NotificationConfig, PushNotification, SMSTemplate } from '@objectstack/spec/system';
+
+// Validate data
+const result = EmailTemplateSchema.parse(data);
+```
+
+---
+
+## EmailTemplate
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Template identifier |
+| **subject** | `string` | ✅ | Email subject |
+| **body** | `string` | ✅ | Email body content |
+| **bodyType** | `Enum<'text' \| 'html' \| 'markdown'>` | optional | Body content type |
+| **variables** | `string[]` | optional | Template variables |
+| **attachments** | `object[]` | optional | Email attachments |
+
+---
+
+## InAppNotification
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **title** | `string` | ✅ | Notification title |
+| **message** | `string` | ✅ | Notification message |
+| **type** | `Enum<'info' \| 'success' \| 'warning' \| 'error'>` | ✅ | Notification type |
+| **actionUrl** | `string` | optional | Action URL |
+| **dismissible** | `boolean` | optional | User dismissible |
+| **expiresAt** | `number` | optional | Expiration timestamp |
+
+---
+
+## NotificationChannel
+
+### Allowed Values
+
+* `email`
+* `sms`
+* `push`
+* `in-app`
+* `slack`
+* `teams`
+* `webhook`
+
+---
+
+## NotificationConfig
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Notification ID |
+| **name** | `string` | ✅ | Notification name |
+| **channel** | `Enum<'email' \| 'sms' \| 'push' \| 'in-app' \| 'slack' \| 'teams' \| 'webhook'>` | ✅ | Notification channel |
+| **template** | `object \| object \| object \| object` | ✅ | Notification template |
+| **recipients** | `object` | ✅ | Recipients |
+| **schedule** | `object` | optional | Scheduling |
+| **retryPolicy** | `object` | optional | Retry policy |
+| **tracking** | `object` | optional | Tracking configuration |
+
+---
+
+## PushNotification
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **title** | `string` | ✅ | Notification title |
+| **body** | `string` | ✅ | Notification body |
+| **icon** | `string` | optional | Notification icon URL |
+| **badge** | `number` | optional | Badge count |
+| **data** | `Record` | optional | Custom data |
+| **actions** | `object[]` | optional | Notification actions |
+
+---
+
+## SMSTemplate
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Template identifier |
+| **message** | `string` | ✅ | SMS message content |
+| **maxLength** | `number` | optional | Maximum message length |
+| **variables** | `string[]` | optional | Template variables |
+
diff --git a/packages/spec/json-schema/data/Document.json b/packages/spec/json-schema/data/Document.json
new file mode 100644
index 000000000..fe369c5af
--- /dev/null
+++ b/packages/spec/json-schema/data/Document.json
@@ -0,0 +1,292 @@
+{
+ "$ref": "#/definitions/Document",
+ "definitions": {
+ "Document": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Document ID"
+ },
+ "name": {
+ "type": "string",
+ "description": "Document name"
+ },
+ "description": {
+ "type": "string",
+ "description": "Document description"
+ },
+ "fileType": {
+ "type": "string",
+ "description": "File MIME type"
+ },
+ "fileSize": {
+ "type": "number",
+ "description": "File size in bytes"
+ },
+ "category": {
+ "type": "string",
+ "description": "Document category"
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Document tags"
+ },
+ "versioning": {
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Versioning enabled"
+ },
+ "versions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "versionNumber": {
+ "type": "number",
+ "description": "Version number"
+ },
+ "createdAt": {
+ "type": "number",
+ "description": "Creation timestamp"
+ },
+ "createdBy": {
+ "type": "string",
+ "description": "Creator user ID"
+ },
+ "size": {
+ "type": "number",
+ "description": "File size in bytes"
+ },
+ "checksum": {
+ "type": "string",
+ "description": "File checksum"
+ },
+ "downloadUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "Download URL"
+ },
+ "isLatest": {
+ "type": "boolean",
+ "default": false,
+ "description": "Is latest version"
+ }
+ },
+ "required": [
+ "versionNumber",
+ "createdAt",
+ "createdBy",
+ "size",
+ "checksum",
+ "downloadUrl"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Version history"
+ },
+ "majorVersion": {
+ "type": "number",
+ "description": "Major version"
+ },
+ "minorVersion": {
+ "type": "number",
+ "description": "Minor version"
+ }
+ },
+ "required": [
+ "enabled",
+ "versions",
+ "majorVersion",
+ "minorVersion"
+ ],
+ "additionalProperties": false,
+ "description": "Version control"
+ },
+ "template": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Template ID"
+ },
+ "name": {
+ "type": "string",
+ "description": "Template name"
+ },
+ "description": {
+ "type": "string",
+ "description": "Template description"
+ },
+ "fileUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "Template file URL"
+ },
+ "fileType": {
+ "type": "string",
+ "description": "File MIME type"
+ },
+ "placeholders": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string",
+ "description": "Placeholder key"
+ },
+ "label": {
+ "type": "string",
+ "description": "Placeholder label"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "text",
+ "number",
+ "date",
+ "image"
+ ],
+ "description": "Placeholder type"
+ },
+ "required": {
+ "type": "boolean",
+ "default": false,
+ "description": "Is required"
+ }
+ },
+ "required": [
+ "key",
+ "label",
+ "type"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Template placeholders"
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "fileUrl",
+ "fileType",
+ "placeholders"
+ ],
+ "additionalProperties": false,
+ "description": "Document template"
+ },
+ "eSignature": {
+ "type": "object",
+ "properties": {
+ "provider": {
+ "type": "string",
+ "enum": [
+ "docusign",
+ "adobe-sign",
+ "hellosign",
+ "custom"
+ ],
+ "description": "E-signature provider"
+ },
+ "enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "E-signature enabled"
+ },
+ "signers": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "description": "Signer email"
+ },
+ "name": {
+ "type": "string",
+ "description": "Signer name"
+ },
+ "role": {
+ "type": "string",
+ "description": "Signer role"
+ },
+ "order": {
+ "type": "number",
+ "description": "Signing order"
+ }
+ },
+ "required": [
+ "email",
+ "name",
+ "role",
+ "order"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Document signers"
+ },
+ "expirationDays": {
+ "type": "number",
+ "default": 30,
+ "description": "Expiration days"
+ },
+ "reminderDays": {
+ "type": "number",
+ "default": 7,
+ "description": "Reminder interval days"
+ }
+ },
+ "required": [
+ "provider",
+ "signers"
+ ],
+ "additionalProperties": false,
+ "description": "E-signature config"
+ },
+ "access": {
+ "type": "object",
+ "properties": {
+ "isPublic": {
+ "type": "boolean",
+ "default": false,
+ "description": "Public access"
+ },
+ "sharedWith": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Shared with"
+ },
+ "expiresAt": {
+ "type": "number",
+ "description": "Access expiration"
+ }
+ },
+ "additionalProperties": false,
+ "description": "Access control"
+ },
+ "metadata": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Custom metadata"
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "fileType",
+ "fileSize"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/data/DocumentTemplate.json b/packages/spec/json-schema/data/DocumentTemplate.json
new file mode 100644
index 000000000..2f391b111
--- /dev/null
+++ b/packages/spec/json-schema/data/DocumentTemplate.json
@@ -0,0 +1,78 @@
+{
+ "$ref": "#/definitions/DocumentTemplate",
+ "definitions": {
+ "DocumentTemplate": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Template ID"
+ },
+ "name": {
+ "type": "string",
+ "description": "Template name"
+ },
+ "description": {
+ "type": "string",
+ "description": "Template description"
+ },
+ "fileUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "Template file URL"
+ },
+ "fileType": {
+ "type": "string",
+ "description": "File MIME type"
+ },
+ "placeholders": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string",
+ "description": "Placeholder key"
+ },
+ "label": {
+ "type": "string",
+ "description": "Placeholder label"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "text",
+ "number",
+ "date",
+ "image"
+ ],
+ "description": "Placeholder type"
+ },
+ "required": {
+ "type": "boolean",
+ "default": false,
+ "description": "Is required"
+ }
+ },
+ "required": [
+ "key",
+ "label",
+ "type"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Template placeholders"
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "fileUrl",
+ "fileType",
+ "placeholders"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/data/DocumentVersion.json b/packages/spec/json-schema/data/DocumentVersion.json
new file mode 100644
index 000000000..0fe34ce72
--- /dev/null
+++ b/packages/spec/json-schema/data/DocumentVersion.json
@@ -0,0 +1,50 @@
+{
+ "$ref": "#/definitions/DocumentVersion",
+ "definitions": {
+ "DocumentVersion": {
+ "type": "object",
+ "properties": {
+ "versionNumber": {
+ "type": "number",
+ "description": "Version number"
+ },
+ "createdAt": {
+ "type": "number",
+ "description": "Creation timestamp"
+ },
+ "createdBy": {
+ "type": "string",
+ "description": "Creator user ID"
+ },
+ "size": {
+ "type": "number",
+ "description": "File size in bytes"
+ },
+ "checksum": {
+ "type": "string",
+ "description": "File checksum"
+ },
+ "downloadUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "Download URL"
+ },
+ "isLatest": {
+ "type": "boolean",
+ "default": false,
+ "description": "Is latest version"
+ }
+ },
+ "required": [
+ "versionNumber",
+ "createdAt",
+ "createdBy",
+ "size",
+ "checksum",
+ "downloadUrl"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/data/ESignatureConfig.json b/packages/spec/json-schema/data/ESignatureConfig.json
new file mode 100644
index 000000000..546b4ad41
--- /dev/null
+++ b/packages/spec/json-schema/data/ESignatureConfig.json
@@ -0,0 +1,74 @@
+{
+ "$ref": "#/definitions/ESignatureConfig",
+ "definitions": {
+ "ESignatureConfig": {
+ "type": "object",
+ "properties": {
+ "provider": {
+ "type": "string",
+ "enum": [
+ "docusign",
+ "adobe-sign",
+ "hellosign",
+ "custom"
+ ],
+ "description": "E-signature provider"
+ },
+ "enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "E-signature enabled"
+ },
+ "signers": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "description": "Signer email"
+ },
+ "name": {
+ "type": "string",
+ "description": "Signer name"
+ },
+ "role": {
+ "type": "string",
+ "description": "Signer role"
+ },
+ "order": {
+ "type": "number",
+ "description": "Signing order"
+ }
+ },
+ "required": [
+ "email",
+ "name",
+ "role",
+ "order"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Document signers"
+ },
+ "expirationDays": {
+ "type": "number",
+ "default": 30,
+ "description": "Expiration days"
+ },
+ "reminderDays": {
+ "type": "number",
+ "default": 7,
+ "description": "Reminder interval days"
+ }
+ },
+ "required": [
+ "provider",
+ "signers"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/data/ExternalDataSource.json b/packages/spec/json-schema/data/ExternalDataSource.json
new file mode 100644
index 000000000..c4eb9ea2c
--- /dev/null
+++ b/packages/spec/json-schema/data/ExternalDataSource.json
@@ -0,0 +1,68 @@
+{
+ "$ref": "#/definitions/ExternalDataSource",
+ "definitions": {
+ "ExternalDataSource": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Data source ID"
+ },
+ "name": {
+ "type": "string",
+ "description": "Data source name"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "odata",
+ "rest-api",
+ "graphql",
+ "custom"
+ ],
+ "description": "Protocol type"
+ },
+ "endpoint": {
+ "type": "string",
+ "format": "uri",
+ "description": "API endpoint URL"
+ },
+ "authentication": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "oauth2",
+ "api-key",
+ "basic",
+ "none"
+ ],
+ "description": "Auth type"
+ },
+ "config": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Auth configuration"
+ }
+ },
+ "required": [
+ "type",
+ "config"
+ ],
+ "additionalProperties": false,
+ "description": "Authentication"
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "type",
+ "endpoint",
+ "authentication"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/data/ExternalLookup.json b/packages/spec/json-schema/data/ExternalLookup.json
new file mode 100644
index 000000000..ae7ea0626
--- /dev/null
+++ b/packages/spec/json-schema/data/ExternalLookup.json
@@ -0,0 +1,210 @@
+{
+ "$ref": "#/definitions/ExternalLookup",
+ "definitions": {
+ "ExternalLookup": {
+ "type": "object",
+ "properties": {
+ "fieldName": {
+ "type": "string",
+ "description": "Field name"
+ },
+ "dataSource": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Data source ID"
+ },
+ "name": {
+ "type": "string",
+ "description": "Data source name"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "odata",
+ "rest-api",
+ "graphql",
+ "custom"
+ ],
+ "description": "Protocol type"
+ },
+ "endpoint": {
+ "type": "string",
+ "format": "uri",
+ "description": "API endpoint URL"
+ },
+ "authentication": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "oauth2",
+ "api-key",
+ "basic",
+ "none"
+ ],
+ "description": "Auth type"
+ },
+ "config": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Auth configuration"
+ }
+ },
+ "required": [
+ "type",
+ "config"
+ ],
+ "additionalProperties": false,
+ "description": "Authentication"
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "type",
+ "endpoint",
+ "authentication"
+ ],
+ "additionalProperties": false,
+ "description": "External data source"
+ },
+ "query": {
+ "type": "object",
+ "properties": {
+ "endpoint": {
+ "type": "string",
+ "description": "Query endpoint path"
+ },
+ "method": {
+ "type": "string",
+ "enum": [
+ "GET",
+ "POST"
+ ],
+ "default": "GET",
+ "description": "HTTP method"
+ },
+ "parameters": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Query parameters"
+ }
+ },
+ "required": [
+ "endpoint"
+ ],
+ "additionalProperties": false,
+ "description": "Query configuration"
+ },
+ "fieldMappings": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "externalField": {
+ "type": "string",
+ "description": "External field name"
+ },
+ "localField": {
+ "type": "string",
+ "description": "Local field name"
+ },
+ "type": {
+ "type": "string",
+ "description": "Field type"
+ },
+ "readonly": {
+ "type": "boolean",
+ "default": true,
+ "description": "Read-only field"
+ }
+ },
+ "required": [
+ "externalField",
+ "localField",
+ "type"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Field mappings"
+ },
+ "caching": {
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "default": true,
+ "description": "Cache enabled"
+ },
+ "ttl": {
+ "type": "number",
+ "default": 300,
+ "description": "Cache TTL (seconds)"
+ },
+ "strategy": {
+ "type": "string",
+ "enum": [
+ "lru",
+ "lfu",
+ "ttl"
+ ],
+ "default": "ttl",
+ "description": "Cache strategy"
+ }
+ },
+ "additionalProperties": false,
+ "description": "Caching configuration"
+ },
+ "fallback": {
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "default": true,
+ "description": "Fallback enabled"
+ },
+ "defaultValue": {
+ "description": "Default fallback value"
+ },
+ "showError": {
+ "type": "boolean",
+ "default": true,
+ "description": "Show error to user"
+ }
+ },
+ "additionalProperties": false,
+ "description": "Fallback configuration"
+ },
+ "rateLimit": {
+ "type": "object",
+ "properties": {
+ "requestsPerSecond": {
+ "type": "number",
+ "description": "Requests per second limit"
+ },
+ "burstSize": {
+ "type": "number",
+ "description": "Burst size"
+ }
+ },
+ "required": [
+ "requestsPerSecond"
+ ],
+ "additionalProperties": false,
+ "description": "Rate limiting"
+ }
+ },
+ "required": [
+ "fieldName",
+ "dataSource",
+ "query",
+ "fieldMappings"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/data/FieldMapping.json b/packages/spec/json-schema/data/FieldMapping.json
new file mode 100644
index 000000000..4ec2dcf95
--- /dev/null
+++ b/packages/spec/json-schema/data/FieldMapping.json
@@ -0,0 +1,34 @@
+{
+ "$ref": "#/definitions/FieldMapping",
+ "definitions": {
+ "FieldMapping": {
+ "type": "object",
+ "properties": {
+ "externalField": {
+ "type": "string",
+ "description": "External field name"
+ },
+ "localField": {
+ "type": "string",
+ "description": "Local field name"
+ },
+ "type": {
+ "type": "string",
+ "description": "Field type"
+ },
+ "readonly": {
+ "type": "boolean",
+ "default": true,
+ "description": "Read-only field"
+ }
+ },
+ "required": [
+ "externalField",
+ "localField",
+ "type"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/ChangeImpact.json b/packages/spec/json-schema/system/ChangeImpact.json
new file mode 100644
index 000000000..ead817cbe
--- /dev/null
+++ b/packages/spec/json-schema/system/ChangeImpact.json
@@ -0,0 +1,55 @@
+{
+ "$ref": "#/definitions/ChangeImpact",
+ "definitions": {
+ "ChangeImpact": {
+ "type": "object",
+ "properties": {
+ "level": {
+ "type": "string",
+ "enum": [
+ "low",
+ "medium",
+ "high",
+ "critical"
+ ],
+ "description": "Impact level"
+ },
+ "affectedSystems": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Affected systems"
+ },
+ "affectedUsers": {
+ "type": "number",
+ "description": "Affected user count"
+ },
+ "downtime": {
+ "type": "object",
+ "properties": {
+ "required": {
+ "type": "boolean",
+ "description": "Downtime required"
+ },
+ "durationMinutes": {
+ "type": "number",
+ "description": "Downtime duration"
+ }
+ },
+ "required": [
+ "required"
+ ],
+ "additionalProperties": false,
+ "description": "Downtime information"
+ }
+ },
+ "required": [
+ "level",
+ "affectedSystems"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/ChangePriority.json b/packages/spec/json-schema/system/ChangePriority.json
new file mode 100644
index 000000000..cdf051598
--- /dev/null
+++ b/packages/spec/json-schema/system/ChangePriority.json
@@ -0,0 +1,15 @@
+{
+ "$ref": "#/definitions/ChangePriority",
+ "definitions": {
+ "ChangePriority": {
+ "type": "string",
+ "enum": [
+ "critical",
+ "high",
+ "medium",
+ "low"
+ ]
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/ChangeRequest.json b/packages/spec/json-schema/system/ChangeRequest.json
new file mode 100644
index 000000000..23bcfd1fc
--- /dev/null
+++ b/packages/spec/json-schema/system/ChangeRequest.json
@@ -0,0 +1,313 @@
+{
+ "$ref": "#/definitions/ChangeRequest",
+ "definitions": {
+ "ChangeRequest": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Change request ID"
+ },
+ "title": {
+ "type": "string",
+ "description": "Change title"
+ },
+ "description": {
+ "type": "string",
+ "description": "Change description"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "standard",
+ "normal",
+ "emergency",
+ "major"
+ ],
+ "description": "Change type"
+ },
+ "priority": {
+ "type": "string",
+ "enum": [
+ "critical",
+ "high",
+ "medium",
+ "low"
+ ],
+ "description": "Change priority"
+ },
+ "status": {
+ "type": "string",
+ "enum": [
+ "draft",
+ "submitted",
+ "in-review",
+ "approved",
+ "scheduled",
+ "in-progress",
+ "completed",
+ "failed",
+ "rolled-back",
+ "cancelled"
+ ],
+ "description": "Change status"
+ },
+ "requestedBy": {
+ "type": "string",
+ "description": "Requester user ID"
+ },
+ "requestedAt": {
+ "type": "number",
+ "description": "Request timestamp"
+ },
+ "impact": {
+ "type": "object",
+ "properties": {
+ "level": {
+ "type": "string",
+ "enum": [
+ "low",
+ "medium",
+ "high",
+ "critical"
+ ],
+ "description": "Impact level"
+ },
+ "affectedSystems": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Affected systems"
+ },
+ "affectedUsers": {
+ "type": "number",
+ "description": "Affected user count"
+ },
+ "downtime": {
+ "type": "object",
+ "properties": {
+ "required": {
+ "type": "boolean",
+ "description": "Downtime required"
+ },
+ "durationMinutes": {
+ "type": "number",
+ "description": "Downtime duration"
+ }
+ },
+ "required": [
+ "required"
+ ],
+ "additionalProperties": false,
+ "description": "Downtime information"
+ }
+ },
+ "required": [
+ "level",
+ "affectedSystems"
+ ],
+ "additionalProperties": false,
+ "description": "Impact assessment"
+ },
+ "implementation": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "Implementation description"
+ },
+ "steps": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "order": {
+ "type": "number",
+ "description": "Step order"
+ },
+ "description": {
+ "type": "string",
+ "description": "Step description"
+ },
+ "estimatedMinutes": {
+ "type": "number",
+ "description": "Estimated duration"
+ }
+ },
+ "required": [
+ "order",
+ "description",
+ "estimatedMinutes"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Implementation steps"
+ },
+ "testing": {
+ "type": "string",
+ "description": "Testing procedure"
+ }
+ },
+ "required": [
+ "description",
+ "steps"
+ ],
+ "additionalProperties": false,
+ "description": "Implementation plan"
+ },
+ "rollbackPlan": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "Rollback description"
+ },
+ "steps": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "order": {
+ "type": "number",
+ "description": "Step order"
+ },
+ "description": {
+ "type": "string",
+ "description": "Step description"
+ },
+ "estimatedMinutes": {
+ "type": "number",
+ "description": "Estimated duration"
+ }
+ },
+ "required": [
+ "order",
+ "description",
+ "estimatedMinutes"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Rollback steps"
+ },
+ "testProcedure": {
+ "type": "string",
+ "description": "Test procedure"
+ }
+ },
+ "required": [
+ "description",
+ "steps"
+ ],
+ "additionalProperties": false,
+ "description": "Rollback plan"
+ },
+ "schedule": {
+ "type": "object",
+ "properties": {
+ "plannedStart": {
+ "type": "number",
+ "description": "Planned start time"
+ },
+ "plannedEnd": {
+ "type": "number",
+ "description": "Planned end time"
+ },
+ "actualStart": {
+ "type": "number",
+ "description": "Actual start time"
+ },
+ "actualEnd": {
+ "type": "number",
+ "description": "Actual end time"
+ }
+ },
+ "required": [
+ "plannedStart",
+ "plannedEnd"
+ ],
+ "additionalProperties": false,
+ "description": "Schedule"
+ },
+ "approval": {
+ "type": "object",
+ "properties": {
+ "required": {
+ "type": "boolean",
+ "description": "Approval required"
+ },
+ "approvers": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "userId": {
+ "type": "string",
+ "description": "Approver user ID"
+ },
+ "approvedAt": {
+ "type": "number",
+ "description": "Approval timestamp"
+ },
+ "comments": {
+ "type": "string",
+ "description": "Approver comments"
+ }
+ },
+ "required": [
+ "userId"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Approvers"
+ }
+ },
+ "required": [
+ "required",
+ "approvers"
+ ],
+ "additionalProperties": false,
+ "description": "Approval workflow"
+ },
+ "attachments": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Attachment name"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "description": "Attachment URL"
+ }
+ },
+ "required": [
+ "name",
+ "url"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Attachments"
+ }
+ },
+ "required": [
+ "id",
+ "title",
+ "description",
+ "type",
+ "priority",
+ "status",
+ "requestedBy",
+ "requestedAt",
+ "impact",
+ "implementation",
+ "rollbackPlan"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/ChangeStatus.json b/packages/spec/json-schema/system/ChangeStatus.json
new file mode 100644
index 000000000..8fb05526f
--- /dev/null
+++ b/packages/spec/json-schema/system/ChangeStatus.json
@@ -0,0 +1,21 @@
+{
+ "$ref": "#/definitions/ChangeStatus",
+ "definitions": {
+ "ChangeStatus": {
+ "type": "string",
+ "enum": [
+ "draft",
+ "submitted",
+ "in-review",
+ "approved",
+ "scheduled",
+ "in-progress",
+ "completed",
+ "failed",
+ "rolled-back",
+ "cancelled"
+ ]
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/ChangeType.json b/packages/spec/json-schema/system/ChangeType.json
new file mode 100644
index 000000000..7d056daa8
--- /dev/null
+++ b/packages/spec/json-schema/system/ChangeType.json
@@ -0,0 +1,15 @@
+{
+ "$ref": "#/definitions/ChangeType",
+ "definitions": {
+ "ChangeType": {
+ "type": "string",
+ "enum": [
+ "standard",
+ "normal",
+ "emergency",
+ "major"
+ ]
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/EmailTemplate.json b/packages/spec/json-schema/system/EmailTemplate.json
new file mode 100644
index 000000000..ca79574b7
--- /dev/null
+++ b/packages/spec/json-schema/system/EmailTemplate.json
@@ -0,0 +1,69 @@
+{
+ "$ref": "#/definitions/EmailTemplate",
+ "definitions": {
+ "EmailTemplate": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Template identifier"
+ },
+ "subject": {
+ "type": "string",
+ "description": "Email subject"
+ },
+ "body": {
+ "type": "string",
+ "description": "Email body content"
+ },
+ "bodyType": {
+ "type": "string",
+ "enum": [
+ "text",
+ "html",
+ "markdown"
+ ],
+ "default": "html",
+ "description": "Body content type"
+ },
+ "variables": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Template variables"
+ },
+ "attachments": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Attachment filename"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "description": "Attachment URL"
+ }
+ },
+ "required": [
+ "name",
+ "url"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Email attachments"
+ }
+ },
+ "required": [
+ "id",
+ "subject",
+ "body"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/InAppNotification.json b/packages/spec/json-schema/system/InAppNotification.json
new file mode 100644
index 000000000..314dcd25c
--- /dev/null
+++ b/packages/spec/json-schema/system/InAppNotification.json
@@ -0,0 +1,48 @@
+{
+ "$ref": "#/definitions/InAppNotification",
+ "definitions": {
+ "InAppNotification": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Notification title"
+ },
+ "message": {
+ "type": "string",
+ "description": "Notification message"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "info",
+ "success",
+ "warning",
+ "error"
+ ],
+ "description": "Notification type"
+ },
+ "actionUrl": {
+ "type": "string",
+ "description": "Action URL"
+ },
+ "dismissible": {
+ "type": "boolean",
+ "default": true,
+ "description": "User dismissible"
+ },
+ "expiresAt": {
+ "type": "number",
+ "description": "Expiration timestamp"
+ }
+ },
+ "required": [
+ "title",
+ "message",
+ "type"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/NotificationChannel.json b/packages/spec/json-schema/system/NotificationChannel.json
new file mode 100644
index 000000000..badc6d0ea
--- /dev/null
+++ b/packages/spec/json-schema/system/NotificationChannel.json
@@ -0,0 +1,18 @@
+{
+ "$ref": "#/definitions/NotificationChannel",
+ "definitions": {
+ "NotificationChannel": {
+ "type": "string",
+ "enum": [
+ "email",
+ "sms",
+ "push",
+ "in-app",
+ "slack",
+ "teams",
+ "webhook"
+ ]
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/NotificationConfig.json b/packages/spec/json-schema/system/NotificationConfig.json
new file mode 100644
index 000000000..2fe8a753a
--- /dev/null
+++ b/packages/spec/json-schema/system/NotificationConfig.json
@@ -0,0 +1,343 @@
+{
+ "$ref": "#/definitions/NotificationConfig",
+ "definitions": {
+ "NotificationConfig": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Notification ID"
+ },
+ "name": {
+ "type": "string",
+ "description": "Notification name"
+ },
+ "channel": {
+ "type": "string",
+ "enum": [
+ "email",
+ "sms",
+ "push",
+ "in-app",
+ "slack",
+ "teams",
+ "webhook"
+ ],
+ "description": "Notification channel"
+ },
+ "template": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Template identifier"
+ },
+ "subject": {
+ "type": "string",
+ "description": "Email subject"
+ },
+ "body": {
+ "type": "string",
+ "description": "Email body content"
+ },
+ "bodyType": {
+ "type": "string",
+ "enum": [
+ "text",
+ "html",
+ "markdown"
+ ],
+ "default": "html",
+ "description": "Body content type"
+ },
+ "variables": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Template variables"
+ },
+ "attachments": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Attachment filename"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "description": "Attachment URL"
+ }
+ },
+ "required": [
+ "name",
+ "url"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Email attachments"
+ }
+ },
+ "required": [
+ "id",
+ "subject",
+ "body"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Template identifier"
+ },
+ "message": {
+ "type": "string",
+ "description": "SMS message content"
+ },
+ "maxLength": {
+ "type": "number",
+ "default": 160,
+ "description": "Maximum message length"
+ },
+ "variables": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Template variables"
+ }
+ },
+ "required": [
+ "id",
+ "message"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Notification title"
+ },
+ "body": {
+ "type": "string",
+ "description": "Notification body"
+ },
+ "icon": {
+ "type": "string",
+ "format": "uri",
+ "description": "Notification icon URL"
+ },
+ "badge": {
+ "type": "number",
+ "description": "Badge count"
+ },
+ "data": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Custom data"
+ },
+ "actions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "description": "Action identifier"
+ },
+ "title": {
+ "type": "string",
+ "description": "Action button title"
+ }
+ },
+ "required": [
+ "action",
+ "title"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Notification actions"
+ }
+ },
+ "required": [
+ "title",
+ "body"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Notification title"
+ },
+ "message": {
+ "type": "string",
+ "description": "Notification message"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "info",
+ "success",
+ "warning",
+ "error"
+ ],
+ "description": "Notification type"
+ },
+ "actionUrl": {
+ "type": "string",
+ "description": "Action URL"
+ },
+ "dismissible": {
+ "type": "boolean",
+ "default": true,
+ "description": "User dismissible"
+ },
+ "expiresAt": {
+ "type": "number",
+ "description": "Expiration timestamp"
+ }
+ },
+ "required": [
+ "title",
+ "message",
+ "type"
+ ],
+ "additionalProperties": false
+ }
+ ],
+ "description": "Notification template"
+ },
+ "recipients": {
+ "type": "object",
+ "properties": {
+ "to": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Primary recipients"
+ },
+ "cc": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "CC recipients"
+ },
+ "bcc": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "BCC recipients"
+ }
+ },
+ "required": [
+ "to"
+ ],
+ "additionalProperties": false,
+ "description": "Recipients"
+ },
+ "schedule": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "immediate",
+ "delayed",
+ "scheduled"
+ ],
+ "description": "Schedule type"
+ },
+ "delay": {
+ "type": "number",
+ "description": "Delay in milliseconds"
+ },
+ "scheduledAt": {
+ "type": "number",
+ "description": "Scheduled timestamp"
+ }
+ },
+ "required": [
+ "type"
+ ],
+ "additionalProperties": false,
+ "description": "Scheduling"
+ },
+ "retryPolicy": {
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "default": true,
+ "description": "Enable retries"
+ },
+ "maxRetries": {
+ "type": "number",
+ "default": 3,
+ "description": "Max retry attempts"
+ },
+ "backoffStrategy": {
+ "type": "string",
+ "enum": [
+ "exponential",
+ "linear",
+ "fixed"
+ ],
+ "description": "Backoff strategy"
+ }
+ },
+ "required": [
+ "backoffStrategy"
+ ],
+ "additionalProperties": false,
+ "description": "Retry policy"
+ },
+ "tracking": {
+ "type": "object",
+ "properties": {
+ "trackOpens": {
+ "type": "boolean",
+ "default": false,
+ "description": "Track opens"
+ },
+ "trackClicks": {
+ "type": "boolean",
+ "default": false,
+ "description": "Track clicks"
+ },
+ "trackDelivery": {
+ "type": "boolean",
+ "default": true,
+ "description": "Track delivery"
+ }
+ },
+ "additionalProperties": false,
+ "description": "Tracking configuration"
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "channel",
+ "template",
+ "recipients"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/PushNotification.json b/packages/spec/json-schema/system/PushNotification.json
new file mode 100644
index 000000000..d98ffcabe
--- /dev/null
+++ b/packages/spec/json-schema/system/PushNotification.json
@@ -0,0 +1,60 @@
+{
+ "$ref": "#/definitions/PushNotification",
+ "definitions": {
+ "PushNotification": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Notification title"
+ },
+ "body": {
+ "type": "string",
+ "description": "Notification body"
+ },
+ "icon": {
+ "type": "string",
+ "format": "uri",
+ "description": "Notification icon URL"
+ },
+ "badge": {
+ "type": "number",
+ "description": "Badge count"
+ },
+ "data": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Custom data"
+ },
+ "actions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "description": "Action identifier"
+ },
+ "title": {
+ "type": "string",
+ "description": "Action button title"
+ }
+ },
+ "required": [
+ "action",
+ "title"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Notification actions"
+ }
+ },
+ "required": [
+ "title",
+ "body"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/RollbackPlan.json b/packages/spec/json-schema/system/RollbackPlan.json
new file mode 100644
index 000000000..a8f8d03d0
--- /dev/null
+++ b/packages/spec/json-schema/system/RollbackPlan.json
@@ -0,0 +1,51 @@
+{
+ "$ref": "#/definitions/RollbackPlan",
+ "definitions": {
+ "RollbackPlan": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "Rollback description"
+ },
+ "steps": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "order": {
+ "type": "number",
+ "description": "Step order"
+ },
+ "description": {
+ "type": "string",
+ "description": "Step description"
+ },
+ "estimatedMinutes": {
+ "type": "number",
+ "description": "Estimated duration"
+ }
+ },
+ "required": [
+ "order",
+ "description",
+ "estimatedMinutes"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Rollback steps"
+ },
+ "testProcedure": {
+ "type": "string",
+ "description": "Test procedure"
+ }
+ },
+ "required": [
+ "description",
+ "steps"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/json-schema/system/SMSTemplate.json b/packages/spec/json-schema/system/SMSTemplate.json
new file mode 100644
index 000000000..a1a5ceb17
--- /dev/null
+++ b/packages/spec/json-schema/system/SMSTemplate.json
@@ -0,0 +1,36 @@
+{
+ "$ref": "#/definitions/SMSTemplate",
+ "definitions": {
+ "SMSTemplate": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Template identifier"
+ },
+ "message": {
+ "type": "string",
+ "description": "SMS message content"
+ },
+ "maxLength": {
+ "type": "number",
+ "default": 160,
+ "description": "Maximum message length"
+ },
+ "variables": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Template variables"
+ }
+ },
+ "required": [
+ "id",
+ "message"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
\ No newline at end of file
diff --git a/packages/spec/src/data/document.test.ts b/packages/spec/src/data/document.test.ts
new file mode 100644
index 000000000..893bdd4c7
--- /dev/null
+++ b/packages/spec/src/data/document.test.ts
@@ -0,0 +1,624 @@
+import { describe, it, expect } from 'vitest';
+import {
+ DocumentVersionSchema,
+ DocumentTemplateSchema,
+ ESignatureConfigSchema,
+ DocumentSchema,
+ type Document,
+ type DocumentVersion,
+ type DocumentTemplate,
+ type ESignatureConfig,
+} from './document.zod';
+
+describe('DocumentVersionSchema', () => {
+ it('should validate complete document version', () => {
+ const validVersion: DocumentVersion = {
+ versionNumber: 2,
+ createdAt: 1704067200000,
+ createdBy: 'user_123',
+ size: 2048576,
+ checksum: 'a1b2c3d4e5f6',
+ downloadUrl: 'https://storage.example.com/docs/v2/file.pdf',
+ isLatest: true,
+ };
+
+ expect(() => DocumentVersionSchema.parse(validVersion)).not.toThrow();
+ });
+
+ it('should accept minimal version', () => {
+ const minimalVersion = {
+ versionNumber: 1,
+ createdAt: Date.now(),
+ createdBy: 'user_456',
+ size: 1024,
+ checksum: 'checksum123',
+ downloadUrl: 'https://example.com/file.pdf',
+ };
+
+ expect(() => DocumentVersionSchema.parse(minimalVersion)).not.toThrow();
+ });
+
+ it('should default isLatest to false', () => {
+ const version = {
+ versionNumber: 1,
+ createdAt: Date.now(),
+ createdBy: 'user_123',
+ size: 1024,
+ checksum: 'abc',
+ downloadUrl: 'https://example.com/file.pdf',
+ };
+
+ const parsed = DocumentVersionSchema.parse(version);
+ expect(parsed.isLatest).toBe(false);
+ });
+
+ it('should validate download URL', () => {
+ const invalidVersion = {
+ versionNumber: 1,
+ createdAt: Date.now(),
+ createdBy: 'user_123',
+ size: 1024,
+ checksum: 'abc',
+ downloadUrl: 'not-a-url',
+ };
+
+ expect(() => DocumentVersionSchema.parse(invalidVersion)).toThrow();
+ });
+});
+
+describe('DocumentTemplateSchema', () => {
+ it('should validate complete document template', () => {
+ const validTemplate: DocumentTemplate = {
+ id: 'contract-template',
+ name: 'Service Agreement',
+ description: 'Standard service agreement template',
+ fileUrl: 'https://example.com/templates/contract.docx',
+ fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ placeholders: [
+ {
+ key: 'client_name',
+ label: 'Client Name',
+ type: 'text',
+ required: true,
+ },
+ {
+ key: 'contract_date',
+ label: 'Contract Date',
+ type: 'date',
+ required: true,
+ },
+ {
+ key: 'amount',
+ label: 'Contract Amount',
+ type: 'number',
+ required: false,
+ },
+ ],
+ };
+
+ expect(() => DocumentTemplateSchema.parse(validTemplate)).not.toThrow();
+ });
+
+ it('should accept minimal template', () => {
+ const minimalTemplate = {
+ id: 'simple-template',
+ name: 'Simple Template',
+ fileUrl: 'https://example.com/template.pdf',
+ fileType: 'application/pdf',
+ placeholders: [],
+ };
+
+ expect(() => DocumentTemplateSchema.parse(minimalTemplate)).not.toThrow();
+ });
+
+ it('should default placeholder required to false', () => {
+ const template = {
+ id: 'template-1',
+ name: 'Template',
+ fileUrl: 'https://example.com/template.pdf',
+ fileType: 'application/pdf',
+ placeholders: [
+ {
+ key: 'field1',
+ label: 'Field 1',
+ type: 'text' as const,
+ },
+ ],
+ };
+
+ const parsed = DocumentTemplateSchema.parse(template);
+ expect(parsed.placeholders[0].required).toBe(false);
+ });
+
+ it('should accept all placeholder types', () => {
+ const types = ['text', 'number', 'date', 'image'] as const;
+
+ types.forEach((type) => {
+ const template = {
+ id: `template-${type}`,
+ name: 'Template',
+ fileUrl: 'https://example.com/template.pdf',
+ fileType: 'application/pdf',
+ placeholders: [
+ {
+ key: 'field',
+ label: 'Field',
+ type,
+ },
+ ],
+ };
+
+ expect(() => DocumentTemplateSchema.parse(template)).not.toThrow();
+ });
+ });
+
+ it('should reject invalid placeholder type', () => {
+ const invalidTemplate = {
+ id: 'invalid-template',
+ name: 'Invalid',
+ fileUrl: 'https://example.com/template.pdf',
+ fileType: 'application/pdf',
+ placeholders: [
+ {
+ key: 'field',
+ label: 'Field',
+ type: 'invalid',
+ },
+ ],
+ };
+
+ expect(() => DocumentTemplateSchema.parse(invalidTemplate)).toThrow();
+ });
+});
+
+describe('ESignatureConfigSchema', () => {
+ it('should validate complete e-signature config', () => {
+ const validConfig: ESignatureConfig = {
+ provider: 'docusign',
+ enabled: true,
+ signers: [
+ {
+ email: 'client@example.com',
+ name: 'John Doe',
+ role: 'Client',
+ order: 1,
+ },
+ {
+ email: 'manager@example.com',
+ name: 'Jane Smith',
+ role: 'Manager',
+ order: 2,
+ },
+ ],
+ expirationDays: 30,
+ reminderDays: 7,
+ };
+
+ expect(() => ESignatureConfigSchema.parse(validConfig)).not.toThrow();
+ });
+
+ it('should accept minimal e-signature config', () => {
+ const minimalConfig = {
+ provider: 'hellosign',
+ signers: [
+ {
+ email: 'signer@example.com',
+ name: 'Signer',
+ role: 'Signer',
+ order: 1,
+ },
+ ],
+ };
+
+ expect(() => ESignatureConfigSchema.parse(minimalConfig)).not.toThrow();
+ });
+
+ it('should default enabled to false', () => {
+ const config = {
+ provider: 'adobe-sign',
+ signers: [
+ {
+ email: 'test@example.com',
+ name: 'Test',
+ role: 'Test',
+ order: 1,
+ },
+ ],
+ };
+
+ const parsed = ESignatureConfigSchema.parse(config);
+ expect(parsed.enabled).toBe(false);
+ });
+
+ it('should default expirationDays to 30', () => {
+ const config = {
+ provider: 'custom',
+ signers: [
+ {
+ email: 'test@example.com',
+ name: 'Test',
+ role: 'Test',
+ order: 1,
+ },
+ ],
+ };
+
+ const parsed = ESignatureConfigSchema.parse(config);
+ expect(parsed.expirationDays).toBe(30);
+ });
+
+ it('should default reminderDays to 7', () => {
+ const config = {
+ provider: 'docusign',
+ signers: [
+ {
+ email: 'test@example.com',
+ name: 'Test',
+ role: 'Test',
+ order: 1,
+ },
+ ],
+ };
+
+ const parsed = ESignatureConfigSchema.parse(config);
+ expect(parsed.reminderDays).toBe(7);
+ });
+
+ it('should accept all provider types', () => {
+ const providers = ['docusign', 'adobe-sign', 'hellosign', 'custom'] as const;
+
+ providers.forEach((provider) => {
+ const config = {
+ provider,
+ signers: [
+ {
+ email: 'test@example.com',
+ name: 'Test',
+ role: 'Test',
+ order: 1,
+ },
+ ],
+ };
+
+ expect(() => ESignatureConfigSchema.parse(config)).not.toThrow();
+ });
+ });
+
+ it('should validate signer email addresses', () => {
+ const invalidConfig = {
+ provider: 'docusign',
+ signers: [
+ {
+ email: 'not-an-email',
+ name: 'Test',
+ role: 'Test',
+ order: 1,
+ },
+ ],
+ };
+
+ expect(() => ESignatureConfigSchema.parse(invalidConfig)).toThrow();
+ });
+
+ it('should accept multiple signers in order', () => {
+ const config = {
+ provider: 'docusign',
+ signers: [
+ {
+ email: 'first@example.com',
+ name: 'First Signer',
+ role: 'Client',
+ order: 1,
+ },
+ {
+ email: 'second@example.com',
+ name: 'Second Signer',
+ role: 'Vendor',
+ order: 2,
+ },
+ {
+ email: 'third@example.com',
+ name: 'Third Signer',
+ role: 'Witness',
+ order: 3,
+ },
+ ],
+ };
+
+ expect(() => ESignatureConfigSchema.parse(config)).not.toThrow();
+ });
+});
+
+describe('DocumentSchema', () => {
+ it('should validate complete document', () => {
+ const validDocument: Document = {
+ id: 'doc_123',
+ name: 'Service Agreement 2024',
+ description: 'Annual service agreement',
+ fileType: 'application/pdf',
+ fileSize: 1048576,
+ category: 'contracts',
+ tags: ['legal', '2024', 'services'],
+ versioning: {
+ enabled: true,
+ versions: [
+ {
+ versionNumber: 1,
+ createdAt: 1704067200000,
+ createdBy: 'user_123',
+ size: 1048576,
+ checksum: 'abc123',
+ downloadUrl: 'https://example.com/docs/v1.pdf',
+ isLatest: true,
+ },
+ ],
+ majorVersion: 1,
+ minorVersion: 0,
+ },
+ access: {
+ isPublic: false,
+ sharedWith: ['user_456', 'team_789'],
+ expiresAt: 1735689600000,
+ },
+ metadata: {
+ author: 'John Doe',
+ department: 'Legal',
+ },
+ };
+
+ expect(() => DocumentSchema.parse(validDocument)).not.toThrow();
+ });
+
+ it('should accept minimal document', () => {
+ const minimalDocument = {
+ id: 'doc_456',
+ name: 'Simple Document',
+ fileType: 'application/pdf',
+ fileSize: 1024,
+ };
+
+ expect(() => DocumentSchema.parse(minimalDocument)).not.toThrow();
+ });
+
+ it('should validate document with template', () => {
+ const documentWithTemplate = {
+ id: 'doc_789',
+ name: 'Generated Contract',
+ fileType: 'application/pdf',
+ fileSize: 2048,
+ template: {
+ id: 'contract-template',
+ name: 'Contract Template',
+ fileUrl: 'https://example.com/template.docx',
+ fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ placeholders: [
+ {
+ key: 'client_name',
+ label: 'Client Name',
+ type: 'text' as const,
+ required: true,
+ },
+ ],
+ },
+ };
+
+ expect(() => DocumentSchema.parse(documentWithTemplate)).not.toThrow();
+ });
+
+ it('should validate document with e-signature', () => {
+ const documentWithSignature = {
+ id: 'doc_101',
+ name: 'Contract for Signature',
+ fileType: 'application/pdf',
+ fileSize: 1536,
+ eSignature: {
+ provider: 'docusign' as const,
+ enabled: true,
+ signers: [
+ {
+ email: 'client@example.com',
+ name: 'Client',
+ role: 'Client',
+ order: 1,
+ },
+ ],
+ expirationDays: 15,
+ reminderDays: 3,
+ },
+ };
+
+ expect(() => DocumentSchema.parse(documentWithSignature)).not.toThrow();
+ });
+
+ it('should validate versioning configuration', () => {
+ const documentWithVersions = {
+ id: 'doc_202',
+ name: 'Versioned Document',
+ fileType: 'application/pdf',
+ fileSize: 2048,
+ versioning: {
+ enabled: true,
+ versions: [
+ {
+ versionNumber: 1,
+ createdAt: 1704000000000,
+ createdBy: 'user_123',
+ size: 1024,
+ checksum: 'v1-checksum',
+ downloadUrl: 'https://example.com/v1.pdf',
+ isLatest: false,
+ },
+ {
+ versionNumber: 2,
+ createdAt: 1704067200000,
+ createdBy: 'user_456',
+ size: 2048,
+ checksum: 'v2-checksum',
+ downloadUrl: 'https://example.com/v2.pdf',
+ isLatest: true,
+ },
+ ],
+ majorVersion: 2,
+ minorVersion: 0,
+ },
+ };
+
+ expect(() => DocumentSchema.parse(documentWithVersions)).not.toThrow();
+ });
+
+ it('should default access.isPublic to false', () => {
+ const document = {
+ id: 'doc_303',
+ name: 'Document',
+ fileType: 'application/pdf',
+ fileSize: 1024,
+ access: {
+ sharedWith: ['user_123'],
+ },
+ };
+
+ const parsed = DocumentSchema.parse(document);
+ expect(parsed.access?.isPublic).toBe(false);
+ });
+
+ it('should validate document with tags and category', () => {
+ const document = {
+ id: 'doc_404',
+ name: 'Tagged Document',
+ fileType: 'application/pdf',
+ fileSize: 1024,
+ category: 'invoices',
+ tags: ['2024', 'Q1', 'paid'],
+ };
+
+ expect(() => DocumentSchema.parse(document)).not.toThrow();
+ });
+
+ it('should validate document with custom metadata', () => {
+ const document = {
+ id: 'doc_505',
+ name: 'Document with Metadata',
+ fileType: 'application/pdf',
+ fileSize: 1024,
+ metadata: {
+ author: 'Jane Doe',
+ department: 'Finance',
+ projectCode: 'PROJ-2024-001',
+ customField: 'Custom Value',
+ },
+ };
+
+ expect(() => DocumentSchema.parse(document)).not.toThrow();
+ });
+
+ it('should validate complete document with all features', () => {
+ const completeDocument: Document = {
+ id: 'doc_complete',
+ name: 'Complete Document Example',
+ description: 'A fully-featured document with all options',
+ fileType: 'application/pdf',
+ fileSize: 5242880,
+ category: 'legal-contracts',
+ tags: ['important', 'signed', '2024', 'annual'],
+ versioning: {
+ enabled: true,
+ versions: [
+ {
+ versionNumber: 1,
+ createdAt: 1704000000000,
+ createdBy: 'user_001',
+ size: 5000000,
+ checksum: 'checksum-v1',
+ downloadUrl: 'https://storage.example.com/docs/complete-v1.pdf',
+ isLatest: false,
+ },
+ {
+ versionNumber: 2,
+ createdAt: 1704067200000,
+ createdBy: 'user_002',
+ size: 5242880,
+ checksum: 'checksum-v2',
+ downloadUrl: 'https://storage.example.com/docs/complete-v2.pdf',
+ isLatest: true,
+ },
+ ],
+ majorVersion: 2,
+ minorVersion: 0,
+ },
+ template: {
+ id: 'annual-contract-template',
+ name: 'Annual Contract Template',
+ description: 'Standard annual contract',
+ fileUrl: 'https://example.com/templates/annual-contract.docx',
+ fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ placeholders: [
+ {
+ key: 'company_name',
+ label: 'Company Name',
+ type: 'text',
+ required: true,
+ },
+ {
+ key: 'contract_value',
+ label: 'Contract Value',
+ type: 'number',
+ required: true,
+ },
+ {
+ key: 'start_date',
+ label: 'Start Date',
+ type: 'date',
+ required: true,
+ },
+ {
+ key: 'company_logo',
+ label: 'Company Logo',
+ type: 'image',
+ required: false,
+ },
+ ],
+ },
+ eSignature: {
+ provider: 'docusign',
+ enabled: true,
+ signers: [
+ {
+ email: 'client@company.com',
+ name: 'John Client',
+ role: 'Client Representative',
+ order: 1,
+ },
+ {
+ email: 'vendor@example.com',
+ name: 'Jane Vendor',
+ role: 'Vendor Representative',
+ order: 2,
+ },
+ {
+ email: 'legal@example.com',
+ name: 'Legal Counsel',
+ role: 'Legal Reviewer',
+ order: 3,
+ },
+ ],
+ expirationDays: 45,
+ reminderDays: 5,
+ },
+ access: {
+ isPublic: false,
+ sharedWith: ['user_001', 'user_002', 'team_legal', 'team_finance'],
+ expiresAt: 1767225600000, // Future date
+ },
+ metadata: {
+ author: 'Legal Department',
+ department: 'Legal',
+ projectCode: 'PROJ-2024-ANNUAL',
+ confidentialityLevel: 'High',
+ retentionYears: 7,
+ complianceStandards: ['SOX', 'GDPR'],
+ },
+ };
+
+ expect(() => DocumentSchema.parse(completeDocument)).not.toThrow();
+ });
+});
diff --git a/packages/spec/src/data/document.zod.ts b/packages/spec/src/data/document.zod.ts
new file mode 100644
index 000000000..62381e880
--- /dev/null
+++ b/packages/spec/src/data/document.zod.ts
@@ -0,0 +1,370 @@
+import { z } from 'zod';
+
+/**
+ * Document Version Schema
+ *
+ * Represents a single version of a document in a version-controlled system.
+ * Each version is immutable and maintains its own metadata and download URL.
+ *
+ * @example
+ * ```json
+ * {
+ * "versionNumber": 2,
+ * "createdAt": 1704067200000,
+ * "createdBy": "user_123",
+ * "size": 2048576,
+ * "checksum": "a1b2c3d4e5f6",
+ * "downloadUrl": "https://storage.example.com/docs/v2/file.pdf",
+ * "isLatest": true
+ * }
+ * ```
+ */
+export const DocumentVersionSchema = z.object({
+ /**
+ * Sequential version number (increments with each new version)
+ */
+ versionNumber: z.number().describe('Version number'),
+
+ /**
+ * Timestamp when this version was created (Unix milliseconds)
+ */
+ createdAt: z.number().describe('Creation timestamp'),
+
+ /**
+ * User ID who created this version
+ */
+ createdBy: z.string().describe('Creator user ID'),
+
+ /**
+ * File size in bytes
+ */
+ size: z.number().describe('File size in bytes'),
+
+ /**
+ * Checksum/hash of the file content (for integrity verification)
+ */
+ checksum: z.string().describe('File checksum'),
+
+ /**
+ * URL to download this specific version
+ */
+ downloadUrl: z.string().url().describe('Download URL'),
+
+ /**
+ * Whether this is the latest version
+ * @default false
+ */
+ isLatest: z.boolean().optional().default(false).describe('Is latest version'),
+});
+
+/**
+ * Document Template Schema
+ *
+ * Defines a reusable document template with dynamic placeholders.
+ * Templates can be used to generate documents with variable content.
+ *
+ * @example
+ * ```json
+ * {
+ * "id": "contract-template",
+ * "name": "Service Agreement",
+ * "description": "Standard service agreement template",
+ * "fileUrl": "https://example.com/templates/contract.docx",
+ * "fileType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ * "placeholders": [
+ * {
+ * "key": "client_name",
+ * "label": "Client Name",
+ * "type": "text",
+ * "required": true
+ * },
+ * {
+ * "key": "contract_date",
+ * "label": "Contract Date",
+ * "type": "date",
+ * "required": true
+ * }
+ * ]
+ * }
+ * ```
+ */
+export const DocumentTemplateSchema = z.object({
+ /**
+ * Unique identifier for the template
+ */
+ id: z.string().describe('Template ID'),
+
+ /**
+ * Human-readable name of the template
+ */
+ name: z.string().describe('Template name'),
+
+ /**
+ * Optional description of the template's purpose
+ */
+ description: z.string().optional().describe('Template description'),
+
+ /**
+ * URL to the template file
+ */
+ fileUrl: z.string().url().describe('Template file URL'),
+
+ /**
+ * MIME type of the template file
+ */
+ fileType: z.string().describe('File MIME type'),
+
+ /**
+ * List of dynamic placeholders in the template
+ */
+ placeholders: z.array(z.object({
+ /**
+ * Placeholder identifier (used in template)
+ */
+ key: z.string().describe('Placeholder key'),
+
+ /**
+ * Human-readable label for the placeholder
+ */
+ label: z.string().describe('Placeholder label'),
+
+ /**
+ * Data type of the placeholder value
+ */
+ type: z.enum(['text', 'number', 'date', 'image']).describe('Placeholder type'),
+
+ /**
+ * Whether this placeholder must be filled
+ * @default false
+ */
+ required: z.boolean().optional().default(false).describe('Is required'),
+ })).describe('Template placeholders'),
+});
+
+/**
+ * E-Signature Configuration Schema
+ *
+ * Configuration for electronic signature workflows.
+ * Supports integration with popular e-signature providers.
+ *
+ * @example
+ * ```json
+ * {
+ * "provider": "docusign",
+ * "enabled": true,
+ * "signers": [
+ * {
+ * "email": "client@example.com",
+ * "name": "John Doe",
+ * "role": "Client",
+ * "order": 1
+ * },
+ * {
+ * "email": "manager@example.com",
+ * "name": "Jane Smith",
+ * "role": "Manager",
+ * "order": 2
+ * }
+ * ],
+ * "expirationDays": 30,
+ * "reminderDays": 7
+ * }
+ * ```
+ */
+export const ESignatureConfigSchema = z.object({
+ /**
+ * E-signature service provider
+ */
+ provider: z.enum(['docusign', 'adobe-sign', 'hellosign', 'custom']).describe('E-signature provider'),
+
+ /**
+ * Whether e-signature is enabled for this document
+ * @default false
+ */
+ enabled: z.boolean().optional().default(false).describe('E-signature enabled'),
+
+ /**
+ * List of signers in signing order
+ */
+ signers: z.array(z.object({
+ /**
+ * Signer's email address
+ */
+ email: z.string().email().describe('Signer email'),
+
+ /**
+ * Signer's full name
+ */
+ name: z.string().describe('Signer name'),
+
+ /**
+ * Signer's role in the document
+ */
+ role: z.string().describe('Signer role'),
+
+ /**
+ * Signing order (lower numbers sign first)
+ */
+ order: z.number().describe('Signing order'),
+ })).describe('Document signers'),
+
+ /**
+ * Days until signature request expires
+ * @default 30
+ */
+ expirationDays: z.number().optional().default(30).describe('Expiration days'),
+
+ /**
+ * Days between reminder emails
+ * @default 7
+ */
+ reminderDays: z.number().optional().default(7).describe('Reminder interval days'),
+});
+
+/**
+ * Document Schema
+ *
+ * Comprehensive document management protocol supporting versioning,
+ * templates, e-signatures, and access control.
+ *
+ * @example
+ * ```json
+ * {
+ * "id": "doc_123",
+ * "name": "Service Agreement 2024",
+ * "description": "Annual service agreement",
+ * "fileType": "application/pdf",
+ * "fileSize": 1048576,
+ * "category": "contracts",
+ * "tags": ["legal", "2024", "services"],
+ * "versioning": {
+ * "enabled": true,
+ * "versions": [
+ * {
+ * "versionNumber": 1,
+ * "createdAt": 1704067200000,
+ * "createdBy": "user_123",
+ * "size": 1048576,
+ * "checksum": "abc123",
+ * "downloadUrl": "https://example.com/docs/v1.pdf",
+ * "isLatest": true
+ * }
+ * ],
+ * "majorVersion": 1,
+ * "minorVersion": 0
+ * },
+ * "access": {
+ * "isPublic": false,
+ * "sharedWith": ["user_456", "team_789"],
+ * "expiresAt": 1735689600000
+ * },
+ * "metadata": {
+ * "author": "John Doe",
+ * "department": "Legal"
+ * }
+ * }
+ * ```
+ */
+export const DocumentSchema = z.object({
+ /**
+ * Unique document identifier
+ */
+ id: z.string().describe('Document ID'),
+
+ /**
+ * Document name
+ */
+ name: z.string().describe('Document name'),
+
+ /**
+ * Optional document description
+ */
+ description: z.string().optional().describe('Document description'),
+
+ /**
+ * MIME type of the document
+ */
+ fileType: z.string().describe('File MIME type'),
+
+ /**
+ * File size in bytes
+ */
+ fileSize: z.number().describe('File size in bytes'),
+
+ /**
+ * Document category for organization
+ */
+ category: z.string().optional().describe('Document category'),
+
+ /**
+ * Tags for searchability and organization
+ */
+ tags: z.array(z.string()).optional().describe('Document tags'),
+
+ /**
+ * Version control configuration
+ */
+ versioning: z.object({
+ /**
+ * Whether versioning is enabled
+ */
+ enabled: z.boolean().describe('Versioning enabled'),
+
+ /**
+ * List of all document versions
+ */
+ versions: z.array(DocumentVersionSchema).describe('Version history'),
+
+ /**
+ * Current major version number
+ */
+ majorVersion: z.number().describe('Major version'),
+
+ /**
+ * Current minor version number
+ */
+ minorVersion: z.number().describe('Minor version'),
+ }).optional().describe('Version control'),
+
+ /**
+ * Template configuration (if document is generated from template)
+ */
+ template: DocumentTemplateSchema.optional().describe('Document template'),
+
+ /**
+ * E-signature configuration
+ */
+ eSignature: ESignatureConfigSchema.optional().describe('E-signature config'),
+
+ /**
+ * Access control settings
+ */
+ access: z.object({
+ /**
+ * Whether document is publicly accessible
+ * @default false
+ */
+ isPublic: z.boolean().optional().default(false).describe('Public access'),
+
+ /**
+ * List of user/team IDs with access
+ */
+ sharedWith: z.array(z.string()).optional().describe('Shared with'),
+
+ /**
+ * Timestamp when access expires (Unix milliseconds)
+ */
+ expiresAt: z.number().optional().describe('Access expiration'),
+ }).optional().describe('Access control'),
+
+ /**
+ * Custom metadata fields
+ */
+ metadata: z.record(z.any()).optional().describe('Custom metadata'),
+});
+
+// Type exports
+export type Document = z.infer;
+export type DocumentVersion = z.infer;
+export type DocumentTemplate = z.infer;
+export type ESignatureConfig = z.infer;
diff --git a/packages/spec/src/data/external-lookup.test.ts b/packages/spec/src/data/external-lookup.test.ts
new file mode 100644
index 000000000..dcac3d193
--- /dev/null
+++ b/packages/spec/src/data/external-lookup.test.ts
@@ -0,0 +1,664 @@
+import { describe, it, expect } from 'vitest';
+import {
+ ExternalDataSourceSchema,
+ FieldMappingSchema,
+ ExternalLookupSchema,
+ type ExternalLookup,
+ type ExternalDataSource,
+ type FieldMapping,
+} from './external-lookup.zod';
+
+describe('ExternalDataSourceSchema', () => {
+ it('should validate complete external data source', () => {
+ const validSource: ExternalDataSource = {
+ id: 'salesforce-accounts',
+ name: 'Salesforce Account Data',
+ type: 'rest-api',
+ endpoint: 'https://api.salesforce.com/services/data/v58.0',
+ authentication: {
+ type: 'oauth2',
+ config: {
+ clientId: 'client_123',
+ clientSecret: 'secret_456',
+ tokenUrl: 'https://login.salesforce.com/services/oauth2/token',
+ },
+ },
+ };
+
+ expect(() => ExternalDataSourceSchema.parse(validSource)).not.toThrow();
+ });
+
+ it('should accept all data source types', () => {
+ const types = ['odata', 'rest-api', 'graphql', 'custom'] as const;
+
+ types.forEach((type) => {
+ const source = {
+ id: `source-${type}`,
+ name: `${type} Source`,
+ type,
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'none' as const,
+ config: {},
+ },
+ };
+
+ expect(() => ExternalDataSourceSchema.parse(source)).not.toThrow();
+ });
+ });
+
+ it('should accept all authentication types', () => {
+ const authTypes = ['oauth2', 'api-key', 'basic', 'none'] as const;
+
+ authTypes.forEach((authType) => {
+ const source = {
+ id: `auth-${authType}`,
+ name: 'Test Source',
+ type: 'rest-api' as const,
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: authType,
+ config: {},
+ },
+ };
+
+ expect(() => ExternalDataSourceSchema.parse(source)).not.toThrow();
+ });
+ });
+
+ it('should validate API key authentication', () => {
+ const source = {
+ id: 'api-key-source',
+ name: 'API Key Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'api-key',
+ config: {
+ apiKey: 'sk-1234567890',
+ headerName: 'X-API-Key',
+ },
+ },
+ };
+
+ expect(() => ExternalDataSourceSchema.parse(source)).not.toThrow();
+ });
+
+ it('should validate basic authentication', () => {
+ const source = {
+ id: 'basic-auth-source',
+ name: 'Basic Auth Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'basic',
+ config: {
+ username: 'user',
+ password: 'pass',
+ },
+ },
+ };
+
+ expect(() => ExternalDataSourceSchema.parse(source)).not.toThrow();
+ });
+
+ it('should reject invalid endpoint URL', () => {
+ const invalidSource = {
+ id: 'invalid-source',
+ name: 'Invalid Source',
+ type: 'rest-api',
+ endpoint: 'not-a-url',
+ authentication: {
+ type: 'none',
+ config: {},
+ },
+ };
+
+ expect(() => ExternalDataSourceSchema.parse(invalidSource)).toThrow();
+ });
+});
+
+describe('FieldMappingSchema', () => {
+ it('should validate complete field mapping', () => {
+ const validMapping: FieldMapping = {
+ externalField: 'AccountName',
+ localField: 'name',
+ type: 'text',
+ readonly: true,
+ };
+
+ expect(() => FieldMappingSchema.parse(validMapping)).not.toThrow();
+ });
+
+ it('should accept minimal field mapping', () => {
+ const minimalMapping = {
+ externalField: 'ExternalField',
+ localField: 'local_field',
+ type: 'text',
+ };
+
+ expect(() => FieldMappingSchema.parse(minimalMapping)).not.toThrow();
+ });
+
+ it('should default readonly to true', () => {
+ const mapping = {
+ externalField: 'Field1',
+ localField: 'field_1',
+ type: 'text',
+ };
+
+ const parsed = FieldMappingSchema.parse(mapping);
+ expect(parsed.readonly).toBe(true);
+ });
+
+ it('should accept writable field mapping', () => {
+ const writableMapping = {
+ externalField: 'Status',
+ localField: 'status',
+ type: 'text',
+ readonly: false,
+ };
+
+ expect(() => FieldMappingSchema.parse(writableMapping)).not.toThrow();
+ });
+
+ it('should accept various field types', () => {
+ const types = ['text', 'number', 'boolean', 'date', 'datetime', 'lookup'];
+
+ types.forEach((type) => {
+ const mapping = {
+ externalField: 'Field',
+ localField: 'field',
+ type,
+ };
+
+ expect(() => FieldMappingSchema.parse(mapping)).not.toThrow();
+ });
+ });
+});
+
+describe('ExternalLookupSchema', () => {
+ it('should validate complete external lookup', () => {
+ const validLookup: ExternalLookup = {
+ fieldName: 'external_account',
+ dataSource: {
+ id: 'salesforce-api',
+ name: 'Salesforce',
+ type: 'rest-api',
+ endpoint: 'https://api.salesforce.com/services/data/v58.0',
+ authentication: {
+ type: 'oauth2',
+ config: { clientId: 'client_123' },
+ },
+ },
+ query: {
+ endpoint: '/sobjects/Account',
+ method: 'GET',
+ parameters: { limit: 100 },
+ },
+ fieldMappings: [
+ {
+ externalField: 'Name',
+ localField: 'account_name',
+ type: 'text',
+ readonly: true,
+ },
+ {
+ externalField: 'Industry',
+ localField: 'industry',
+ type: 'text',
+ readonly: true,
+ },
+ ],
+ caching: {
+ enabled: true,
+ ttl: 300,
+ strategy: 'ttl',
+ },
+ fallback: {
+ enabled: true,
+ showError: true,
+ },
+ rateLimit: {
+ requestsPerSecond: 10,
+ burstSize: 20,
+ },
+ };
+
+ expect(() => ExternalLookupSchema.parse(validLookup)).not.toThrow();
+ });
+
+ it('should accept minimal external lookup', () => {
+ const minimalLookup = {
+ fieldName: 'external_field',
+ dataSource: {
+ id: 'source-1',
+ name: 'Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'none',
+ config: {},
+ },
+ },
+ query: {
+ endpoint: '/data',
+ },
+ fieldMappings: [
+ {
+ externalField: 'Field1',
+ localField: 'field_1',
+ type: 'text',
+ },
+ ],
+ };
+
+ expect(() => ExternalLookupSchema.parse(minimalLookup)).not.toThrow();
+ });
+
+ it('should default query method to GET', () => {
+ const lookup = {
+ fieldName: 'test_field',
+ dataSource: {
+ id: 'source-1',
+ name: 'Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'none',
+ config: {},
+ },
+ },
+ query: {
+ endpoint: '/data',
+ },
+ fieldMappings: [],
+ };
+
+ const parsed = ExternalLookupSchema.parse(lookup);
+ expect(parsed.query.method).toBe('GET');
+ });
+
+ it('should accept POST query method', () => {
+ const lookup = {
+ fieldName: 'test_field',
+ dataSource: {
+ id: 'source-1',
+ name: 'Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'none',
+ config: {},
+ },
+ },
+ query: {
+ endpoint: '/search',
+ method: 'POST' as const,
+ parameters: {
+ query: 'search term',
+ },
+ },
+ fieldMappings: [],
+ };
+
+ expect(() => ExternalLookupSchema.parse(lookup)).not.toThrow();
+ });
+
+ it('should default caching to enabled with 300s TTL', () => {
+ const lookup = {
+ fieldName: 'test_field',
+ dataSource: {
+ id: 'source-1',
+ name: 'Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'none',
+ config: {},
+ },
+ },
+ query: {
+ endpoint: '/data',
+ },
+ fieldMappings: [],
+ caching: {},
+ };
+
+ const parsed = ExternalLookupSchema.parse(lookup);
+ expect(parsed.caching?.enabled).toBe(true);
+ expect(parsed.caching?.ttl).toBe(300);
+ expect(parsed.caching?.strategy).toBe('ttl');
+ });
+
+ it('should accept all cache strategies', () => {
+ const strategies = ['lru', 'lfu', 'ttl'] as const;
+
+ strategies.forEach((strategy) => {
+ const lookup = {
+ fieldName: 'test_field',
+ dataSource: {
+ id: 'source-1',
+ name: 'Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'none',
+ config: {},
+ },
+ },
+ query: {
+ endpoint: '/data',
+ },
+ fieldMappings: [],
+ caching: {
+ strategy,
+ },
+ };
+
+ expect(() => ExternalLookupSchema.parse(lookup)).not.toThrow();
+ });
+ });
+
+ it('should validate custom cache TTL', () => {
+ const lookup = {
+ fieldName: 'test_field',
+ dataSource: {
+ id: 'source-1',
+ name: 'Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'none',
+ config: {},
+ },
+ },
+ query: {
+ endpoint: '/data',
+ },
+ fieldMappings: [],
+ caching: {
+ enabled: true,
+ ttl: 600,
+ strategy: 'ttl' as const,
+ },
+ };
+
+ const parsed = ExternalLookupSchema.parse(lookup);
+ expect(parsed.caching?.ttl).toBe(600);
+ });
+
+ it('should default fallback to enabled with showError true', () => {
+ const lookup = {
+ fieldName: 'test_field',
+ dataSource: {
+ id: 'source-1',
+ name: 'Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'none',
+ config: {},
+ },
+ },
+ query: {
+ endpoint: '/data',
+ },
+ fieldMappings: [],
+ fallback: {},
+ };
+
+ const parsed = ExternalLookupSchema.parse(lookup);
+ expect(parsed.fallback?.enabled).toBe(true);
+ expect(parsed.fallback?.showError).toBe(true);
+ });
+
+ it('should accept custom fallback value', () => {
+ const lookup = {
+ fieldName: 'test_field',
+ dataSource: {
+ id: 'source-1',
+ name: 'Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'none',
+ config: {},
+ },
+ },
+ query: {
+ endpoint: '/data',
+ },
+ fieldMappings: [],
+ fallback: {
+ enabled: true,
+ defaultValue: 'N/A',
+ showError: false,
+ },
+ };
+
+ const parsed = ExternalLookupSchema.parse(lookup);
+ expect(parsed.fallback?.defaultValue).toBe('N/A');
+ expect(parsed.fallback?.showError).toBe(false);
+ });
+
+ it('should validate rate limiting', () => {
+ const lookup = {
+ fieldName: 'test_field',
+ dataSource: {
+ id: 'source-1',
+ name: 'Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'none',
+ config: {},
+ },
+ },
+ query: {
+ endpoint: '/data',
+ },
+ fieldMappings: [],
+ rateLimit: {
+ requestsPerSecond: 5,
+ burstSize: 10,
+ },
+ };
+
+ expect(() => ExternalLookupSchema.parse(lookup)).not.toThrow();
+ });
+
+ it('should accept rate limit without burst size', () => {
+ const lookup = {
+ fieldName: 'test_field',
+ dataSource: {
+ id: 'source-1',
+ name: 'Source',
+ type: 'rest-api',
+ endpoint: 'https://api.example.com',
+ authentication: {
+ type: 'none',
+ config: {},
+ },
+ },
+ query: {
+ endpoint: '/data',
+ },
+ fieldMappings: [],
+ rateLimit: {
+ requestsPerSecond: 10,
+ },
+ };
+
+ expect(() => ExternalLookupSchema.parse(lookup)).not.toThrow();
+ });
+
+ it('should validate OData external lookup', () => {
+ const odataLookup = {
+ fieldName: 'odata_products',
+ dataSource: {
+ id: 'odata-service',
+ name: 'OData Product Service',
+ type: 'odata' as const,
+ endpoint: 'https://services.odata.org/V4/Northwind/Northwind.svc',
+ authentication: {
+ type: 'none' as const,
+ config: {},
+ },
+ },
+ query: {
+ endpoint: '/Products',
+ method: 'GET' as const,
+ parameters: {
+ $filter: "ProductName eq 'Chai'",
+ $select: 'ProductID,ProductName,UnitPrice',
+ },
+ },
+ fieldMappings: [
+ {
+ externalField: 'ProductID',
+ localField: 'product_id',
+ type: 'number',
+ readonly: true,
+ },
+ {
+ externalField: 'ProductName',
+ localField: 'product_name',
+ type: 'text',
+ readonly: true,
+ },
+ {
+ externalField: 'UnitPrice',
+ localField: 'unit_price',
+ type: 'currency',
+ readonly: true,
+ },
+ ],
+ };
+
+ expect(() => ExternalLookupSchema.parse(odataLookup)).not.toThrow();
+ });
+
+ it('should validate GraphQL external lookup', () => {
+ const graphqlLookup = {
+ fieldName: 'graphql_users',
+ dataSource: {
+ id: 'graphql-api',
+ name: 'GraphQL API',
+ type: 'graphql' as const,
+ endpoint: 'https://api.example.com/graphql',
+ authentication: {
+ type: 'api-key' as const,
+ config: {
+ apiKey: 'key_123',
+ headerName: 'Authorization',
+ },
+ },
+ },
+ query: {
+ endpoint: '',
+ method: 'POST' as const,
+ parameters: {
+ query: '{ users { id name email } }',
+ },
+ },
+ fieldMappings: [
+ {
+ externalField: 'id',
+ localField: 'user_id',
+ type: 'text',
+ readonly: true,
+ },
+ {
+ externalField: 'name',
+ localField: 'user_name',
+ type: 'text',
+ readonly: true,
+ },
+ {
+ externalField: 'email',
+ localField: 'user_email',
+ type: 'email',
+ readonly: true,
+ },
+ ],
+ caching: {
+ enabled: true,
+ ttl: 180,
+ strategy: 'lru' as const,
+ },
+ };
+
+ expect(() => ExternalLookupSchema.parse(graphqlLookup)).not.toThrow();
+ });
+
+ it('should validate complete Salesforce-like external lookup', () => {
+ const salesforceLookup: ExternalLookup = {
+ fieldName: 'salesforce_contacts',
+ dataSource: {
+ id: 'salesforce-prod',
+ name: 'Salesforce Production',
+ type: 'rest-api',
+ endpoint: 'https://na1.salesforce.com/services/data/v58.0',
+ authentication: {
+ type: 'oauth2',
+ config: {
+ clientId: 'client_id',
+ clientSecret: 'client_secret',
+ tokenUrl: 'https://login.salesforce.com/services/oauth2/token',
+ scope: 'api',
+ },
+ },
+ },
+ query: {
+ endpoint: '/query',
+ method: 'GET',
+ parameters: {
+ q: 'SELECT Id, Name, Email, Phone FROM Contact WHERE IsActive = true LIMIT 1000',
+ },
+ },
+ fieldMappings: [
+ {
+ externalField: 'Id',
+ localField: 'salesforce_id',
+ type: 'text',
+ readonly: true,
+ },
+ {
+ externalField: 'Name',
+ localField: 'contact_name',
+ type: 'text',
+ readonly: true,
+ },
+ {
+ externalField: 'Email',
+ localField: 'email',
+ type: 'email',
+ readonly: true,
+ },
+ {
+ externalField: 'Phone',
+ localField: 'phone',
+ type: 'phone',
+ readonly: true,
+ },
+ ],
+ caching: {
+ enabled: true,
+ ttl: 600,
+ strategy: 'ttl',
+ },
+ fallback: {
+ enabled: true,
+ defaultValue: null,
+ showError: true,
+ },
+ rateLimit: {
+ requestsPerSecond: 5,
+ burstSize: 15,
+ },
+ };
+
+ expect(() => ExternalLookupSchema.parse(salesforceLookup)).not.toThrow();
+ });
+});
diff --git a/packages/spec/src/data/external-lookup.zod.ts b/packages/spec/src/data/external-lookup.zod.ts
new file mode 100644
index 000000000..68557d3bf
--- /dev/null
+++ b/packages/spec/src/data/external-lookup.zod.ts
@@ -0,0 +1,255 @@
+import { z } from 'zod';
+
+/**
+ * External Data Source Schema
+ *
+ * Configuration for connecting to external data systems.
+ * Similar to Salesforce External Objects for real-time data integration.
+ *
+ * @example
+ * ```json
+ * {
+ * "id": "salesforce-accounts",
+ * "name": "Salesforce Account Data",
+ * "type": "rest-api",
+ * "endpoint": "https://api.salesforce.com/services/data/v58.0",
+ * "authentication": {
+ * "type": "oauth2",
+ * "config": {
+ * "clientId": "...",
+ * "clientSecret": "...",
+ * "tokenUrl": "https://login.salesforce.com/services/oauth2/token"
+ * }
+ * }
+ * }
+ * ```
+ */
+export const ExternalDataSourceSchema = z.object({
+ /**
+ * Unique identifier for the external data source
+ */
+ id: z.string().describe('Data source ID'),
+
+ /**
+ * Human-readable name of the data source
+ */
+ name: z.string().describe('Data source name'),
+
+ /**
+ * Protocol type for connecting to the data source
+ */
+ type: z.enum(['odata', 'rest-api', 'graphql', 'custom']).describe('Protocol type'),
+
+ /**
+ * Base URL endpoint for the external system
+ */
+ endpoint: z.string().url().describe('API endpoint URL'),
+
+ /**
+ * Authentication configuration
+ */
+ authentication: z.object({
+ /**
+ * Authentication method
+ */
+ type: z.enum(['oauth2', 'api-key', 'basic', 'none']).describe('Auth type'),
+
+ /**
+ * Authentication-specific configuration
+ * Structure varies based on auth type
+ */
+ config: z.record(z.any()).describe('Auth configuration'),
+ }).describe('Authentication'),
+});
+
+/**
+ * Field Mapping Schema
+ *
+ * Maps external system fields to local object fields.
+ * Defines data type and read/write permissions.
+ *
+ * @example
+ * ```json
+ * {
+ * "externalField": "AccountName",
+ * "localField": "name",
+ * "type": "text",
+ * "readonly": true
+ * }
+ * ```
+ */
+export const FieldMappingSchema = z.object({
+ /**
+ * Field name in the external system
+ */
+ externalField: z.string().describe('External field name'),
+
+ /**
+ * Corresponding local field name (snake_case)
+ */
+ localField: z.string().describe('Local field name'),
+
+ /**
+ * Data type of the field
+ */
+ type: z.string().describe('Field type'),
+
+ /**
+ * Whether the field is read-only
+ * @default true
+ */
+ readonly: z.boolean().optional().default(true).describe('Read-only field'),
+});
+
+/**
+ * External Lookup Schema
+ *
+ * Real-time data lookup protocol for external systems.
+ * Enables querying external data sources without replication.
+ * Inspired by Salesforce External Objects and OData protocols.
+ *
+ * @example
+ * ```json
+ * {
+ * "fieldName": "external_account",
+ * "dataSource": {
+ * "id": "salesforce-api",
+ * "name": "Salesforce",
+ * "type": "rest-api",
+ * "endpoint": "https://api.salesforce.com/services/data/v58.0",
+ * "authentication": {
+ * "type": "oauth2",
+ * "config": {"clientId": "..."}
+ * }
+ * },
+ * "query": {
+ * "endpoint": "/sobjects/Account",
+ * "method": "GET",
+ * "parameters": {"limit": 100}
+ * },
+ * "fieldMappings": [
+ * {
+ * "externalField": "Name",
+ * "localField": "account_name",
+ * "type": "text",
+ * "readonly": true
+ * }
+ * ],
+ * "caching": {
+ * "enabled": true,
+ * "ttl": 300,
+ * "strategy": "ttl"
+ * },
+ * "fallback": {
+ * "enabled": true,
+ * "showError": true
+ * },
+ * "rateLimit": {
+ * "requestsPerSecond": 10,
+ * "burstSize": 20
+ * }
+ * }
+ * ```
+ */
+export const ExternalLookupSchema = z.object({
+ /**
+ * Name of the field that uses external lookup
+ */
+ fieldName: z.string().describe('Field name'),
+
+ /**
+ * External data source configuration
+ */
+ dataSource: ExternalDataSourceSchema.describe('External data source'),
+
+ /**
+ * Query configuration for fetching external data
+ */
+ query: z.object({
+ /**
+ * API endpoint path (relative to base endpoint)
+ */
+ endpoint: z.string().describe('Query endpoint path'),
+
+ /**
+ * HTTP method for the query
+ * @default 'GET'
+ */
+ method: z.enum(['GET', 'POST']).optional().default('GET').describe('HTTP method'),
+
+ /**
+ * Query parameters or request body
+ */
+ parameters: z.record(z.any()).optional().describe('Query parameters'),
+ }).describe('Query configuration'),
+
+ /**
+ * Mapping between external and local fields
+ */
+ fieldMappings: z.array(FieldMappingSchema).describe('Field mappings'),
+
+ /**
+ * Cache configuration for external data
+ */
+ caching: z.object({
+ /**
+ * Whether caching is enabled
+ * @default true
+ */
+ enabled: z.boolean().optional().default(true).describe('Cache enabled'),
+
+ /**
+ * Time-to-live in seconds
+ * @default 300
+ */
+ ttl: z.number().optional().default(300).describe('Cache TTL (seconds)'),
+
+ /**
+ * Cache eviction strategy
+ * @default 'ttl'
+ */
+ strategy: z.enum(['lru', 'lfu', 'ttl']).optional().default('ttl').describe('Cache strategy'),
+ }).optional().describe('Caching configuration'),
+
+ /**
+ * Fallback behavior when external system is unavailable
+ */
+ fallback: z.object({
+ /**
+ * Whether fallback is enabled
+ * @default true
+ */
+ enabled: z.boolean().optional().default(true).describe('Fallback enabled'),
+
+ /**
+ * Default value to use when external system fails
+ */
+ defaultValue: z.any().optional().describe('Default fallback value'),
+
+ /**
+ * Whether to show error message to user
+ * @default true
+ */
+ showError: z.boolean().optional().default(true).describe('Show error to user'),
+ }).optional().describe('Fallback configuration'),
+
+ /**
+ * Rate limiting to prevent overwhelming external system
+ */
+ rateLimit: z.object({
+ /**
+ * Maximum requests per second
+ */
+ requestsPerSecond: z.number().describe('Requests per second limit'),
+
+ /**
+ * Burst size for handling spikes
+ */
+ burstSize: z.number().optional().describe('Burst size'),
+ }).optional().describe('Rate limiting'),
+});
+
+// Type exports
+export type ExternalLookup = z.infer;
+export type ExternalDataSource = z.infer;
+export type FieldMapping = z.infer;
diff --git a/packages/spec/src/data/index.ts b/packages/spec/src/data/index.ts
index 25ef5bbb0..9c356d1fa 100644
--- a/packages/spec/src/data/index.ts
+++ b/packages/spec/src/data/index.ts
@@ -5,4 +5,10 @@ export * from './field.zod';
export * from './validation.zod';
export * from './hook.zod';
-export * from './dataset.zod';
\ No newline at end of file
+export * from './dataset.zod';
+
+// Document Management Protocol
+export * from './document.zod';
+
+// External Lookup Protocol
+export * from './external-lookup.zod';
\ No newline at end of file
diff --git a/packages/spec/src/system/change-management.test.ts b/packages/spec/src/system/change-management.test.ts
new file mode 100644
index 000000000..0882e2a51
--- /dev/null
+++ b/packages/spec/src/system/change-management.test.ts
@@ -0,0 +1,656 @@
+import { describe, it, expect } from 'vitest';
+import {
+ ChangeTypeSchema,
+ ChangePrioritySchema,
+ ChangeStatusSchema,
+ ChangeImpactSchema,
+ RollbackPlanSchema,
+ ChangeRequestSchema,
+ type ChangeRequest,
+ type ChangeImpact,
+ type RollbackPlan,
+} from './change-management.zod';
+
+describe('ChangeTypeSchema', () => {
+ it('should accept all valid change types', () => {
+ const validTypes = ['standard', 'normal', 'emergency', 'major'];
+
+ validTypes.forEach((type) => {
+ expect(() => ChangeTypeSchema.parse(type)).not.toThrow();
+ });
+ });
+
+ it('should reject invalid change type', () => {
+ expect(() => ChangeTypeSchema.parse('invalid')).toThrow();
+ });
+});
+
+describe('ChangePrioritySchema', () => {
+ it('should accept all valid priorities', () => {
+ const validPriorities = ['critical', 'high', 'medium', 'low'];
+
+ validPriorities.forEach((priority) => {
+ expect(() => ChangePrioritySchema.parse(priority)).not.toThrow();
+ });
+ });
+
+ it('should reject invalid priority', () => {
+ expect(() => ChangePrioritySchema.parse('urgent')).toThrow();
+ });
+});
+
+describe('ChangeStatusSchema', () => {
+ it('should accept all valid statuses', () => {
+ const validStatuses = [
+ 'draft',
+ 'submitted',
+ 'in-review',
+ 'approved',
+ 'scheduled',
+ 'in-progress',
+ 'completed',
+ 'failed',
+ 'rolled-back',
+ 'cancelled',
+ ];
+
+ validStatuses.forEach((status) => {
+ expect(() => ChangeStatusSchema.parse(status)).not.toThrow();
+ });
+ });
+
+ it('should reject invalid status', () => {
+ expect(() => ChangeStatusSchema.parse('pending')).toThrow();
+ });
+});
+
+describe('ChangeImpactSchema', () => {
+ it('should validate complete impact assessment', () => {
+ const validImpact: ChangeImpact = {
+ level: 'high',
+ affectedSystems: ['crm-api', 'customer-portal'],
+ affectedUsers: 5000,
+ downtime: {
+ required: true,
+ durationMinutes: 30,
+ },
+ };
+
+ expect(() => ChangeImpactSchema.parse(validImpact)).not.toThrow();
+ });
+
+ it('should accept minimal impact assessment', () => {
+ const minimalImpact = {
+ level: 'low',
+ affectedSystems: ['test-system'],
+ };
+
+ expect(() => ChangeImpactSchema.parse(minimalImpact)).not.toThrow();
+ });
+
+ it('should accept all impact levels', () => {
+ const levels = ['low', 'medium', 'high', 'critical'] as const;
+
+ levels.forEach((level) => {
+ const impact = {
+ level,
+ affectedSystems: ['system-1'],
+ };
+
+ expect(() => ChangeImpactSchema.parse(impact)).not.toThrow();
+ });
+ });
+
+ it('should validate downtime configuration', () => {
+ const impact = {
+ level: 'medium',
+ affectedSystems: ['api-gateway'],
+ downtime: {
+ required: false,
+ },
+ };
+
+ expect(() => ChangeImpactSchema.parse(impact)).not.toThrow();
+ });
+
+ it('should reject invalid impact level', () => {
+ const invalidImpact = {
+ level: 'severe',
+ affectedSystems: ['system'],
+ };
+
+ expect(() => ChangeImpactSchema.parse(invalidImpact)).toThrow();
+ });
+});
+
+describe('RollbackPlanSchema', () => {
+ it('should validate complete rollback plan', () => {
+ const validPlan: RollbackPlan = {
+ description: 'Revert database schema to previous version',
+ steps: [
+ {
+ order: 1,
+ description: 'Stop application servers',
+ estimatedMinutes: 5,
+ },
+ {
+ order: 2,
+ description: 'Restore database backup',
+ estimatedMinutes: 15,
+ },
+ {
+ order: 3,
+ description: 'Restart application servers',
+ estimatedMinutes: 5,
+ },
+ ],
+ testProcedure: 'Verify application login and basic functionality',
+ };
+
+ expect(() => RollbackPlanSchema.parse(validPlan)).not.toThrow();
+ });
+
+ it('should accept rollback plan without test procedure', () => {
+ const planWithoutTest = {
+ description: 'Simple rollback',
+ steps: [
+ {
+ order: 1,
+ description: 'Revert changes',
+ estimatedMinutes: 10,
+ },
+ ],
+ };
+
+ expect(() => RollbackPlanSchema.parse(planWithoutTest)).not.toThrow();
+ });
+
+ it('should validate multiple rollback steps', () => {
+ const plan = {
+ description: 'Multi-step rollback',
+ steps: [
+ {
+ order: 1,
+ description: 'Step 1',
+ estimatedMinutes: 5,
+ },
+ {
+ order: 2,
+ description: 'Step 2',
+ estimatedMinutes: 10,
+ },
+ {
+ order: 3,
+ description: 'Step 3',
+ estimatedMinutes: 15,
+ },
+ {
+ order: 4,
+ description: 'Step 4',
+ estimatedMinutes: 20,
+ },
+ ],
+ };
+
+ expect(() => RollbackPlanSchema.parse(plan)).not.toThrow();
+ });
+});
+
+describe('ChangeRequestSchema', () => {
+ it('should validate complete change request', () => {
+ const validRequest: ChangeRequest = {
+ id: 'CHG-2024-001',
+ title: 'Upgrade CRM Database Schema',
+ description: 'Migrate customer database to new schema version 2.0',
+ type: 'normal',
+ priority: 'high',
+ status: 'approved',
+ requestedBy: 'user_123',
+ requestedAt: 1704067200000,
+ impact: {
+ level: 'high',
+ affectedSystems: ['crm-api', 'customer-portal'],
+ affectedUsers: 5000,
+ downtime: {
+ required: true,
+ durationMinutes: 30,
+ },
+ },
+ implementation: {
+ description: 'Execute database migration scripts',
+ steps: [
+ {
+ order: 1,
+ description: 'Backup current database',
+ estimatedMinutes: 10,
+ },
+ {
+ order: 2,
+ description: 'Run migration scripts',
+ estimatedMinutes: 15,
+ },
+ ],
+ testing: 'Run integration test suite',
+ },
+ rollbackPlan: {
+ description: 'Restore from backup',
+ steps: [
+ {
+ order: 1,
+ description: 'Restore backup',
+ estimatedMinutes: 15,
+ },
+ ],
+ },
+ schedule: {
+ plannedStart: 1704153600000,
+ plannedEnd: 1704155400000,
+ },
+ };
+
+ expect(() => ChangeRequestSchema.parse(validRequest)).not.toThrow();
+ });
+
+ it('should accept minimal change request', () => {
+ const minimalRequest = {
+ id: 'CHG-2024-002',
+ title: 'Simple Change',
+ description: 'A simple change',
+ type: 'standard',
+ priority: 'low',
+ status: 'draft',
+ requestedBy: 'user_456',
+ requestedAt: Date.now(),
+ impact: {
+ level: 'low',
+ affectedSystems: ['test-system'],
+ },
+ implementation: {
+ description: 'Make the change',
+ steps: [
+ {
+ order: 1,
+ description: 'Execute change',
+ estimatedMinutes: 5,
+ },
+ ],
+ },
+ rollbackPlan: {
+ description: 'Undo the change',
+ steps: [
+ {
+ order: 1,
+ description: 'Revert',
+ estimatedMinutes: 5,
+ },
+ ],
+ },
+ };
+
+ expect(() => ChangeRequestSchema.parse(minimalRequest)).not.toThrow();
+ });
+
+ it('should validate standard change type', () => {
+ const standardChange = {
+ id: 'CHG-STD-001',
+ title: 'Standard Change',
+ description: 'Pre-approved standard change',
+ type: 'standard',
+ priority: 'low',
+ status: 'approved',
+ requestedBy: 'user_789',
+ requestedAt: Date.now(),
+ impact: {
+ level: 'low',
+ affectedSystems: ['component-a'],
+ },
+ implementation: {
+ description: 'Standard procedure',
+ steps: [
+ {
+ order: 1,
+ description: 'Execute',
+ estimatedMinutes: 10,
+ },
+ ],
+ },
+ rollbackPlan: {
+ description: 'Standard rollback',
+ steps: [
+ {
+ order: 1,
+ description: 'Revert',
+ estimatedMinutes: 5,
+ },
+ ],
+ },
+ };
+
+ expect(() => ChangeRequestSchema.parse(standardChange)).not.toThrow();
+ });
+
+ it('should validate emergency change type', () => {
+ const emergencyChange = {
+ id: 'CHG-EMG-001',
+ title: 'Emergency Security Patch',
+ description: 'Critical security vulnerability fix',
+ type: 'emergency',
+ priority: 'critical',
+ status: 'in-progress',
+ requestedBy: 'security_team',
+ requestedAt: Date.now(),
+ impact: {
+ level: 'critical',
+ affectedSystems: ['all-systems'],
+ affectedUsers: 50000,
+ downtime: {
+ required: true,
+ durationMinutes: 15,
+ },
+ },
+ implementation: {
+ description: 'Apply security patch',
+ steps: [
+ {
+ order: 1,
+ description: 'Deploy patch',
+ estimatedMinutes: 10,
+ },
+ ],
+ },
+ rollbackPlan: {
+ description: 'Remove patch',
+ steps: [
+ {
+ order: 1,
+ description: 'Rollback',
+ estimatedMinutes: 5,
+ },
+ ],
+ },
+ };
+
+ expect(() => ChangeRequestSchema.parse(emergencyChange)).not.toThrow();
+ });
+
+ it('should validate major change requiring CAB approval', () => {
+ const majorChange = {
+ id: 'CHG-MAJ-001',
+ title: 'Major Infrastructure Upgrade',
+ description: 'Upgrade core infrastructure',
+ type: 'major',
+ priority: 'high',
+ status: 'in-review',
+ requestedBy: 'infrastructure_team',
+ requestedAt: Date.now(),
+ impact: {
+ level: 'critical',
+ affectedSystems: ['core-infrastructure', 'all-applications'],
+ affectedUsers: 100000,
+ downtime: {
+ required: true,
+ durationMinutes: 120,
+ },
+ },
+ implementation: {
+ description: 'Multi-phase infrastructure upgrade',
+ steps: [
+ {
+ order: 1,
+ description: 'Phase 1: Database upgrade',
+ estimatedMinutes: 30,
+ },
+ {
+ order: 2,
+ description: 'Phase 2: Application upgrade',
+ estimatedMinutes: 45,
+ },
+ ],
+ testing: 'Comprehensive integration testing',
+ },
+ rollbackPlan: {
+ description: 'Restore from snapshots',
+ steps: [
+ {
+ order: 1,
+ description: 'Restore infrastructure snapshot',
+ estimatedMinutes: 60,
+ },
+ ],
+ },
+ approval: {
+ required: true,
+ approvers: [
+ {
+ userId: 'cab_member_1',
+ approvedAt: 1704067200000,
+ comments: 'Approved with conditions',
+ },
+ {
+ userId: 'cab_member_2',
+ },
+ ],
+ },
+ };
+
+ expect(() => ChangeRequestSchema.parse(majorChange)).not.toThrow();
+ });
+
+ it('should validate schedule with actual times', () => {
+ const scheduledChange = {
+ id: 'CHG-2024-003',
+ title: 'Scheduled Maintenance',
+ description: 'Routine maintenance',
+ type: 'normal',
+ priority: 'medium',
+ status: 'completed',
+ requestedBy: 'ops_team',
+ requestedAt: Date.now(),
+ impact: {
+ level: 'medium',
+ affectedSystems: ['web-servers'],
+ },
+ implementation: {
+ description: 'Update web servers',
+ steps: [
+ {
+ order: 1,
+ description: 'Update',
+ estimatedMinutes: 20,
+ },
+ ],
+ },
+ rollbackPlan: {
+ description: 'Rollback update',
+ steps: [
+ {
+ order: 1,
+ description: 'Revert',
+ estimatedMinutes: 10,
+ },
+ ],
+ },
+ schedule: {
+ plannedStart: 1704153600000,
+ plannedEnd: 1704155400000,
+ actualStart: 1704153650000,
+ actualEnd: 1704155350000,
+ },
+ };
+
+ expect(() => ChangeRequestSchema.parse(scheduledChange)).not.toThrow();
+ });
+
+ it('should validate attachments', () => {
+ const changeWithAttachments = {
+ id: 'CHG-2024-004',
+ title: 'Change with Documentation',
+ description: 'Well-documented change',
+ type: 'normal',
+ priority: 'medium',
+ status: 'submitted',
+ requestedBy: 'user_123',
+ requestedAt: Date.now(),
+ impact: {
+ level: 'medium',
+ affectedSystems: ['api'],
+ },
+ implementation: {
+ description: 'API update',
+ steps: [
+ {
+ order: 1,
+ description: 'Deploy',
+ estimatedMinutes: 15,
+ },
+ ],
+ },
+ rollbackPlan: {
+ description: 'Rollback',
+ steps: [
+ {
+ order: 1,
+ description: 'Revert',
+ estimatedMinutes: 10,
+ },
+ ],
+ },
+ attachments: [
+ {
+ name: 'implementation-plan.pdf',
+ url: 'https://example.com/docs/plan.pdf',
+ },
+ {
+ name: 'architecture-diagram.png',
+ url: 'https://example.com/diagrams/arch.png',
+ },
+ ],
+ };
+
+ expect(() => ChangeRequestSchema.parse(changeWithAttachments)).not.toThrow();
+ });
+
+ it('should validate attachment URLs', () => {
+ const invalidChange = {
+ id: 'CHG-2024-005',
+ title: 'Invalid Attachment',
+ description: 'Change with invalid attachment URL',
+ type: 'normal',
+ priority: 'low',
+ status: 'draft',
+ requestedBy: 'user_123',
+ requestedAt: Date.now(),
+ impact: {
+ level: 'low',
+ affectedSystems: ['test'],
+ },
+ implementation: {
+ description: 'Test',
+ steps: [
+ {
+ order: 1,
+ description: 'Test',
+ estimatedMinutes: 5,
+ },
+ ],
+ },
+ rollbackPlan: {
+ description: 'Test',
+ steps: [
+ {
+ order: 1,
+ description: 'Test',
+ estimatedMinutes: 5,
+ },
+ ],
+ },
+ attachments: [
+ {
+ name: 'document.pdf',
+ url: 'not-a-valid-url',
+ },
+ ],
+ };
+
+ expect(() => ChangeRequestSchema.parse(invalidChange)).toThrow();
+ });
+
+ it('should validate failed change status', () => {
+ const failedChange = {
+ id: 'CHG-2024-006',
+ title: 'Failed Change',
+ description: 'Change that failed',
+ type: 'normal',
+ priority: 'high',
+ status: 'failed',
+ requestedBy: 'user_123',
+ requestedAt: Date.now(),
+ impact: {
+ level: 'high',
+ affectedSystems: ['database'],
+ },
+ implementation: {
+ description: 'Database update',
+ steps: [
+ {
+ order: 1,
+ description: 'Update schema',
+ estimatedMinutes: 20,
+ },
+ ],
+ },
+ rollbackPlan: {
+ description: 'Restore backup',
+ steps: [
+ {
+ order: 1,
+ description: 'Restore',
+ estimatedMinutes: 15,
+ },
+ ],
+ },
+ };
+
+ expect(() => ChangeRequestSchema.parse(failedChange)).not.toThrow();
+ });
+
+ it('should validate rolled-back change status', () => {
+ const rolledBackChange = {
+ id: 'CHG-2024-007',
+ title: 'Rolled Back Change',
+ description: 'Change that was rolled back',
+ type: 'normal',
+ priority: 'high',
+ status: 'rolled-back',
+ requestedBy: 'user_123',
+ requestedAt: Date.now(),
+ impact: {
+ level: 'high',
+ affectedSystems: ['application'],
+ },
+ implementation: {
+ description: 'App update',
+ steps: [
+ {
+ order: 1,
+ description: 'Deploy',
+ estimatedMinutes: 15,
+ },
+ ],
+ },
+ rollbackPlan: {
+ description: 'Revert deployment',
+ steps: [
+ {
+ order: 1,
+ description: 'Rollback',
+ estimatedMinutes: 10,
+ },
+ ],
+ testProcedure: 'Verify app functionality',
+ },
+ };
+
+ expect(() => ChangeRequestSchema.parse(rolledBackChange)).not.toThrow();
+ });
+});
diff --git a/packages/spec/src/system/change-management.zod.ts b/packages/spec/src/system/change-management.zod.ts
new file mode 100644
index 000000000..08e641aaa
--- /dev/null
+++ b/packages/spec/src/system/change-management.zod.ts
@@ -0,0 +1,372 @@
+import { z } from 'zod';
+
+/**
+ * Change Type Enum
+ *
+ * Classification of change requests based on risk and approval requirements.
+ * Follows ITIL change management best practices.
+ */
+export const ChangeTypeSchema = z.enum([
+ 'standard', // Pre-approved, low-risk changes
+ 'normal', // Requires standard approval process
+ 'emergency', // Fast-track approval for critical issues
+ 'major', // Requires CAB (Change Advisory Board) approval
+]);
+
+/**
+ * Change Priority Enum
+ *
+ * Priority level for change request processing.
+ */
+export const ChangePrioritySchema = z.enum([
+ 'critical',
+ 'high',
+ 'medium',
+ 'low',
+]);
+
+/**
+ * Change Status Enum
+ *
+ * Current status of a change request in its lifecycle.
+ */
+export const ChangeStatusSchema = z.enum([
+ 'draft',
+ 'submitted',
+ 'in-review',
+ 'approved',
+ 'scheduled',
+ 'in-progress',
+ 'completed',
+ 'failed',
+ 'rolled-back',
+ 'cancelled',
+]);
+
+/**
+ * Change Impact Schema
+ *
+ * Assessment of the impact and scope of a change request.
+ * Used for risk evaluation and approval routing.
+ *
+ * @example
+ * ```json
+ * {
+ * "level": "high",
+ * "affectedSystems": ["crm-api", "customer-portal"],
+ * "affectedUsers": 5000,
+ * "downtime": {
+ * "required": true,
+ * "durationMinutes": 30
+ * }
+ * }
+ * ```
+ */
+export const ChangeImpactSchema = z.object({
+ /**
+ * Overall impact level of the change
+ */
+ level: z.enum(['low', 'medium', 'high', 'critical']).describe('Impact level'),
+
+ /**
+ * List of systems affected by this change
+ */
+ affectedSystems: z.array(z.string()).describe('Affected systems'),
+
+ /**
+ * Estimated number of users affected
+ */
+ affectedUsers: z.number().optional().describe('Affected user count'),
+
+ /**
+ * Downtime requirements
+ */
+ downtime: z.object({
+ /**
+ * Whether downtime is required
+ */
+ required: z.boolean().describe('Downtime required'),
+
+ /**
+ * Duration of downtime in minutes
+ */
+ durationMinutes: z.number().optional().describe('Downtime duration'),
+ }).optional().describe('Downtime information'),
+});
+
+/**
+ * Rollback Plan Schema
+ *
+ * Detailed procedure for reverting changes if implementation fails.
+ * Required for all non-standard changes.
+ *
+ * @example
+ * ```json
+ * {
+ * "description": "Revert database schema to previous version",
+ * "steps": [
+ * {
+ * "order": 1,
+ * "description": "Stop application servers",
+ * "estimatedMinutes": 5
+ * },
+ * {
+ * "order": 2,
+ * "description": "Restore database backup",
+ * "estimatedMinutes": 15
+ * }
+ * ],
+ * "testProcedure": "Verify application login and basic functionality"
+ * }
+ * ```
+ */
+export const RollbackPlanSchema = z.object({
+ /**
+ * High-level description of the rollback approach
+ */
+ description: z.string().describe('Rollback description'),
+
+ /**
+ * Sequential steps to execute rollback
+ */
+ steps: z.array(z.object({
+ /**
+ * Step execution order
+ */
+ order: z.number().describe('Step order'),
+
+ /**
+ * Detailed description of this step
+ */
+ description: z.string().describe('Step description'),
+
+ /**
+ * Estimated time to complete this step
+ */
+ estimatedMinutes: z.number().describe('Estimated duration'),
+ })).describe('Rollback steps'),
+
+ /**
+ * Testing procedure to verify successful rollback
+ */
+ testProcedure: z.string().optional().describe('Test procedure'),
+});
+
+/**
+ * Change Request Schema
+ *
+ * Comprehensive change management protocol for IT governance.
+ * Supports change requests, deployment tracking, and ITIL compliance.
+ *
+ * @example
+ * ```json
+ * {
+ * "id": "CHG-2024-001",
+ * "title": "Upgrade CRM Database Schema",
+ * "description": "Migrate customer database to new schema version 2.0",
+ * "type": "normal",
+ * "priority": "high",
+ * "status": "approved",
+ * "requestedBy": "user_123",
+ * "requestedAt": 1704067200000,
+ * "impact": {
+ * "level": "high",
+ * "affectedSystems": ["crm-api", "customer-portal"],
+ * "affectedUsers": 5000,
+ * "downtime": {
+ * "required": true,
+ * "durationMinutes": 30
+ * }
+ * },
+ * "implementation": {
+ * "description": "Execute database migration scripts",
+ * "steps": [
+ * {
+ * "order": 1,
+ * "description": "Backup current database",
+ * "estimatedMinutes": 10
+ * }
+ * ],
+ * "testing": "Run integration test suite"
+ * },
+ * "rollbackPlan": {
+ * "description": "Restore from backup",
+ * "steps": [
+ * {
+ * "order": 1,
+ * "description": "Restore backup",
+ * "estimatedMinutes": 15
+ * }
+ * ]
+ * },
+ * "schedule": {
+ * "plannedStart": 1704153600000,
+ * "plannedEnd": 1704155400000
+ * }
+ * }
+ * ```
+ */
+export const ChangeRequestSchema = z.object({
+ /**
+ * Unique change request identifier
+ */
+ id: z.string().describe('Change request ID'),
+
+ /**
+ * Short descriptive title of the change
+ */
+ title: z.string().describe('Change title'),
+
+ /**
+ * Detailed description of the change and its purpose
+ */
+ description: z.string().describe('Change description'),
+
+ /**
+ * Change classification type
+ */
+ type: ChangeTypeSchema.describe('Change type'),
+
+ /**
+ * Priority level for processing
+ */
+ priority: ChangePrioritySchema.describe('Change priority'),
+
+ /**
+ * Current status in the change lifecycle
+ */
+ status: ChangeStatusSchema.describe('Change status'),
+
+ /**
+ * User ID of the change requester
+ */
+ requestedBy: z.string().describe('Requester user ID'),
+
+ /**
+ * Timestamp when change was requested (Unix milliseconds)
+ */
+ requestedAt: z.number().describe('Request timestamp'),
+
+ /**
+ * Impact assessment of the change
+ */
+ impact: ChangeImpactSchema.describe('Impact assessment'),
+
+ /**
+ * Implementation plan and procedures
+ */
+ implementation: z.object({
+ /**
+ * High-level implementation description
+ */
+ description: z.string().describe('Implementation description'),
+
+ /**
+ * Sequential implementation steps
+ */
+ steps: z.array(z.object({
+ /**
+ * Step execution order
+ */
+ order: z.number().describe('Step order'),
+
+ /**
+ * Detailed description of this step
+ */
+ description: z.string().describe('Step description'),
+
+ /**
+ * Estimated time to complete this step
+ */
+ estimatedMinutes: z.number().describe('Estimated duration'),
+ })).describe('Implementation steps'),
+
+ /**
+ * Testing procedures to verify successful implementation
+ */
+ testing: z.string().optional().describe('Testing procedure'),
+ }).describe('Implementation plan'),
+
+ /**
+ * Rollback plan in case of failure
+ */
+ rollbackPlan: RollbackPlanSchema.describe('Rollback plan'),
+
+ /**
+ * Change schedule and timing
+ */
+ schedule: z.object({
+ /**
+ * Planned start time (Unix milliseconds)
+ */
+ plannedStart: z.number().describe('Planned start time'),
+
+ /**
+ * Planned end time (Unix milliseconds)
+ */
+ plannedEnd: z.number().describe('Planned end time'),
+
+ /**
+ * Actual start time (Unix milliseconds)
+ */
+ actualStart: z.number().optional().describe('Actual start time'),
+
+ /**
+ * Actual end time (Unix milliseconds)
+ */
+ actualEnd: z.number().optional().describe('Actual end time'),
+ }).optional().describe('Schedule'),
+
+ /**
+ * Approval workflow configuration
+ */
+ approval: z.object({
+ /**
+ * Whether approval is required for this change
+ */
+ required: z.boolean().describe('Approval required'),
+
+ /**
+ * List of approvers and their approval status
+ */
+ approvers: z.array(z.object({
+ /**
+ * Approver user ID
+ */
+ userId: z.string().describe('Approver user ID'),
+
+ /**
+ * Timestamp when approval was granted (Unix milliseconds)
+ */
+ approvedAt: z.number().optional().describe('Approval timestamp'),
+
+ /**
+ * Comments from the approver
+ */
+ comments: z.string().optional().describe('Approver comments'),
+ })).describe('Approvers'),
+ }).optional().describe('Approval workflow'),
+
+ /**
+ * Supporting documentation and files
+ */
+ attachments: z.array(z.object({
+ /**
+ * Attachment file name
+ */
+ name: z.string().describe('Attachment name'),
+
+ /**
+ * URL to download the attachment
+ */
+ url: z.string().url().describe('Attachment URL'),
+ })).optional().describe('Attachments'),
+});
+
+// Type exports
+export type ChangeRequest = z.infer;
+export type ChangeType = z.infer;
+export type ChangeStatus = z.infer;
+export type ChangePriority = z.infer;
+export type ChangeImpact = z.infer;
+export type RollbackPlan = z.infer;
diff --git a/packages/spec/src/system/index.ts b/packages/spec/src/system/index.ts
index 118bc9b6a..789435139 100644
--- a/packages/spec/src/system/index.ts
+++ b/packages/spec/src/system/index.ts
@@ -56,6 +56,12 @@ export * from './encryption.zod';
export * from './compliance.zod';
export * from './masking.zod';
+// Notification Protocol
+export * from './notification.zod';
+
+// Change Management Protocol
+export * from './change-management.zod';
+
// Note: Auth, Identity, Policy, Role, Organization moved to @objectstack/spec/auth
// Note: Territory moved to @objectstack/spec/permission
// Note: Connector Protocol moved to @objectstack/spec/integration
diff --git a/packages/spec/src/system/notification.test.ts b/packages/spec/src/system/notification.test.ts
new file mode 100644
index 000000000..3f1abf63a
--- /dev/null
+++ b/packages/spec/src/system/notification.test.ts
@@ -0,0 +1,560 @@
+import { describe, it, expect } from 'vitest';
+import {
+ EmailTemplateSchema,
+ SMSTemplateSchema,
+ PushNotificationSchema,
+ InAppNotificationSchema,
+ NotificationChannelSchema,
+ NotificationConfigSchema,
+ type NotificationConfig,
+ type EmailTemplate,
+ type SMSTemplate,
+} from './notification.zod';
+
+describe('EmailTemplateSchema', () => {
+ it('should validate complete email template', () => {
+ const validTemplate: EmailTemplate = {
+ id: 'welcome-email',
+ subject: 'Welcome to {{company_name}}',
+ body: 'Welcome {{user_name}}!
',
+ bodyType: 'html',
+ variables: ['company_name', 'user_name'],
+ attachments: [
+ {
+ name: 'guide.pdf',
+ url: 'https://example.com/guide.pdf',
+ },
+ ],
+ };
+
+ expect(() => EmailTemplateSchema.parse(validTemplate)).not.toThrow();
+ });
+
+ it('should accept minimal email template', () => {
+ const minimalTemplate = {
+ id: 'simple-email',
+ subject: 'Test Email',
+ body: 'Simple text body',
+ };
+
+ expect(() => EmailTemplateSchema.parse(minimalTemplate)).not.toThrow();
+ });
+
+ it('should default bodyType to html', () => {
+ const template = {
+ id: 'test',
+ subject: 'Test',
+ body: 'Body',
+ };
+
+ const parsed = EmailTemplateSchema.parse(template);
+ expect(parsed.bodyType).toBe('html');
+ });
+
+ it('should accept text bodyType', () => {
+ const template = {
+ id: 'text-email',
+ subject: 'Plain Text',
+ body: 'Plain text body',
+ bodyType: 'text' as const,
+ };
+
+ expect(() => EmailTemplateSchema.parse(template)).not.toThrow();
+ });
+
+ it('should accept markdown bodyType', () => {
+ const template = {
+ id: 'markdown-email',
+ subject: 'Markdown Email',
+ body: '# Header\n\nContent',
+ bodyType: 'markdown' as const,
+ };
+
+ expect(() => EmailTemplateSchema.parse(template)).not.toThrow();
+ });
+
+ it('should validate attachment URLs', () => {
+ const invalidTemplate = {
+ id: 'email-1',
+ subject: 'Test',
+ body: 'Body',
+ attachments: [
+ {
+ name: 'file.pdf',
+ url: 'not-a-url',
+ },
+ ],
+ };
+
+ expect(() => EmailTemplateSchema.parse(invalidTemplate)).toThrow();
+ });
+});
+
+describe('SMSTemplateSchema', () => {
+ it('should validate complete SMS template', () => {
+ const validTemplate: SMSTemplate = {
+ id: 'verification-sms',
+ message: 'Your verification code is {{code}}',
+ maxLength: 160,
+ variables: ['code'],
+ };
+
+ expect(() => SMSTemplateSchema.parse(validTemplate)).not.toThrow();
+ });
+
+ it('should accept minimal SMS template', () => {
+ const minimalTemplate = {
+ id: 'simple-sms',
+ message: 'Hello World',
+ };
+
+ expect(() => SMSTemplateSchema.parse(minimalTemplate)).not.toThrow();
+ });
+
+ it('should default maxLength to 160', () => {
+ const template = {
+ id: 'sms-1',
+ message: 'Test message',
+ };
+
+ const parsed = SMSTemplateSchema.parse(template);
+ expect(parsed.maxLength).toBe(160);
+ });
+
+ it('should accept custom maxLength', () => {
+ const template = {
+ id: 'long-sms',
+ message: 'Long message',
+ maxLength: 320,
+ };
+
+ const parsed = SMSTemplateSchema.parse(template);
+ expect(parsed.maxLength).toBe(320);
+ });
+});
+
+describe('PushNotificationSchema', () => {
+ it('should validate complete push notification', () => {
+ const validPush = {
+ title: 'New Message',
+ body: 'You have a new message from John',
+ icon: 'https://example.com/icon.png',
+ badge: 5,
+ data: { messageId: 'msg_123' },
+ actions: [
+ { action: 'view', title: 'View' },
+ { action: 'dismiss', title: 'Dismiss' },
+ ],
+ };
+
+ expect(() => PushNotificationSchema.parse(validPush)).not.toThrow();
+ });
+
+ it('should accept minimal push notification', () => {
+ const minimalPush = {
+ title: 'Alert',
+ body: 'Something happened',
+ };
+
+ expect(() => PushNotificationSchema.parse(minimalPush)).not.toThrow();
+ });
+
+ it('should validate icon URL', () => {
+ const invalidPush = {
+ title: 'Test',
+ body: 'Body',
+ icon: 'not-a-url',
+ };
+
+ expect(() => PushNotificationSchema.parse(invalidPush)).toThrow();
+ });
+
+ it('should accept custom data payload', () => {
+ const push = {
+ title: 'Order Update',
+ body: 'Your order has shipped',
+ data: {
+ orderId: 'ord_123',
+ trackingNumber: 'TRK456',
+ status: 'shipped',
+ },
+ };
+
+ expect(() => PushNotificationSchema.parse(push)).not.toThrow();
+ });
+});
+
+describe('InAppNotificationSchema', () => {
+ it('should validate complete in-app notification', () => {
+ const validNotification = {
+ title: 'System Update',
+ message: 'New features are now available',
+ type: 'info' as const,
+ actionUrl: '/updates',
+ dismissible: true,
+ expiresAt: 1704067200000,
+ };
+
+ expect(() => InAppNotificationSchema.parse(validNotification)).not.toThrow();
+ });
+
+ it('should accept minimal in-app notification', () => {
+ const minimalNotification = {
+ title: 'Alert',
+ message: 'Important message',
+ type: 'warning' as const,
+ };
+
+ expect(() => InAppNotificationSchema.parse(minimalNotification)).not.toThrow();
+ });
+
+ it('should default dismissible to true', () => {
+ const notification = {
+ title: 'Test',
+ message: 'Message',
+ type: 'info' as const,
+ };
+
+ const parsed = InAppNotificationSchema.parse(notification);
+ expect(parsed.dismissible).toBe(true);
+ });
+
+ it('should accept all notification types', () => {
+ const types = ['info', 'success', 'warning', 'error'] as const;
+
+ types.forEach((type) => {
+ const notification = {
+ title: 'Test',
+ message: 'Message',
+ type,
+ };
+
+ expect(() => InAppNotificationSchema.parse(notification)).not.toThrow();
+ });
+ });
+
+ it('should reject invalid notification type', () => {
+ const invalidNotification = {
+ title: 'Test',
+ message: 'Message',
+ type: 'invalid',
+ };
+
+ expect(() => InAppNotificationSchema.parse(invalidNotification)).toThrow();
+ });
+});
+
+describe('NotificationChannelSchema', () => {
+ it('should accept all valid channels', () => {
+ const validChannels = [
+ 'email',
+ 'sms',
+ 'push',
+ 'in-app',
+ 'slack',
+ 'teams',
+ 'webhook',
+ ];
+
+ validChannels.forEach((channel) => {
+ expect(() => NotificationChannelSchema.parse(channel)).not.toThrow();
+ });
+ });
+
+ it('should reject invalid channel', () => {
+ expect(() => NotificationChannelSchema.parse('invalid')).toThrow();
+ });
+});
+
+describe('NotificationConfigSchema', () => {
+ it('should validate email notification config', () => {
+ const validConfig: NotificationConfig = {
+ id: 'welcome-email',
+ name: 'Welcome Email',
+ channel: 'email',
+ template: {
+ id: 'tpl-001',
+ subject: 'Welcome to ObjectStack',
+ body: 'Welcome!
',
+ bodyType: 'html',
+ },
+ recipients: {
+ to: ['user@example.com'],
+ },
+ };
+
+ expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow();
+ });
+
+ it('should validate SMS notification config', () => {
+ const validConfig = {
+ id: 'verification-sms',
+ name: 'Verification SMS',
+ channel: 'sms',
+ template: {
+ id: 'sms-001',
+ message: 'Your code is {{code}}',
+ },
+ recipients: {
+ to: ['+1234567890'],
+ },
+ };
+
+ expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow();
+ });
+
+ it('should validate push notification config', () => {
+ const validConfig = {
+ id: 'push-alert',
+ name: 'Push Alert',
+ channel: 'push',
+ template: {
+ title: 'New Message',
+ body: 'You have a new message',
+ },
+ recipients: {
+ to: ['device_token_123'],
+ },
+ };
+
+ expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow();
+ });
+
+ it('should validate in-app notification config', () => {
+ const validConfig = {
+ id: 'system-alert',
+ name: 'System Alert',
+ channel: 'in-app',
+ template: {
+ title: 'Update Available',
+ message: 'A new version is available',
+ type: 'info' as const,
+ },
+ recipients: {
+ to: ['user_123'],
+ },
+ };
+
+ expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow();
+ });
+
+ it('should accept CC and BCC recipients', () => {
+ const config = {
+ id: 'email-with-cc',
+ name: 'Email with CC',
+ channel: 'email',
+ template: {
+ id: 'tpl-002',
+ subject: 'Test',
+ body: 'Body',
+ },
+ recipients: {
+ to: ['user@example.com'],
+ cc: ['manager@example.com'],
+ bcc: ['archive@example.com'],
+ },
+ };
+
+ expect(() => NotificationConfigSchema.parse(config)).not.toThrow();
+ });
+
+ it('should validate immediate schedule', () => {
+ const config = {
+ id: 'immediate-notification',
+ name: 'Immediate',
+ channel: 'email',
+ template: {
+ id: 'tpl-003',
+ subject: 'Test',
+ body: 'Body',
+ },
+ recipients: {
+ to: ['user@example.com'],
+ },
+ schedule: {
+ type: 'immediate' as const,
+ },
+ };
+
+ expect(() => NotificationConfigSchema.parse(config)).not.toThrow();
+ });
+
+ it('should validate delayed schedule', () => {
+ const config = {
+ id: 'delayed-notification',
+ name: 'Delayed',
+ channel: 'email',
+ template: {
+ id: 'tpl-004',
+ subject: 'Test',
+ body: 'Body',
+ },
+ recipients: {
+ to: ['user@example.com'],
+ },
+ schedule: {
+ type: 'delayed' as const,
+ delay: 3600000, // 1 hour
+ },
+ };
+
+ expect(() => NotificationConfigSchema.parse(config)).not.toThrow();
+ });
+
+ it('should validate scheduled notification', () => {
+ const config = {
+ id: 'scheduled-notification',
+ name: 'Scheduled',
+ channel: 'email',
+ template: {
+ id: 'tpl-005',
+ subject: 'Test',
+ body: 'Body',
+ },
+ recipients: {
+ to: ['user@example.com'],
+ },
+ schedule: {
+ type: 'scheduled' as const,
+ scheduledAt: 1704067200000,
+ },
+ };
+
+ expect(() => NotificationConfigSchema.parse(config)).not.toThrow();
+ });
+
+ it('should validate retry policy with defaults', () => {
+ const config = {
+ id: 'notification-with-retry',
+ name: 'With Retry',
+ channel: 'email',
+ template: {
+ id: 'tpl-006',
+ subject: 'Test',
+ body: 'Body',
+ },
+ recipients: {
+ to: ['user@example.com'],
+ },
+ retryPolicy: {
+ backoffStrategy: 'exponential' as const,
+ },
+ };
+
+ const parsed = NotificationConfigSchema.parse(config);
+ expect(parsed.retryPolicy?.enabled).toBe(true);
+ expect(parsed.retryPolicy?.maxRetries).toBe(3);
+ });
+
+ it('should validate retry policy backoff strategies', () => {
+ const strategies = ['exponential', 'linear', 'fixed'] as const;
+
+ strategies.forEach((strategy) => {
+ const config = {
+ id: `retry-${strategy}`,
+ name: 'Test',
+ channel: 'email',
+ template: {
+ id: 'tpl-007',
+ subject: 'Test',
+ body: 'Body',
+ },
+ recipients: {
+ to: ['user@example.com'],
+ },
+ retryPolicy: {
+ backoffStrategy: strategy,
+ },
+ };
+
+ expect(() => NotificationConfigSchema.parse(config)).not.toThrow();
+ });
+ });
+
+ it('should validate tracking configuration with defaults', () => {
+ const config = {
+ id: 'notification-with-tracking',
+ name: 'With Tracking',
+ channel: 'email',
+ template: {
+ id: 'tpl-008',
+ subject: 'Test',
+ body: 'Body',
+ },
+ recipients: {
+ to: ['user@example.com'],
+ },
+ tracking: {},
+ };
+
+ const parsed = NotificationConfigSchema.parse(config);
+ expect(parsed.tracking?.trackOpens).toBe(false);
+ expect(parsed.tracking?.trackClicks).toBe(false);
+ expect(parsed.tracking?.trackDelivery).toBe(true);
+ });
+
+ it('should accept custom tracking configuration', () => {
+ const config = {
+ id: 'notification-custom-tracking',
+ name: 'Custom Tracking',
+ channel: 'email',
+ template: {
+ id: 'tpl-009',
+ subject: 'Test',
+ body: 'Body',
+ },
+ recipients: {
+ to: ['user@example.com'],
+ },
+ tracking: {
+ trackOpens: true,
+ trackClicks: true,
+ trackDelivery: true,
+ },
+ };
+
+ expect(() => NotificationConfigSchema.parse(config)).not.toThrow();
+ });
+
+ it('should validate complete notification config with all options', () => {
+ const completeConfig: NotificationConfig = {
+ id: 'complete-notification',
+ name: 'Complete Notification',
+ channel: 'email',
+ template: {
+ id: 'tpl-complete',
+ subject: 'Complete Email {{user_name}}',
+ body: '{{content}}',
+ bodyType: 'html',
+ variables: ['user_name', 'content'],
+ attachments: [
+ {
+ name: 'report.pdf',
+ url: 'https://example.com/reports/report.pdf',
+ },
+ ],
+ },
+ recipients: {
+ to: ['user@example.com', 'admin@example.com'],
+ cc: ['manager@example.com'],
+ bcc: ['archive@example.com'],
+ },
+ schedule: {
+ type: 'scheduled',
+ scheduledAt: 1704067200000,
+ },
+ retryPolicy: {
+ enabled: true,
+ maxRetries: 5,
+ backoffStrategy: 'exponential',
+ },
+ tracking: {
+ trackOpens: true,
+ trackClicks: true,
+ trackDelivery: true,
+ },
+ };
+
+ expect(() => NotificationConfigSchema.parse(completeConfig)).not.toThrow();
+ });
+});
diff --git a/packages/spec/src/system/notification.zod.ts b/packages/spec/src/system/notification.zod.ts
new file mode 100644
index 000000000..e2ed60414
--- /dev/null
+++ b/packages/spec/src/system/notification.zod.ts
@@ -0,0 +1,379 @@
+import { z } from 'zod';
+
+/**
+ * Email Template Schema
+ *
+ * Defines the structure and content of email notifications.
+ * Supports variables for personalization and file attachments.
+ *
+ * @example
+ * ```json
+ * {
+ * "id": "welcome-email",
+ * "subject": "Welcome to {{company_name}}",
+ * "body": "Welcome {{user_name}}!
",
+ * "bodyType": "html",
+ * "variables": ["company_name", "user_name"],
+ * "attachments": [
+ * {
+ * "name": "guide.pdf",
+ * "url": "https://example.com/guide.pdf"
+ * }
+ * ]
+ * }
+ * ```
+ */
+export const EmailTemplateSchema = z.object({
+ /**
+ * Unique identifier for the email template
+ */
+ id: z.string().describe('Template identifier'),
+
+ /**
+ * Email subject line (supports variable interpolation)
+ */
+ subject: z.string().describe('Email subject'),
+
+ /**
+ * Email body content
+ */
+ body: z.string().describe('Email body content'),
+
+ /**
+ * Content type of the email body
+ * @default 'html'
+ */
+ bodyType: z.enum(['text', 'html', 'markdown']).optional().default('html').describe('Body content type'),
+
+ /**
+ * List of template variables for dynamic content
+ */
+ variables: z.array(z.string()).optional().describe('Template variables'),
+
+ /**
+ * File attachments to include with the email
+ */
+ attachments: z.array(z.object({
+ name: z.string().describe('Attachment filename'),
+ url: z.string().url().describe('Attachment URL'),
+ })).optional().describe('Email attachments'),
+});
+
+/**
+ * SMS Template Schema
+ *
+ * Defines the structure of SMS text message notifications.
+ * Includes character limits and variable support.
+ *
+ * @example
+ * ```json
+ * {
+ * "id": "verification-sms",
+ * "message": "Your code is {{code}}",
+ * "maxLength": 160,
+ * "variables": ["code"]
+ * }
+ * ```
+ */
+export const SMSTemplateSchema = z.object({
+ /**
+ * Unique identifier for the SMS template
+ */
+ id: z.string().describe('Template identifier'),
+
+ /**
+ * SMS message content (supports variable interpolation)
+ */
+ message: z.string().describe('SMS message content'),
+
+ /**
+ * Maximum character length for the SMS
+ * @default 160
+ */
+ maxLength: z.number().optional().default(160).describe('Maximum message length'),
+
+ /**
+ * List of template variables for dynamic content
+ */
+ variables: z.array(z.string()).optional().describe('Template variables'),
+});
+
+/**
+ * Push Notification Schema
+ *
+ * Defines mobile and web push notification structure.
+ * Supports rich notifications with actions and badges.
+ *
+ * @example
+ * ```json
+ * {
+ * "title": "New Message",
+ * "body": "You have a new message from John",
+ * "icon": "https://example.com/icon.png",
+ * "badge": 5,
+ * "data": {"messageId": "msg_123"},
+ * "actions": [
+ * {"action": "view", "title": "View"},
+ * {"action": "dismiss", "title": "Dismiss"}
+ * ]
+ * }
+ * ```
+ */
+export const PushNotificationSchema = z.object({
+ /**
+ * Notification title
+ */
+ title: z.string().describe('Notification title'),
+
+ /**
+ * Notification body text
+ */
+ body: z.string().describe('Notification body'),
+
+ /**
+ * Icon URL to display with notification
+ */
+ icon: z.string().url().optional().describe('Notification icon URL'),
+
+ /**
+ * Badge count to display on app icon
+ */
+ badge: z.number().optional().describe('Badge count'),
+
+ /**
+ * Custom data payload
+ */
+ data: z.record(z.any()).optional().describe('Custom data'),
+
+ /**
+ * Action buttons for the notification
+ */
+ actions: z.array(z.object({
+ action: z.string().describe('Action identifier'),
+ title: z.string().describe('Action button title'),
+ })).optional().describe('Notification actions'),
+});
+
+/**
+ * In-App Notification Schema
+ *
+ * Defines in-application notification banners and toasts.
+ * Includes severity levels and auto-dismiss settings.
+ *
+ * @example
+ * ```json
+ * {
+ * "title": "System Update",
+ * "message": "New features are now available",
+ * "type": "info",
+ * "actionUrl": "/updates",
+ * "dismissible": true,
+ * "expiresAt": 1704067200000
+ * }
+ * ```
+ */
+export const InAppNotificationSchema = z.object({
+ /**
+ * Notification title
+ */
+ title: z.string().describe('Notification title'),
+
+ /**
+ * Notification message content
+ */
+ message: z.string().describe('Notification message'),
+
+ /**
+ * Notification severity type
+ */
+ type: z.enum(['info', 'success', 'warning', 'error']).describe('Notification type'),
+
+ /**
+ * Optional URL to navigate to when clicked
+ */
+ actionUrl: z.string().optional().describe('Action URL'),
+
+ /**
+ * Whether the notification can be dismissed by the user
+ * @default true
+ */
+ dismissible: z.boolean().optional().default(true).describe('User dismissible'),
+
+ /**
+ * Timestamp when notification expires (Unix milliseconds)
+ */
+ expiresAt: z.number().optional().describe('Expiration timestamp'),
+});
+
+/**
+ * Notification Channel Enum
+ *
+ * Supported notification delivery channels.
+ */
+export const NotificationChannelSchema = z.enum([
+ 'email',
+ 'sms',
+ 'push',
+ 'in-app',
+ 'slack',
+ 'teams',
+ 'webhook',
+]);
+
+/**
+ * Notification Configuration Schema
+ *
+ * Unified notification management protocol supporting multiple channels.
+ * Includes scheduling, retry policies, and delivery tracking.
+ *
+ * @example
+ * ```json
+ * {
+ * "id": "welcome-notification",
+ * "name": "Welcome Email",
+ * "channel": "email",
+ * "template": {
+ * "id": "tpl-001",
+ * "subject": "Welcome!",
+ * "body": "Welcome
",
+ * "bodyType": "html"
+ * },
+ * "recipients": {
+ * "to": ["user@example.com"],
+ * "cc": ["admin@example.com"]
+ * },
+ * "schedule": {
+ * "type": "immediate"
+ * },
+ * "retryPolicy": {
+ * "enabled": true,
+ * "maxRetries": 3,
+ * "backoffStrategy": "exponential"
+ * },
+ * "tracking": {
+ * "trackOpens": true,
+ * "trackClicks": true,
+ * "trackDelivery": true
+ * }
+ * }
+ * ```
+ */
+export const NotificationConfigSchema = z.object({
+ /**
+ * Unique identifier for this notification configuration
+ */
+ id: z.string().describe('Notification ID'),
+
+ /**
+ * Human-readable name for this notification
+ */
+ name: z.string().describe('Notification name'),
+
+ /**
+ * Delivery channel for the notification
+ */
+ channel: NotificationChannelSchema.describe('Notification channel'),
+
+ /**
+ * Notification template based on channel type
+ */
+ template: z.union([
+ EmailTemplateSchema,
+ SMSTemplateSchema,
+ PushNotificationSchema,
+ InAppNotificationSchema,
+ ]).describe('Notification template'),
+
+ /**
+ * Recipient configuration
+ */
+ recipients: z.object({
+ /**
+ * Primary recipients
+ */
+ to: z.array(z.string()).describe('Primary recipients'),
+
+ /**
+ * CC recipients (email only)
+ */
+ cc: z.array(z.string()).optional().describe('CC recipients'),
+
+ /**
+ * BCC recipients (email only)
+ */
+ bcc: z.array(z.string()).optional().describe('BCC recipients'),
+ }).describe('Recipients'),
+
+ /**
+ * Scheduling configuration
+ */
+ schedule: z.object({
+ /**
+ * Scheduling type
+ */
+ type: z.enum(['immediate', 'delayed', 'scheduled']).describe('Schedule type'),
+
+ /**
+ * Delay in milliseconds (for delayed type)
+ */
+ delay: z.number().optional().describe('Delay in milliseconds'),
+
+ /**
+ * Scheduled send time (Unix timestamp in milliseconds)
+ */
+ scheduledAt: z.number().optional().describe('Scheduled timestamp'),
+ }).optional().describe('Scheduling'),
+
+ /**
+ * Retry policy for failed deliveries
+ */
+ retryPolicy: z.object({
+ /**
+ * Enable automatic retries
+ * @default true
+ */
+ enabled: z.boolean().optional().default(true).describe('Enable retries'),
+
+ /**
+ * Maximum number of retry attempts
+ * @default 3
+ */
+ maxRetries: z.number().optional().default(3).describe('Max retry attempts'),
+
+ /**
+ * Backoff strategy for retries
+ */
+ backoffStrategy: z.enum(['exponential', 'linear', 'fixed']).describe('Backoff strategy'),
+ }).optional().describe('Retry policy'),
+
+ /**
+ * Delivery tracking configuration
+ */
+ tracking: z.object({
+ /**
+ * Track when emails are opened
+ * @default false
+ */
+ trackOpens: z.boolean().optional().default(false).describe('Track opens'),
+
+ /**
+ * Track when links are clicked
+ * @default false
+ */
+ trackClicks: z.boolean().optional().default(false).describe('Track clicks'),
+
+ /**
+ * Track delivery status
+ * @default true
+ */
+ trackDelivery: z.boolean().optional().default(true).describe('Track delivery'),
+ }).optional().describe('Tracking configuration'),
+});
+
+// Type exports
+export type NotificationConfig = z.infer;
+export type NotificationChannel = z.infer;
+export type EmailTemplate = z.infer;
+export type SMSTemplate = z.infer;
+export type PushNotification = z.infer;
+export type InAppNotification = z.infer;