@zackbart/connecta 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +294 -0
- package/README.md +24 -20
- package/dist/auth/bearer.d.ts +4 -3
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +10 -8
- package/dist/auth/bearer.js.map +1 -1
- package/dist/auth/clerk.d.ts +8 -7
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/auth/clerk.js +27 -8
- package/dist/auth/clerk.js.map +1 -1
- package/dist/connector-scope.d.ts +13 -0
- package/dist/connector-scope.d.ts.map +1 -0
- package/dist/connector-scope.js +35 -0
- package/dist/connector-scope.js.map +1 -0
- package/dist/connectors/api.d.ts +5 -5
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +27 -4
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +400 -19
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credential-health.d.ts +8 -5
- package/dist/credential-health.d.ts.map +1 -1
- package/dist/credential-health.js +99 -51
- package/dist/credential-health.js.map +1 -1
- package/dist/credentials.d.ts +51 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +68 -3
- package/dist/credentials.js.map +1 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +10 -8
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +84 -83
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -24
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +28 -3
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +131 -16
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +3 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +4 -3
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +152 -52
- package/dist/server.js.map +1 -1
- package/dist/toolkits.js +1 -1
- package/dist/types.d.ts +41 -27
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +24 -10
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +594 -172
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/auth/bearer.ts +10 -8
- package/src/auth/clerk.ts +28 -9
- package/src/connector-scope.ts +41 -0
- package/src/connectors/api.ts +5 -5
- package/src/connectors/remote-mcp.ts +489 -35
- package/src/credential-health.ts +120 -57
- package/src/credentials.ts +96 -3
- package/src/execute.ts +18 -7
- package/src/index.ts +174 -107
- package/src/meta-tools.ts +173 -24
- package/src/registry.ts +4 -3
- package/src/server.ts +191 -70
- package/src/toolkits.ts +1 -1
- package/src/types.ts +41 -27
- package/src/ui.ts +631 -170
- package/src/version.ts +1 -1
package/src/ui.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
credentialTestRule,
|
|
3
|
+
describeUndeclaredCredentialFields,
|
|
4
|
+
storedCredentialShape,
|
|
5
|
+
} from "./credentials.js";
|
|
2
6
|
import type { CredentialVault } from "./credentials.js";
|
|
7
|
+
import { closeConnectorScope } from "./connector-scope.js";
|
|
3
8
|
import type { Registry } from "./registry.js";
|
|
4
9
|
import type { ConnectaBranding, UiAuthConfig } from "./types.js";
|
|
5
10
|
|
|
@@ -148,8 +153,8 @@ const URL_STRIPPED_CHARS = /[\t\n\r]/g;
|
|
|
148
153
|
* default href is the relative `/favicon.svg`, which `isSafeHttpUrl` alone would
|
|
149
154
|
* reject — and it is kept narrow on both ends.
|
|
150
155
|
*
|
|
151
|
-
* Root-relative only, because
|
|
152
|
-
* depths and a document-relative path would resolve differently
|
|
156
|
+
* Root-relative only, because operator and OAuth callback pages sit at
|
|
157
|
+
* different depths and a document-relative path would resolve differently.
|
|
153
158
|
*
|
|
154
159
|
* "Root-relative" is enforced structurally: exactly one leading `/` followed by
|
|
155
160
|
* a character that is neither `/` nor `\`. Both of those would make the value an
|
|
@@ -173,7 +178,7 @@ export function isSafeIconHref(href: unknown): boolean {
|
|
|
173
178
|
|
|
174
179
|
/**
|
|
175
180
|
* True only for an absolute `https:` URL — the gate every `uiAuth` URL passes:
|
|
176
|
-
* `frontendApiUrl`, which becomes the
|
|
181
|
+
* `frontendApiUrl`, which becomes the operator shell's sign-in loader source,
|
|
177
182
|
* and `signInUrl`/`signUpUrl`, which ClerkJS uses as *navigation targets* when
|
|
178
183
|
* the operator signs in. With those three gated, no operator-config value
|
|
179
184
|
* reaches the browser in a URL position — attribute or navigation — without
|
|
@@ -182,7 +187,7 @@ export function isSafeIconHref(href: unknown): boolean {
|
|
|
182
187
|
* Stricter than `isSafeHttpUrl` on purpose: no `http:` carve-out, no loopback
|
|
183
188
|
* carve-out, and no relative form. Nobody types `frontendApiUrl` — the shipped
|
|
184
189
|
* Clerk adapter derives it from the publishable key, and Clerk's Frontend API is
|
|
185
|
-
* always https — and a cleartext script source on
|
|
190
|
+
* always https — and a cleartext script source on an operator page would be a
|
|
186
191
|
* downgrade even where a browser's mixed-content rules had not already blocked
|
|
187
192
|
* it. `signInUrl`/`signUpUrl` *are* typed by the operator, but what belongs
|
|
188
193
|
* there is a hosted Account Portal address (`https://accounts.<domain>` or
|
|
@@ -279,6 +284,12 @@ export interface UiConnector {
|
|
|
279
284
|
updatedAt?: string;
|
|
280
285
|
testable: boolean;
|
|
281
286
|
error?: string;
|
|
287
|
+
/**
|
|
288
|
+
* Something true and non-blocking about a working credential — today, that
|
|
289
|
+
* the vault still holds fields the connector has stopped declaring.
|
|
290
|
+
* Distinct from `error`, which means the credential cannot be used.
|
|
291
|
+
*/
|
|
292
|
+
notice?: string;
|
|
282
293
|
};
|
|
283
294
|
}
|
|
284
295
|
|
|
@@ -286,6 +297,46 @@ export interface UiData {
|
|
|
286
297
|
serverInfo: { name: string; version: string };
|
|
287
298
|
connectors: UiConnector[];
|
|
288
299
|
activityEnabled: boolean;
|
|
300
|
+
credentialManagement: CredentialManagementCapability;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export type CredentialManagementCapability =
|
|
304
|
+
| "available"
|
|
305
|
+
| "requires_clerk"
|
|
306
|
+
| "vault_not_configured"
|
|
307
|
+
| "no_slots";
|
|
308
|
+
|
|
309
|
+
export type OperatorPage = "connections" | "credentials" | "activity";
|
|
310
|
+
|
|
311
|
+
const OPERATOR_PAGE_LABELS: Readonly<Record<OperatorPage, string>> = {
|
|
312
|
+
connections: "Connections",
|
|
313
|
+
credentials: "Credentials",
|
|
314
|
+
activity: "Activity",
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export function operatorPageForPath(path: string): OperatorPage | undefined {
|
|
318
|
+
if (path === "/") return "connections";
|
|
319
|
+
if (path === "/credentials") return "credentials";
|
|
320
|
+
if (path === "/activity") return "activity";
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function operatorPageTitle(
|
|
325
|
+
page: OperatorPage,
|
|
326
|
+
configuredTitle: string,
|
|
327
|
+
): string {
|
|
328
|
+
return `${OPERATOR_PAGE_LABELS[page]} — ${configuredTitle}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function credentialManagementCapability(input: {
|
|
332
|
+
eligibleClerkOperator: boolean;
|
|
333
|
+
hasCredentialSlots: boolean;
|
|
334
|
+
hasCredentialVault: boolean;
|
|
335
|
+
}): CredentialManagementCapability {
|
|
336
|
+
if (!input.eligibleClerkOperator) return "requires_clerk";
|
|
337
|
+
if (!input.hasCredentialSlots) return "no_slots";
|
|
338
|
+
if (!input.hasCredentialVault) return "vault_not_configured";
|
|
339
|
+
return "available";
|
|
289
340
|
}
|
|
290
341
|
|
|
291
342
|
export interface FilteredUiConnector {
|
|
@@ -337,10 +388,14 @@ export async function buildUiData(
|
|
|
337
388
|
serverInfo: { name: string; version: string },
|
|
338
389
|
credentialVault?: CredentialVault,
|
|
339
390
|
activityEnabled = false,
|
|
391
|
+
credentialManagement: CredentialManagementCapability = credentialVault
|
|
392
|
+
? "available"
|
|
393
|
+
: "requires_clerk",
|
|
340
394
|
): Promise<UiData> {
|
|
341
395
|
const requestScope = {};
|
|
396
|
+
const connectorSet = registry.listConnectors();
|
|
342
397
|
const connectors = await Promise.all(
|
|
343
|
-
|
|
398
|
+
connectorSet.map(async (c): Promise<UiConnector> => {
|
|
344
399
|
const status = await registry.statusFor(c.id, baseUrl, requestScope);
|
|
345
400
|
const credentialCheck = await registry.credentialHealthFor(c.id);
|
|
346
401
|
let tools: UiTool[] = [];
|
|
@@ -367,7 +422,7 @@ export async function buildUiData(
|
|
|
367
422
|
// One rule, shared with the test route: only the hook matching the
|
|
368
423
|
// declared credential shape can run, so the button is offered only
|
|
369
424
|
// where a click can succeed (src/credentials.ts).
|
|
370
|
-
const
|
|
425
|
+
const testRule = credentialTestRule(c);
|
|
371
426
|
const credentialFields = (
|
|
372
427
|
metadata?: Awaited<ReturnType<CredentialVault["metadata"]>>,
|
|
373
428
|
) =>
|
|
@@ -395,6 +450,10 @@ export async function buildUiData(
|
|
|
395
450
|
try {
|
|
396
451
|
const metadata = await credentialVault.metadata(c.id);
|
|
397
452
|
const fields = credentialFields(metadata);
|
|
453
|
+
const shape = storedCredentialShape(
|
|
454
|
+
c.credential,
|
|
455
|
+
metadata?.fields ?? null,
|
|
456
|
+
);
|
|
398
457
|
credential = {
|
|
399
458
|
label: c.credential.label,
|
|
400
459
|
...(c.credential.description
|
|
@@ -404,9 +463,7 @@ export async function buildUiData(
|
|
|
404
463
|
? { placeholder: c.credential.placeholder }
|
|
405
464
|
: {}),
|
|
406
465
|
...(fields?.length ? { fields } : {}),
|
|
407
|
-
configured:
|
|
408
|
-
? fields.every((field) => field.configured)
|
|
409
|
-
: Boolean(metadata),
|
|
466
|
+
configured: shape.state === "valid",
|
|
410
467
|
removable: Boolean(metadata),
|
|
411
468
|
...(metadata
|
|
412
469
|
? {
|
|
@@ -414,7 +471,21 @@ export async function buildUiData(
|
|
|
414
471
|
updatedAt: metadata.updatedAt,
|
|
415
472
|
}
|
|
416
473
|
: {}),
|
|
417
|
-
testable
|
|
474
|
+
testable:
|
|
475
|
+
testRule.mode !== null && shape.state !== "mismatch",
|
|
476
|
+
...(shape.state === "mismatch"
|
|
477
|
+
? { error: shape.message }
|
|
478
|
+
: {}),
|
|
479
|
+
// A dropped field leaves its secret in the vault, and the field
|
|
480
|
+
// list below only renders fields the connector still declares —
|
|
481
|
+
// so without this line there is nowhere an operator could see it.
|
|
482
|
+
...(shape.state === "valid" && shape.undeclared.length
|
|
483
|
+
? {
|
|
484
|
+
notice: describeUndeclaredCredentialFields(
|
|
485
|
+
shape.undeclared,
|
|
486
|
+
),
|
|
487
|
+
}
|
|
488
|
+
: {}),
|
|
418
489
|
};
|
|
419
490
|
} catch {
|
|
420
491
|
const fields = credentialFields();
|
|
@@ -429,7 +500,7 @@ export async function buildUiData(
|
|
|
429
500
|
...(fields?.length ? { fields } : {}),
|
|
430
501
|
configured: false,
|
|
431
502
|
removable: true,
|
|
432
|
-
testable,
|
|
503
|
+
testable: testRule.mode !== null,
|
|
433
504
|
error: "Stored credential could not be read.",
|
|
434
505
|
};
|
|
435
506
|
}
|
|
@@ -459,8 +530,22 @@ export async function buildUiData(
|
|
|
459
530
|
...(credential ? { credential } : {}),
|
|
460
531
|
};
|
|
461
532
|
}),
|
|
462
|
-
)
|
|
463
|
-
|
|
533
|
+
).finally(async () => {
|
|
534
|
+
await Promise.all(
|
|
535
|
+
connectorSet.map((connector) =>
|
|
536
|
+
closeConnectorScope(
|
|
537
|
+
connector,
|
|
538
|
+
registry.contextFor(connector.id, baseUrl, requestScope),
|
|
539
|
+
),
|
|
540
|
+
),
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
return {
|
|
544
|
+
serverInfo,
|
|
545
|
+
connectors,
|
|
546
|
+
activityEnabled,
|
|
547
|
+
credentialManagement,
|
|
548
|
+
};
|
|
464
549
|
}
|
|
465
550
|
|
|
466
551
|
function escapeHtmlAttr(value: string): string {
|
|
@@ -479,16 +564,15 @@ function jsonForInlineScript(value: unknown): string {
|
|
|
479
564
|
}
|
|
480
565
|
|
|
481
566
|
/**
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
* normal sign-in flow and a short-lived session token; bearer-only deployments
|
|
485
|
-
* retain the manual-token fallback.
|
|
567
|
+
* Every operator page serves this same data-free shell. Connector, credential,
|
|
568
|
+
* and activity data arrives only through the authenticated `/ui/*` APIs.
|
|
486
569
|
*/
|
|
487
570
|
export function renderUiHtml(
|
|
488
571
|
uiAuth?: UiAuthConfig,
|
|
489
572
|
mcpUrl = "/mcp",
|
|
490
573
|
branding?: ConnectaBranding,
|
|
491
574
|
nonce?: string,
|
|
575
|
+
page: OperatorPage = "connections",
|
|
492
576
|
): string {
|
|
493
577
|
const clerk = uiAuth?.kind === "clerk" ? uiAuth : undefined;
|
|
494
578
|
// The Clerk loader's origin. A value that fails the gate is dropped rather
|
|
@@ -520,8 +604,8 @@ export function renderUiHtml(
|
|
|
520
604
|
}
|
|
521
605
|
: (uiAuth ?? { kind: "bearer" as const });
|
|
522
606
|
const brand = resolveBranding(branding);
|
|
523
|
-
const title = brand.pageTitle;
|
|
524
|
-
// When
|
|
607
|
+
const title = operatorPageTitle(page, brand.pageTitle);
|
|
608
|
+
// When an operator shell ships a nonce-based CSP, every script it emits must
|
|
525
609
|
// carry that nonce to run; without a nonce the markup is unchanged.
|
|
526
610
|
const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
|
|
527
611
|
// Top-left corner. With an owner set it reads "<owner> <product>"; without
|
|
@@ -562,6 +646,7 @@ ${clerkScript}
|
|
|
562
646
|
--paper: #fff;
|
|
563
647
|
--rule: #ccc;
|
|
564
648
|
--muted: #666;
|
|
649
|
+
--trace: #f5f5f5;
|
|
565
650
|
--shell: 70rem;
|
|
566
651
|
--pad: 1rem;
|
|
567
652
|
--gap: 1.5rem;
|
|
@@ -608,6 +693,15 @@ ${clerkScript}
|
|
|
608
693
|
outline: 1px solid var(--ink);
|
|
609
694
|
}
|
|
610
695
|
:is(a, button, summary):focus-visible { outline-offset: 2px; }
|
|
696
|
+
.skip-link {
|
|
697
|
+
background: var(--paper);
|
|
698
|
+
left: var(--pad);
|
|
699
|
+
padding: .5rem;
|
|
700
|
+
position: fixed;
|
|
701
|
+
top: -4rem;
|
|
702
|
+
z-index: 10;
|
|
703
|
+
}
|
|
704
|
+
.skip-link:focus { top: var(--pad); }
|
|
611
705
|
|
|
612
706
|
.shell {
|
|
613
707
|
margin: 0 auto;
|
|
@@ -626,6 +720,15 @@ ${clerkScript}
|
|
|
626
720
|
.cap, .meta { color: var(--muted); font-size: .9em; }
|
|
627
721
|
.mono { font-family: var(--mono); font-size: .78rem; }
|
|
628
722
|
.hidden { display: none !important; }
|
|
723
|
+
.visually-hidden {
|
|
724
|
+
clip: rect(0 0 0 0);
|
|
725
|
+
clip-path: inset(50%);
|
|
726
|
+
height: 1px;
|
|
727
|
+
overflow: hidden;
|
|
728
|
+
position: absolute;
|
|
729
|
+
white-space: nowrap;
|
|
730
|
+
width: 1px;
|
|
731
|
+
}
|
|
629
732
|
|
|
630
733
|
.masthead {
|
|
631
734
|
align-items: start;
|
|
@@ -650,6 +753,17 @@ ${clerkScript}
|
|
|
650
753
|
justify-content: flex-end;
|
|
651
754
|
min-width: 0;
|
|
652
755
|
}
|
|
756
|
+
.page-nav,
|
|
757
|
+
.session-actions {
|
|
758
|
+
display: flex;
|
|
759
|
+
flex-wrap: wrap;
|
|
760
|
+
gap: .5rem var(--gap);
|
|
761
|
+
}
|
|
762
|
+
.mast-actions :is(a, button) {
|
|
763
|
+
align-items: center;
|
|
764
|
+
display: inline-flex;
|
|
765
|
+
min-height: 2rem;
|
|
766
|
+
}
|
|
653
767
|
.navlink,
|
|
654
768
|
.linklike {
|
|
655
769
|
text-decoration: underline;
|
|
@@ -659,7 +773,7 @@ ${clerkScript}
|
|
|
659
773
|
.navlink { text-decoration-color: transparent; }
|
|
660
774
|
.navlink:hover,
|
|
661
775
|
.navlink:focus-visible,
|
|
662
|
-
.navlink
|
|
776
|
+
.navlink[aria-current="page"] { text-decoration-color: currentColor; }
|
|
663
777
|
.linklike { text-decoration-color: currentColor; }
|
|
664
778
|
.linklike:hover,
|
|
665
779
|
.linklike:focus-visible { text-decoration-color: transparent; }
|
|
@@ -707,7 +821,24 @@ ${clerkScript}
|
|
|
707
821
|
#notice:empty { display: none; }
|
|
708
822
|
#notice:not(:empty) { text-decoration: underline; }
|
|
709
823
|
.error-notice, .msg { text-decoration: underline; }
|
|
710
|
-
.card
|
|
824
|
+
.card,
|
|
825
|
+
.credential-card,
|
|
826
|
+
.activity-item {
|
|
827
|
+
padding-left: 1.25rem;
|
|
828
|
+
position: relative;
|
|
829
|
+
}
|
|
830
|
+
.card::before,
|
|
831
|
+
.credential-card::before,
|
|
832
|
+
.activity-item::before {
|
|
833
|
+
background: var(--rule);
|
|
834
|
+
bottom: 0;
|
|
835
|
+
content: "";
|
|
836
|
+
left: .25rem;
|
|
837
|
+
position: absolute;
|
|
838
|
+
top: 0;
|
|
839
|
+
width: 1px;
|
|
840
|
+
}
|
|
841
|
+
.card { border-top: 1px solid var(--rule); padding-bottom: .75rem; padding-top: .75rem; }
|
|
711
842
|
.connector-head {
|
|
712
843
|
display: grid;
|
|
713
844
|
gap: var(--gap);
|
|
@@ -718,14 +849,25 @@ ${clerkScript}
|
|
|
718
849
|
display: flex;
|
|
719
850
|
gap: .5rem;
|
|
720
851
|
}
|
|
852
|
+
.connector-title .dot,
|
|
853
|
+
.activity-stamp .dot {
|
|
854
|
+
margin-left: -1.25rem;
|
|
855
|
+
}
|
|
856
|
+
.activity-stamp {
|
|
857
|
+
align-items: baseline;
|
|
858
|
+
display: flex;
|
|
859
|
+
gap: .75rem;
|
|
860
|
+
}
|
|
721
861
|
.card h2 { overflow-wrap: anywhere; }
|
|
722
862
|
.connector-state { text-align: right; }
|
|
723
863
|
.dot {
|
|
864
|
+
background: var(--paper);
|
|
724
865
|
border: 1px solid var(--ink);
|
|
725
866
|
display: inline-block;
|
|
726
867
|
flex: none;
|
|
727
868
|
height: .5rem;
|
|
728
869
|
width: .5rem;
|
|
870
|
+
z-index: 1;
|
|
729
871
|
}
|
|
730
872
|
.dot.ok { background: var(--ink); }
|
|
731
873
|
.dot.auth_required {
|
|
@@ -735,7 +877,8 @@ ${clerkScript}
|
|
|
735
877
|
.connector-message,
|
|
736
878
|
.connector-auth { margin-top: .75rem; }
|
|
737
879
|
|
|
738
|
-
.credential { border-
|
|
880
|
+
.credential-ledger { border-bottom: 1px solid var(--rule); }
|
|
881
|
+
.credential-card { border-top: 1px solid var(--rule); padding-bottom: .75rem; padding-top: .75rem; }
|
|
739
882
|
.credential-head {
|
|
740
883
|
align-items: baseline;
|
|
741
884
|
display: flex;
|
|
@@ -744,7 +887,27 @@ ${clerkScript}
|
|
|
744
887
|
justify-content: space-between;
|
|
745
888
|
}
|
|
746
889
|
.credential-copy { margin-top: .25rem; max-width: 40rem; }
|
|
890
|
+
.credential-field-summary {
|
|
891
|
+
border-top: 1px solid var(--rule);
|
|
892
|
+
margin-top: .75rem;
|
|
893
|
+
}
|
|
894
|
+
.credential-field-summary > div {
|
|
895
|
+
border-bottom: 1px solid var(--rule);
|
|
896
|
+
display: flex;
|
|
897
|
+
flex-wrap: wrap;
|
|
898
|
+
gap: .25rem var(--gap);
|
|
899
|
+
justify-content: space-between;
|
|
900
|
+
padding: .5rem 0;
|
|
901
|
+
}
|
|
747
902
|
.credential-actions { display: flex; flex-wrap: wrap; gap: var(--gap); margin-top: .75rem; }
|
|
903
|
+
.credential-actions button,
|
|
904
|
+
.credential-form button,
|
|
905
|
+
.activity-controls button,
|
|
906
|
+
.activity-more {
|
|
907
|
+
align-items: center;
|
|
908
|
+
display: inline-flex;
|
|
909
|
+
min-height: 2.75rem;
|
|
910
|
+
}
|
|
748
911
|
.credential-form {
|
|
749
912
|
align-items: center;
|
|
750
913
|
display: flex;
|
|
@@ -788,7 +951,8 @@ ${clerkScript}
|
|
|
788
951
|
display: grid;
|
|
789
952
|
gap: .25rem var(--gap);
|
|
790
953
|
grid-template-columns: minmax(9rem, .85fr) minmax(12rem, 1.4fr) minmax(8rem, .9fr);
|
|
791
|
-
padding: .75rem
|
|
954
|
+
padding-bottom: .75rem;
|
|
955
|
+
padding-top: .75rem;
|
|
792
956
|
}
|
|
793
957
|
.activity-time,
|
|
794
958
|
.activity-actor,
|
|
@@ -799,6 +963,16 @@ ${clerkScript}
|
|
|
799
963
|
.activity-item.timeout .activity-outcome { text-decoration: underline; }
|
|
800
964
|
.activity-empty { border-top: 1px solid var(--rule); padding: .75rem 0; }
|
|
801
965
|
.activity-more { margin-top: .75rem; }
|
|
966
|
+
.unavailable {
|
|
967
|
+
background: var(--trace);
|
|
968
|
+
border-bottom: 1px solid var(--rule);
|
|
969
|
+
border-top: 1px solid var(--rule);
|
|
970
|
+
padding: .75rem;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
@media (prefers-reduced-motion: reduce) {
|
|
974
|
+
html:focus-within { scroll-behavior: auto; }
|
|
975
|
+
}
|
|
802
976
|
|
|
803
977
|
@media (max-width: 36.99rem) {
|
|
804
978
|
.pgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
@@ -807,7 +981,12 @@ ${clerkScript}
|
|
|
807
981
|
.masthead .brand { grid-column: 1; }
|
|
808
982
|
.mast-nav { grid-column: 1 / -1; grid-row: 2; justify-content: flex-start; }
|
|
809
983
|
.product { display: none; }
|
|
810
|
-
.mast-actions {
|
|
984
|
+
.mast-actions {
|
|
985
|
+
align-items: flex-start;
|
|
986
|
+
flex-direction: column;
|
|
987
|
+
font-size: .875rem;
|
|
988
|
+
gap: .25rem;
|
|
989
|
+
}
|
|
811
990
|
.lead { margin-top: 4rem; }
|
|
812
991
|
.section,
|
|
813
992
|
.section + .section { margin-top: 2.5rem; }
|
|
@@ -816,49 +995,58 @@ ${clerkScript}
|
|
|
816
995
|
.activity-item { grid-template-columns: 1fr; }
|
|
817
996
|
.connector-state { text-align: left; }
|
|
818
997
|
.credential-field { align-items: start; grid-template-columns: 1fr; gap: .25rem; }
|
|
998
|
+
input { min-height: 2.75rem; }
|
|
819
999
|
}
|
|
820
1000
|
</style>
|
|
821
1001
|
</head>
|
|
822
1002
|
<body>
|
|
1003
|
+
<a class="skip-link" href="#operatorContent">Skip to operator page</a>
|
|
823
1004
|
<header class="masthead shell pgrid">
|
|
824
1005
|
${owner}
|
|
825
1006
|
<div class="mast-nav">
|
|
826
1007
|
${product}
|
|
827
|
-
<
|
|
828
|
-
<
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1008
|
+
<div id="appNav" class="mast-actions hidden">
|
|
1009
|
+
<nav class="page-nav" aria-label="Operator pages">
|
|
1010
|
+
<a id="connectionsNav" class="navlink" href="/"
|
|
1011
|
+
data-operator-page="connections"${page === "connections" ? ' aria-current="page"' : ""}>Connections</a>
|
|
1012
|
+
<a id="credentialsNav" class="navlink hidden" href="/credentials"
|
|
1013
|
+
data-operator-page="credentials"${page === "credentials" ? ' aria-current="page"' : ""}>Credentials</a>
|
|
1014
|
+
<a id="activityNav" class="navlink hidden" href="/activity"
|
|
1015
|
+
data-operator-page="activity"${page === "activity" ? ' aria-current="page"' : ""}>Activity</a>
|
|
1016
|
+
</nav>
|
|
1017
|
+
<div class="session-actions" aria-label="Session actions">
|
|
1018
|
+
<button id="change" class="navlink hidden" type="button">Change token</button>
|
|
1019
|
+
<button id="signout" class="navlink hidden" type="button">Sign out</button>
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
835
1022
|
</div>
|
|
836
1023
|
</header>
|
|
837
1024
|
|
|
838
|
-
<main id="
|
|
839
|
-
<section
|
|
840
|
-
<
|
|
841
|
-
|
|
842
|
-
<
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
<
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
<
|
|
851
|
-
|
|
1025
|
+
<main id="operatorContent" class="page shell" tabindex="-1">
|
|
1026
|
+
<section id="gate" class="hidden">
|
|
1027
|
+
<div class="lead pgrid">
|
|
1028
|
+
<h1 id="gateHeading" class="pcap" tabindex="-1">${OPERATOR_PAGE_LABELS[page]}</h1>
|
|
1029
|
+
<div class="pbody lead-copy">
|
|
1030
|
+
<p>${escapeHtmlAttr(brand.description)}</p>
|
|
1031
|
+
<p id="gateCopy" class="meta"></p>
|
|
1032
|
+
<div id="tokenGate" class="row gate-actions hidden">
|
|
1033
|
+
<input id="token" type="password" placeholder="Bearer token" autocomplete="off"
|
|
1034
|
+
aria-label="Bearer token">
|
|
1035
|
+
<button id="save" class="linklike" type="button">Open operator pages</button>
|
|
1036
|
+
</div>
|
|
1037
|
+
<div id="clerkGate" class="actions gate-actions hidden">
|
|
1038
|
+
<button id="signin" class="linklike" type="button">Team sign in</button>
|
|
1039
|
+
<button id="gateSignout" class="linklike hidden" type="button">Sign out</button>
|
|
1040
|
+
</div>
|
|
1041
|
+
<p id="err" role="alert"></p>
|
|
852
1042
|
</div>
|
|
853
|
-
<p id="err"></p>
|
|
854
1043
|
</div>
|
|
855
1044
|
</section>
|
|
856
|
-
</main>
|
|
857
1045
|
|
|
858
|
-
<
|
|
859
|
-
<section id="
|
|
1046
|
+
<div id="app" class="hidden">
|
|
1047
|
+
<section id="connectionsView"${page === "connections" ? "" : ' class="hidden"'}>
|
|
860
1048
|
<div class="lead pgrid">
|
|
861
|
-
<h1 class="pcap">
|
|
1049
|
+
<h1 id="connectionsHeading" class="pcap" tabindex="-1">Connections</h1>
|
|
862
1050
|
<div class="pbody lead-copy">
|
|
863
1051
|
<p>Use this endpoint to give an MCP client access to the tools below.</p>
|
|
864
1052
|
<div class="endpoint">
|
|
@@ -867,50 +1055,80 @@ ${clerkScript}
|
|
|
867
1055
|
<button id="copyMcpUrl" class="linklike" type="button">Copy URL</button>
|
|
868
1056
|
</div>
|
|
869
1057
|
</div>
|
|
870
|
-
<p class="cap" id="serverInfo">${escapeHtmlAttr(brand.productName)}
|
|
1058
|
+
<p class="cap" id="serverInfo">${escapeHtmlAttr(brand.productName)} operator</p>
|
|
871
1059
|
</div>
|
|
872
1060
|
</div>
|
|
873
|
-
<section class="section pgrid" aria-labelledby="
|
|
874
|
-
<h2 class="pcap" id="
|
|
1061
|
+
<section class="section pgrid" aria-labelledby="connectorLedgerHeading">
|
|
1062
|
+
<h2 class="pcap" id="connectorLedgerHeading">Connectors</h2>
|
|
875
1063
|
<div class="pbody">
|
|
876
1064
|
<div class="row toolbar">
|
|
877
1065
|
<input id="filter" type="search" placeholder="Filter connectors or tools…"
|
|
878
1066
|
aria-label="Filter connectors or tools">
|
|
879
1067
|
</div>
|
|
880
|
-
<
|
|
881
|
-
<div id="list" class="connector-tools"></div>
|
|
1068
|
+
<div id="list" class="connector-tools" aria-busy="false"></div>
|
|
882
1069
|
</div>
|
|
883
1070
|
</section>
|
|
884
1071
|
</section>
|
|
885
|
-
|
|
1072
|
+
|
|
1073
|
+
<section id="credentialsView"${page === "credentials" ? "" : ' class="hidden"'}>
|
|
886
1074
|
<div class="lead pgrid">
|
|
887
|
-
<h1 class="pcap">
|
|
1075
|
+
<h1 id="credentialsHeading" class="pcap" tabindex="-1">Credentials</h1>
|
|
1076
|
+
<div class="pbody">
|
|
1077
|
+
<p class="activity-copy">Rotate operator-managed connector credentials. Stored values are never returned or displayed.</p>
|
|
1078
|
+
<p id="credentialNotice" class="meta" role="status" aria-live="polite"
|
|
1079
|
+
tabindex="-1"></p>
|
|
1080
|
+
<div id="credentialUnavailable" class="unavailable hidden"></div>
|
|
1081
|
+
<div id="credentialList" class="credential-ledger" aria-busy="false"></div>
|
|
1082
|
+
</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
</section>
|
|
1085
|
+
|
|
1086
|
+
<section id="activityView"${page === "activity" ? "" : ' class="hidden"'}>
|
|
1087
|
+
<div class="lead pgrid">
|
|
1088
|
+
<h1 id="activityHeading" class="pcap" tabindex="-1">Activity</h1>
|
|
888
1089
|
<div class="pbody">
|
|
889
1090
|
<p class="activity-copy" id="activitySummary">Arguments and results are never stored.</p>
|
|
890
|
-
<div class="
|
|
891
|
-
<
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1091
|
+
<div id="activityUnavailable" class="unavailable hidden">
|
|
1092
|
+
Activity history is not configured. Add an <span class="mono">activity.store</span>
|
|
1093
|
+
with a list reader to enable this page.
|
|
1094
|
+
</div>
|
|
1095
|
+
<div id="activityAvailable">
|
|
1096
|
+
<div class="row activity-controls">
|
|
1097
|
+
<input id="activitySearch" type="search"
|
|
1098
|
+
placeholder="Search user, tool, or outcome…"
|
|
1099
|
+
aria-label="Search loaded activity">
|
|
1100
|
+
<button id="refreshActivity" class="linklike" type="button">Refresh</button>
|
|
1101
|
+
</div>
|
|
1102
|
+
<p id="activityNotice" class="meta" role="status" aria-live="polite"></p>
|
|
1103
|
+
<div id="activityList" class="activity-ledger" aria-busy="false"></div>
|
|
1104
|
+
<button id="moreActivity" class="linklike activity-more hidden" type="button">Load older</button>
|
|
895
1105
|
</div>
|
|
896
|
-
<p id="activityNotice" class="meta" role="status" aria-live="polite"></p>
|
|
897
|
-
<div id="activityList" class="activity-ledger"></div>
|
|
898
|
-
<button id="moreActivity" class="linklike activity-more hidden" type="button">Load older</button>
|
|
899
1106
|
</div>
|
|
900
1107
|
</div>
|
|
901
1108
|
</section>
|
|
1109
|
+
</div>
|
|
902
1110
|
</main>
|
|
903
1111
|
|
|
904
1112
|
<script${nonceAttr}>
|
|
905
1113
|
const AUTH = ${jsonForInlineScript(auth)};
|
|
906
1114
|
const MCP_URL = ${jsonForInlineScript(mcpUrl)};
|
|
1115
|
+
const INITIAL_PAGE = ${jsonForInlineScript(page)};
|
|
1116
|
+
const TITLE_SUFFIX = ${jsonForInlineScript(brand.pageTitle)};
|
|
907
1117
|
const filterUiConnectors = ${filterUiConnectors.toString()};
|
|
908
1118
|
const KEY = "connecta:token";
|
|
909
1119
|
const $ = (id) => document.getElementById(id);
|
|
1120
|
+
const PAGE_META = {
|
|
1121
|
+
connections: { path: "/", label: "Connections" },
|
|
1122
|
+
credentials: { path: "/credentials", label: "Credentials" },
|
|
1123
|
+
activity: { path: "/activity", label: "Activity" },
|
|
1124
|
+
};
|
|
910
1125
|
let DATA = null;
|
|
911
1126
|
let ACTIVITY = [];
|
|
912
1127
|
let ACTIVITY_CURSOR = null;
|
|
913
1128
|
let ACTIVITY_LOADED = false;
|
|
1129
|
+
let CURRENT_PAGE = INITIAL_PAGE;
|
|
1130
|
+
let SESSION_GENERATION = 0;
|
|
1131
|
+
let ACTIVITY_GENERATION = 0;
|
|
914
1132
|
$("mcpUrl").textContent = MCP_URL;
|
|
915
1133
|
|
|
916
1134
|
function esc(s) {
|
|
@@ -934,8 +1152,9 @@ function formatDate(value) {
|
|
|
934
1152
|
}
|
|
935
1153
|
|
|
936
1154
|
function setNotice(message, isError) {
|
|
937
|
-
$("
|
|
938
|
-
$("
|
|
1155
|
+
$("credentialNotice").textContent = message || "";
|
|
1156
|
+
$("credentialNotice").classList.toggle("error-notice", Boolean(isError));
|
|
1157
|
+
$("credentialNotice").setAttribute("role", isError ? "alert" : "status");
|
|
939
1158
|
}
|
|
940
1159
|
|
|
941
1160
|
async function sessionToken() {
|
|
@@ -944,7 +1163,45 @@ async function sessionToken() {
|
|
|
944
1163
|
: localStorage.getItem(KEY);
|
|
945
1164
|
}
|
|
946
1165
|
|
|
1166
|
+
function clearActivityState() {
|
|
1167
|
+
ACTIVITY_GENERATION += 1;
|
|
1168
|
+
ACTIVITY = [];
|
|
1169
|
+
ACTIVITY_CURSOR = null;
|
|
1170
|
+
ACTIVITY_LOADED = false;
|
|
1171
|
+
$("activityList").innerHTML = "";
|
|
1172
|
+
$("activityList").setAttribute("aria-busy", "false");
|
|
1173
|
+
$("activityNotice").textContent = "";
|
|
1174
|
+
$("activityNotice").setAttribute("role", "status");
|
|
1175
|
+
$("activitySummary").textContent = "Arguments and results are never stored.";
|
|
1176
|
+
$("activitySearch").value = "";
|
|
1177
|
+
$("refreshActivity").disabled = false;
|
|
1178
|
+
$("moreActivity").disabled = false;
|
|
1179
|
+
$("moreActivity").classList.add("hidden");
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function clearIdentityState() {
|
|
1183
|
+
SESSION_GENERATION += 1;
|
|
1184
|
+
DATA = null;
|
|
1185
|
+
clearActivityState();
|
|
1186
|
+
$("list").innerHTML = "";
|
|
1187
|
+
$("filter").value = "";
|
|
1188
|
+
$("credentialList").innerHTML = "";
|
|
1189
|
+
$("credentialList").setAttribute("aria-busy", "false");
|
|
1190
|
+
$("credentialNotice").textContent = "";
|
|
1191
|
+
$("credentialNotice").setAttribute("role", "status");
|
|
1192
|
+
$("credentialNotice").classList.remove("error-notice");
|
|
1193
|
+
$("credentialUnavailable").textContent = "";
|
|
1194
|
+
$("credentialUnavailable").classList.add("hidden");
|
|
1195
|
+
$("credentialList").classList.add("hidden");
|
|
1196
|
+
$("activityUnavailable").classList.add("hidden");
|
|
1197
|
+
$("activityAvailable").classList.add("hidden");
|
|
1198
|
+
$("credentialsNav").classList.add("hidden");
|
|
1199
|
+
$("activityNav").classList.add("hidden");
|
|
1200
|
+
$("serverInfo").textContent = ${escapeScriptString(brand.productName + " operator")};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
947
1203
|
function showGate(msg) {
|
|
1204
|
+
clearIdentityState();
|
|
948
1205
|
$("app").classList.add("hidden");
|
|
949
1206
|
$("appNav").classList.add("hidden");
|
|
950
1207
|
$("gate").classList.remove("hidden");
|
|
@@ -952,8 +1209,8 @@ function showGate(msg) {
|
|
|
952
1209
|
if (AUTH.kind === "clerk") {
|
|
953
1210
|
const signedIn = Boolean(window.Clerk && Clerk.user);
|
|
954
1211
|
$("gateCopy").textContent = signedIn
|
|
955
|
-
? "Signed in with Clerk, but this account cannot open
|
|
956
|
-
: "Sign in with Clerk to open
|
|
1212
|
+
? "Signed in with Clerk, but this account cannot open deployment-wide operator pages."
|
|
1213
|
+
: "Sign in with Clerk to open this operator page.";
|
|
957
1214
|
$("signin").classList.toggle("hidden", signedIn);
|
|
958
1215
|
$("gateSignout").classList.toggle("hidden", !signedIn);
|
|
959
1216
|
} else {
|
|
@@ -961,20 +1218,87 @@ function showGate(msg) {
|
|
|
961
1218
|
}
|
|
962
1219
|
}
|
|
963
1220
|
|
|
1221
|
+
function pageForPath(path) {
|
|
1222
|
+
if (path === "/credentials") return "credentials";
|
|
1223
|
+
if (path === "/activity") return "activity";
|
|
1224
|
+
return "connections";
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function credentialUnavailableCopy(capability) {
|
|
1228
|
+
if (capability === "no_slots") {
|
|
1229
|
+
return "No connectors declare operator-managed credential slots. Connector credentials remain configuration-as-code until a slot is declared.";
|
|
1230
|
+
}
|
|
1231
|
+
if (capability === "vault_not_configured") {
|
|
1232
|
+
return "Credential storage is not configured. Set credentials.encryptionKey before managing connector credentials here.";
|
|
1233
|
+
}
|
|
1234
|
+
return "Credential management requires an eligible Clerk operator. Bearer-authenticated sessions can inspect connections but cannot manage stored credentials.";
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function updateCapabilities() {
|
|
1238
|
+
const credentialsAvailable =
|
|
1239
|
+
DATA?.credentialManagement === "available";
|
|
1240
|
+
$("credentialsNav").classList.toggle("hidden", !credentialsAvailable);
|
|
1241
|
+
$("activityNav").classList.toggle("hidden", !DATA?.activityEnabled);
|
|
1242
|
+
$("credentialUnavailable").classList.toggle("hidden", credentialsAvailable);
|
|
1243
|
+
$("credentialList").classList.toggle("hidden", !credentialsAvailable);
|
|
1244
|
+
$("credentialUnavailable").textContent = credentialsAvailable
|
|
1245
|
+
? ""
|
|
1246
|
+
: credentialUnavailableCopy(DATA?.credentialManagement);
|
|
1247
|
+
$("activityUnavailable").classList.toggle("hidden", Boolean(DATA?.activityEnabled));
|
|
1248
|
+
$("activityAvailable").classList.toggle("hidden", !DATA?.activityEnabled);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function activatePage(page, options) {
|
|
1252
|
+
const next = PAGE_META[page] ? page : "connections";
|
|
1253
|
+
CURRENT_PAGE = next;
|
|
1254
|
+
for (const name of Object.keys(PAGE_META)) {
|
|
1255
|
+
$(name + "View").classList.toggle("hidden", name !== next);
|
|
1256
|
+
const link = $(name + "Nav");
|
|
1257
|
+
if (name === next) link.setAttribute("aria-current", "page");
|
|
1258
|
+
else link.removeAttribute("aria-current");
|
|
1259
|
+
}
|
|
1260
|
+
$("gateHeading").textContent = PAGE_META[next].label;
|
|
1261
|
+
document.title = PAGE_META[next].label + " — " + TITLE_SUFFIX;
|
|
1262
|
+
if (DATA) {
|
|
1263
|
+
updateCapabilities();
|
|
1264
|
+
if (next === "connections") renderConnections();
|
|
1265
|
+
if (next === "credentials") renderCredentials();
|
|
1266
|
+
if (next === "activity" && DATA.activityEnabled && !ACTIVITY_LOADED) {
|
|
1267
|
+
loadActivity(true);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (next !== "credentials") $("credentialList").innerHTML = "";
|
|
1271
|
+
// Focus what is actually on screen. While gated the page views are hidden, so
|
|
1272
|
+
// focusing their heading is a silent no-op that drops focus to <body> and
|
|
1273
|
+
// restarts the next Tab from the top of the document — the gate's own h1 is
|
|
1274
|
+
// the visible heading, and activatePage has just relabelled it.
|
|
1275
|
+
if (options?.focus) $(DATA ? next + "Heading" : "gateHeading").focus();
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function navigateTo(page, href) {
|
|
1279
|
+
history.pushState({ operatorPage: page }, "", href);
|
|
1280
|
+
activatePage(page, { focus: true });
|
|
1281
|
+
}
|
|
1282
|
+
|
|
964
1283
|
async function load() {
|
|
1284
|
+
const generation = SESSION_GENERATION;
|
|
965
1285
|
let token;
|
|
966
1286
|
try {
|
|
967
1287
|
token = await sessionToken();
|
|
968
1288
|
} catch (e) {
|
|
1289
|
+
if (generation !== SESSION_GENERATION) return;
|
|
969
1290
|
return showGate("Could not read the Clerk session: " + e.message);
|
|
970
1291
|
}
|
|
1292
|
+
if (generation !== SESSION_GENERATION) return;
|
|
971
1293
|
if (!token) return showGate("");
|
|
972
1294
|
let res;
|
|
973
1295
|
try {
|
|
974
1296
|
res = await fetch("/ui/data", { headers: { Authorization: "Bearer " + token } });
|
|
975
1297
|
} catch (e) {
|
|
1298
|
+
if (generation !== SESSION_GENERATION) return;
|
|
976
1299
|
return showGate("Network error: " + e.message);
|
|
977
1300
|
}
|
|
1301
|
+
if (generation !== SESSION_GENERATION) return;
|
|
978
1302
|
if (res.status === 401 || res.status === 403) {
|
|
979
1303
|
if (AUTH.kind === "clerk") {
|
|
980
1304
|
return showGate(
|
|
@@ -987,25 +1311,22 @@ async function load() {
|
|
|
987
1311
|
return showGate("Token rejected — enter a valid bearer token.");
|
|
988
1312
|
}
|
|
989
1313
|
if (!res.ok) return showGate("Error " + res.status);
|
|
990
|
-
|
|
1314
|
+
let data;
|
|
1315
|
+
try {
|
|
1316
|
+
data = await res.json();
|
|
1317
|
+
} catch (e) {
|
|
1318
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1319
|
+
return showGate("Operator data could not be read.");
|
|
1320
|
+
}
|
|
1321
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1322
|
+
DATA = data;
|
|
991
1323
|
$("gate").classList.add("hidden");
|
|
992
1324
|
$("app").classList.remove("hidden");
|
|
993
1325
|
$("appNav").classList.remove("hidden");
|
|
994
1326
|
const si = DATA.serverInfo || {};
|
|
995
1327
|
$("serverInfo").textContent = (si.name || ${escapeScriptString(brand.productName)}) + " v" + (si.version || "?");
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
function showView(view) {
|
|
1001
|
-
const activity = view === "activity";
|
|
1002
|
-
$("configView").classList.toggle("hidden", activity);
|
|
1003
|
-
$("activityView").classList.toggle("hidden", !activity);
|
|
1004
|
-
$("configTab").classList.toggle("active", !activity);
|
|
1005
|
-
$("activityTab").classList.toggle("active", activity);
|
|
1006
|
-
$("configTab").setAttribute("aria-pressed", String(!activity));
|
|
1007
|
-
$("activityTab").setAttribute("aria-pressed", String(activity));
|
|
1008
|
-
if (activity && !ACTIVITY_LOADED) loadActivity(true);
|
|
1328
|
+
updateCapabilities();
|
|
1329
|
+
activatePage(CURRENT_PAGE);
|
|
1009
1330
|
}
|
|
1010
1331
|
|
|
1011
1332
|
function actorLabel(actor) {
|
|
@@ -1044,12 +1365,21 @@ function renderActivity() {
|
|
|
1044
1365
|
}
|
|
1045
1366
|
for (const event of visible) {
|
|
1046
1367
|
const item = document.createElement("article");
|
|
1047
|
-
|
|
1048
|
-
|
|
1368
|
+
const outcomeClass = ["success", "error", "timeout"].includes(event.outcome)
|
|
1369
|
+
? event.outcome
|
|
1370
|
+
: "error";
|
|
1371
|
+
item.className = "activity-item " + outcomeClass;
|
|
1372
|
+
const retryCopy = event.attempts > 1
|
|
1373
|
+
? " · " + esc(event.attempts) + " attempts"
|
|
1374
|
+
: "";
|
|
1049
1375
|
const errorCopy = event.errorCode ? " · " + esc(event.errorCode) : "";
|
|
1050
1376
|
item.innerHTML =
|
|
1051
|
-
'<div
|
|
1052
|
-
|
|
1377
|
+
'<div class="activity-stamp"><span class="dot ' +
|
|
1378
|
+
(outcomeClass === "success" ? "ok" : "") +
|
|
1379
|
+
'" aria-hidden="true"></span><div><time class="activity-time" datetime="' +
|
|
1380
|
+
esc(event.occurredAt) + '">' +
|
|
1381
|
+
esc(formatDate(event.occurredAt)) +
|
|
1382
|
+
'</time><div class="activity-actor">' + esc(actorLabel(event.actor)) + '</div></div></div>' +
|
|
1053
1383
|
'<div><div class="activity-address">' + esc(event.address) +
|
|
1054
1384
|
'</div><div class="activity-detail">' + esc(event.source) + retryCopy +
|
|
1055
1385
|
errorCopy + '</div></div>' +
|
|
@@ -1062,22 +1392,45 @@ function renderActivity() {
|
|
|
1062
1392
|
|
|
1063
1393
|
async function loadActivity(reset) {
|
|
1064
1394
|
if (!DATA?.activityEnabled) return;
|
|
1395
|
+
const sessionGeneration = SESSION_GENERATION;
|
|
1396
|
+
if (reset) ACTIVITY_GENERATION += 1;
|
|
1397
|
+
const activityGeneration = ACTIVITY_GENERATION;
|
|
1398
|
+
const isCurrent = () =>
|
|
1399
|
+
sessionGeneration === SESSION_GENERATION &&
|
|
1400
|
+
activityGeneration === ACTIVITY_GENERATION;
|
|
1065
1401
|
$("activityNotice").textContent = "Loading activity…";
|
|
1402
|
+
$("activityNotice").setAttribute("role", "status");
|
|
1403
|
+
$("activityList").setAttribute("aria-busy", "true");
|
|
1066
1404
|
$("refreshActivity").disabled = true;
|
|
1067
1405
|
$("moreActivity").disabled = true;
|
|
1068
1406
|
try {
|
|
1069
1407
|
const token = await sessionToken();
|
|
1070
|
-
if (!
|
|
1408
|
+
if (!isCurrent()) return;
|
|
1409
|
+
if (!token) return showGate("Your session has expired.");
|
|
1071
1410
|
const cursor = reset ? null : ACTIVITY_CURSOR;
|
|
1072
1411
|
const params = new URLSearchParams({ limit: "50" });
|
|
1073
1412
|
if (cursor) params.set("cursor", cursor);
|
|
1074
1413
|
const res = await fetch("/ui/activity?" + params, {
|
|
1075
1414
|
headers: { Authorization: "Bearer " + token },
|
|
1076
1415
|
});
|
|
1416
|
+
if (!isCurrent()) return;
|
|
1077
1417
|
let payload = {};
|
|
1078
1418
|
try { payload = await res.json(); } catch (e) {}
|
|
1419
|
+
if (!isCurrent()) return;
|
|
1420
|
+
if (res.status === 401) {
|
|
1421
|
+
return showGate("Your session was not accepted. Sign in again.");
|
|
1422
|
+
}
|
|
1423
|
+
if (res.status === 403) {
|
|
1424
|
+
clearActivityState();
|
|
1425
|
+
$("activityNotice").setAttribute("role", "alert");
|
|
1426
|
+
$("activityNotice").textContent =
|
|
1427
|
+
"This identity may not read activity history.";
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1079
1430
|
if (!res.ok) {
|
|
1080
|
-
throw new Error(
|
|
1431
|
+
throw new Error(
|
|
1432
|
+
payload.error || "Activity could not be loaded (" + res.status + ")."
|
|
1433
|
+
);
|
|
1081
1434
|
}
|
|
1082
1435
|
ACTIVITY = reset
|
|
1083
1436
|
? (payload.events || [])
|
|
@@ -1087,14 +1440,19 @@ async function loadActivity(reset) {
|
|
|
1087
1440
|
$("activityNotice").textContent = "";
|
|
1088
1441
|
renderActivity();
|
|
1089
1442
|
} catch (e) {
|
|
1443
|
+
if (!isCurrent()) return;
|
|
1444
|
+
$("activityNotice").setAttribute("role", "alert");
|
|
1090
1445
|
$("activityNotice").textContent = e.message || "Activity could not be loaded.";
|
|
1091
1446
|
} finally {
|
|
1092
|
-
|
|
1093
|
-
|
|
1447
|
+
if (isCurrent()) {
|
|
1448
|
+
$("activityList").setAttribute("aria-busy", "false");
|
|
1449
|
+
$("refreshActivity").disabled = false;
|
|
1450
|
+
$("moreActivity").disabled = false;
|
|
1451
|
+
}
|
|
1094
1452
|
}
|
|
1095
1453
|
}
|
|
1096
1454
|
|
|
1097
|
-
function
|
|
1455
|
+
function renderConnections() {
|
|
1098
1456
|
const q = $("filter").value.trim().toLowerCase();
|
|
1099
1457
|
const list = $("list");
|
|
1100
1458
|
list.innerHTML = "";
|
|
@@ -1142,66 +1500,8 @@ function render() {
|
|
|
1142
1500
|
esc(c.authorizationUrl) + "</p>";
|
|
1143
1501
|
}
|
|
1144
1502
|
if (c.credential) {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
const removable = configured || Boolean(cred.removable);
|
|
1148
|
-
const state = configured
|
|
1149
|
-
? "configured · ••••" + esc(cred.lastFour || "")
|
|
1150
|
-
: "not configured";
|
|
1151
|
-
const updated = configured && cred.updatedAt
|
|
1152
|
-
? " · updated " + esc(formatDate(cred.updatedAt))
|
|
1153
|
-
: "";
|
|
1154
|
-
head += '<section class="credential" aria-label="' + esc(cred.label) + '">';
|
|
1155
|
-
head += '<div class="credential-head"><span class="credential-label">' +
|
|
1156
|
-
esc(cred.label) + '</span><span class="credential-state">' + state +
|
|
1157
|
-
updated + "</span></div>";
|
|
1158
|
-
if (cred.description) {
|
|
1159
|
-
head += '<p class="credential-copy meta">' + esc(cred.description) + "</p>";
|
|
1160
|
-
}
|
|
1161
|
-
if (cred.error) {
|
|
1162
|
-
head += '<div class="msg">' + esc(cred.error) + "</div>";
|
|
1163
|
-
}
|
|
1164
|
-
if (AUTH.kind === "clerk") {
|
|
1165
|
-
head += '<div class="credential-actions">';
|
|
1166
|
-
head += '<button class="linklike" type="button" data-credential-action="edit" data-connector="' +
|
|
1167
|
-
esc(c.id) + '">' + (removable ? "Replace" : "Add credential") + "</button>";
|
|
1168
|
-
if (configured && cred.testable) {
|
|
1169
|
-
head += '<button class="linklike" type="button" data-credential-action="test" data-connector="' +
|
|
1170
|
-
esc(c.id) + '">Test</button>';
|
|
1171
|
-
}
|
|
1172
|
-
if (removable) {
|
|
1173
|
-
head += '<button type="button" class="linklike danger" data-credential-action="remove" data-connector="' +
|
|
1174
|
-
esc(c.id) + '">Remove</button>';
|
|
1175
|
-
}
|
|
1176
|
-
head += "</div>";
|
|
1177
|
-
head += '<div class="credential-form hidden" data-credential-form="' +
|
|
1178
|
-
esc(c.id) + '">';
|
|
1179
|
-
if (cred.fields && cred.fields.length) {
|
|
1180
|
-
head += '<div class="credential-fields">';
|
|
1181
|
-
for (const field of cred.fields) {
|
|
1182
|
-
head += '<div class="credential-field"><label>' +
|
|
1183
|
-
esc(field.label) + '</label><input type="' +
|
|
1184
|
-
esc(field.inputType || "password") + '" data-credential-field="' +
|
|
1185
|
-
esc(field.name) + '" aria-label="' + esc(field.label) +
|
|
1186
|
-
'" placeholder="' + esc(field.placeholder || field.label) +
|
|
1187
|
-
'" autocomplete="' +
|
|
1188
|
-
(field.inputType === "password" ? "new-password" : "off") +
|
|
1189
|
-
'" autocapitalize="none" spellcheck="false"></div>';
|
|
1190
|
-
}
|
|
1191
|
-
head += "</div>";
|
|
1192
|
-
} else {
|
|
1193
|
-
head += '<input type="password" data-credential-input="' +
|
|
1194
|
-
esc(c.id) + '" aria-label="' + esc(cred.label) + '" placeholder="' +
|
|
1195
|
-
esc(cred.placeholder || "Paste credential") +
|
|
1196
|
-
'" autocomplete="new-password" autocapitalize="none" spellcheck="false">';
|
|
1197
|
-
}
|
|
1198
|
-
head += '<button class="linklike" type="button" data-credential-action="save" data-connector="' +
|
|
1199
|
-
esc(c.id) + '">Save</button><button class="linklike" type="button" data-credential-action="cancel" data-connector="' +
|
|
1200
|
-
esc(c.id) + '">Cancel</button></div>';
|
|
1201
|
-
} else {
|
|
1202
|
-
head += '<p class="credential-copy meta">Team sign in is required to manage this credential.</p>';
|
|
1203
|
-
}
|
|
1204
|
-
head += "</section>";
|
|
1503
|
+
head += '<p class="connector-auth"><a class="linklike" href="/credentials" ' +
|
|
1504
|
+
'data-operator-page="credentials">Manage credential →</a></p>';
|
|
1205
1505
|
}
|
|
1206
1506
|
let body = "";
|
|
1207
1507
|
if (tools.length) {
|
|
@@ -1219,12 +1519,122 @@ function render() {
|
|
|
1219
1519
|
list.appendChild(el);
|
|
1220
1520
|
}
|
|
1221
1521
|
if (!list.children.length) {
|
|
1222
|
-
list.innerHTML = '<p class="empty">
|
|
1522
|
+
list.innerHTML = '<p class="empty">' +
|
|
1523
|
+
(q
|
|
1524
|
+
? "No connectors or tools match this filter."
|
|
1525
|
+
: "No connectors are declared in this deployment.") +
|
|
1526
|
+
"</p>";
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function renderCredentials() {
|
|
1531
|
+
const list = $("credentialList");
|
|
1532
|
+
list.innerHTML = "";
|
|
1533
|
+
if (DATA.credentialManagement !== "available") return;
|
|
1534
|
+
for (const c of DATA.connectors.filter((connector) => connector.credential)) {
|
|
1535
|
+
const cred = c.credential;
|
|
1536
|
+
const configured = Boolean(cred.configured);
|
|
1537
|
+
const removable = configured || Boolean(cred.removable);
|
|
1538
|
+
const state = configured
|
|
1539
|
+
? cred.fields?.length
|
|
1540
|
+
? "configured"
|
|
1541
|
+
: "configured · ••••" + esc(cred.lastFour || "")
|
|
1542
|
+
: "not configured";
|
|
1543
|
+
const updated = configured && cred.updatedAt
|
|
1544
|
+
? " · updated " + esc(formatDate(cred.updatedAt))
|
|
1545
|
+
: "";
|
|
1546
|
+
const el = document.createElement("section");
|
|
1547
|
+
el.className = "credential-card";
|
|
1548
|
+
el.id = "credential-" + c.id;
|
|
1549
|
+
el.setAttribute("aria-labelledby", "credential-title-" + c.id);
|
|
1550
|
+
let body = '<div class="credential-head"><div class="connector-title">' +
|
|
1551
|
+
'<span class="dot ' + (configured ? "ok" : "auth_required") +
|
|
1552
|
+
'" aria-hidden="true"></span><h2 id="credential-title-' + esc(c.id) + '">' +
|
|
1553
|
+
esc(c.title || c.id) + '</h2></div><span class="credential-state">' +
|
|
1554
|
+
state + updated + "</span></div>";
|
|
1555
|
+
body += '<p class="mono">' + esc(c.id) + " · " + esc(cred.label) + "</p>";
|
|
1556
|
+
if (cred.description) {
|
|
1557
|
+
body += '<p class="credential-copy meta">' + esc(cred.description) + "</p>";
|
|
1558
|
+
}
|
|
1559
|
+
if (cred.fields?.length) {
|
|
1560
|
+
body += '<div class="credential-field-summary">';
|
|
1561
|
+
for (const field of cred.fields) {
|
|
1562
|
+
const fieldState = field.configured
|
|
1563
|
+
? "configured · ••••" + esc(field.lastFour || "") +
|
|
1564
|
+
(field.updatedAt ? " · updated " + esc(formatDate(field.updatedAt)) : "")
|
|
1565
|
+
: "not configured";
|
|
1566
|
+
body += '<div><span>' + esc(field.label) +
|
|
1567
|
+
'</span><span class="meta">' + fieldState + "</span></div>";
|
|
1568
|
+
}
|
|
1569
|
+
body += "</div>";
|
|
1570
|
+
}
|
|
1571
|
+
if (c.credentialCheck) {
|
|
1572
|
+
const check = c.credentialCheck;
|
|
1573
|
+
const verdict = check.state === "ok"
|
|
1574
|
+
? "healthy"
|
|
1575
|
+
: check.state === "auth_required"
|
|
1576
|
+
? "needs authorization"
|
|
1577
|
+
: "check failed";
|
|
1578
|
+
body += '<p class="connector-check meta">Liveness: ' + esc(verdict) +
|
|
1579
|
+
" · " + esc(formatDate(check.checkedAt)) +
|
|
1580
|
+
(check.message ? " — " + esc(check.message) : "") + "</p>";
|
|
1581
|
+
}
|
|
1582
|
+
if (cred.error) body += '<div class="msg">' + esc(cred.error) + "</div>";
|
|
1583
|
+
// Leftover stored fields are not an error — the credential still works, so
|
|
1584
|
+
// this stays muted copy rather than the msg block an actual failure earns.
|
|
1585
|
+
if (cred.notice) {
|
|
1586
|
+
body += '<p class="credential-copy meta">' + esc(cred.notice) + "</p>";
|
|
1587
|
+
}
|
|
1588
|
+
body += '<div class="credential-actions">';
|
|
1589
|
+
body += '<button class="linklike" type="button" data-credential-action="edit" data-connector="' +
|
|
1590
|
+
esc(c.id) + '">' + (removable ? "Replace" : "Add credential") + "</button>";
|
|
1591
|
+
if (configured && cred.testable) {
|
|
1592
|
+
body += '<button class="linklike" type="button" data-credential-action="test" data-connector="' +
|
|
1593
|
+
esc(c.id) + '">Test</button>';
|
|
1594
|
+
}
|
|
1595
|
+
if (removable) {
|
|
1596
|
+
body += '<button type="button" class="linklike danger" data-credential-action="remove" data-connector="' +
|
|
1597
|
+
esc(c.id) + '">Remove</button>';
|
|
1598
|
+
}
|
|
1599
|
+
body += "</div>";
|
|
1600
|
+
body += '<div class="credential-form hidden" data-credential-form="' +
|
|
1601
|
+
esc(c.id) + '">';
|
|
1602
|
+
if (cred.fields && cred.fields.length) {
|
|
1603
|
+
body += '<div class="credential-fields">';
|
|
1604
|
+
for (let index = 0; index < cred.fields.length; index += 1) {
|
|
1605
|
+
const field = cred.fields[index];
|
|
1606
|
+
const inputId = "credential-input-" + c.id + "-" + index;
|
|
1607
|
+
body += '<div class="credential-field"><label for="' + esc(inputId) + '">' +
|
|
1608
|
+
esc(field.label) + '</label><input id="' + esc(inputId) + '" type="' +
|
|
1609
|
+
esc(field.inputType || "password") + '" data-credential-field="' +
|
|
1610
|
+
esc(field.name) + '" placeholder="' + esc(field.placeholder || field.label) +
|
|
1611
|
+
'" autocomplete="' +
|
|
1612
|
+
(field.inputType === "password" ? "new-password" : "off") +
|
|
1613
|
+
'" autocapitalize="none" spellcheck="false"></div>';
|
|
1614
|
+
}
|
|
1615
|
+
body += "</div>";
|
|
1616
|
+
} else {
|
|
1617
|
+
const inputId = "credential-input-" + c.id;
|
|
1618
|
+
body += '<label class="visually-hidden" for="' + esc(inputId) + '">' +
|
|
1619
|
+
esc(cred.label) + '</label><input id="' + esc(inputId) +
|
|
1620
|
+
'" type="password" data-credential-input="' +
|
|
1621
|
+
esc(c.id) + '" aria-label="' + esc(cred.label) + '" placeholder="' +
|
|
1622
|
+
esc(cred.placeholder || "Paste credential") +
|
|
1623
|
+
'" autocomplete="new-password" autocapitalize="none" spellcheck="false">';
|
|
1624
|
+
}
|
|
1625
|
+
body += '<button class="linklike" type="button" data-credential-action="save" data-connector="' +
|
|
1626
|
+
esc(c.id) + '">Save</button><button class="linklike" type="button" data-credential-action="cancel" data-connector="' +
|
|
1627
|
+
esc(c.id) + '">Cancel</button></div>';
|
|
1628
|
+
el.innerHTML = body;
|
|
1629
|
+
list.appendChild(el);
|
|
1223
1630
|
}
|
|
1224
1631
|
}
|
|
1225
1632
|
|
|
1226
|
-
async function credentialRequest(connector, method, action, body) {
|
|
1633
|
+
async function credentialRequest(connector, method, action, body, generation) {
|
|
1227
1634
|
const token = await sessionToken();
|
|
1635
|
+
if (generation !== SESSION_GENERATION) {
|
|
1636
|
+
throw new Error("The operator session changed.");
|
|
1637
|
+
}
|
|
1228
1638
|
if (!token) throw new Error("Your Clerk session has expired.");
|
|
1229
1639
|
const suffix = action ? "/" + action : "";
|
|
1230
1640
|
const res = await fetch(
|
|
@@ -1250,12 +1660,13 @@ function credentialForm(connector) {
|
|
|
1250
1660
|
CSS.escape(connector) + '"]');
|
|
1251
1661
|
}
|
|
1252
1662
|
|
|
1253
|
-
$("
|
|
1663
|
+
$("credentialList").onclick = async (event) => {
|
|
1254
1664
|
const button = event.target.closest("[data-credential-action]");
|
|
1255
1665
|
if (!button) return;
|
|
1256
1666
|
const connector = button.dataset.connector;
|
|
1257
1667
|
const action = button.dataset.credentialAction;
|
|
1258
1668
|
const form = credentialForm(connector);
|
|
1669
|
+
const generation = SESSION_GENERATION;
|
|
1259
1670
|
|
|
1260
1671
|
if (action === "edit") {
|
|
1261
1672
|
form.classList.remove("hidden");
|
|
@@ -1265,6 +1676,10 @@ $("list").onclick = async (event) => {
|
|
|
1265
1676
|
if (action === "cancel") {
|
|
1266
1677
|
form.querySelectorAll("input").forEach((input) => { input.value = ""; });
|
|
1267
1678
|
form.classList.add("hidden");
|
|
1679
|
+
document.querySelector(
|
|
1680
|
+
'[data-credential-action="edit"][data-connector="' +
|
|
1681
|
+
CSS.escape(connector) + '"]'
|
|
1682
|
+
)?.focus();
|
|
1268
1683
|
return;
|
|
1269
1684
|
}
|
|
1270
1685
|
if (action === "remove" && !window.confirm(
|
|
@@ -1272,6 +1687,7 @@ $("list").onclick = async (event) => {
|
|
|
1272
1687
|
)) return;
|
|
1273
1688
|
|
|
1274
1689
|
setNotice("");
|
|
1690
|
+
$("credentialList").setAttribute("aria-busy", "true");
|
|
1275
1691
|
const buttons = [...document.querySelectorAll(
|
|
1276
1692
|
'[data-connector="' + CSS.escape(connector) + '"]'
|
|
1277
1693
|
)];
|
|
@@ -1286,42 +1702,65 @@ $("list").onclick = async (event) => {
|
|
|
1286
1702
|
if (!value) throw new Error("Complete every credential field before saving.");
|
|
1287
1703
|
values[input.dataset.credentialField] = value;
|
|
1288
1704
|
}
|
|
1289
|
-
await credentialRequest(connector, "PUT", "", { values });
|
|
1705
|
+
await credentialRequest(connector, "PUT", "", { values }, generation);
|
|
1290
1706
|
} else {
|
|
1291
1707
|
const input = form.querySelector("[data-credential-input]");
|
|
1292
1708
|
const value = input.value.trim();
|
|
1293
1709
|
if (!value) throw new Error("Paste a credential before saving.");
|
|
1294
|
-
await credentialRequest(connector, "PUT", "", { value });
|
|
1710
|
+
await credentialRequest(connector, "PUT", "", { value }, generation);
|
|
1295
1711
|
}
|
|
1712
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1296
1713
|
form.querySelectorAll("input").forEach((input) => { input.value = ""; });
|
|
1297
1714
|
setNotice("Credential saved.");
|
|
1298
1715
|
await load();
|
|
1716
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1717
|
+
$("credentialNotice").focus();
|
|
1299
1718
|
} else if (action === "remove") {
|
|
1300
|
-
await credentialRequest(connector, "DELETE");
|
|
1719
|
+
await credentialRequest(connector, "DELETE", "", null, generation);
|
|
1720
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1301
1721
|
setNotice("Credential removed.");
|
|
1302
1722
|
await load();
|
|
1723
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1724
|
+
$("credentialNotice").focus();
|
|
1303
1725
|
} else if (action === "test") {
|
|
1304
|
-
const result = await credentialRequest(
|
|
1726
|
+
const result = await credentialRequest(
|
|
1727
|
+
connector,
|
|
1728
|
+
"POST",
|
|
1729
|
+
"test",
|
|
1730
|
+
null,
|
|
1731
|
+
generation,
|
|
1732
|
+
);
|
|
1733
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1305
1734
|
setNotice(
|
|
1306
1735
|
result.message || (result.ok ? "Credential is valid." : "Credential test failed."),
|
|
1307
1736
|
!result.ok,
|
|
1308
1737
|
);
|
|
1309
1738
|
}
|
|
1310
1739
|
} catch (e) {
|
|
1740
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1311
1741
|
setNotice(e.message || "Credential action failed.", true);
|
|
1312
1742
|
} finally {
|
|
1313
|
-
|
|
1743
|
+
if (generation === SESSION_GENERATION) {
|
|
1744
|
+
$("credentialList").setAttribute("aria-busy", "false");
|
|
1745
|
+
buttons.forEach((item) => { item.disabled = false; });
|
|
1746
|
+
}
|
|
1314
1747
|
}
|
|
1315
1748
|
};
|
|
1316
1749
|
|
|
1317
|
-
$("save").onclick = () => {
|
|
1750
|
+
$("save").onclick = async () => {
|
|
1318
1751
|
const v = $("token").value.trim();
|
|
1319
1752
|
if (!v) return;
|
|
1753
|
+
clearIdentityState();
|
|
1320
1754
|
localStorage.setItem(KEY, v);
|
|
1321
1755
|
$("token").value = "";
|
|
1322
|
-
load();
|
|
1756
|
+
await load();
|
|
1757
|
+
if (DATA) $(CURRENT_PAGE + "Heading").focus();
|
|
1758
|
+
};
|
|
1759
|
+
$("change").onclick = () => {
|
|
1760
|
+
localStorage.removeItem(KEY);
|
|
1761
|
+
showGate("");
|
|
1762
|
+
$("token").focus();
|
|
1323
1763
|
};
|
|
1324
|
-
$("change").onclick = () => { localStorage.removeItem(KEY); showGate(""); };
|
|
1325
1764
|
$("copyMcpUrl").onclick = async () => {
|
|
1326
1765
|
const button = $("copyMcpUrl");
|
|
1327
1766
|
try {
|
|
@@ -1336,14 +1775,35 @@ $("signin").onclick = () => Clerk.redirectToSignIn({
|
|
|
1336
1775
|
signInFallbackRedirectUrl: window.location.href,
|
|
1337
1776
|
signUpFallbackRedirectUrl: window.location.href,
|
|
1338
1777
|
});
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
$("
|
|
1778
|
+
function signOut() {
|
|
1779
|
+
clearIdentityState();
|
|
1780
|
+
return Clerk.signOut({ redirectUrl: window.location.href });
|
|
1781
|
+
}
|
|
1782
|
+
$("gateSignout").onclick = signOut;
|
|
1783
|
+
$("signout").onclick = signOut;
|
|
1784
|
+
$("filter").oninput = () => { if (DATA) renderConnections(); };
|
|
1344
1785
|
$("refreshActivity").onclick = () => loadActivity(true);
|
|
1345
1786
|
$("moreActivity").onclick = () => loadActivity(false);
|
|
1346
1787
|
$("activitySearch").oninput = () => renderActivity();
|
|
1788
|
+
document.addEventListener("click", (event) => {
|
|
1789
|
+
const link = event.target.closest("a[data-operator-page]");
|
|
1790
|
+
if (
|
|
1791
|
+
!link ||
|
|
1792
|
+
event.defaultPrevented ||
|
|
1793
|
+
event.button !== 0 ||
|
|
1794
|
+
event.metaKey ||
|
|
1795
|
+
event.ctrlKey ||
|
|
1796
|
+
event.shiftKey ||
|
|
1797
|
+
event.altKey
|
|
1798
|
+
) return;
|
|
1799
|
+
const target = new URL(link.href, window.location.href);
|
|
1800
|
+
if (target.origin !== window.location.origin) return;
|
|
1801
|
+
event.preventDefault();
|
|
1802
|
+
navigateTo(link.dataset.operatorPage, target.pathname + target.search + target.hash);
|
|
1803
|
+
});
|
|
1804
|
+
window.addEventListener("popstate", () => {
|
|
1805
|
+
activatePage(pageForPath(window.location.pathname), { focus: true });
|
|
1806
|
+
});
|
|
1347
1807
|
|
|
1348
1808
|
async function init() {
|
|
1349
1809
|
if (AUTH.kind === "clerk") {
|
|
@@ -1367,6 +1827,7 @@ async function init() {
|
|
|
1367
1827
|
$("tokenGate").classList.remove("hidden");
|
|
1368
1828
|
$("change").classList.remove("hidden");
|
|
1369
1829
|
}
|
|
1830
|
+
activatePage(pageForPath(window.location.pathname));
|
|
1370
1831
|
await load();
|
|
1371
1832
|
}
|
|
1372
1833
|
|