The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 81afd7789b27Commit detailsView latest

apps/dashboard/src/preview.ts 679 lines · 21.2 KB · typescript
1
export const previewKey = 'openclasp.preview.v1';
2
3
export const previewSession = {
4
  user: {
5
    sub: 'local-preview',
6
    name: 'Local preview',
7
    email: 'preview@localhost',
8
  },
9
};
10
11
export type DashboardData = {
12
  agents: Record<string, any>[];
13
  projects: Record<string, any>[];
14
  installations: Record<string, any>[];
15
  setupRequests: Record<string, any>[];
16
  publications: Record<string, any>[];
17
  interactions: Record<string, any>[];
18
  federatedInteractions: Record<string, any>[];
19
  liveSessions: Record<string, any>[];
20
  hostedThreads: Record<string, any>[];
21
  events: Record<string, any>[];
22
  conflicts: Record<string, any>[];
23
  receipts: Record<string, any>[];
24
  profiles: Record<string, any>[];
25
  counterpartyBriefs: Record<string, any>[];
26
  completionReports: Record<string, any>[];
27
  feedbackRequests: Record<string, any>[];
28
  interactionFeedback: Record<string, any>[];
29
  interactionConclusions: Record<string, any>[];
30
  learningEligibility: Record<string, any>[];
31
  profileDeltas: Record<string, any>[];
32
  runtimes: Record<string, any>[];
33
  accessTokens: Record<string, any>[];
34
};
35
36
export type Settings = {
37
  displayName: string;
38
  contributionEnabled: boolean;
39
  retentionDays: number;
40
  evidenceSharing: 'never' | 'ask' | 'contract_only';
41
  rawConversationsStored: false;
42
};
43
44
export const defaultPreviewSettings: Settings = {
45
  displayName: 'Local operator',
46
  contributionEnabled: false,
47
  retentionDays: 30,
48
  evidenceSharing: 'ask',
49
  rawConversationsStored: false,
50
};
51
52
const ago = (minutes: number) => new Date(Date.now() - minutes * 60_000).toISOString();
53
const ahead = (minutes: number) => new Date(Date.now() + minutes * 60_000).toISOString();
54
55
export function createPreviewData(): DashboardData {
56
  return {
57
    projects: [
58
      { projectId: 'proj_ops', name: 'Operations', createdAt: ago(21 * 24 * 60) },
59
      { projectId: 'proj_research', name: 'Research', createdAt: ago(12 * 24 * 60) },
60
    ],
61
    agents: [
62
      {
63
        agentId: 'agent_atlas',
64
        projectId: 'proj_research',
65
        name: 'Atlas Research',
66
        description: 'Literature review and planning',
67
        framework: 'Claude',
68
        agentVersion: '1.4.2',
69
        agentMode: 'persistent_runtime',
70
        a2aEndpoint: 'https://atlas.example.com/a2a',
71
        autoPublish: true,
72
        autoAcceptPolicy: 'safe_matching',
73
        autoAcceptTaskCategories: ['research', 'planning'],
74
        capabilities: ['research', 'planning', 'summarization'],
75
        limitations: ['no payments'],
76
        identityMode: 'oauth_installation',
77
        status: 'active',
78
        createdAt: ago(12 * 24 * 60),
79
        updatedAt: ago(8),
80
        presence: { status: 'online', lastSeenAt: ago(1) },
81
      },
82
      {
83
        agentId: 'agent_harbor',
84
        projectId: 'proj_ops',
85
        name: 'Harbor Ops',
86
        description: 'Incident triage and runbooks',
87
        framework: 'GPT',
88
        agentVersion: '2.1.0',
89
        agentMode: 'temporary_chat',
90
        autoPublish: false,
91
        autoAcceptPolicy: 'off',
92
        autoAcceptTaskCategories: [],
93
        capabilities: ['ops', 'triage'],
94
        limitations: ['no production writes'],
95
        identityMode: 'oauth_installation',
96
        status: 'active',
97
        createdAt: ago(21 * 24 * 60),
98
        updatedAt: ago(180),
99
        presence: { status: 'offline', lastSeenAt: ago(180) },
100
      },
101
    ],
102
    installations: [
103
      {
104
        installationId: 'inst_atlas',
105
        clientId: 'mcp_atlas',
106
        agentId: 'agent_atlas',
107
        projectId: 'proj_research',
108
        connectedAt: ago(12 * 24 * 60),
109
        updatedAt: ago(8),
110
      },
111
    ],
112
    setupRequests: [
113
      {
114
        requestId: 'setup_lumen',
115
        clientId: 'mcp_lumen',
116
        action: 'connect',
117
        status: 'pending',
118
        agentName: 'Lumen Writer',
119
        projectName: 'Content',
120
        framework: 'Claude',
121
        agentVersion: '0.9.0',
122
        autoPublish: true,
123
        autoAcceptPolicy: 'safe_matching',
124
        autoAcceptTaskCategories: ['writing', 'editing'],
125
        capabilities: ['writing', 'editing', 'research'],
126
        limitations: ['no outbound email'],
127
        requestedAt: ago(18),
128
      },
129
    ],
130
    publications: [{ agentId: 'agent_atlas', published: true, updatedAt: ago(40) }],
131
    interactions: [
132
      {
133
        interactionId: 'ix_local_brief',
134
        agentId: 'agent_atlas',
135
        status: 'completed',
136
        contract: {
137
          purpose: 'Produce a source-backed research brief',
138
          taskCategory: 'research',
139
        },
140
        createdAt: ago(240),
141
        completedAt: ago(220),
142
      },
143
    ],
144
    federatedInteractions: [
145
      {
146
        interactionId: 'ix_shared_review',
147
        initiatorAgentId: 'agent_peer_nova',
148
        responderAgentId: 'agent_atlas',
149
        status: 'pending',
150
        termsHash: '7f3c91a0b2e84d11c6aa09f1d3e8b470',
151
        createdAt: ago(26),
152
        contract: {
153
          purpose: 'Review a research brief for scope and sources',
154
          taskCategory: 'research',
155
        },
156
      },
157
      {
158
        interactionId: 'ix_shared_ops',
159
        initiatorAgentId: 'agent_atlas',
160
        responderAgentId: 'agent_peer_keel',
161
        status: 'active',
162
        termsHash: '12ab44c0e91f77d0aa3310bc44ee9012',
163
        createdAt: ago(90),
164
        contract: {
165
          purpose: 'Share a sanitized incident timeline',
166
          taskCategory: 'coordination',
167
        },
168
        acceptances: {
169
          agent_peer_keel: { method: 'policy_auto_accept' },
170
        },
171
      },
172
    ],
173
    liveSessions: [
174
      {
175
        interactionId: 'ix_shared_ops',
176
        initiatorAgentId: 'agent_atlas',
177
        responderAgentId: 'agent_peer_keel',
178
        status: 'active',
179
        createdAt: ago(88),
180
        activatedAt: ago(87),
181
        expiresAt: ahead(12),
182
      },
183
    ],
184
    hostedThreads: [],
185
    events: [
186
      {
187
        eventId: 'evt_1',
188
        eventType: 'interaction_started',
189
        agentId: 'agent_atlas',
190
        interactionId: 'ix_shared_ops',
191
        timestamp: ago(88),
192
        visibility: 'participants',
193
      },
194
      {
195
        eventId: 'evt_2',
196
        eventType: 'policy_warning',
197
        agentId: 'agent_harbor',
198
        interactionId: 'ix_local_brief',
199
        timestamp: ago(200),
200
        visibility: 'owner',
201
      },
202
      {
203
        eventId: 'evt_3',
204
        eventType: 'claim_checked',
205
        agentId: 'agent_atlas',
206
        interactionId: 'ix_local_brief',
207
        timestamp: ago(230),
208
        visibility: 'participants',
209
      },
210
      {
211
        eventId: 'evt_4',
212
        eventType: 'session_activated',
213
        agentId: 'agent_atlas',
214
        interactionId: 'ix_shared_ops',
215
        timestamp: ago(87),
216
        visibility: 'participants',
217
      },
218
    ],
219
    conflicts: [
220
      {
221
        conflictId: 'dsp_1',
222
        interactionId: 'ix_local_brief',
223
        status: 'open',
224
        createdAt: ago(190),
225
      },
226
    ],
227
    receipts: [
228
      {
229
        receiptId: 'rcpt_1',
230
        interactionId: 'ix_local_brief',
231
        agentId: 'agent_atlas',
232
        outcome: 'success',
233
        completedAt: ago(220),
234
      },
235
      {
236
        receiptId: 'rcpt_ops_provisional',
237
        interactionId: 'ix_shared_ops',
238
        outcome: 'partial',
239
        unilateral: true,
240
        provisional: true,
241
        confidence: 0.42,
242
        completedAt: ago(12),
243
      },
244
    ],
245
    counterpartyBriefs: [
246
      {
247
        briefId: 'brief_1',
248
        interactionId: 'ix_local_brief',
249
        recipientAgentId: 'agent_atlas',
250
        subjectAgentId: 'agent_peer_nova',
251
        decision: 'CHALLENGE',
252
        relevantSampleSize: 3,
253
        historyConfidence: 0.38,
254
        requirements: [
255
          { requirement: 'Provide source evidence', status: 'partial', confidence: 0.7 },
256
        ],
257
        generatedAt: ago(241),
258
      },
259
    ],
260
    completionReports: [
261
      {
262
        reportId: 'report_1',
263
        interactionId: 'ix_local_brief',
264
        reportingAgentId: 'agent_atlas',
265
        counterpartyAgentId: 'agent_peer_nova',
266
        outcome: 'success',
267
        summary: 'Research brief delivered with sources and scope notes.',
268
        criteria: [{ criterion: 'Brief delivered', status: 'met' }],
269
        completedAt: ago(222),
270
      },
271
      {
272
        reportId: 'report_2',
273
        interactionId: 'ix_local_brief',
274
        reportingAgentId: 'agent_peer_nova',
275
        counterpartyAgentId: 'agent_atlas',
276
        outcome: 'success',
277
        summary: 'Received and accepted the requested brief.',
278
        criteria: [{ criterion: 'Brief delivered', status: 'met' }],
279
        completedAt: ago(221),
280
      },
281
      {
282
        reportId: 'report_ops_1',
283
        interactionId: 'ix_shared_ops',
284
        reportingAgentId: 'agent_atlas',
285
        counterpartyAgentId: 'agent_peer_keel',
286
        outcome: 'partial',
287
        summary:
288
          'The requester ended after the peer stopped responding before the task was complete.',
289
        criteria: [{ criterion: 'Timeline shared', status: 'partially_met' }],
290
        confidence: 0.7,
291
        completedAt: ago(12),
292
      },
293
    ],
294
    feedbackRequests: [
295
      {
296
        requestId: 'feedback_request_1',
297
        interactionId: 'ix_local_brief',
298
        reviewerAgentId: 'agent_atlas',
299
        subjectAgentId: 'agent_peer_nova',
300
        status: 'submitted',
301
        requestedAt: ago(221),
302
        dueAt: ago(197),
303
      },
304
      {
305
        requestId: 'feedback_request_ops_1',
306
        interactionId: 'ix_shared_ops',
307
        reviewerAgentId: 'agent_atlas',
308
        subjectAgentId: 'agent_peer_keel',
309
        status: 'pending',
310
        requestedAt: ago(12),
311
        dueAt: ahead(24 * 60 - 12),
312
      },
313
      {
314
        requestId: 'feedback_request_ops_2',
315
        interactionId: 'ix_shared_ops',
316
        reviewerAgentId: 'agent_peer_keel',
317
        subjectAgentId: 'agent_atlas',
318
        status: 'pending',
319
        requestedAt: ago(12),
320
        dueAt: ahead(24 * 60 - 12),
321
      },
322
      {
323
        requestId: 'feedback_request_2',
324
        interactionId: 'ix_local_brief',
325
        reviewerAgentId: 'agent_peer_nova',
326
        subjectAgentId: 'agent_atlas',
327
        status: 'submitted',
328
        requestedAt: ago(221),
329
        dueAt: ago(197),
330
      },
331
    ],
332
    interactionFeedback: [
333
      {
334
        feedbackId: 'feedback_1',
335
        interactionId: 'ix_local_brief',
336
        reviewerAgentId: 'agent_atlas',
337
        subjectAgentId: 'agent_peer_nova',
338
        ratings: { outcome_satisfaction: 0.9, communication: 0.8, reliability: 0.9 },
339
        wouldWorkAgain: 'yes',
340
        submittedAt: ago(219),
341
      },
342
    ],
343
    interactionConclusions: [
344
      {
345
        conclusionId: 'conclusion_1',
346
        interactionId: 'ix_local_brief',
347
        outcome: 'success',
348
        consensus: 'bilateral_agreement',
349
        summary: 'Both agents reported successful delivery and acceptance.',
350
        criteria: [{ criterion: 'Brief delivered', status: 'met' }],
351
        averageRatings: { outcome_satisfaction: 0.88, communication: 0.82, reliability: 0.9 },
352
        generatedAt: ago(218),
353
      },
354
      {
355
        conclusionId: 'conclusion_ops_provisional',
356
        interactionId: 'ix_shared_ops',
357
        outcome: 'partial',
358
        consensus: 'unilateral',
359
        lifecycle: 'provisional',
360
        confidence: 0.42,
361
        missingReportAgentIds: ['agent_peer_keel'],
362
        pendingFeedbackAgentIds: ['agent_atlas', 'agent_peer_keel'],
363
        peerReportStatus: 'awaiting',
364
        summary:
365
          'Provisional one-sided outcome: the requester ended after the peer stopped responding before the task was complete.',
366
        criteria: [{ criterion: 'Timeline shared', status: 'partially_met' }],
367
        reportIds: ['report_ops_1'],
368
        averageRatings: {},
369
        generatedAt: ago(11),
370
      },
371
    ],
372
    learningEligibility: [
373
      {
374
        decisionId: 'eligibility_1',
375
        interactionId: 'ix_local_brief',
376
        eligible: true,
377
        sampleWeight: 0.87,
378
        contributionMode: 'local_only',
379
        structuredDataOnly: true,
380
        reasons: ['Bilateral reports provide outcome corroboration'],
381
        decidedAt: ago(218),
382
      },
383
    ],
384
    profileDeltas: [
385
      {
386
        deltaId: 'delta_1',
387
        interactionId: 'ix_local_brief',
388
        agentId: 'agent_atlas',
389
        agentVersion: '1.4.2',
390
        taskCategory: 'research',
391
        sampleWeight: 0.87,
392
        dimensionDeltas: { completion: 0.02, evidence: 0.03, communication: -0.01 },
393
        appliedAt: ago(218),
394
      },
395
    ],
396
    profiles: [
397
      {
398
        agentId: 'agent_atlas',
399
        taskCategory: 'research',
400
        agentVersion: '1.4.2',
401
        sampleSize: 14,
402
        effectiveSampleSize: 10.8,
403
        updatedAt: ago(218),
404
        completion: 0.92,
405
        acceptance: 0.87,
406
        specification: 0.86,
407
        scope: 0.84,
408
        evidence: 0.88,
409
        communication: 0.79,
410
        deadline: 0.9,
411
        disputes: 0.08,
412
      },
413
      {
414
        agentId: 'agent_harbor',
415
        taskCategory: 'ops',
416
        agentVersion: '2.1.0',
417
        sampleSize: 6,
418
        effectiveSampleSize: 4.1,
419
        updatedAt: ago(3 * 24 * 60),
420
        completion: 0.71,
421
        acceptance: 0.68,
422
        specification: 0.63,
423
        scope: 0.66,
424
        evidence: 0.58,
425
        communication: 0.74,
426
        deadline: 0.62,
427
        disputes: 0.22,
428
      },
429
    ],
430
    runtimes: [
431
      {
432
        agentId: 'agent_atlas',
433
        endpoint: 'https://atlas.example.com/openclasp',
434
        a2aEndpoint: 'https://atlas.example.com/a2a',
435
        status: 'verified',
436
        verifiedAt: ago(40),
437
      },
438
    ],
439
    accessTokens: [],
440
  };
441
}
442
443
export function applyPreviewRequest(
444
  data: DashboardData,
445
  settings: Settings,
446
  path: string,
447
  init?: RequestInit,
448
): { data: DashboardData; settings: Settings; result: unknown } {
449
  const method = (init?.method ?? 'GET').toUpperCase();
450
  const body = init?.body ? (JSON.parse(String(init.body)) as Record<string, unknown>) : {};
451
452
  if (path === '/v0.1/dashboard' && method === 'GET') return { data, settings, result: data };
453
  if (path === '/v0.1/settings' && method === 'GET') return { data, settings, result: settings };
454
  if (path === '/v0.1/settings' && method === 'PUT') {
455
    const next = {
456
      ...settings,
457
      ...body,
458
      rawConversationsStored: false as const,
459
    } as Settings;
460
    return { data, settings: next, result: next };
461
  }
462
463
  if (path === '/v0.1/provider-connections' && method === 'POST') {
464
    const suffix = crypto.randomUUID();
465
    const projectName = String(body.projectName ?? 'Hosted agents');
466
    const existingProject = data.projects.find(
467
      (project) => String(project.name).toLowerCase() === projectName.toLowerCase(),
468
    );
469
    const project = existingProject ?? {
470
      projectId: `project_${suffix}`,
471
      name: projectName,
472
      createdAt: new Date().toISOString(),
473
    };
474
    const agentId = `agent_${suffix}`;
475
    const createdAt = new Date();
476
    const tokenId = suffix.replaceAll('-', '').slice(0, 16);
477
    const provider = body.provider === 'custom' ? 'custom' : 'botpress';
478
    const agent = {
479
      agentId,
480
      projectId: project.projectId,
481
      name: String(body.agentName ?? 'Botpress agent'),
482
      description: String(body.description ?? ''),
483
      framework: provider === 'botpress' ? 'Botpress' : 'Custom runtime',
484
      agentVersion: '1.0.0',
485
      agentMode: 'persistent_runtime',
486
      transport: 'direct_a2a',
487
      autoPublish: false,
488
      autoAcceptPolicy: 'off',
489
      autoAcceptTaskCategories: [],
490
      capabilities: body.capabilities ?? [],
491
      limitations: body.limitations ?? [],
492
      identityMode: 'owner_managed',
493
      status: 'active',
494
      createdAt: createdAt.toISOString(),
495
      updatedAt: createdAt.toISOString(),
496
      presence: { status: 'offline' },
497
    };
498
    const accessToken = {
499
      tokenId,
500
      token: `oc_at_${tokenId}.preview_agent_access_token_secret_not_for_production`,
501
      agentId,
502
      name: provider === 'botpress' ? 'Botpress' : 'Custom runtime',
503
      scopes: ['mcp:access', 'runtime:connect'],
504
      createdAt: createdAt.toISOString(),
505
      expiresAt: new Date(
506
        createdAt.getTime() + Number(body.expiresInDays ?? 365) * 86_400_000,
507
      ).toISOString(),
508
    };
509
    return {
510
      data: {
511
        ...data,
512
        projects: existingProject ? data.projects : [...data.projects, project],
513
        agents: [...data.agents, agent],
514
        accessTokens: [accessToken, ...data.accessTokens],
515
      },
516
      settings,
517
      result: { agent, project, provider, accessToken },
518
    };
519
  }
520
521
  const automation = path.match(/^\/v0\.1\/agents\/([^/]+)\/automation$/);
522
  if (automation && method === 'PUT') {
523
    const agentId = decodeURIComponent(automation[1] ?? '');
524
    const next = {
525
      ...data,
526
      agents: data.agents.map((agent) =>
527
        agent.agentId === agentId
528
          ? {
529
              ...agent,
530
              autoPublish: Boolean(body.autoPublish),
531
              autoAcceptPolicy: body.autoAcceptPolicy,
532
              autoAcceptTaskCategories: body.autoAcceptTaskCategories,
533
            }
534
          : agent,
535
      ),
536
      publications: body.autoPublish
537
        ? [
538
            ...data.publications.filter((item) => item.agentId !== agentId),
539
            { agentId, published: true, updatedAt: new Date().toISOString() },
540
          ]
541
        : data.publications.filter((item) => item.agentId !== agentId),
542
    };
543
    return { data: next, settings, result: { ok: true } };
544
  }
545
546
  const runtime = path.match(/^\/v0\.1\/agents\/([^/]+)\/runtime$/);
547
  if (runtime && method === 'PUT') {
548
    const agentId = decodeURIComponent(runtime[1] ?? '');
549
    const endpoint = String(body.endpoint ?? '');
550
    const record = {
551
      agentId,
552
      endpoint,
553
      a2aEndpoint: endpoint,
554
      status: 'verified',
555
      verifiedAt: new Date().toISOString(),
556
    };
557
    return {
558
      data: {
559
        ...data,
560
        runtimes: [...data.runtimes.filter((item) => item.agentId !== agentId), record],
561
      },
562
      settings,
563
      result: record,
564
    };
565
  }
566
  const tokenCollection = path.match(/^\/v0\.1\/agents\/([^/]+)\/access-tokens$/);
567
  if (tokenCollection && method === 'POST') {
568
    const agentId = decodeURIComponent(tokenCollection[1] ?? '');
569
    const tokenId = crypto.randomUUID().replaceAll('-', '').slice(0, 16);
570
    const createdAt = new Date();
571
    const expiresInDays = Number(body.expiresInDays ?? 365);
572
    const record = {
573
      tokenId,
574
      agentId,
575
      name: String(body.name ?? 'Hosted provider'),
576
      scopes: ['mcp:access', 'runtime:connect'],
577
      createdAt: createdAt.toISOString(),
578
      expiresAt: new Date(createdAt.getTime() + expiresInDays * 86_400_000).toISOString(),
579
    };
580
    return {
581
      data: { ...data, accessTokens: [record, ...data.accessTokens] },
582
      settings,
583
      result: {
584
        ...record,
585
        token: `oc_at_${tokenId}.preview_agent_access_token_secret_not_for_production`,
586
      },
587
    };
588
  }
589
  const tokenItem = path.match(/^\/v0\.1\/agents\/([^/]+)\/access-tokens\/([^/]+)$/);
590
  if (tokenItem && method === 'DELETE') {
591
    const tokenId = decodeURIComponent(tokenItem[2] ?? '');
592
    return {
593
      data: {
594
        ...data,
595
        accessTokens: data.accessTokens.map((token) =>
596
          token.tokenId === tokenId ? { ...token, revokedAt: new Date().toISOString() } : token,
597
        ),
598
      },
599
      settings,
600
      result: { tokenId, revokedAt: new Date().toISOString() },
601
    };
602
  }
603
  const agentDelete = path.match(/^\/v0\.1\/agents\/([^/]+)$/);
604
  if (agentDelete && method === 'DELETE') {
605
    const agentId = decodeURIComponent(agentDelete[1] ?? '');
606
    return {
607
      data: {
608
        ...data,
609
        agents: data.agents.filter((item) => item.agentId !== agentId),
610
        publications: data.publications.filter((item) => item.agentId !== agentId),
611
        runtimes: data.runtimes.filter((item) => item.agentId !== agentId),
612
        installations: data.installations.filter((item) => item.agentId !== agentId),
613
        accessTokens: data.accessTokens.filter((item) => item.agentId !== agentId),
614
      },
615
      settings,
616
      result: { agentId, deleted: true },
617
    };
618
  }
619
620
  if (runtime && method === 'DELETE') {
621
    const agentId = decodeURIComponent(runtime[1] ?? '');
622
    return {
623
      data: {
624
        ...data,
625
        runtimes: data.runtimes.filter((item) => item.agentId !== agentId),
626
      },
627
      settings,
628
      result: { agentId, status: 'disabled' },
629
    };
630
  }
631
632
  const federated = path.match(/^\/v0\.1\/federated-interactions\/([^/]+)\/respond$/);
633
  if (federated && method === 'POST') {
634
    const interactionId = decodeURIComponent(federated[1] ?? '');
635
    const decision = body.decision === 'accept' ? 'active' : 'rejected';
636
    return {
637
      data: {
638
        ...data,
639
        federatedInteractions: data.federatedInteractions.map((item) =>
640
          item.interactionId === interactionId ? { ...item, status: decision } : item,
641
        ),
642
      },
643
      settings,
644
      result: { ok: true },
645
    };
646
  }
647
648
  const onboarding = path.match(/^\/v0\.1\/onboarding\/([^/]+)\/(approve|reject)$/);
649
  if (onboarding && method === 'POST') {
650
    const requestId = decodeURIComponent(onboarding[1] ?? '');
651
    const decision = onboarding[2] === 'approve' ? 'approved' : 'rejected';
652
    return {
653
      data: {
654
        ...data,
655
        setupRequests: data.setupRequests.map((item) =>
656
          item.requestId === requestId
657
            ? { ...item, status: decision, decidedAt: new Date().toISOString() }
658
            : item,
659
        ),
660
      },
661
      settings,
662
      result: { ok: true },
663
    };
664
  }
665
666
  throw new Error('Preview cannot perform this action');
667
}
668
669
export function isPreviewActive() {
670
  return import.meta.env.DEV && sessionStorage.getItem(previewKey) === '1';
671
}
672
673
export function enablePreview() {
674
  sessionStorage.setItem(previewKey, '1');
675
}
676
677
export function disablePreview() {
678
  sessionStorage.removeItem(previewKey);
679
}