Skip to content

Commit 8e5aa99

Browse files
Copilothotlong
andcommitted
feat(kernel): add preview runtime mode with auto-login and simulated identity
Revert marketplace changes (misunderstanding). Instead, add 'preview' to RuntimeMode enum and PreviewModeConfigSchema to KernelContextSchema. This allows the kernel to signal preview mode to the frontend, bypassing auth screens and simulating an admin identity for demos. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent a463c0f commit 8e5aa99

6 files changed

Lines changed: 157 additions & 405 deletions

File tree

packages/spec/src/cloud/app-store.test.ts

Lines changed: 0 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ import {
1414
InstalledAppSummarySchema,
1515
ListInstalledAppsRequestSchema,
1616
ListInstalledAppsResponseSchema,
17-
PreviewRequestSchema,
18-
PreviewObjectSummarySchema,
19-
PreviewViewSummarySchema,
20-
PreviewResponseSchema,
2117
} from './app-store.zod';
2218

2319
describe('ReviewModerationStatusSchema', () => {
@@ -392,180 +388,3 @@ describe('ListInstalledAppsResponseSchema', () => {
392388
expect(parsed.total).toBe(1);
393389
});
394390
});
395-
396-
// ==========================================
397-
// Preview / Demo Mode Tests
398-
// ==========================================
399-
400-
describe('PreviewRequestSchema', () => {
401-
it('should accept minimal preview request', () => {
402-
const request = { listingId: 'listing-001' };
403-
const parsed = PreviewRequestSchema.parse(request);
404-
expect(parsed.listingId).toBe('listing-001');
405-
expect(parsed.version).toBeUndefined();
406-
expect(parsed.includeContent).toBeUndefined();
407-
});
408-
409-
it('should accept preview request with specific version and content types', () => {
410-
const request = {
411-
listingId: 'listing-001',
412-
version: '2.0.0',
413-
includeContent: ['objects', 'views', 'sample_data'] as const,
414-
};
415-
const parsed = PreviewRequestSchema.parse(request);
416-
expect(parsed.version).toBe('2.0.0');
417-
expect(parsed.includeContent).toHaveLength(3);
418-
expect(parsed.includeContent).toContain('objects');
419-
});
420-
421-
it('should accept all valid content types', () => {
422-
const request = {
423-
listingId: 'listing-001',
424-
includeContent: ['objects', 'views', 'dashboards', 'flows', 'sample_data', 'navigation'] as const,
425-
};
426-
const parsed = PreviewRequestSchema.parse(request);
427-
expect(parsed.includeContent).toHaveLength(6);
428-
});
429-
430-
it('should reject invalid content type', () => {
431-
const request = {
432-
listingId: 'listing-001',
433-
includeContent: ['invalid_type'],
434-
};
435-
expect(() => PreviewRequestSchema.parse(request)).toThrow();
436-
});
437-
});
438-
439-
describe('PreviewObjectSummarySchema', () => {
440-
it('should accept object summary with fields', () => {
441-
const summary = {
442-
name: 'lead',
443-
label: 'Lead',
444-
fieldCount: 3,
445-
fields: [
446-
{ name: 'first_name', label: 'First Name', type: 'text' },
447-
{ name: 'email', label: 'Email', type: 'text' },
448-
{ name: 'status', label: 'Status', type: 'select' },
449-
],
450-
};
451-
const parsed = PreviewObjectSummarySchema.parse(summary);
452-
expect(parsed.name).toBe('lead');
453-
expect(parsed.fieldCount).toBe(3);
454-
expect(parsed.fields).toHaveLength(3);
455-
});
456-
457-
it('should accept object summary without fields', () => {
458-
const summary = {
459-
name: 'account',
460-
label: 'Account',
461-
fieldCount: 15,
462-
};
463-
const parsed = PreviewObjectSummarySchema.parse(summary);
464-
expect(parsed.fields).toBeUndefined();
465-
});
466-
});
467-
468-
describe('PreviewViewSummarySchema', () => {
469-
it('should accept list view summary', () => {
470-
const summary = {
471-
name: 'all_leads',
472-
label: 'All Leads',
473-
type: 'list' as const,
474-
objectName: 'lead',
475-
};
476-
const parsed = PreviewViewSummarySchema.parse(summary);
477-
expect(parsed.type).toBe('list');
478-
});
479-
480-
it('should accept form view summary', () => {
481-
const summary = {
482-
name: 'lead_form',
483-
label: 'Lead Form',
484-
type: 'form' as const,
485-
objectName: 'lead',
486-
};
487-
const parsed = PreviewViewSummarySchema.parse(summary);
488-
expect(parsed.type).toBe('form');
489-
});
490-
491-
it('should reject invalid view type', () => {
492-
expect(() => PreviewViewSummarySchema.parse({
493-
name: 'test',
494-
label: 'Test',
495-
type: 'invalid',
496-
objectName: 'test',
497-
})).toThrow();
498-
});
499-
});
500-
501-
describe('PreviewResponseSchema', () => {
502-
it('should accept minimal preview response', () => {
503-
const response = {
504-
listingId: 'listing-001',
505-
name: 'Acme CRM',
506-
version: '2.0.0',
507-
};
508-
const parsed = PreviewResponseSchema.parse(response);
509-
expect(parsed.listingId).toBe('listing-001');
510-
expect(parsed.objects).toBeUndefined();
511-
expect(parsed.views).toBeUndefined();
512-
});
513-
514-
it('should accept full preview response with all content', () => {
515-
const response = {
516-
listingId: 'listing-001',
517-
name: 'Acme CRM',
518-
version: '2.0.0',
519-
demoUrl: 'https://demo.acme.com/crm',
520-
objects: [
521-
{ name: 'lead', label: 'Lead', fieldCount: 10, fields: [
522-
{ name: 'first_name', label: 'First Name', type: 'text' },
523-
]},
524-
{ name: 'account', label: 'Account', fieldCount: 8 },
525-
],
526-
views: [
527-
{ name: 'all_leads', label: 'All Leads', type: 'list' as const, objectName: 'lead' },
528-
{ name: 'lead_form', label: 'Lead Form', type: 'form' as const, objectName: 'lead' },
529-
],
530-
dashboards: [
531-
{ name: 'sales_overview', label: 'Sales Overview' },
532-
],
533-
flows: [
534-
{ name: 'lead_assignment', label: 'Lead Assignment', type: 'autolaunched' },
535-
],
536-
navigation: [
537-
{ id: 'nav_leads', label: 'Leads', type: 'object' },
538-
{ id: 'nav_dashboard', label: 'Dashboard', type: 'dashboard' },
539-
],
540-
sampleData: {
541-
lead: 50,
542-
account: 20,
543-
},
544-
expiresAt: '2025-06-01T12:00:00Z',
545-
};
546-
const parsed = PreviewResponseSchema.parse(response);
547-
expect(parsed.objects).toHaveLength(2);
548-
expect(parsed.views).toHaveLength(2);
549-
expect(parsed.dashboards).toHaveLength(1);
550-
expect(parsed.flows).toHaveLength(1);
551-
expect(parsed.navigation).toHaveLength(2);
552-
expect(parsed.sampleData?.lead).toBe(50);
553-
expect(parsed.demoUrl).toBe('https://demo.acme.com/crm');
554-
expect(parsed.expiresAt).toBe('2025-06-01T12:00:00Z');
555-
});
556-
557-
it('should accept preview response with only objects', () => {
558-
const response = {
559-
listingId: 'listing-001',
560-
name: 'Acme Utils',
561-
version: '1.0.0',
562-
objects: [
563-
{ name: 'task', label: 'Task', fieldCount: 5 },
564-
],
565-
};
566-
const parsed = PreviewResponseSchema.parse(response);
567-
expect(parsed.objects).toHaveLength(1);
568-
expect(parsed.views).toBeUndefined();
569-
expect(parsed.dashboards).toBeUndefined();
570-
});
571-
});

