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

Viewing files at commit 874239cc591fCommit detailsView latest

apps/dashboard/src/preview.ts 706 lines · 22.1 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
        contractRevision: 1,
152
        createdAt: ago(26),
153
        contract: {
154
          purpose: 'Review a research brief for scope and sources',
155
          taskCategory: 'research',
156
          parties: ['agent_peer_nova', 'agent_atlas'],
157
        },
158
        contractRevisions: [
159
          {
160
            revisionId: '17c3a10b-2e84-4d11-86aa-09f1d3e8b470',
161
            revision: 1,
162
            termsHash: '7f3c91a0b2e84d11c6aa09f1d3e8b470',
163
            proposedByAgentId: 'agent_peer_nova',
164
            status: 'proposed',
165
            acceptances: { agent_peer_nova: { method: 'oauth_installation' } },
166
          },
167
        ],
168
      },
169
      {
170
        interactionId: 'ix_shared_ops',
171
        initiatorAgentId: 'agent_atlas',
172
        responderAgentId: 'agent_peer_keel',
173
        status: 'active',
174
        termsHash: '12ab44c0e91f77d0aa3310bc44ee9012',
175
        contractRevision: 1,
176
        createdAt: ago(90),
177
        contract: {
178
          purpose: 'Share a sanitized incident timeline',
179
          taskCategory: 'coordination',
180
          parties: ['agent_atlas', 'agent_peer_keel'],
181
        },
182
        acceptances: {
183
          agent_peer_keel: { method: 'policy_auto_accept' },
184
        },
185
        contractRevisions: [
186
          {
187
            revisionId: '22ab44c0-e91f-47d0-aa33-10bc44ee9012',
188
            revision: 1,
189
            termsHash: '12ab44c0e91f77d0aa3310bc44ee9012',
190
            proposedByAgentId: 'agent_atlas',
191
            status: 'accepted',
192
            acceptances: {
193
              agent_atlas: { method: 'oauth_installation' },
194
              agent_peer_keel: { method: 'policy_auto_accept' },
195
            },
196
          },
197
        ],
198
      },
199
    ],
200
    liveSessions: [
201
      {
202
        interactionId: 'ix_shared_ops',
203
        initiatorAgentId: 'agent_atlas',
204
        responderAgentId: 'agent_peer_keel',
205
        status: 'active',
206
        createdAt: ago(88),
207
        activatedAt: ago(87),
208
        expiresAt: ahead(12),
209
      },
210
    ],
211
    hostedThreads: [],
212
    events: [
213
      {
214
        eventId: 'evt_1',
215
        eventType: 'interaction_started',
216
        agentId: 'agent_atlas',
217
        interactionId: 'ix_shared_ops',
218
        timestamp: ago(88),
219
        visibility: 'participants',
220
      },
221
      {
222
        eventId: 'evt_2',
223
        eventType: 'policy_warning',
224
        agentId: 'agent_harbor',
225
        interactionId: 'ix_local_brief',
226
        timestamp: ago(200),
227
        visibility: 'owner',
228
      },
229
      {
230
        eventId: 'evt_3',
231
        eventType: 'claim_checked',
232
        agentId: 'agent_atlas',
233
        interactionId: 'ix_local_brief',
234
        timestamp: ago(230),
235
        visibility: 'participants',
236
      },
237
      {
238
        eventId: 'evt_4',
239
        eventType: 'session_activated',
240
        agentId: 'agent_atlas',
241
        interactionId: 'ix_shared_ops',
242
        timestamp: ago(87),
243
        visibility: 'participants',
244
      },
245
    ],
246
    conflicts: [
247
      {
248
        conflictId: 'dsp_1',
249
        interactionId: 'ix_local_brief',
250
        status: 'open',
251
        createdAt: ago(190),
252
      },
253
    ],
254
    receipts: [
255
      {
256
        receiptId: 'rcpt_1',
257
        interactionId: 'ix_local_brief',
258
        agentId: 'agent_atlas',
259
        outcome: 'success',
260
        completedAt: ago(220),
261
      },
262
      {
263
        receiptId: 'rcpt_ops_provisional',
264
        interactionId: 'ix_shared_ops',
265
        outcome: 'partial',
266
        unilateral: true,
267
        provisional: true,
268
        confidence: 0.42,
269
        completedAt: ago(12),
270
      },
271
    ],
