diff --git a/content/docs/references/kernel/context.mdx b/content/docs/references/kernel/context.mdx index 8e7b3c9a4e..2a317a8130 100644 --- a/content/docs/references/kernel/context.mdx +++ b/content/docs/references/kernel/context.mdx @@ -14,8 +14,8 @@ Defines the operating mode of the kernel ## TypeScript Usage ```typescript -import { KernelContext, RuntimeMode } from '@objectstack/spec/kernel'; -import type { KernelContext, RuntimeMode } from '@objectstack/spec/kernel'; +import { KernelContext, RuntimeMode, PreviewModeConfig } from '@objectstack/spec/kernel'; +import type { KernelContext, RuntimeMode, PreviewModeConfig } from '@objectstack/spec/kernel'; // Validate data const result = KernelContext.parse(data); @@ -30,13 +30,14 @@ const result = KernelContext.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **instanceId** | `string` | ✅ | Unique UUID for this running kernel process | -| **mode** | `Enum<'development' \| 'production' \| 'test' \| 'provisioning'>` | ✅ | Kernel operating mode | +| **mode** | `Enum<'development' \| 'production' \| 'test' \| 'provisioning' \| 'preview'>` | ✅ | Kernel operating mode | | **version** | `string` | ✅ | Kernel version | | **appName** | `string` | optional | Host application name | | **cwd** | `string` | ✅ | Current working directory | | **workspaceRoot** | `string` | optional | Workspace root if different from cwd | | **startTime** | `integer` | ✅ | Boot timestamp (ms) | | **features** | `Record` | ✅ | Global feature toggles | +| **previewMode** | `PreviewModeConfig` | optional | Preview/demo mode configuration (used when mode is `'preview'`) | --- @@ -47,11 +48,56 @@ Kernel operating mode ### Allowed Values -* `development` -* `production` -* `test` -* `provisioning` +* `development` — Hot-reload, verbose logging +* `production` — Optimized, strict security +* `test` — Mocked interfaces +* `provisioning` — Setup/Migration mode +* `preview` — Demo/preview mode — bypass auth, simulate admin identity --- +## PreviewModeConfig + +Configures the kernel's preview/demo mode behaviour. When `mode` is set to `'preview'`, the platform +skips authentication screens and simulates an admin identity so visitors can explore the system without +registering or logging in. + + +**Security:** Preview mode should **never** be used in production environments. + + +### Properties + +| Property | Type | Default | Description | +| :--- | :--- | :--- | :--- | +| **autoLogin** | `boolean` | `true` | Auto-login as simulated user, skipping login/registration pages | +| **simulatedRole** | `Enum<'admin' \| 'user' \| 'viewer'>` | `'admin'` | Permission role for the simulated preview user | +| **simulatedUserName** | `string` | `'Preview User'` | Display name for the simulated preview user | +| **readOnly** | `boolean` | `false` | Restrict the preview session to read-only operations | +| **expiresInSeconds** | `integer` | `0` | Preview session duration in seconds (0 = no expiration) | +| **bannerMessage** | `string` | — | Banner message displayed in the UI during preview mode | + +### Example + +```typescript +import { KernelContextSchema } from '@objectstack/spec/kernel'; + +const ctx = KernelContextSchema.parse({ + instanceId: '550e8400-e29b-41d4-a716-446655440000', + mode: 'preview', + version: '1.0.0', + cwd: '/app', + startTime: Date.now(), + previewMode: { + autoLogin: true, + simulatedRole: 'admin', + simulatedUserName: 'Demo Admin', + readOnly: false, + bannerMessage: 'You are exploring a demo — data will be reset periodically.', + }, +}); +``` + +--- + diff --git a/examples/README.md b/examples/README.md index 7a7e990566..988d315f11 100644 --- a/examples/README.md +++ b/examples/README.md @@ -152,14 +152,18 @@ pnpm build **Level:** 🔴 Advanced **Protocols:** System, API, Data -**Complete server implementation** showing how to build a metadata-driven backend. Features dynamic schema loading from plugins, auto-generated REST APIs, unified metadata API, and plugin orchestration. +**Complete server implementation** showing how to build a metadata-driven backend. Features dynamic schema loading from plugins, auto-generated REST APIs, unified metadata API, plugin orchestration, and **preview/demo mode**. + +**Preview Mode:** Run with `OS_MODE=preview` to bypass login/registration and simulate an admin identity — ideal for marketplace demos and app showcases. **Quick Start:** ```bash cd examples/app-host pnpm install pnpm dev -# API available at http://localhost:3000 + +# Preview mode (no login required) +OS_MODE=preview pnpm dev ``` --- @@ -225,6 +229,7 @@ pnpm typecheck |----------|---------|----------| | Manifest | ✅ Complete | All examples with `objectstack.config.ts` | | Plugin System | ✅ Complete | [App Host](./app-host/) | +| Preview Mode | ✅ Complete | [App Host](./app-host/) — `OS_MODE=preview` | | Datasources | 🟡 Partial | [App Host](./app-host/) | | I18n / Translations | ✅ Complete | [Todo Translations](./app-todo/src/translations/), [CRM Translations](./app-crm/src/translations/) | | Job Scheduling | 🔴 Missing | _Planned_ | diff --git a/examples/app-host/README.md b/examples/app-host/README.md index 430c5fad09..d16943fcce 100644 --- a/examples/app-host/README.md +++ b/examples/app-host/README.md @@ -9,6 +9,7 @@ It demonstrates how to build a metadata-driven backend that dynamically loads ob - **Unified Metadata API**: `/api/v1/meta/objects` - **Unified Data API**: `/api/v1/data/:object` (CRUD) - **Zero-Code Backend**: No creating routes or controllers per object. +- **Preview Mode**: Run in demo mode — bypass login, auto-simulate admin identity. ## Setup @@ -28,6 +29,62 @@ It demonstrates how to build a metadata-driven backend that dynamically loads ob # Expected: Server starts at http://localhost:3000 ``` +3. Run in **preview mode** (skip login, simulate admin): + ```bash + OS_MODE=preview pnpm dev + # Expected: Server starts in preview mode — no login required + ``` + +## Preview / Demo Mode + +Preview mode allows visitors (e.g. marketplace customers) to explore the platform +without registering or logging in. The kernel boots with `mode: 'preview'` and the +frontend skips authentication screens, automatically simulating an admin session. + +### How It Works + +1. The runtime reads `OS_MODE=preview` from the environment (or the stack config). +2. The `KernelContext` is created with `mode: 'preview'` and a `previewMode` config. +3. The frontend detects `mode === 'preview'` and: + - Hides the login / registration pages. + - Automatically creates a simulated admin session. + - Shows a preview banner to indicate demo mode. + +### Configuration + +```typescript +import { KernelContextSchema } from '@objectstack/spec/kernel'; + +const ctx = KernelContextSchema.parse({ + instanceId: '550e8400-e29b-41d4-a716-446655440000', + mode: 'preview', + version: '1.0.0', + cwd: process.cwd(), + startTime: Date.now(), + previewMode: { + autoLogin: true, // Skip login/registration pages + simulatedRole: 'admin', // Simulated user role (admin | user | viewer) + simulatedUserName: 'Demo Admin', + readOnly: false, // Allow writes (set true for read-only demos) + expiresInSeconds: 3600, // Session expires after 1 hour (0 = no expiration) + bannerMessage: 'You are exploring a demo — data will be reset periodically.', + }, +}); +``` + +### PreviewModeConfig Properties + +| Property | Type | Default | Description | +|:---|:---|:---|:---| +| **autoLogin** | `boolean` | `true` | Auto-login as simulated user, skip login/registration | +| **simulatedRole** | `'admin' \| 'user' \| 'viewer'` | `'admin'` | Permission role for the simulated user | +| **simulatedUserName** | `string` | `'Preview User'` | Display name shown in the UI | +| **readOnly** | `boolean` | `false` | Block all write operations | +| **expiresInSeconds** | `integer` | `0` | Session duration (0 = no expiration) | +| **bannerMessage** | `string` | — | Banner message displayed in the UI | + +> **⚠️ Security:** Preview mode should NEVER be used in production environments. + ## API Usage Examples ### 1. Get All Objects diff --git a/examples/app-host/objectstack.config.ts b/examples/app-host/objectstack.config.ts index 0bf276a241..464c95a2d8 100644 --- a/examples/app-host/objectstack.config.ts +++ b/examples/app-host/objectstack.config.ts @@ -33,3 +33,65 @@ export default defineStack({ new AppPlugin(BiPluginManifest) ] }); + +/** + * Preview Mode Host Example + * + * Demonstrates how to run the platform in "preview" mode. + * When `mode` is set to `'preview'`, the kernel signals the frontend to: + * - Skip login/registration screens + * - Automatically simulate an admin identity + * - Display a preview-mode banner to the user + * + * Use this for marketplace demos, app showcases, or onboarding + * tours where visitors should explore the system without signing up. + * + * ## Usage + * + * Set the `OS_MODE` environment variable to `preview` at boot: + * + * ```bash + * OS_MODE=preview pnpm dev + * ``` + * + * Or use this stack definition directly as a starting point. + * + * ## KernelContext (created by the Runtime at boot) + * + * ```ts + * import { KernelContextSchema } from '@objectstack/spec/kernel'; + * + * const ctx = KernelContextSchema.parse({ + * instanceId: '550e8400-e29b-41d4-a716-446655440000', + * mode: 'preview', + * version: '1.0.0', + * cwd: process.cwd(), + * startTime: Date.now(), + * previewMode: { + * autoLogin: true, + * simulatedRole: 'admin', + * simulatedUserName: 'Demo Admin', + * readOnly: false, + * bannerMessage: 'You are exploring a demo — data will be reset periodically.', + * }, + * }); + * ``` + */ +export const PreviewHostExample = defineStack({ + manifest: { + id: 'app-host-preview', + name: 'app_host_preview', + version: '1.0.0', + description: 'Host application in preview/demo mode — bypasses login, simulates admin user', + type: 'app', + }, + + // Same plugins as the standard host + plugins: [ + new ObjectQLPlugin(), + new DriverPlugin(new InMemoryDriver()), + new AppPlugin(CrmApp), + new AppPlugin(TodoApp), + new AppPlugin(BiPluginManifest) + ] +}); diff --git a/packages/spec/src/kernel/context.test.ts b/packages/spec/src/kernel/context.test.ts index 9f07970459..d6a26fc87c 100644 --- a/packages/spec/src/kernel/context.test.ts +++ b/packages/spec/src/kernel/context.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import { RuntimeMode, KernelContextSchema, + PreviewModeConfigSchema, type KernelContext, } from './context.zod'; @@ -11,6 +12,7 @@ describe('RuntimeMode', () => { expect(() => RuntimeMode.parse('production')).not.toThrow(); expect(() => RuntimeMode.parse('test')).not.toThrow(); expect(() => RuntimeMode.parse('provisioning')).not.toThrow(); + expect(() => RuntimeMode.parse('preview')).not.toThrow(); }); it('should reject invalid runtime modes', () => { @@ -87,10 +89,81 @@ describe('KernelContextSchema', () => { }); it('should accept all runtime modes in context', () => { - const modes = ['development', 'production', 'test', 'provisioning'] as const; + const modes = ['development', 'production', 'test', 'provisioning', 'preview'] as const; modes.forEach(mode => { const parsed = KernelContextSchema.parse({ ...validContext, mode }); expect(parsed.mode).toBe(mode); }); }); + + it('should accept preview mode with previewMode config', () => { + const parsed = KernelContextSchema.parse({ + ...validContext, + mode: 'preview', + previewMode: { + autoLogin: true, + simulatedRole: 'admin', + simulatedUserName: 'Demo Admin', + readOnly: true, + expiresInSeconds: 3600, + bannerMessage: 'You are viewing a demo of this application.', + }, + }); + expect(parsed.mode).toBe('preview'); + expect(parsed.previewMode?.autoLogin).toBe(true); + expect(parsed.previewMode?.simulatedRole).toBe('admin'); + expect(parsed.previewMode?.simulatedUserName).toBe('Demo Admin'); + expect(parsed.previewMode?.readOnly).toBe(true); + expect(parsed.previewMode?.expiresInSeconds).toBe(3600); + expect(parsed.previewMode?.bannerMessage).toContain('demo'); + }); + + it('should accept context without previewMode (optional)', () => { + const parsed = KernelContextSchema.parse(validContext); + expect(parsed.previewMode).toBeUndefined(); + }); +}); + +describe('PreviewModeConfigSchema', () => { + it('should apply defaults for zero-config preview', () => { + const parsed = PreviewModeConfigSchema.parse({}); + expect(parsed.autoLogin).toBe(true); + expect(parsed.simulatedRole).toBe('admin'); + expect(parsed.simulatedUserName).toBe('Preview User'); + expect(parsed.readOnly).toBe(false); + expect(parsed.expiresInSeconds).toBe(0); + expect(parsed.bannerMessage).toBeUndefined(); + }); + + it('should accept all simulated roles', () => { + const roles = ['admin', 'user', 'viewer'] as const; + roles.forEach(role => { + const parsed = PreviewModeConfigSchema.parse({ simulatedRole: role }); + expect(parsed.simulatedRole).toBe(role); + }); + }); + + it('should reject invalid simulated role', () => { + expect(() => PreviewModeConfigSchema.parse({ simulatedRole: 'superadmin' })).toThrow(); + }); + + it('should accept read-only preview for marketplace demos', () => { + const parsed = PreviewModeConfigSchema.parse({ + autoLogin: true, + simulatedRole: 'viewer', + readOnly: true, + bannerMessage: 'This is a preview. Sign up to get started!', + }); + expect(parsed.readOnly).toBe(true); + expect(parsed.simulatedRole).toBe('viewer'); + expect(parsed.bannerMessage).toContain('preview'); + }); + + it('should reject negative expiresInSeconds', () => { + expect(() => PreviewModeConfigSchema.parse({ expiresInSeconds: -1 })).toThrow(); + }); + + it('should reject non-integer expiresInSeconds', () => { + expect(() => PreviewModeConfigSchema.parse({ expiresInSeconds: 1.5 })).toThrow(); + }); }); diff --git a/packages/spec/src/kernel/context.zod.ts b/packages/spec/src/kernel/context.zod.ts index 06cf306350..7904a9539b 100644 --- a/packages/spec/src/kernel/context.zod.ts +++ b/packages/spec/src/kernel/context.zod.ts @@ -10,11 +10,84 @@ export const RuntimeMode = z.enum([ 'development', // Hot-reload, verbose logging 'production', // Optimized, strict security 'test', // Mocked interfaces - 'provisioning' // Setup/Migration mode + 'provisioning', // Setup/Migration mode + 'preview', // Demo/preview mode — bypass auth, simulate admin identity ]).describe('Kernel operating mode'); export type RuntimeMode = z.infer; +/** + * Preview Mode Configuration Schema + * + * Configures the kernel's preview/demo mode behaviour. + * When `mode` is set to `'preview'`, the platform skips authentication + * screens and optionally simulates an admin identity so that visitors + * (e.g. app-marketplace customers) can explore the system without + * registering or logging in. + * + * **Security note:** preview mode should NEVER be used in production. + * The runtime must enforce this constraint. + * + * @example + * ```ts + * const ctx = KernelContextSchema.parse({ + * instanceId: '550e8400-e29b-41d4-a716-446655440000', + * mode: 'preview', + * version: '1.0.0', + * cwd: '/app', + * startTime: Date.now(), + * previewMode: { + * autoLogin: true, + * simulatedRole: 'admin', + * }, + * }); + * ``` + */ +export const PreviewModeConfigSchema = z.object({ + /** + * Automatically log in as a simulated user on startup. + * When enabled, the frontend skips login/registration screens entirely. + */ + autoLogin: z.boolean().default(true) + .describe('Auto-login as simulated user, skipping login/registration pages'), + + /** + * Role of the simulated user. + * Determines the permission level of the auto-created preview session. + */ + simulatedRole: z.enum(['admin', 'user', 'viewer']).default('admin') + .describe('Permission role for the simulated preview user'), + + /** + * Display name for the simulated user shown in the UI. + */ + simulatedUserName: z.string().default('Preview User') + .describe('Display name for the simulated preview user'), + + /** + * Whether the preview session is read-only. + * When true, all write operations (create, update, delete) are blocked. + */ + readOnly: z.boolean().default(false) + .describe('Restrict the preview session to read-only operations'), + + /** + * Session duration in seconds. After expiry the preview session ends. + * 0 means no expiration. + */ + expiresInSeconds: z.number().int().min(0).default(0) + .describe('Preview session duration in seconds (0 = no expiration)'), + + /** + * Optional banner message shown in the UI to indicate preview mode. + * Useful for marketplace demos so visitors know they are in a sandbox. + */ + bannerMessage: z.string().optional() + .describe('Banner message displayed in the UI during preview mode'), +}); + +export type PreviewModeConfig = z.infer; + /** * Kernel Context Schema * Defines the static environment information available to the Kernel at boot. @@ -46,7 +119,15 @@ export const KernelContextSchema = z.object({ /** * Feature Flags (Global) */ - features: z.record(z.string(), z.boolean()).default({}).describe('Global feature toggles') + features: z.record(z.string(), z.boolean()).default({}).describe('Global feature toggles'), + + /** + * Preview Mode Configuration. + * Only relevant when `mode` is `'preview'`. Configures auto-login, + * simulated identity, read-only restrictions, and UI banner. + */ + previewMode: PreviewModeConfigSchema.optional() + .describe('Preview/demo mode configuration (used when mode is "preview")'), }); export type KernelContext = z.infer;