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

Viewing files at commit 6450fe9d331dCommit detailsView latest

apps/dashboard/src/main.tsx 753 lines · 22.7 KB · tsx
1
import {
2
  AuthProvider,
3
  Descope,
4
  getSessionToken,
5
  useDescope,
6
  useSession,
7
  useUser,
8
} from '@descope/react-sdk';
9
import React, { useCallback, useEffect, useMemo, useState } from 'react';
10
import { createRoot } from 'react-dom/client';
11
import './styles.css';
12
13
declare const __DESCOPE_PROJECT_ID__: string;
14
15
type DashboardData = {
16
  agents: Record<string, any>[];
17
  interactions: Record<string, any>[];
18
  events: Record<string, any>[];
19
  conflicts: Record<string, any>[];
20
  receipts: Record<string, any>[];
21
  profiles: Record<string, any>[];
22
};
23
24
type Settings = {
25
  displayName: string;
26
  contributionEnabled: boolean;
27
  retentionDays: number;
28
  evidenceSharing: 'never' | 'ask' | 'contract_only';
29
  rawConversationsStored: false;
30
};
31
32
const emptyData: DashboardData = {
33
  agents: [],
34
  interactions: [],
35
  events: [],
36
  conflicts: [],
37
  receipts: [],
38
  profiles: [],
39
};
40
const defaultSettings: Settings = {
41
  displayName: '',
42
  contributionEnabled: false,
43
  retentionDays: 30,
44
  evidenceSharing: 'ask',
45
  rawConversationsStored: false,
46
};
47
const pages = ['dashboard', 'history', 'agents', 'insights', 'connect', 'settings'] as const;
48
type Page = (typeof pages)[number];
49
50
function api(path: string, init?: RequestInit) {
51
  const token = getSessionToken();
52
  return fetch(path, {
53
    ...init,
54
    headers: {
55
      'content-type': 'application/json',
56
      ...(init?.headers ?? {}),
57
      ...(token ? { authorization: `Bearer ${token}` } : {}),
58
    },
59
  }).then(async (response) => {
60
    if (!response.ok)
61
      throw new Error((await response.text()) || `Request failed: ${response.status}`);
62
    return response.json();
63
  });
64
}
65
66
function route(): Page {
67
  const value = location.pathname.slice(1) || 'dashboard';
68
  return pages.includes(value as Page) ? (value as Page) : 'dashboard';
69
}
70
71
function App() {
72
  const { isAuthenticated, isSessionLoading } = useSession();
73
  const { user, isUserLoading } = useUser();
74
  const { logout } = useDescope();
75
  const [page, setPage] = useState<Page>(route());
76
  const [data, setData] = useState<DashboardData>(emptyData);
77
  const [settings, setSettings] = useState<Settings>(defaultSettings);
78
  const [loading, setLoading] = useState(true);
79
  const [error, setError] = useState('');
80
81
  useEffect(() => {
82
    const onPopState = () => setPage(route());
83
    addEventListener('popstate', onPopState);
84
    return () => removeEventListener('popstate', onPopState);
85
  }, []);
86
87
  useEffect(() => {
88
    if (!isSessionLoading && !isAuthenticated && location.pathname !== '/login')
89
      history.replaceState({}, '', '/login');
90
    if (!isAuthenticated) return;
91
    if (location.pathname === '/login') history.replaceState({}, '', '/dashboard');
92
    setPage(route());
93
    Promise.all([api('/v0.1/dashboard'), api('/v0.1/settings')])
94
      .then(([dashboard, accountSettings]) => {
95
        setData(dashboard as DashboardData);
96
        setSettings(accountSettings as Settings);
97
      })
98
      .catch((reason: unknown) =>
99
        setError(reason instanceof Error ? reason.message : 'Load failed'),
100
      )
101
      .finally(() => setLoading(false));
102
  }, [isAuthenticated, isSessionLoading]);
103
104
  const navigate = (next: Page) => {
105
    history.pushState({}, '', `/${next}`);
106
    setPage(next);
107
  };
108
109
  if (isSessionLoading || isUserLoading) return <Loading />;
110
  if (!isAuthenticated) return <Login />;
111
112
  return (
113
    <div className="appShell">
114
      <aside>
115
        <div className="brand">
116
          <div className="mark">OC</div>
117
          <div>
118
            <strong>OpenClasp</strong>
119
            <small>ASSURANCE NETWORK</small>
120
          </div>
121
        </div>
122
        <nav>
123
          <Nav page="dashboard" active={page} onClick={navigate} label="Overview" glyph="⌂" />
124
          <Nav page="history" active={page} onClick={navigate} label="History" glyph="≡" />
125
          <Nav page="agents" active={page} onClick={navigate} label="Agents" glyph="◇" />
126
          <Nav page="insights" active={page} onClick={navigate} label="Insights" glyph="⌁" />
127
          <Nav page="connect" active={page} onClick={navigate} label="Connect" glyph="+" />
128
          <Nav page="settings" active={page} onClick={navigate} label="Settings" glyph="⚙" />
129
        </nav>
130
        <div className="privacyStamp">
131
          <span className="liveDot" />
132
          <div>
133
            <strong>Structured-only</strong>
134
            <small>Raw conversations stay local</small>
135
          </div>
136
        </div>
137
        <button className="account" onClick={() => void logout()}>
138
          <span>{initials(user?.name || user?.email || 'OC')}</span>
139
          <div>
140
            <strong>{user?.name || 'OpenClasp user'}</strong>
141
            <small>{user?.email || 'Sign out'}</small>
142
          </div>
143
          <b>↗</b>
144
        </button>
145
      </aside>
146
      <main>
147
        {error && <div className="errorBar">{error}</div>}
148
        {loading ? (
149
          <Loading compact />
150
        ) : (
151
          <PageContent
152
            page={page}
153
            data={data}
154
            settings={settings}
155
            setSettings={setSettings}
156
            navigate={navigate}
157
          />
158
        )}
159
      </main>
160
    </div>
161
  );
162
}
163
164
function Login() {
165
  return (
166
    <div className="loginPage">
167
      <section className="loginPitch">
168
        <div className="brand">
169
          <div className="mark">OC</div>
170
          <strong>OpenClasp</strong>
171
        </div>
172
        <div>
173
          <p className="eyebrow">AGENT-TO-AGENT ASSURANCE</p>
174
          <h1>Know who your agent is dealing with.</h1>
175
          <p>
176
            Verified identities, signed outcomes, private warnings, and contextual
177
            reliability—without collecting raw conversations.
178
          </p>
179
        </div>
180
        <div className="loginProof">
181
          <span>01</span> User-owned history<span>02</span> No universal trust score<span>03</span>{' '}
182
          Explicit network consent
183
        </div>
184
      </section>
185
      <section className="loginCard">
186
        <div>
187
          <p className="eyebrow">SECURE ACCESS</p>
188
          <h2>Sign in to OpenClasp</h2>
189
          <p>
190
            Manage connected agents, review signed history, and control what contributes to the
191
            network.
192
          </p>
193
        </div>
194
        <Descope flowId="sign-up-or-in" theme="dark" />
195
        <small>Authentication is handled by Descope. OpenClasp never receives your password.</small>
196
      </section>
197
    </div>
198
  );
199
}
200
201
function PageContent({
202
  page,
203
  data,
204
  settings,
205
  setSettings,
206
  navigate,
207
}: {
208
  page: Page;
209
  data: DashboardData;
210
  settings: Settings;
211
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
212
  navigate: (page: Page) => void;
213
}) {
214
  if (page === 'history') return <History data={data} />;
215
  if (page === 'agents') return <Agents data={data} navigate={navigate} />;
216
  if (page === 'insights') return <Insights data={data} />;
217
  if (page === 'connect') return <Connect />;
218
  if (page === 'settings') return <SettingsPage settings={settings} setSettings={setSettings} />;
219
  return <Overview data={data} navigate={navigate} />;
220
}
221
222
function Overview({ data, navigate }: { data: DashboardData; navigate: (page: Page) => void }) {
223
  const completed = data.receipts.filter((item) => item.outcome === 'success').length;
224
  const warnings = data.events.filter((item) =>
225
    ['policy_warning', 'policy_violation', 'objection'].includes(String(item.eventType)),
226
  ).length;
227
  return (
228
    <>
229
      <PageHead
230
        eyebrow="NETWORK OVERVIEW"
231
        title="Assurance, at a glance."
232
        action="Connect agent"
233
        onAction={() => navigate('connect')}
234
      />
235
      <section className="metrics">
236
        <Metric label="Connected agents" value={data.agents.length} note="verified identities" />
237
        <Metric label="Interactions" value={data.interactions.length} note="signed or active" />
238
        <Metric label="Successful outcomes" value={completed} note="receipt-backed" />
239
        <Metric label="Open warnings" value={warnings} note="needs attention" warn={warnings > 0} />
240
      </section>
241
      <section className="contentGrid">
242
        <Panel title="Recent activity" subtitle="Structured events and signed outcomes">
243
          <Timeline events={data.events.slice(-6).reverse()} />
244
          <TextButton onClick={() => navigate('history')}>View complete history →</TextButton>
245
        </Panel>
246
        <Panel
247
          title="Reliability context"
248
          subtitle="Task-specific profiles, never a universal score"
249
        >
250
          {data.profiles.length ? (
251
            data.profiles
252
              .slice(0, 4)
253
              .map((profile) => (
254
                <Profile key={`${profile.agentId}-${profile.taskCategory}`} profile={profile} />
255
              ))
256
          ) : (
257
            <Empty
258
              title="No reliability profile yet"
259
              text="Profiles appear after signed receipts and eligible bilateral feedback."
260
            />
261
          )}
262
        </Panel>
263
      </section>
264
    </>
265
  );
266
}
267
268
function History({ data }: { data: DashboardData }) {
269
  const records = useMemo(
270
    () =>
271
      [
272
        ...data.interactions.map((value) => ({ ...value, _kind: 'interaction' })),
273
        ...data.events.map((value) => ({ ...value, _kind: 'event' })),
274
        ...data.receipts.map((value) => ({ ...value, _kind: 'receipt' })),
275
        ...data.conflicts.map((value) => ({ ...value, _kind: 'dispute' })),
276
      ].sort((a, b) => Date.parse(timestamp(b)) - Date.parse(timestamp(a))),
277
    [data],
278
  );
279
  return (
280
    <>
281
      <PageHead eyebrow="AUDIT HISTORY" title="What happened, and what proves it." />
282
      <Panel
283
        title="Account history"
284
        subtitle="Only structured metadata, signatures, hashes, and permitted evidence are hosted"
285
      >
286
        {records.length ? (
287
          records.map((item, index) => (
288
            <HistoryRow
289
              key={String(
290
                (item as Record<string, any>).eventId ??
291
                  (item as Record<string, any>).receiptId ??
292
                  (item as Record<string, any>).interactionId ??
293
                  index,
294
              )}
295
              item={item}
296
            />
297
          ))
298
        ) : (
299
          <Empty
300
            title="No interactions recorded"
301
            text="Connect an agent and start an assured interaction. Raw message bodies will not appear here."
302
          />
303
        )}
304
      </Panel>
305
    </>
306
  );
307
}
308
309
function Agents({ data, navigate }: { data: DashboardData; navigate: (page: Page) => void }) {
310
  return (
311
    <>
312
      <PageHead
313
        eyebrow="IDENTITY REGISTRY"
314
        title="Your connected agents."
315
        action="Connect agent"
316
        onAction={() => navigate('connect')}
317
      />
318
      <section className="agentGrid">
319
        {data.agents.length ? (
320
          data.agents.map((agent) => <AgentCard key={agent.agentId} agent={agent} />)
321
        ) : (
322
          <Empty
323
            title="No agents connected"
324
            text="Install the remote MCP endpoint in an agent, sign in, then create and register its identity."
325
            action="Open connection guide"
326
            onAction={() => navigate('connect')}
327
          />
328
        )}
329
      </section>
330
    </>
331
  );
332
}
333
334
function Insights({ data }: { data: DashboardData }) {
335
  return (
336
    <>
337
      <PageHead eyebrow="BEHAVIOURAL CONTEXT" title="Evidence, not reputation theatre." />
338
      <div className="notice">
339
        <strong>Context matters.</strong> OpenClasp profiles an agent by task category and version.
340
        It does not produce a single universal trust score.
341
      </div>
342
      <section className="agentGrid">
343
        {data.profiles.length ? (
344
          data.profiles.map((profile) => (
345
            <Panel
346
              key={`${profile.agentId}-${profile.taskCategory}`}
347
              title={profile.agentId}
348
              subtitle={`${profile.taskCategory} · v${profile.agentVersion}`}
349
            >
350
              <Profile profile={profile} expanded />
351
            </Panel>
352
          ))
353
        ) : (
354
          <Empty
355
            title="Not enough verified evidence"
356
            text="Insights grow only from signed outcomes, eligible feedback, corrections, disputes, and version-aware history."
357
          />
358
        )}
359
      </section>
360
    </>
361
  );
362
}
363
364
function Connect() {
365
  const endpoint = 'https://openclasp.vercel.app/mcp';
366
  const [copied, setCopied] = useState(false);
367
  return (
368
    <>
369
      <PageHead eyebrow="AGENT CONNECTION" title="Add OpenClasp to an agent." />
370
      <section className="connectLayout">
371
        <Panel title="Remote MCP endpoint" subtitle="For OAuth-capable MCP clients">
372
          <div className="endpoint">
373
            <code>{endpoint}</code>
374
            <button
375
              onClick={() => {
376
                void navigator.clipboard.writeText(endpoint);
377
                setCopied(true);
378
              }}
379
            >
380
              {copied ? 'Copied' : 'Copy'}
381
            </button>
382
          </div>
383
          <ol>
384
            <li>Add the URL as a remote MCP server in your agent or framework.</li>
385
            <li>The client discovers OAuth and opens the Descope login page.</li>
386
            <li>Sign in with this account and approve access.</li>
387
            <li>Ask the agent to create and register its OpenClasp identity.</li>
388
          </ol>
389
        </Panel>
390
        <Panel
391
          title="What the agent receives"
392
          subtitle="Assurance tools, not a replacement transport"
393
        >
394
          <ul className="checkList">
395
            <li>Identity and delegation verification</li>
396
            <li>Counterparty-specific reliability clues</li>
397
            <li>Signed contracts, events, feedback, and receipts</li>
398
            <li>Policy challenges and consented mediation</li>
399
            <li>No hidden rewriting or raw-message upload</li>
400
          </ul>
401
        </Panel>
402
      </section>
403
    </>
404
  );
405
}
406
407
function SettingsPage({
408
  settings,
409
  setSettings,
410
}: {
411
  settings: Settings;
412
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
413
}) {
414
  const [saving, setSaving] = useState(false);
415
  const [saved, setSaved] = useState(false);
416
  const save = useCallback(async () => {
417
    setSaving(true);
418
    setSaved(false);
419
    const updated = (await api('/v0.1/settings', {
420
      method: 'PUT',
421
      body: JSON.stringify(settings),
422
    })) as Settings;
423
    setSettings(updated);
424
    setSaving(false);
425
    setSaved(true);
426
  }, [settings, setSettings]);
427
  return (
428
    <>
429
      <PageHead eyebrow="ACCOUNT CONTROLS" title="Privacy and network settings." />
430
      <section className="settingsCard">
431
        <Setting label="Display name" description="Shown only inside your OpenClasp account.">
432
          <input
433
            value={settings.displayName}
434
            onChange={(event) =>
435
              setSettings((value) => ({ ...value, displayName: event.target.value }))
436
            }
437
            placeholder="Team or operator name"
438
          />
439
        </Setting>
440
        <Setting
441
          label="Network contribution"
442
          description="Allow eligible structured aggregates to improve contextual intelligence. Raw conversations are never included."
443
        >
444
          <Toggle
445
            checked={settings.contributionEnabled}
446
            onChange={(checked) =>
447
              setSettings((value) => ({ ...value, contributionEnabled: checked }))
448
            }
449
          />
450
        </Setting>
451
        <Setting
452
          label="Evidence sharing"
453
          description="Controls when permitted evidence references may be shared with interaction participants."
454
        >
455
          <select
456
            value={settings.evidenceSharing}
457
            onChange={(event) =>
458
              setSettings((value) => ({
459
                ...value,
460
                evidenceSharing: event.target.value as Settings['evidenceSharing'],
461
              }))
462
            }
463
          >
464
            <option value="never">Never</option>
465
            <option value="ask">Ask every time</option>
466
            <option value="contract_only">When contract permits</option>
467
          </select>
468
        </Setting>
469
        <Setting
470
          label="Structured record retention"
471
          description="Retention for hosted account records. Signed protocol records may require separate revocation markers."
472
        >
473
          <select
474
            value={settings.retentionDays}
475
            onChange={(event) =>
476
              setSettings((value) => ({ ...value, retentionDays: Number(event.target.value) }))
477
            }
478
          >
479
            <option value={7}>7 days</option>
480
            <option value={30}>30 days</option>
481
            <option value={90}>90 days</option>
482
            <option value={365}>1 year</option>
483
            <option value={0}>No automatic deletion</option>
484
          </select>
485
        </Setting>
486
        <Setting
487
          label="Raw conversations"
488
          description="Message bodies remain local and user-owned. This cannot be enabled on the hosted service."
489
        >
490
          <span className="locked">ALWAYS OFF</span>
491
        </Setting>
492
        <div className="saveRow">
493
          <button className="primary" onClick={() => void save()} disabled={saving}>
494
            {saving ? 'Saving…' : 'Save settings'}
495
          </button>
496
          {saved && <span>Saved</span>}
497
        </div>
498
      </section>
499
    </>
500
  );
501
}
502
503
function PageHead({
504
  eyebrow,
505
  title,
506
  action,
507
  onAction,
508
}: {
509
  eyebrow: string;
510
  title: string;
511
  action?: string;
512
  onAction?: () => void;
513
}) {
514
  return (
515
    <header className="pageHead">
516
      <div>
517
        <p className="eyebrow">{eyebrow}</p>
518
        <h1>{title}</h1>
519
      </div>
520
      {action && (
521
        <button className="primary" onClick={onAction}>
522
          {action}
523
        </button>
524
      )}
525
    </header>
526
  );
527
}
528
function Nav({
529
  page,
530
  active,
531
  onClick,
532
  label,
533
  glyph,
534
}: {
535
  page: Page;
536
  active: Page;
537
  onClick: (page: Page) => void;
538
  label: string;
539
  glyph: string;
540
}) {
541
  return (
542
    <button className={active === page ? 'active' : ''} onClick={() => onClick(page)}>
543
      <span>{glyph}</span>
544
      {label}
545
    </button>
546
  );
547
}
548
function Metric({
549
  label,
550
  value,
551
  note,
552
  warn,
553
}: {
554
  label: string;
555
  value: number;
556
  note: string;
557
  warn?: boolean;
558
}) {
559
  return (
560
    <article className={warn ? 'metric warn' : 'metric'}>
561
      <span>{label}</span>
562
      <strong>{String(value).padStart(2, '0')}</strong>
563
      <small>{note}</small>
564
    </article>
565
  );
566
}
567
function Panel({
568
  title,
569
  subtitle,
570
  children,
571
}: React.PropsWithChildren<{ title: string; subtitle: string }>) {
572
  return (
573
    <section className="panel">
574
      <div className="panelHead">
575
        <div>
576
          <h2>{title}</h2>
577
          <p>{subtitle}</p>
578
        </div>
579
      </div>
580
      {children}
581
    </section>
582
  );
583
}
584
function Timeline({ events }: { events: Record<string, any>[] }) {
585
  return events.length ? (
586
    <div className="timeline">
587
      {events.map((event) => (
588
        <HistoryRow key={event.eventId} item={{ ...event, _kind: 'event' }} />
589
      ))}
590
    </div>
591
  ) : (
592
    <Empty
593
      title="Nothing recorded yet"
594
      text="Activity appears after your connected agents submit signed structured events."
595
    />
596
  );
597
}
598
function HistoryRow({ item }: { item: Record<string, any> }) {
599
  const kind = String(item.eventType ?? item._kind ?? 'record').replaceAll('_', ' ');
600
  const id = String(item.agentId ?? item.interactionId ?? item.receiptId ?? 'OpenClasp');
601
  return (
602
    <article className="historyRow">
603
      <span className={`eventDot ${item.eventType ?? item._kind}`} />
604
      <div>
605
        <strong>{kind}</strong>
606
        <small>
607
          {id} · {new Date(timestamp(item)).toLocaleString()}
608
        </small>
609
      </div>
610
      <b>{item.visibility ?? item.outcome ?? item.status ?? 'SIGNED'}</b>
611
    </article>
612
  );
613
}
614
function AgentCard({ agent }: { agent: Record<string, any> }) {
615
  return (
616
    <article className="agentCard">
617
      <div className="agentTop">
618
        <span className="agentGlyph">◇</span>
619
        <b className={agent.revoked ? 'bad' : ''}>{agent.revoked ? 'REVOKED' : 'VERIFIED'}</b>
620
      </div>
621
      <h2>{agent.agentId}</h2>
622
      <p>Version {agent.agentVersion}</p>
623
      <div className="tags">
624
        {(agent.capabilities ?? []).slice(0, 4).map((capability: string) => (
625
          <span key={capability}>{capability}</span>
626
        ))}
627
      </div>
628
      <small>Created {new Date(agent.createdAt).toLocaleDateString()}</small>
629
    </article>
630
  );
631
}
632
function Profile({ profile, expanded }: { profile: Record<string, any>; expanded?: boolean }) {
633
  const dimensions = [
634
    ['Completion', profile.completion],
635
    ['Scope', profile.scope],
636
    ['Evidence', profile.evidence],
637
    ...(expanded
638
      ? [
639
          ['Communication', profile.communication],
640
          ['Deadline', profile.deadline],
641
        ]
642
      : []),
643
  ] as [string, number][];
644
  return (
645
    <article className="profile">
646
      <div>
647
        <strong>{profile.agentId}</strong>
648
        <small>
649
          {profile.taskCategory} · {profile.sampleSize} outcome(s)
650
        </small>
651
      </div>
652
      {dimensions.map(([label, value]) => (
653
        <Meter key={label} label={label} value={value ?? 0} />
654
      ))}
655
    </article>
656
  );
657
}
658
function Meter({ label, value }: { label: string; value: number }) {
659
  return (
660
    <div className="meter">
661
      <span>{label}</span>
662
      <i>
663
        <b style={{ width: `${Math.round(value * 100)}%` }} />
664
      </i>
665
      <strong>{Math.round(value * 100)}%</strong>
666
    </div>
667
  );
668
}
669
function Empty({
670
  title,
671
  text,
672
  action,
673
  onAction,
674
}: {
675
  title: string;
676
  text: string;
677
  action?: string;
678
  onAction?: () => void;
679
}) {
680
  return (
681
    <div className="empty">
682
      <span>◇</span>
683
      <strong>{title}</strong>
684
      <p>{text}</p>
685
      {action && (
686
        <button className="secondary" onClick={onAction}>
687
          {action}
688
        </button>
689
      )}
690
    </div>
691
  );
692
}
693
function TextButton({ children, onClick }: React.PropsWithChildren<{ onClick: () => void }>) {
694
  return (
695
    <button className="textButton" onClick={onClick}>
696
      {children}
697
    </button>
698
  );
699
}
700
function Setting({
701
  label,
702
  description,
703
  children,
704
}: React.PropsWithChildren<{ label: string; description: string }>) {
705
  return (
706
    <div className="setting">
707
      <div>
708
        <strong>{label}</strong>
709
        <p>{description}</p>
710
      </div>
711
      <div>{children}</div>
712
    </div>
713
  );
714
}
715
function Toggle({ checked, onChange }: { checked: boolean; onChange: (checked: boolean) => void }) {
716
  return (
717
    <button
718
      className={checked ? 'toggle on' : 'toggle'}
719
      role="switch"
720
      aria-checked={checked}
721
      onClick={() => onChange(!checked)}
722
    >
723
      <span />
724
    </button>
725
  );
726
}
727
function Loading({ compact }: { compact?: boolean }) {
728
  return (
729
    <div className={compact ? 'loading compact' : 'loading'}>
730
      <span className="mark">OC</span>
731
      <p>Verifying session…</p>
732
    </div>
733
  );
734
}
735
function initials(value: string) {
736
  return value
737
    .split(/\s+|@/)
738
    .slice(0, 2)
739
    .map((part) => part[0]?.toUpperCase())
740
    .join('');
741
}
742
function timestamp(item: Record<string, any>) {
743
  return String(item.timestamp ?? item.completedAt ?? item.createdAt ?? new Date(0).toISOString());
744
}
745
746
if (!__DESCOPE_PROJECT_ID__) throw new Error('Descope project ID is not configured');
747
createRoot(document.getElementById('root')!).render(
748
  <React.StrictMode>
749
    <AuthProvider projectId={__DESCOPE_PROJECT_ID__} persistTokens autoRefresh>
750
      <App />
751
    </AuthProvider>
752
  </React.StrictMode>,
753
);