272
    counterpartyBriefs: [
273
      {
274
        briefId: 'brief_1',
275
        interactionId: 'ix_local_brief',
276
        recipientAgentId: 'agent_atlas',
277
        subjectAgentId: 'agent_peer_nova',
278
        decision: 'CHALLENGE',
279
        relevantSampleSize: 3,
280
        historyConfidence: 0.38,
281
        requirements: [
282
          { requirement: 'Provide source evidence', status: 'partial', confidence: 0.7 },
283
        ],
284
        generatedAt: ago(241),
285
      },
286
    ],
287
    completionReports: [
288
      {
289
        reportId: 'report_1',
290
        interactionId: 'ix_local_brief',
291
        reportingAgentId: 'agent_atlas',
292
        counterpartyAgentId: 'agent_peer_nova',
293
        outcome: 'success',
294
        summary: 'Research brief delivered with sources and scope notes.',
295
        criteria: [{ criterion: 'Brief delivered', status: 'met' }],
296
        completedAt: ago(222),
297
      },
298
      {
299
        reportId: 'report_2',
300
        interactionId: 'ix_local_brief',
301
        reportingAgentId: 'agent_peer_nova',
302
        counterpartyAgentId: 'agent_atlas',
303
        outcome: 'success',
304
        summary: 'Received and accepted the requested brief.',
305
        criteria: [{ criterion: 'Brief delivered', status: 'met' }],
306
        completedAt: ago(221),
307
      },
308
      {
309
        reportId: 'report_ops_1',
310
        interactionId: 'ix_shared_ops',
311
        reportingAgentId: 'agent_atlas',
312
        counterpartyAgentId: 'agent_peer_keel',
313
        outcome: 'partial',
314
        summary:
315
          'The requester ended after the peer stopped responding before the task was complete.',
316
        criteria: [{ criterion: 'Timeline shared', status: 'partially_met' }],
317
        confidence: 0.7,
318
        completedAt: ago(12),
319
      },
320
    ],
321
    feedbackRequests: [
322
      {
323
        requestId: 'feedback_request_1',
324
        interactionId: 'ix_local_brief',
325
        reviewerAgentId: 'agent_atlas',
326
        subjectAgentId: 'agent_peer_nova',
327
        status: 'submitted',
328
        requestedAt: ago(221),
329
        dueAt: ago(197),
330
      },
331
      {
332
        requestId: 'feedback_request_ops_1',
333
        interactionId: 'ix_shared_ops',
334
        reviewerAgentId: 'agent_atlas',
335
        subjectAgentId: 'agent_peer_keel',
336
        status: 'pending',
337
        requestedAt: ago(12),
338
        dueAt: ahead(24 * 60 - 12),
339
      },
340
      {
341
        requestId: 'feedback_request_ops_2',
342
        interactionId: 'ix_shared_ops',
343
        reviewerAgentId: 'agent_peer_keel',
344
        subjectAgentId: 'agent_atlas',
345
        status: 'pending',
346
        requestedAt: ago(12),
347
        dueAt: ahead(24 * 60 - 12),
348
      },
349
      {
350
        requestId: 'feedback_request_2',
351
        interactionId: 'ix_local_brief',
352
        reviewerAgentId: 'agent_peer_nova',
353
        subjectAgentId: 'agent_atlas',
354
        status: 'submitted',
355
        requestedAt: ago(221),
356
        dueAt: ago(197),
357
      },
358
    ],
359
    interactionFeedback: [
360
      {
361
        feedbackId: 'feedback_1',
362
        interactionId: 'ix_local_brief',
363
        reviewerAgentId: 'agent_atlas',
364
        subjectAgentId: 'agent_peer_nova',
365
        ratings: { outcome_satisfaction: 0.9, communication: 0.8, reliability: 0.9 },
366
        wouldWorkAgain: 'yes',
367
        submittedAt: ago(219),
368
      },
369
    ],