packages/spec/src/cloud/app-store.zod.ts

Lines changed: 0 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -376,139 +376,6 @@ export const ListInstalledAppsResponseSchema = z.object({
376376
pageSize: z.number().int().min(1),
377377
});
378378

379-
// ==========================================
380-
// Preview / Demo Mode (No Login Required)
381-
// ==========================================
382-
383-
/**
384-
* Preview Request Schema
385-
*
386-
* Allows customers to request a preview of a marketplace listing's content
387-
* without registration or login. Analogous to Salesforce AppExchange
388-
* "Test Drive" or Shopify App Store live demo.
389-
*
390-
* ## Use Case
391-
* A marketplace visitor wants to evaluate a package before committing
392-
* to install. They can browse object definitions, view layouts, sample
393-
* data, and navigation structure — all without authentication.
394-
*/
395-
export const PreviewRequestSchema = z.object({
396-
/** Listing ID to preview */
397-
listingId: z.string().describe('Marketplace listing ID to preview'),
398-
399-
/** Specific version to preview (defaults to latest) */
400-
version: z.string().optional().describe('Version to preview (defaults to latest)'),
401-
402-
/**
403-
* Content types to include in the preview response.
404-
* If omitted, returns all content types enabled by the publisher.
405-
*/
406-
includeContent: z.array(z.enum([
407-
'objects', // Object definitions (fields, relationships)
408-
'views', // List and form view definitions
409-
'dashboards', // Dashboard layouts
410-
'flows', // Automation flow definitions
411-
'sample_data', // Seed/demo data
412-
'navigation', // App navigation structure
413-
])).optional()
414-
.describe('Content types to include (defaults to all enabled by publisher)'),
415-
});
416-
417-
/**
418-
* Preview Object Summary — a read-only snapshot of an object definition
419-
*/
420-
export const PreviewObjectSummarySchema = z.object({
421-
/** Object name (snake_case) */
422-
name: z.string().describe('Object machine name'),
423-
424-
/** Display label */
425-
label: z.string().describe('Object display label'),
426-
427-
/** Number of fields */
428-
fieldCount: z.number().int().min(0).describe('Total field count'),
429-
430-
/** Field summaries (name + type + label) */
431-
fields: z.array(z.object({
432-
name: z.string().describe('Field machine name'),
433-
label: z.string().describe('Field display label'),
434-
type: z.string().describe('Field type (text, number, lookup, etc.)'),
435-
})).optional().describe('Field summaries'),
436-
});
437-
438-
/**
439-
* Preview View Summary — a read-only snapshot of a view definition
440-
*/
441-
export const PreviewViewSummarySchema = z.object({
442-
/** View name */
443-
name: z.string().describe('View name'),
444-
445-
/** Display label */
446-
label: z.string().describe('View display label'),
447-
448-
/** View type (list or form) */
449-
type: z.enum(['list', 'form']).describe('View type'),
450-
451-
/** Target object name */
452-
objectName: z.string().describe('Target object name'),
453-
});
454-
455-
/**
456-
* Preview Response Schema
457-
*
458-
* A read-only snapshot of the package contents for evaluation.
459-
* No authentication required to receive this response.
460-
*/
461-
export const PreviewResponseSchema = z.object({
462-
/** Listing ID */
463-
listingId: z.string().describe('Marketplace listing ID'),
464-
465-
/** Package display name */
466-
name: z.string().describe('Package display name'),
467-
468-
/** Version being previewed */
469-
version: z.string().describe('Version being previewed'),
470-
471-
/** External demo URL (if available) */
472-
demoUrl: z.string().url().optional()
473-
.describe('External demo URL for live interactive preview'),
474-
475-
/** Object definitions included in the package */
476-
objects: z.array(PreviewObjectSummarySchema).optional()
477-
.describe('Object definitions'),
478-
479-
/** View definitions included in the package */
480-
views: z.array(PreviewViewSummarySchema).optional()
481-
.describe('View definitions'),
482-
483-
/** Dashboard names included in the package */
484-
dashboards: z.array(z.object({
485-
name: z.string(),
486-
label: z.string(),
487-
})).optional().describe('Dashboard summaries'),
488-
489-
/** Flow/automation names included in the package */
490-
flows: z.array(z.object({
491-
name: z.string(),
492-
label: z.string(),
493-
type: z.string().describe('Flow type (autolaunched, screen, schedule, etc.)'),
494-
})).optional().describe('Automation flow summaries'),
495-
496-
/** Navigation structure */
497-
navigation: z.array(z.object({
498-
id: z.string(),
499-
label: z.string(),
500-
type: z.string().describe('Navigation item type'),
501-
})).optional().describe('Navigation structure'),
502-
503-
/** Sample data record counts by object */
504-
sampleData: z.record(z.string(), z.number().int().min(0)).optional()
505-
.describe('Sample data record counts keyed by object name'),
506-
507-
/** Preview session expiration (if applicable) */
508-
expiresAt: z.string().datetime().optional()
509-
.describe('Preview session expiration timestamp'),
510-
});
511-
512379
// ==========================================
513380
// Export Types
514381
// ==========================================
@@ -527,7 +394,3 @@ export type AppSubscription = z.infer<typeof AppSubscriptionSchema>;
527394
export type InstalledAppSummary = z.infer<typeof InstalledAppSummarySchema>;
528395
export type ListInstalledAppsRequest = z.infer<typeof ListInstalledAppsRequestSchema>;
529396
export type ListInstalledAppsResponse = z.infer<typeof ListInstalledAppsResponseSchema>;
530-
export type PreviewRequest = z.infer<typeof PreviewRequestSchema>;
531-
export type PreviewObjectSummary = z.infer<typeof PreviewObjectSummarySchema>;
532-
export type PreviewViewSummary = z.infer<typeof PreviewViewSummarySchema>;
533-
export type PreviewResponse = z.infer<typeof PreviewResponseSchema>;