370
    interactionConclusions: [
371
      {
372
        conclusionId: 'conclusion_1',
373
        interactionId: 'ix_local_brief',
374
        outcome: 'success',
375
        consensus: 'bilateral_agreement',
376
        summary: 'Both agents reported successful delivery and acceptance.',
377
        criteria: [{ criterion: 'Brief delivered', status: 'met' }],
378
        averageRatings: { outcome_satisfaction: 0.88, communication: 0.82, reliability: 0.9 },
379
        generatedAt: ago(218),
380
      },
381
      {
382
        conclusionId: 'conclusion_ops_provisional',
383
        interactionId: 'ix_shared_ops',
384
        outcome: 'partial',
385
        consensus: 'unilateral',
386
        lifecycle: 'provisional',
387
        confidence: 0.42,
388
        missingReportAgentIds: ['agent_peer_keel'],
389
        pendingFeedbackAgentIds: ['agent_atlas', 'agent_peer_keel'],
390
        peerReportStatus: 'awaiting',
391
        summary:
392
          'Provisional one-sided outcome: the requester ended after the peer stopped responding before the task was complete.',
393
        criteria: [{ criterion: 'Timeline shared', status: 'partially_met' }],
394
        reportIds: ['report_ops_1'],
395
        averageRatings: {},
396
        generatedAt: ago(11),
397
      },
398
    ],
399
    learningEligibility: [
400
      {
401
        decisionId: 'eligibility_1',
402
        interactionId: 'ix_local_brief',
403
        eligible: true,
404
        sampleWeight: 0.87,
405
        contributionMode: 'local_only',
406
        structuredDataOnly: true,
407
        reasons: ['Bilateral reports provide outcome corroboration'],
408
        decidedAt: ago(218),
409
      },
410
    ],
411
    profileDeltas: [
412
      {
413
        deltaId: 'delta_1',
414
        interactionId: 'ix_local_brief',
415
        agentId: 'agent_atlas',
416
        agentVersion: '1.4.2',
417
        taskCategory: 'research',
418
        sampleWeight: 0.87,
419
        dimensionDeltas: { completion: 0.02, evidence: 0.03, communication: -0.01 },
420
        appliedAt: ago(218),
421
      },
422
    ],
423
    profiles: [
424
      {
425
        agentId: 'agent_atlas',
426
        taskCategory: 'research',
427
        agentVersion: '1.4.2',
428
        sampleSize: 14,
429
        effectiveSampleSize: 10.8,
430
        updatedAt: ago(218),
431
        completion: 0.92,
432
        acceptance: 0.87,
433
        specification: 0.86,
434
        scope: 0.84,
435
        evidence: 0.88,
436
        communication: 0.79,
437
        deadline: 0.9,
438
        disputes: 0.08,
439
      },
440
      {
441
        agentId: 'agent_harbor',
442
        taskCategory: 'ops',
443
        agentVersion: '2.1.0',
444
        sampleSize: 6,
445
        effectiveSampleSize: 4.1,
446
        updatedAt: ago(3 * 24 * 60),
447
        completion: 0.71,
448
        acceptance: 0.68,
449
        specification: 0.63,
450
        scope: 0.66,
451
        evidence: 0.58,
452
        communication: 0.74,
453
        deadline: 0.62,
454
        disputes: 0.22,
455
      },
456
    ],
457
    runtimes: [
458
      {
459
        agentId: 'agent_atlas',
460
        endpoint: 'https://atlas.example.com/openclasp',
461
        a2aEndpoint: 'https://atlas.example.com/a2a',
462
        status: 'verified',
463
        verifiedAt: ago(40),
464
      },
465
    ],
466
    accessTokens: [],
467
  };