packages/spec/src/cloud/marketplace.test.ts

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -167,55 +167,6 @@ describe('MarketplaceListingSchema', () => {
167167
};
168168
expect(() => MarketplaceListingSchema.parse(listing)).toThrow();
169169
});
170-
171-
it('should accept listing with preview mode enabled', () => {
172-
const listing = {
173-
id: 'listing-002',
174-
packageId: 'com.acme.crm',
175-
publisherId: 'pub-001',
176-
name: 'Acme CRM',
177-
category: 'crm' as const,
178-
latestVersion: '1.0.0',
179-
preview: {
180-
enabled: true,
181-
demoUrl: 'https://demo.acme.com/crm',
182-
includedContent: ['objects', 'views', 'navigation'],
183-
expiresInSeconds: 3600,
184-
},
185-
};
186-
const parsed = MarketplaceListingSchema.parse(listing);
187-
expect(parsed.preview?.enabled).toBe(true);
188-
expect(parsed.preview?.demoUrl).toBe('https://demo.acme.com/crm');
189-
expect(parsed.preview?.includedContent).toHaveLength(3);
190-
expect(parsed.preview?.expiresInSeconds).toBe(3600);
191-
});
192-
193-
it('should default preview.enabled to false', () => {
194-
const listing = {
195-
id: 'listing-003',
196-
packageId: 'com.acme.utils',
197-
publisherId: 'pub-001',
198-
name: 'Acme Utils',
199-
category: 'other' as const,
200-
latestVersion: '1.0.0',
201-
preview: {},
202-
};
203-
const parsed = MarketplaceListingSchema.parse(listing);
204-
expect(parsed.preview?.enabled).toBe(false);
205-
});
206-
207-
it('should accept listing without preview (optional)', () => {
208-
const listing = {
209-
id: 'listing-004',
210-
packageId: 'com.acme.tools',
211-
publisherId: 'pub-001',
212-
name: 'Acme Tools',
213-
category: 'other' as const,
214-
latestVersion: '1.0.0',
215-
};
216-
const parsed = MarketplaceListingSchema.parse(listing);
217-
expect(parsed.preview).toBeUndefined();
218-
});
219170
});
220171

221172
describe('PackageSubmissionSchema', () => {

0 commit comments

Comments
 (0)