468
}
469
470
export function applyPreviewRequest(
471
  data: DashboardData,
472
  settings: Settings,
473
  path: string,
474
  init?: RequestInit,
475
): { data: DashboardData; settings: Settings; result: unknown } {
476
  const method = (init?.method ?? 'GET').toUpperCase();
477
  const body = init?.body ? (JSON.parse(String(init.body)) as Record<string, unknown>) : {};
478
479
  if (path === '/v0.1/dashboard' && method === 'GET') return { data, settings, result: data };
480
  if (path === '/v0.1/settings' && method === 'GET') return { data, settings, result: settings };
481
  if (path === '/v0.1/settings' && method === 'PUT') {
482
    const next = {
483
      ...settings,
484
      ...body,
485
      rawConversationsStored: false as const,
486
    } as Settings;
487
    return { data, settings: next, result: next };
488
  }
489
490
  if (path === '/v0.1/provider-connections' && method === 'POST') {
491
    const suffix = crypto.randomUUID();
492
    const projectName = String(body.projectName ?? 'Hosted agents');
493
    const existingProject = data.projects.find(
494
      (project) => String(project.name).toLowerCase() === projectName.toLowerCase(),
495
    );
496
    const project = existingProject ?? {
497
      projectId: `project_${suffix}`,
498
      name: projectName,
499
      createdAt: new Date().toISOString(),
500
    };
501
    const agentId = `agent_${suffix}`;
502
    const createdAt = new Date();
503
    const tokenId = suffix.replaceAll('-', '').slice(0, 16);
504
    const provider = body.provider === 'custom' ? 'custom' : 'botpress';
505
    const agent = {
506
      agentId,
507
      projectId: project.projectId,
508
      name: String(body.agentName ?? 'Botpress agent'),
509
      description: String(body.description ?? ''),
510
      framework: provider === 'botpress' ? 'Botpress' : 'Custom runtime',
511
      agentVersion: '1.0.0',
512
      agentMode: 'persistent_runtime',
513
      transport: 'direct_a2a',
514
      autoPublish: false,
515
      autoAcceptPolicy: 'off',
516
      autoAcceptTaskCategories: [],
517
      capabilities: body.capabilities ?? [],
518
      limitations: body.limitations ?? [],
519
      identityMode: 'owner_managed',
520
      status: 'active',
521
      createdAt: createdAt.toISOString(),
522
      updatedAt: createdAt.toISOString(),
523
      presence: { status: 'offline' },
524
    };
525
    const accessToken = {
526
      tokenId,
527
      token: `oc_at_${tokenId}.preview_agent_access_token_secret_not_for_production`,
528
      agentId,
529
      name: provider === 'botpress' ? 'Botpress' : 'Custom runtime',
530
      scopes: ['mcp:access', 'runtime:connect'],
531
      createdAt: createdAt.toISOString(),
532
      expiresAt: new Date(
533
        createdAt.getTime() + Number(body.expiresInDays ?? 365) * 86_400_000,
534
      ).toISOString(),
535
    };
536
    return {
537
      data: {
538
        ...data,
539
        projects: existingProject ? data.projects : [...data.projects, project],
540
        agents: [...data.agents, agent],
541
        accessTokens: [accessToken, ...data.accessTokens],
542
      },
543
      settings,
544
      result: { agent, project, provider, accessToken },
545
    };
546
  }
547
548
  const automation = path.match(/^\/v0\.1\/agents\/([^/]+)\/automation$/);
549
  if (automation && method === 'PUT') {
550
    const agentId = decodeURIComponent(automation[1] ?? '');
551
    const next = {
552
      ...data,
553
      agents: data.agents.map((agent) =>
554
        agent.agentId === agentId
555
          ? {
556
              ...agent,
557
              autoPublish: Boolean(body.autoPublish),
558
              autoAcceptPolicy: body.autoAcceptPolicy,
559
              autoAcceptTaskCategories: body.autoAcceptTaskCategories,
560
            }
561
          : agent,
562
      ),
563
      publications: body.autoPublish
564
        ? [
565
            ...data.publications.filter((item) => item.agentId !== agentId),
566
            { agentId, published: true, updatedAt: new Date().toISOString() },
567
          ]
568
        : data.publications.filter((item) => item.agentId !== agentId),
569
    };
570
    return { data: next, settings, result: { ok: true } };
571
  }
572
573
  const runtime = path.match(/^\/v0\.1\/agents\/([^/]+)\/runtime$/);
574
  if (runtime && method === 'PUT') {
575
    const agentId = decodeURIComponent(runtime[1] ?? '');
576
    const endpoint = String(body.endpoint ?? '');
577
    const record = {
578
      agentId,
579
      endpoint,
580
      a2aEndpoint: endpoint,
581
      status: 'verified',
582
      verifiedAt: new Date().toISOString(),
583
    };
584
    return {
585
      data: {
586
        ...data,
587
        runtimes: [...data.runtimes.filter((item) => item.agentId !== agentId), record],
588
      },
589
      settings,
590
      result: record,
591
    };
592
  }
593
  const tokenCollection = path.match(/^\/v0\.1\/agents\/([^/]+)\/access-tokens$/);
594
  if (tokenCollection && method === 'POST') {
595
    const agentId = decodeURIComponent(tokenCollection[1] ?? '');
596
    const tokenId = crypto.randomUUID().replaceAll('-', '').slice(0, 16);
597
    const createdAt = new Date();
598
    const expiresInDays = Number(body.expiresInDays ?? 365);
599
    const record = {
600
      tokenId,
601
      agentId,
602
      name: String(body.name ?? 'Hosted provider'),
603
      scopes: ['mcp:access', 'runtime:connect'],
604
      createdAt: createdAt.toISOString(),
605
      expiresAt: new Date(createdAt.getTime() + expiresInDays * 86_400_000).toISOString(),
606
    };
607
    return {
608
      data: { ...data, accessTokens: [record, ...data.accessTokens] },
609
      settings,
610
      result: {
611
        ...record,
612
        token: `oc_at_${tokenId}.preview_agent_access_token_secret_not_for_production`,
613
      },
614
    };
615
  }
616
  const tokenItem = path.match(/^\/v0\.1\/agents\/([^/]+)\/access-tokens\/([^/]+)$/);
617
  if (tokenItem && method === 'DELETE') {
618
    const tokenId = decodeURIComponent(tokenItem[2] ?? '');
619
    return {
620
      data: {
621
        ...data,
622
        accessTokens: data.accessTokens.map((token) =>
623
          token.tokenId === tokenId ? { ...token, revokedAt: new Date().toISOString() } : token,
624
        ),
625
      },
626
      settings,
627
      result: { tokenId, revokedAt: new Date().toISOString() },
628
    };
629
  }
630
  const agentDelete = path.match(/^\/v0\.1\/agents\/([^/]+)$/);
631
  if (agentDelete && method === 'DELETE') {
632
    const agentId = decodeURIComponent(agentDelete[1] ?? '');
633
    return {
634
      data: {
635
        ...data,
636
        agents: data.agents.filter((item) => item.agentId !== agentId),
637
        publications: data.publications.filter((item) => item.agentId !== agentId),
638
        runtimes: data.runtimes.filter((item) => item.agentId !== agentId),
639
        installations: data.installations.filter((item) => item.agentId !== agentId),
640
        accessTokens: data.accessTokens.filter((item) => item.agentId !== agentId),
641
      },
642
      settings,
643
      result: { agentId, deleted: true },
644
    };
645
  }
646
647
  if (runtime && method === 'DELETE') {
648
    const agentId = decodeURIComponent(runtime[1] ?? '');
649
    return {
650
      data: {
651
        ...data,
652
        runtimes: data.runtimes.filter((item) => item.agentId !== agentId),
653
      },
654
      settings,
655
      result: { agentId, status: 'disabled' },
656
    };
657
  }
658
659
  const federated = path.match(/^\/v0\.1\/federated-interactions\/([^/]+)\/respond$/);
660
  if (federated && method === 'POST') {
661
    const interactionId = decodeURIComponent(federated[1] ?? '');
662
    const decision = body.decision === 'accept' ? 'active' : 'rejected';
663
    return {
664
      data: {
665
        ...data,
666
        federatedInteractions: data.federatedInteractions.map((item) =>
667
          item.interactionId === interactionId ? { ...item, status: decision } : item,
668
        ),
669
      },
670
      settings,
671
      result: { ok: true },
672
    };
673
  }
674
675
  const onboarding = path.match(/^\/v0\.1\/onboarding\/([^/]+)\/(approve|reject)$/);
676
  if (onboarding && method === 'POST') {
677
    const requestId = decodeURIComponent(onboarding[1] ?? '');
678
    const decision = onboarding[2] === 'approve' ? 'approved' : 'rejected';
679
    return {
680
      data: {
681
        ...data,
682
        setupRequests: data.setupRequests.map((item) =>
683
          item.requestId === requestId
684
            ? { ...item, status: decision, decidedAt: new Date().toISOString() }
685
            : item,
686
        ),
687
      },
688
      settings,
689
      result: { ok: true },
690
    };
691
  }
692
693
  throw new Error('Preview cannot perform this action');
694
}
695
696
export function isPreviewActive() {
697
  return import.meta.env.DEV && sessionStorage.getItem(previewKey) === '1';
698
}
699
700
export function enablePreview() {
701
  sessionStorage.setItem(previewKey, '1');
702
}
703
704
export function disablePreview() {
705
  sessionStorage.removeItem(previewKey);
706
}