diff --git a/CHANGELOG.md b/CHANGELOG.md index bf1aefd2..c2582e0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ All notable changes to this package are documented here. ## Unreleased +## 0.26.2 — 2026-09-27 + +This patch restores named client tokens as an optional auth module. Deployments +upgrading from v0.23 can keep their existing stored records and client secrets, +without rotation, by configuring `accessTokens(storage)` with the same storage +namespace and preserving their identity grants. The old boolean configuration +must be replaced. Deployments that omit the module are unchanged. The eight MCP +tools remain unchanged, and the Node template now pins 0.26.2. + +### Added + +- `@zackbart/connecta/auth/access-tokens` verifies v0.23 `cta_…` tokens and + restores create, show-once, list, rename, and revoke in the operator UI. + `identity.accessTokenManagement` explicitly permits interactive operators; + client tokens cannot administer tokens or connection credentials. New issuance + requires atomic storage and reserves capacity across concurrent instances. + +### Fixed + +- Existing token ids, activity labels, and principal bindings survive the upgrade. + Malformed or corrupt records fail closed, including principal fields that + JavaScript regular expressions previously coerced into strings. Revocation + deletes the admission lookup before updating metadata. + + ## 0.26.1 — 2026-09-26 This patch fixes slow OAuth restarts, unbounded downstream authorization waits, diff --git a/README.md b/README.md index 05bf434f..8258d71a 100644 --- a/README.md +++ b/README.md @@ -132,3 +132,14 @@ breaks and what a deployment can ignore. Built for its author's deployments first and published openly. Breaking changes are expected before 1.0. See the [changelog](./CHANGELOG.md) and [security policy](./SECURITY.md). + +### Existing client tokens + +Upgrading from v0.23 does not require rotating managed `cta_…` tokens. Import +`accessTokens` from `@zackbart/connecta/auth/access-tokens`, replace the old +`accessTokens: true` with `accessTokens: accessTokens(storage)`, and keep the +same persistent storage namespace and identity/tool/pool grant rules. Enable +`identity.accessTokenManagement` only for the interactive operators who should +manage tokens. New issuance needs storage with atomic `compareAndSet`; older +storage adapters can still verify existing tokens. See the package's +`documentation/auth.md` for the migration and storage requirements. diff --git a/documentation/architecture.md b/documentation/architecture.md index 143781de..c3ca93bd 100644 --- a/documentation/architecture.md +++ b/documentation/architecture.md @@ -614,6 +614,6 @@ compiling and configuring the real thing. connector limiters, then the executor, then the Connecta's runtime; Node's `listen()` calls it on SIGTERM/SIGINT. - **Structural mistakes throw at construction.** A duplicate connector id, an - invalid admission rule, the removed `accessTokens` option, a missing executor: + invalid admission rule, the old boolean `accessTokens` option, a missing executor: all refuse to boot (`test/config.test.ts`, `test/registry.test.ts`). Starting in the wrong shape is worse than not starting. diff --git a/documentation/auth.md b/documentation/auth.md index 0995fb05..b6ee28af 100644 --- a/documentation/auth.md +++ b/documentation/auth.md @@ -7,7 +7,7 @@ static bearers are checked first, then other providers in configuration order. An `InboundAuth` provider's `authorize(request, baseUrl, runtimeContext)` returns either `{ ok: true, userId?, subjectId?, principal? }` or a refusal carrying its own `Response`, so the provider owns its challenge. Connecta -issues no tokens of its own and serves no token-management routes. +issues managed client tokens only when the optional `accessTokens` module is configured. The bearer adapter challenges with `WWW-Authenticate: Bearer` and deliberately omits `resource_metadata`: its credential is configured out of band, so it has no @@ -16,6 +16,67 @@ or the edge own OAuth discovery. An open deployment with any connector warns at construction — including API connectors carrying static auth headers, and with sharper wording for credential and OAuth connectors. +## Managed client tokens and upgrading from v0.23 + +Import `accessTokens` from `@zackbart/connecta/auth/access-tokens` and pass its +module to `createConnecta`. It installs its inbound adapter and, beside `ui`, +the Access tokens page and `/ui/access-tokens` lifecycle routes. Omitting the +module loads none of its implementation and serves none of those routes. + +```ts +import { accessTokens } from "@zackbart/connecta/auth/access-tokens"; + +createConnecta({ + storage, + accessTokens: accessTokens(storage), + auth: clerkAuth({ publishableKey, secretKey }), + ui: operatorUi(), + identity: { + connectorAccess, // Keep the existing principal and token-id grant rules. + accessTokenManagement: ({ principal }) => + principal?.namespace === "clerk:your-existing-namespace" && + principal.id === "your-operator-id", + }, + connectors, + executor, +}); +``` + +For a v0.23 deployment, replace the old `accessTokens: true` or options object +with `accessTokens: accessTokens(storage)`, using **the same storage and key +namespace**. Keep the existing `access-token:v1:record:*` and +`access-token:v1:lookup:*` records. Their unrevoked `cta_…` secrets keep working; +no secret recovery, rewrite, or client rotation is needed. Preserve the existing +identity namespaces and connector/pool grant callbacks too. Storage compatibility +does not translate deployment configuration or invent replacement grants. + +The adapter preserves `access_token` actor ids, the +`connecta:access-tokens:v1` activity namespace, friendly names, and any stored +principal. A token without a principal stays unbound. Every request evaluates +current identity/tool/pool grants; a token never becomes an interactive operator. +Names are labels, never permissions. New UI-issued tokens belong to the issuing +human's principal, and any explicitly permitted token manager can list, rename, +or revoke deployment tokens. Static and managed bearers cannot manage tokens or +connection credentials. Operators need an interactive auth provider. + +Storage must implement `list`. Existing-token verification, rename, and revoke +also work on older adapters without `compareAndSet`; **new issuance requires +atomic `compareAndSet`**, because counting records before writing admits too many +concurrent creates. Active capacity defaults to 100, configurable with +`accessTokens(storage, { maxActive: 200 })`, up to 1,000. A durable reservation +counts before a secret is written. An interrupted create can consume capacity +without returning a token; it is never automatically retried or released after +an uncertain write. Avoid creating new tokens through old-version instances once +new-version issuance has started, since those instances do not honor reservations. + +Secrets contain 256 random bits and only their SHA-256 digests persist. Creation +returns the secret once; list and rename never return it. Revocation removes its +lookup before updating metadata, and authorization has no token cache. A strongly +consistent store makes revocation effective on the next authorization; an +eventually consistent backend retains its own propagation delay. Requests already +admitted are not recalled. Management writes require an exact same-origin Origin, +and responses are private and non-cacheable. + ## Origins `allowedOrigins?: readonly string[] | "*"` bounds which browsers may speak to @@ -409,7 +470,7 @@ from caller input. interactive human, the one default here that is open, because a single-operator deployment would otherwise be locked out of its own event stream. Team deployments should set it. There is no general administrator role and no -token-management authority. +implicit token-management authority. `identity.accessTokenManagement` is a separate boolean permission, false by default, evaluated only for interactive humans. Lifecycle routes also require a stable principal. ```ts createConnecta({ diff --git a/ethos.md b/ethos.md index abc7f5cb..ee5cc1be 100644 --- a/ethos.md +++ b/ethos.md @@ -40,7 +40,7 @@ carries them. | Optional deployment modules | accepted | typed slots select UI, activity, vault, and inbound auth; core keeps discovery, execution, invocation, and enforcement | | Artifacts module | accepted | a built-in connector for team-only sandboxed pages over stored JSON; immutable versions let writes skip approval. Supersedes [#287](https://github.com/zackbart/connecta/issues/287) | | Plugin lifecycle, provider registry, or marketplace | refused | modules are deployment code, not runtime installs; prebuilt connections are imports, discovered in docs ([#297](https://github.com/zackbart/connecta/issues/297)) | -| Connecta-issued access tokens | removed | inbound providers authenticate clients; bearer auth stays an optional adapter | +| Connecta-issued access tokens | accepted | optional /auth/access-tokens preserves v0.23 secrets; config owns grants, interactive management needs explicit permission ([#619](https://github.com/zackbart/connecta/issues/619)) | | Expanded Notion page create/update options | refused | different workflows, not missing fields; use `api()` ([#408](https://github.com/zackbart/connecta/issues/408)) | | Resources, prompts, and downstream MCP Apps templates | refused | tools only; clients own presentation ([#266](https://github.com/zackbart/connecta/issues/266)) | | Protocol sessions, server push, elicitation passthrough | refused | stateless per request; elicitation has no route | diff --git a/package-lock.json b/package-lock.json index 9f0369cb..c263cdd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zackbart/connecta", - "version": "0.26.1", + "version": "0.26.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zackbart/connecta", - "version": "0.26.1", + "version": "0.26.2", "license": "MIT", "dependencies": { "@cfworker/json-schema": "^4.1.1", diff --git a/package.json b/package.json index ac858376..fd00cbd9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zackbart/connecta", - "version": "0.26.1", + "version": "0.26.2", "type": "module", "sideEffects": false, "description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of eight meta-tools.", @@ -114,6 +114,10 @@ "./artifacts": { "types": "./dist/artifacts.d.ts", "import": "./dist/artifacts.js" + }, + "./auth/access-tokens": { + "types": "./dist/access-tokens.d.ts", + "import": "./dist/access-tokens.js" } }, "scripts": { diff --git a/scripts/bundle-budget.json b/scripts/bundle-budget.json index 55382c96..90d4ef1e 100644 --- a/scripts/bundle-budget.json +++ b/scripts/bundle-budget.json @@ -3,7 +3,8 @@ "Gzip-9 bytes of each Web-facing entry as scripts/bundle-size.mjs bundles it. baselineGzip is the P1-S19 measurement at 0.25.0, with the Effect core in place; main before the conversion (4f536a8) was 235,346 B at the root and 263,949 B for the Worker example.", "Caps: every entry gets baseline + 60,000 B. The conversion-era allowances are retired: the root's flat 386,000 B cap covered a rewrite that is finished, and the +160 KB held for HttpApi in ./ui, ./activity and the Worker example was never spent, because P1-S18 measured HttpApi and did not use it.", "A cap moves only in a change that says why. Raising one to make a check pass is the failure this file exists to catch.", - "./artifacts is measured where it was introduced (#562): the store, the hand-written HTML tokenizer, the Markdown renderer, and validation, with no Effect and no dependency. The connector, guide, and page routes that follow it spend from the same baseline + 60,000 B allowance." + "./artifacts is measured where it was introduced (#562): the store, the hand-written HTML tokenizer, the Markdown renderer, and validation, with no Effect and no dependency. The connector, guide, and page routes that follow it spend from the same baseline + 60,000 B allowance.", + "./auth/access-tokens starts at 4,222 B gzip in #619. Its legacy record verifier and operator lifecycle use no runtime dependency; the cap uses the existing baseline + 60,000 B policy." ], "entries": { ".": { @@ -73,6 +74,10 @@ "examples/worker": { "baselineGzip": 314336, "maxGzip": 374336 + }, + "./auth/access-tokens": { + "baselineGzip": 4222, + "maxGzip": 64222 } } } diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs index 7db3cf07..1eff5581 100644 --- a/scripts/check-package.mjs +++ b/scripts/check-package.mjs @@ -369,6 +369,9 @@ if (typeof core.createConnecta !== "function") throw new Error("missing core"); if (typeof core.validateToolInput !== "function") { throw new Error("missing validateToolInput"); } +const tokenModule = await import("@zackbart/connecta/auth/access-tokens"); +if (typeof tokenModule.accessTokens !== "function") throw new Error("missing managed-token module"); +if ("accessTokens" in core || "AccessTokenManager" in core) throw new Error("token implementation leaked into core"); const jsonSchema = await import("@zackbart/connecta/json-schema"); if (typeof jsonSchema.Validator !== "function") { throw new Error("missing Validator re-export"); @@ -725,6 +728,41 @@ try { await stopChild(deployment); } + // Exercise the installed package and real QuickJS with an original v0.23 + // secret. The configured static bearer is different, so only the restored + // module can admit this doctor request. + const legacyTokens = JSON.parse(await readFile( + join(root, "test", "fixtures", "access-tokens-v023.json"), "utf8", + )); + const legacyState = join(generatedRoot, "legacy-token-state.json"); + await writeFile(legacyState, JSON.stringify(Object.fromEntries( + Object.entries(legacyTokens.records).map(([key, value]) => [key, { value }]), + ))); + await writeFile(join(generatedRoot, "src", "managed-tokens.ts"), + 'import { accessTokens } from "@zackbart/connecta/auth/access-tokens";\n' + + generatedSource.replace(createCall, createCall + ' accessTokens: accessTokens(storage),\n'), + ); + const legacyPort = await freePort(); + let legacyOutput = ""; + const legacyDeployment = spawn(generatedTsx, ["src/managed-tokens.ts"], { + cwd: generatedRoot, + env: { ...process.env, CONNECTA_TOKEN: smokeToken, CONNECTA_STATE_FILE: legacyState, PORT: String(legacyPort) }, + stdio: ["ignore", "pipe", "pipe"], + }); + const retainLegacyOutput = chunk => { legacyOutput = (legacyOutput + chunk.toString()).slice(-8_000); }; + legacyDeployment.stdout.on("data", retainLegacyOutput); + legacyDeployment.stderr.on("data", retainLegacyOutput); + try { + await waitForHealth(`http://127.0.0.1:${legacyPort}/health`, legacyDeployment, () => legacyOutput); + const doctorOutput = run( + join(generatedRoot, "node_modules", ".bin", process.platform === "win32" ? "connecta.cmd" : "connecta"), + ["doctor", "--url", `http://127.0.0.1:${legacyPort}`], generatedRoot, + { CONNECTA_TOKEN: legacyTokens.bound.token }, + ); + if (!doctorOutput.includes("QuickJS executed")) throw new Error("Legacy token doctor did not prove execution"); + console.log("v0.23 token compatibility: doctor passed with the original secret"); + } finally { await stopChild(legacyDeployment); } + // The generated deployment is also the container: `connecta init` ships the // Dockerfile and Compose file, so the source that just answered over tsx has // to answer again from `docker compose up` (#344). Docker is not a diff --git a/src/access-tokens.ts b/src/access-tokens.ts new file mode 100644 index 00000000..e3ff8a2e --- /dev/null +++ b/src/access-tokens.ts @@ -0,0 +1,364 @@ +import type { + AuthResult, + IdentityReference, + InboundAuth, + KVStorage, +} from "./types.js"; +import { routeAccessTokens } from "./routes/access-tokens.js"; +import type { AccessTokensModule } from "./module-contracts.js"; +import { validIdentityReference } from "./identity.js"; + +const TOKEN_PREFIX = "cta_"; +const TOKEN_BYTES = 32; +const TOKEN_VALUE_RE = /^cta_[A-Za-z0-9_-]{43}$/; +const RECORD_PREFIX = "access-token:v1:record:"; +const LOOKUP_PREFIX = "access-token:v1:lookup:"; +const MAX_NAME_CHARACTERS = 80; +const DEFAULT_MAX_ACTIVE = 100; +const MAX_CONFIGURED_ACTIVE = 1_000; +const ACTIVE_KEY = "access-token:v1:active"; +const ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u; +const encoder = new TextEncoder(); + +interface StoredAccessToken { + version: 1; + id: string; + name: string; + tokenHash: string; + tokenPrefix: string; + createdAt: string; + createdBy: string; + principal?: IdentityReference; + revokedAt?: string; + revokedBy?: string; +} + +interface TokenLookup { + version: 1; + id: string; +} + +export interface AccessTokenMetadata { + id: string; + name: string; + tokenPrefix: string; + createdAt: string; + revokedAt?: string; +} + +export interface CreatedAccessToken { + token: string; + accessToken: AccessTokenMetadata; +} + +function recordKey(id: string): string { + return `${RECORD_PREFIX}${id}`; +} + +function lookupKey(hash: string): string { + return `${LOOKUP_PREFIX}${hash}`; +} + +function bytesToBase64Url(bytes: Uint8Array): string { + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary) + .replaceAll("+", "-") + .replaceAll("/", "_") + .replace(/=+$/u, ""); +} + +function bytesToHex(bytes: Uint8Array): string { + return [...bytes] + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); +} + +async function hashToken(token: string): Promise { + return bytesToHex( + new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(token))), + ); +} + +function normalizeName(value: unknown): string { + if (typeof value !== "string") { + throw new Error("Token name must be a string"); + } + const compact = value.replace(/\s+/gu, " ").trim(); + if (!compact) throw new Error("Token name cannot be empty"); + if (Array.from(compact).length > MAX_NAME_CHARACTERS) { + throw new Error( + `Token name cannot exceed ${MAX_NAME_CHARACTERS} characters`, + ); + } + return compact; +} + +function parseRecord(raw: string): StoredAccessToken { + try { + const value = JSON.parse(raw) as Partial; + if ( + value.version !== 1 || + typeof value.id !== "string" || + !ID_RE.test(value.id) || + typeof value.name !== "string" || + !value.name.trim() || + Array.from(value.name).length > MAX_NAME_CHARACTERS || + typeof value.tokenHash !== "string" || + !/^[0-9a-f]{64}$/u.test(value.tokenHash) || + typeof value.tokenPrefix !== "string" || + !/^cta_[A-Za-z0-9_-]{8}$/u.test(value.tokenPrefix) || + typeof value.createdAt !== "string" || + !Number.isFinite(Date.parse(value.createdAt)) || + typeof value.createdBy !== "string" || + (value.principal !== undefined && + !validIdentityReference(value.principal)) || + (value.revokedAt !== undefined && + (typeof value.revokedAt !== "string" || !value.revokedAt || !Number.isFinite(Date.parse(value.revokedAt)))) || + (value.revokedBy !== undefined && + typeof value.revokedBy !== "string") + ) { + throw new Error("invalid token record"); + } + return value as StoredAccessToken; + } catch { + throw new Error("Stored access token metadata is invalid or corrupted"); + } +} + +function parseLookup(raw: string): TokenLookup | null { + try { + const value = JSON.parse(raw) as Partial; + return value.version === 1 && typeof value.id === "string" && ID_RE.test(value.id) + ? { version: 1, id: value.id } + : null; + } catch { + return null; + } +} + +function metadata(record: StoredAccessToken): AccessTokenMetadata { + return { + id: record.id, + name: record.name, + tokenPrefix: record.tokenPrefix, + createdAt: record.createdAt, + ...(record.revokedAt ? { revokedAt: record.revokedAt } : {}), + }; +} + +function unauthorized(): AuthResult { + return { + ok: false, + response: new Response(JSON.stringify({ error: "unauthorized" }), { + status: 401, + headers: { + "Content-Type": "application/json", + "WWW-Authenticate": "Bearer", + }, + }), + }; +} + +/** + * Deployment-scoped personal access tokens. Secret material is never + * recoverable: authentication indexes a SHA-256 digest of a random 256-bit + * token, while separately enumerable metadata powers operator management. + */ +export class AccessTokenManager { + readonly auth: InboundAuth; + private readonly maxActive: number; + + constructor( + private readonly storage: KVStorage, + options: { maxActive?: number } = {}, + ) { + if (!storage.list) { + throw new Error( + "accessTokens requires a storage adapter that implements list(prefix)", + ); + } + const maxActive = options.maxActive ?? DEFAULT_MAX_ACTIVE; + if ( + !Number.isInteger(maxActive) || + maxActive < 1 || + maxActive > MAX_CONFIGURED_ACTIVE + ) { + throw new Error( + `accessTokens.maxActive must be a whole number from 1 to ${MAX_CONFIGURED_ACTIVE}`, + ); + } + this.maxActive = maxActive; + this.auth = { + kind: "access_token", + activityActorNamespace: "connecta:access-tokens:v1", + activityActorLabel: async (id) => { + try { + return (await this.read(id))?.name; + } catch { + return undefined; + } + }, + authorize: (request) => this.authorize(request).catch(() => unauthorized()), + }; + } + + private async read(id: string): Promise { + if (!ID_RE.test(id)) return null; + const raw = await this.storage.get(recordKey(id)); + const record = raw ? parseRecord(raw) : null; + if (record && record.id !== id) throw new Error("Stored access token id mismatch"); + return record; + } + + async list(): Promise { + const keys = await this.storage.list!(RECORD_PREFIX); + const records = await Promise.all( + keys.map(async (key) => { + const raw = await this.storage.get(key); + return raw ? parseRecord(raw) : null; + }), + ); + return records + .filter((record): record is StoredAccessToken => Boolean(record)) + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + .map(metadata); + } + + async create( + name: unknown, + createdBy: string | IdentityReference, + ): Promise { + const normalizedName = normalizeName(name); + if (!this.storage.compareAndSet) { + throw new Error("Creating access tokens requires atomic compareAndSet storage"); + } + if (typeof createdBy !== "string" && !validIdentityReference(createdBy)) { + throw new Error("Invalid token owner"); + } + const secretBytes = crypto.getRandomValues(new Uint8Array(TOKEN_BYTES)); + const token = TOKEN_PREFIX + bytesToBase64Url(secretBytes); + const hash = await hashToken(token); + if (await this.storage.get(lookupKey(hash))) { + throw new Error("Access token collision; create another token"); + } + const record: StoredAccessToken = { + version: 1, + id: crypto.randomUUID(), + name: normalizedName, + tokenHash: hash, + tokenPrefix: token.slice(0, 12), + createdAt: new Date().toISOString(), + createdBy: typeof createdBy === "string" + ? createdBy + : `${createdBy.namespace}:${createdBy.id}`, + ...(typeof createdBy === "string" + ? {} + : { principal: { ...createdBy } }), + }; + await this.updateActive(record.id, true); + await this.storage.set(recordKey(record.id), JSON.stringify(record)); + // A failed lookup write may have committed. Keep its reservation and + // metadata, so a manager can revoke it without ever returning the secret. + await this.storage.set( + lookupKey(hash), + JSON.stringify({ version: 1, id: record.id } satisfies TokenLookup), + ); + return { token, accessToken: metadata(record) }; + } + + async rename( + id: string, + name: unknown, + ): Promise { + const normalized = normalizeName(name); + const record = await this.updateRecord(id, current => ({ ...current, name: normalized })); + return record ? metadata(record) : null; + } + + async revoke( + id: string, + revokedBy: string, + ): Promise { + const record = await this.read(id); + if (!record) return null; + // Remove admission first. A metadata failure cannot leave a successful + // revocation's lookup alive. Retry deletion even if already marked revoked. + await this.storage.delete(lookupKey(record.tokenHash)); + const updated = await this.updateRecord(id, current => current.revokedAt ? current : { + ...current, revokedAt: new Date().toISOString(), revokedBy, + }); + if (this.storage.compareAndSet) await this.updateActive(id, false); + return updated ? metadata(updated) : null; + } + + private async updateRecord( + id: string, + update: (record: StoredAccessToken) => StoredAccessToken, + ): Promise { + if (!ID_RE.test(id)) return null; + for (let attempt = 0; attempt < 32; attempt++) { + const raw = await this.storage.get(recordKey(id)); + if (!raw) return null; + const record = parseRecord(raw); + if (record.id !== id) throw new Error("Stored access token id mismatch"); + const next = update(record); + if (this.storage.compareAndSet) { + // A rename racing revocation must retain revokedAt, including when + // an in-flight create has not yet published its lookup. + if (!await this.storage.compareAndSet(recordKey(id), raw, JSON.stringify(next))) continue; + } else { + // Legacy adapters cannot create, so no late lookup writer exists. + // Revocation's deleted lookup still refuses a stale metadata write. + await this.storage.set(recordKey(id), JSON.stringify(next)); + } + return next; + } + throw new Error("Access token metadata is busy; retry the operation"); + } + + private async updateActive(id: string, adding: boolean): Promise { + const cas = this.storage.compareAndSet; + if (!cas) throw new Error("Creating access tokens requires atomic compareAndSet storage"); + for (let attempt = 0; attempt < 32; attempt++) { + const raw = await this.storage.get(ACTIVE_KEY); + const ids: unknown = raw === null + ? (await this.list()).filter(token => !token.revokedAt).map(token => token.id) + : JSON.parse(raw); + if (!Array.isArray(ids) || !ids.every(value => typeof value === "string" && ID_RE.test(value)) || new Set(ids).size !== ids.length) { + throw new Error("Stored access token capacity is invalid"); + } + if (adding && ids.length >= this.maxActive) throw new Error(`This deployment already has the maximum of ${this.maxActive} active access tokens`); + const next = adding ? [...ids, id] : ids.filter(value => value !== id); + if (await cas.call(this.storage, ACTIVE_KEY, raw, JSON.stringify(next))) return; + } + throw new Error("Access token capacity is busy; retry the operation"); + } + + private async authorize(request: Request): Promise { + const header = request.headers.get("authorization") ?? ""; + const match = /^Bearer\s+(.+)$/iu.exec(header); + const token = match?.[1]; + if (!token || !TOKEN_VALUE_RE.test(token)) return unauthorized(); + const hash = await hashToken(token); + const lookupRaw = await this.storage.get(lookupKey(hash)); + if (!lookupRaw) return unauthorized(); + const lookup = parseLookup(lookupRaw); + if (!lookup) return unauthorized(); + const record = await this.read(lookup.id); + if (!record || record.revokedAt || record.tokenHash !== hash || record.tokenPrefix !== token.slice(0, 12)) { + return unauthorized(); + } + return { + ok: true, + subjectId: record.id, + ...(record.principal ? { principal: { ...record.principal } } : {}), + }; + } +} + +/** Opt in using the same storage namespace that held the v0.23 records. */ +export function accessTokens(storage: KVStorage, options: { maxActive?: number } = {}): AccessTokensModule { + const manager = new AccessTokenManager(storage, options); + return { auth: manager.auth, handle: context => routeAccessTokens(context, manager) }; +} diff --git a/src/identity.ts b/src/identity.ts index cc7b288f..15291621 100644 --- a/src/identity.ts +++ b/src/identity.ts @@ -8,6 +8,8 @@ export function validIdentityReference( ): value is IdentityReference { return Boolean( value && + typeof value.namespace === "string" && + typeof value.id === "string" && IDENTITY_PART_RE.test(value.namespace) && IDENTITY_PART_RE.test(value.id), ); diff --git a/src/index.ts b/src/index.ts index b3c3e10b..44a512f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ import { withExecutorAdmission, } from "./executor-admission.js"; import type { + AccessTokensModule, ActivityModule, ArtifactsModule, OperatorSurface, @@ -34,6 +35,7 @@ import { disposeEdgeRuntime } from "./runtime/run.js"; import { NO_EXEMPTIONS, type ApprovalPolicy } from "./tool-safety.js"; import { createCoreRuntime, resolveLogger } from "./runtime/services.js"; export type { + AccessTokensModule, ActivityModule, ArtifactsModule, OperatorSurface, @@ -242,6 +244,10 @@ export interface ConnectaIdentityConfig { credentialAdministration?( identity: Readonly, ): ConnectorPermission | Promise; + /** Client-token lifecycle management by interactive humans. Defaults to false. */ + accessTokenManagement?( + identity: Readonly, + ): boolean | Promise; /** Connecting or changing the caller's personal account. Defaults to none. */ personalConnection?( identity: Readonly, @@ -297,6 +303,8 @@ export interface ConnectaConfig { * because the links it hands out are shared. */ artifacts?: ArtifactsModule; + /** Optional managed client tokens, created by accessTokens() from /auth/access-tokens. */ + accessTokens?: AccessTokensModule; /** Optional dedicated HTTPS origin that serves only artifact pages and their library. */ artifactOrigin?: string; /** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */ @@ -407,6 +415,7 @@ const CONFIG_SCHEMA = { activityAccess: null, credentialAdministration: null, personalConnection: null, + accessTokenManagement: null, } satisfies ClosedOptionSchema, pools: null, storage: null, @@ -416,6 +425,7 @@ const CONFIG_SCHEMA = { vault: null, ui: null, artifacts: null, + accessTokens: null, artifactOrigin: null, discovery: { concurrency: null, @@ -504,7 +514,6 @@ function rejectUnknownOptions(paths: string[]): void { `Unknown Connecta configuration option${paths.length === 1 ? "" : "s"}:\n` + paths.map((path) => `- ${path}`).join("\n") + (paths.includes("ConnectaConfig.credentials") ? "\nUse vault: encryptedCredentialVault(storage, key) from @zackbart/connecta/credentials." : "") + - (paths.includes("ConnectaConfig.accessTokens") ? "\nConnecta-issued tokens were removed. Configure an inbound auth adapter instead." : "") + (paths.includes("ConnectaConfig.branding") ? "\nMove branding into ui: operatorUi({ branding }) from @zackbart/connecta/ui." : ""), ); } @@ -538,6 +547,12 @@ function assertKnownConfig(config: ConnectaConfig): void { "ConnectaConfig.activity must be created with activityHistory(...)", ); } + if (config.accessTokens !== undefined && (!config.accessTokens || + typeof config.accessTokens.auth?.authorize !== "function" || + config.accessTokens.auth.interactiveOperator || + typeof config.accessTokens.handle !== "function")) { + throw new Error("ConnectaConfig.accessTokens must be created with accessTokens(storage) from @zackbart/connecta/auth/access-tokens; reuse your existing storage to preserve tokens"); + } const artifacts = config.artifacts as unknown; if (artifacts !== undefined) { const connector = (artifacts as Partial | null)?.connector; @@ -927,7 +942,10 @@ export function createConnecta(config: ConnectaConfig): Connecta { const storage = config.storage ?? memoryStorage(); const logger = resolveLogger(config.logger); const credentialVault = config.vault; - const configuredAuth = normalizeAuth(config.auth); + const configuredAuth = normalizeAuth([ + ...(config.accessTokens ? [config.accessTokens.auth] : []), + ...normalizeAuth(config.auth), + ]); const serverInfo = { ...config.serverInfo, name: config.serverInfo?.name ?? "connecta", @@ -1075,6 +1093,7 @@ export function createConnecta(config: ConnectaConfig): Connecta { activity: config.activity?.store, activityModule: config.activity, artifactsModule: config.artifacts, + accessTokens: config.accessTokens, activityReadGate: config.activity?.readGate, activityDeploymentId: config.activity?.deploymentId, executor, diff --git a/src/module-contracts.ts b/src/module-contracts.ts index 11a1e9f6..a99d6732 100644 --- a/src/module-contracts.ts +++ b/src/module-contracts.ts @@ -42,3 +42,9 @@ export interface ArtifactsModule { /** Bind the optional refresh runner after core has built its registry and executor. */ bindRefresh?(runtime: ArtifactRefreshRuntime): void; } + +/** Optional client-token authentication and operator lifecycle. */ +export interface AccessTokensModule { + readonly auth: import("./types.js").InboundAuth; + handle(context: RouteContext): Promise; +} diff --git a/src/operator-ui/app/main.tsx b/src/operator-ui/app/main.tsx index 5477e502..a1287d80 100644 --- a/src/operator-ui/app/main.tsx +++ b/src/operator-ui/app/main.tsx @@ -16,6 +16,7 @@ import { type OperatorState, } from "../view.js"; import { auth, homeUrl, productDescription, titleSuffix } from "./config.js"; +import { TokensPage } from "./tokens.js"; import { ActivityPage } from "./activity.js"; import { ArtifactPage, ArtifactsPage } from "./artifacts.js"; import { ConnectionsPage } from "./connections.js"; @@ -26,6 +27,7 @@ import { forgetBearer, getState, loadActivity, + loadAccessTokens, signIn, signInWithBearer, signOut, @@ -63,6 +65,7 @@ function visiblePages(state: OperatorState): OperatorPage[] { // artifact origin, a first visit — only what this page can vouch for shows. const hint = state.data ? null : navHint(); return OPERATOR_PAGES.filter((page) => { + if (page === "tokens") return state.data?.accessTokenManagement === "available"; if (page === "activity") return hint ? hint.activity : Boolean(state.data?.activityEnabled); if (page === "artifacts") { return isArtifactPage(state.page) || (hint ? hint.artifacts : Boolean(state.data?.artifactsEnabled)); @@ -198,6 +201,7 @@ function Gate({ state }: { state: OperatorState }) { } function CurrentPage({ state }: { state: OperatorState }) { + if (state.page === "tokens") return ; if (state.page === "activity") return ; if (state.page === "artifacts") return ; if (state.page === "artifact") return ; @@ -219,6 +223,9 @@ function OperatorApp() { // identity opens it, and again after an identity change resets it to idle. useEffect(() => { if (!ready) return; + if (state.page === "tokens" && state.data?.accessTokenManagement === "available" && state.tokenPhase === "idle") { + void loadAccessTokens(); + } if ( state.page === "activity" && state.data?.activityEnabled && diff --git a/src/operator-ui/app/store.ts b/src/operator-ui/app/store.ts index 760d3ccf..d3694ab4 100644 --- a/src/operator-ui/app/store.ts +++ b/src/operator-ui/app/store.ts @@ -27,6 +27,7 @@ import { type RequestFailureKind, type RowAction, type UiActivityEvent, + type UiAccessToken, } from "../view.js"; import { auth, initialPage, productName, TOKEN_KEY } from "./config.js"; @@ -139,6 +140,9 @@ function gate(notice: Notice | null = null): void { } interface OperatorResponse { + token?: string; + accessToken?: UiAccessToken; + accessTokens?: UiAccessToken[]; ok?: boolean; state?: string; problem?: string; @@ -1034,3 +1038,133 @@ export async function refreshConnector(id: string, quiet = false): Promise if (quiet && outcome.kind !== "detail") return; applyDetail(id, outcome); } + +/* Access tokens ----------------------------------------------------------- */ + +export async function loadAccessTokens(): Promise { + const current = fence(); + set({ tokenPhase: "loading", tokenNotice: null }); + try { + const payload = await operatorRequest("/ui/access-tokens", "GET", current); + if (!current()) return; + set({ tokenPhase: "ready", tokens: payload?.accessTokens ?? [] }); + } catch { + if (!current()) return; + set({ + tokenPhase: "error", + tokenNotice: failure( + "Access tokens could not be loaded.", + ), + }); + } +} + +function tokenFailure(tokenNotice: Notice): Partial { + return { tokenBusy: false, tokenNotice, pendingFocus: "tokenNotice" }; +} + +/** + * Resolves true only when the token exists. `mutate` lands a handled failure in + * state and resolves like any other outcome, so a caller that clears its form on + * resolution would throw away what the operator typed the moment the POST + * failed — the dead end every other flow here avoids. The form clears on this + * boolean instead. + */ +export function createAccessToken(name: string): Promise { + if (state.tokenBusy) return Promise.resolve(false); + if (!name) { + set(tokenFailure(failure("Name the MCP client before creating a token."))); + return Promise.resolve(false); + } + let created = false; + return mutate({ + request: (current) => + operatorRequest("/ui/access-tokens", "POST", current, { name }), + busy: { tokenBusy: true, tokenNotice: null }, + done: (payload) => { + const issued = payload?.accessToken; + if (!payload?.token || !issued) { + throw new Error("The created token was not returned."); + } + created = true; + return { + tokenBusy: false, + tokenPhase: "ready", + tokens: [ + issued, + ...state.tokens.filter((token) => token.id !== issued.id), + ], + createdToken: state.page === "tokens" ? payload.token : null, + tokenNotice: info("Access token created."), + pendingFocus: state.page === "tokens" ? "tokenRevealHeading" : null, + }; + }, + failed: () => tokenFailure(failure("Access token could not be created. Check the name, capacity, and storage.")), + }).then(() => created); +} + +export function dismissCreatedToken(): void { + set({ createdToken: null }); +} + +export function renameAccessToken(id: string | null): void { + set({ tokenRenaming: id }); +} + +function accessTokenMutation( + id: string, + method: "DELETE" | "PUT", + body: object | undefined, + success: string, + fallback: string, +): Promise { + return mutate({ + request: (current) => + operatorRequest( + `/ui/access-tokens/${encodeURIComponent(id)}`, + method, + current, + body, + ), + busy: { tokenBusy: true, tokenNotice: null }, + done: (payload) => ({ + tokenBusy: false, + tokenRenaming: null, + tokenNotice: info(success), + pendingFocus: "tokenNotice", + ...(payload?.accessToken + ? { + tokens: state.tokens.map((token) => + token.id === id ? payload.accessToken! : token, + ), + } + : {}), + }), + failed: () => tokenFailure(failure(fallback)), + }); +} + +export function saveAccessTokenName(id: string, name: string): Promise { + return accessTokenMutation( + id, + "PUT", + { name }, + "Access token renamed.", + "Access token could not be renamed.", + ); +} + +export function revokeAccessToken(id: string): Promise { + const named = state.tokens.find((token) => token.id === id); + const confirmed = window.confirm( + `Revoke ${named?.name || "this access token"}? Its MCP client will immediately lose access.`, + ); + if (!confirmed) return Promise.resolve(); + return accessTokenMutation( + id, + "DELETE", + undefined, + "Access token revoked.", + "Access token could not be revoked.", + ); +} diff --git a/src/operator-ui/app/tokens.tsx b/src/operator-ui/app/tokens.tsx new file mode 100644 index 00000000..51b4ace2 --- /dev/null +++ b/src/operator-ui/app/tokens.tsx @@ -0,0 +1,232 @@ +import { useState } from "preact/hooks"; +import { + accessTokenUnavailableCopy, + formatDate, + type OperatorState, + type UiAccessToken, +} from "../view.js"; +import { CopyButton, Empty, NoticeLine, Unavailable } from "./parts.js"; +import { + createAccessToken, + dismissCreatedToken, + loadAccessTokens, + renameAccessToken, + revokeAccessToken, + saveAccessTokenName, +} from "./store.js"; + +function CreateForm({ busy }: { busy: boolean }) { + const [name, setName] = useState(""); + return ( +
{ + event.preventDefault(); + // Only a token that exists empties the field. A rejected POST leaves + // the typed client name where it was, so the retry is one click. + void createAccessToken(name.trim()).then((created) => { + if (created) setName(""); + }); + }} + > + +
+ setName(event.currentTarget.value)} + /> + +
+
+ ); +} + +function Reveal({ token }: { token: string }) { + return ( +
+
+

+ Copy this token now +

+ Shown once +
+

+ Store it in the MCP client before leaving this page. It cannot be + displayed again. +

+
+ + {token} + + +
+ +
+ ); +} + +function TokenCard({ + token, + renaming, + busy, +}: { + token: UiAccessToken; + renaming: boolean; + busy: boolean; +}) { + const [name, setName] = useState(token.name); + const revoked = Boolean(token.revokedAt); + return ( +
+
+
+

{token.name}

+

{token.tokenPrefix}…

+
+
+ {revoked + ? `Revoked ${formatDate(token.revokedAt)}` + : `Created ${formatDate(token.createdAt)}`} +
+
+
+ + {revoked ? null : ( + + )} +
+ {renaming ? ( +
{ + event.preventDefault(); + const next = name.trim(); + if (next) void saveAccessTokenName(token.id, next); + }} + > + + setName(event.currentTarget.value)} + /> + + +
+ ) : null} +
+ ); +} + +export function TokensPage({ state }: { state: OperatorState }) { + const available = state.data?.accessTokenManagement === "available"; + return ( +
+
+

+ Access tokens +

+
+

+ Create named Bearer tokens for MCP clients. Each secret is shown + once; revoke it when that client should lose access. +

+ + {!available ? ( + + {accessTokenUnavailableCopy(state.data?.accessTokenManagement)} + + ) : ( +
+ {state.createdToken ? ( + + ) : ( + + )} +
+ {state.tokenPhase === "loading" ? ( + Loading access tokens… + ) : state.tokenPhase === "error" ? ( +

+ +

+ ) : state.tokens.length === 0 ? ( + + No access tokens yet. Name the first MCP client above. + + ) : ( + state.tokens.map((token) => ( + + )) + )} +
+
+ )} +
+
+
+ ); +} diff --git a/src/operator-ui/browser.css b/src/operator-ui/browser.css index ada15845..639d64ea 100644 --- a/src/operator-ui/browser.css +++ b/src/operator-ui/browser.css @@ -414,3 +414,13 @@ .artifact-row { grid-template-columns: minmax(0, 1fr); } .artifact-badges { justify-content: flex-start; } } + +/* Client token lifecycle uses the same cards and controls as connection auth. */ +.token-create, .token-reveal { margin-bottom: 24px; } +.token-create > label { display: block; margin-bottom: 8px; } +.token-create input { flex: 1; min-width: 0; } +.token-reveal { border: 1px solid var(--rule); padding: 20px; } +.token-reveal-head, .token-card-head { display: flex; justify-content: space-between; gap: 16px; } +.token-secret { overflow-wrap: anywhere; margin: 12px 0; } +.token-card { border-top: 1px solid var(--rule); padding: 20px 0; } +.token-card.revoked { color: var(--muted); } diff --git a/src/operator-ui/generated.ts b/src/operator-ui/generated.ts index 7e08f955..547902f3 100644 --- a/src/operator-ui/generated.ts +++ b/src/operator-ui/generated.ts @@ -1,4 +1,4 @@ // Generated by scripts/build-operator-ui.mjs. Do not edit. // Source: src/operator-ui/app/main.tsx and src/operator-ui/browser.css. -export const OPERATOR_UI_CSS: string = "/* src/operator-ui/tokens.css */\n:root {\n color-scheme: light;\n --accent: #2f5fe0;\n --radius: 10px;\n --sans:\n ui-sans-serif,\n system-ui,\n -apple-system,\n \"Segoe UI\",\n Roboto,\n \"Helvetica Neue\",\n Arial,\n sans-serif;\n --mono:\n ui-monospace,\n \"SF Mono\",\n Menlo,\n Monaco,\n \"Cascadia Code\",\n Consolas,\n monospace;\n --bg: #f6f7f9;\n --surface: #ffffff;\n --surface-2: #f1f3f6;\n --border: #e2e5ea;\n --border-strong: #cdd2da;\n --text: #131820;\n --muted: #5b6472;\n --ok: #12734a;\n --warn: #9a5b06;\n --danger: #bb3226;\n --on-accent: #ffffff;\n --link: var(--accent);\n --tint: color-mix(in srgb, var(--accent) 8%, var(--surface));\n --shell: 68rem;\n --pad: 1.25rem;\n --gap: 1rem;\n}\n@media (prefers-color-scheme: dark) {\n html:not([data-scheme=light]) {\n color-scheme: dark;\n }\n}\nhtml[data-scheme=dark] {\n color-scheme: dark;\n}\n@media (prefers-color-scheme: dark) {\n html:not([data-scheme=light]) {\n --bg: #0d1016;\n --surface: #151a21;\n --surface-2: #1c222c;\n --border: #262d39;\n --border-strong: #38414f;\n --text: #e6eaf1;\n --muted: #97a1b2;\n --ok: #4cc48c;\n --warn: #e2a33f;\n --danger: #f4776c;\n --link: color-mix(in srgb, var(--accent) 55%, #ffffff);\n --tint: color-mix(in srgb, var(--accent) 16%, var(--surface));\n }\n}\nhtml[data-scheme=dark] {\n --bg: #0d1016;\n --surface: #151a21;\n --surface-2: #1c222c;\n --border: #262d39;\n --border-strong: #38414f;\n --text: #e6eaf1;\n --muted: #97a1b2;\n --ok: #4cc48c;\n --warn: #e2a33f;\n --danger: #f4776c;\n --link: color-mix(in srgb, var(--accent) 55%, #ffffff);\n --tint: color-mix(in srgb, var(--accent) 16%, var(--surface));\n}\n\n/* src/operator-ui/page.css */\n* {\n box-sizing: border-box;\n}\nhtml {\n background: var(--bg);\n color: var(--text);\n font-family: var(--sans);\n font-size: 16px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n}\nbody {\n margin: 0;\n min-height: 100vh;\n}\n:is(h1, h2, h3, p, ul, ol) {\n margin: 0;\n padding: 0;\n}\n:is(h1, h2, h3) {\n font-weight: 600;\n line-height: 1.3;\n}\nh1 {\n font-size: 1.5rem;\n letter-spacing: -.01em;\n}\nh2 {\n font-size: 1rem;\n}\n:is(ul, ol) {\n list-style: none;\n}\na {\n color: var(--link);\n text-decoration-thickness: 1px;\n text-underline-offset: 2px;\n}\nbutton,\ninput {\n font: inherit;\n}\n:is(a, button, input, summary):focus-visible {\n border-radius: calc(var(--radius) / 2);\n outline: 2px solid var(--link);\n outline-offset: 2px;\n}\n.skip-link {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n left: var(--pad);\n padding: .5rem .75rem;\n position: fixed;\n top: -4rem;\n z-index: 10;\n}\n.skip-link:focus {\n top: var(--pad);\n}\n.shell {\n margin: 0 auto;\n max-width: var(--shell);\n padding-left: var(--pad);\n padding-right: var(--pad);\n}\n.cap,\n.meta {\n color: var(--muted);\n font-size: .875rem;\n}\n.mono {\n font-family: var(--mono);\n font-size: .8125rem;\n}\n.visually-hidden {\n clip-path: inset(50%);\n height: 1px;\n overflow: hidden;\n position: absolute;\n white-space: nowrap;\n width: 1px;\n}\n.masthead {\n align-items: center;\n background: var(--surface);\n border-bottom: 1px solid var(--border);\n display: flex;\n gap: var(--gap);\n justify-content: space-between;\n max-width: none;\n padding-bottom: .75rem;\n padding-top: .75rem;\n position: sticky;\n top: 0;\n z-index: 5;\n}\n.masthead-inner {\n align-items: center;\n display: flex;\n gap: var(--gap);\n justify-content: space-between;\n margin: 0 auto;\n max-width: var(--shell);\n width: 100%;\n}\n.mast-nav,\n.mast-actions,\n.page-nav,\n.session-actions {\n align-items: center;\n display: flex;\n flex-wrap: wrap;\n gap: .25rem;\n}\n.mast-nav {\n gap: var(--gap);\n}\n.mast-actions {\n gap: var(--gap);\n}\n.navlink {\n background: none;\n border: 0;\n border-radius: calc(var(--radius) - 2px);\n color: var(--muted);\n cursor: pointer;\n font-size: .9375rem;\n padding: .35rem .6rem;\n text-decoration: none;\n}\n.navlink:hover {\n background: var(--surface-2);\n color: var(--text);\n}\n.navlink[aria-current=page] {\n background: var(--tint);\n color: var(--link);\n font-weight: 500;\n}\n.brand,\n.product {\n padding-left: 0;\n padding-right: 0;\n text-decoration: none;\n}\n.brand {\n color: var(--text);\n font-weight: 600;\n}\n.brand:hover,\n.product:hover {\n background: none;\n}\n.product {\n color: var(--muted);\n}\n.page {\n padding-bottom: 4rem;\n padding-top: 2rem;\n}\n.lead {\n margin-bottom: 1.5rem;\n}\n.lead-copy {\n color: var(--muted);\n display: grid;\n gap: .5rem;\n margin-top: .35rem;\n}\n.lead-copy p {\n max-width: 60ch;\n}\n.btn {\n background: var(--surface);\n border: 1px solid var(--border-strong);\n border-radius: calc(var(--radius) - 2px);\n color: var(--text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n font-size: .875rem;\n gap: .35rem;\n padding: .35rem .7rem;\n text-decoration: none;\n white-space: nowrap;\n}\n.btn:hover {\n background: var(--surface-2);\n}\n.btn:disabled {\n cursor: not-allowed;\n opacity: .55;\n}\n.btn.primary {\n background: var(--accent);\n border-color: var(--accent);\n color: var(--on-accent);\n}\n.btn.primary:hover {\n background: color-mix(in srgb, var(--accent) 88%, black);\n}\n.btn.danger {\n color: var(--danger);\n}\n.btn.danger:hover {\n background: color-mix(in srgb, var(--danger) 10%, var(--surface));\n}\n.btn.quiet {\n background: none;\n border-color: var(--border);\n color: var(--muted);\n}\n.btn.quiet:hover {\n background: var(--surface-2);\n color: var(--text);\n}\n.badge {\n align-items: center;\n background: var(--surface-2);\n border-radius: 999px;\n color: var(--muted);\n display: inline-flex;\n font-size: .75rem;\n gap: .3rem;\n line-height: 1.6;\n padding: .1rem .5rem;\n white-space: nowrap;\n}\n.badge.ok {\n background: color-mix(in srgb, var(--ok) 12%, var(--surface));\n color: var(--ok);\n}\n.badge.warn {\n background: color-mix(in srgb, var(--warn) 14%, var(--surface));\n color: var(--warn);\n}\n.badge.danger {\n background: color-mix(in srgb, var(--danger) 12%, var(--surface));\n color: var(--danger);\n}\n.badge.accent {\n background: var(--tint);\n color: var(--link);\n}\n.msg {\n background: color-mix(in srgb, var(--danger) 8%, var(--surface));\n border: 1px solid color-mix(in srgb, var(--danger) 28%, var(--surface));\n border-radius: calc(var(--radius) - 2px);\n color: var(--danger);\n font-size: .875rem;\n padding: .5rem .75rem;\n}\n.status-page {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n display: grid;\n gap: .75rem;\n margin: 1.5rem auto 0;\n max-width: 34rem;\n padding: 1.5rem;\n}\n.status-head {\n align-items: center;\n display: flex;\n gap: .6rem;\n}\n.status-mark {\n align-items: center;\n background: var(--surface-2);\n border-radius: 50%;\n color: var(--muted);\n display: inline-flex;\n flex: none;\n height: 2rem;\n justify-content: center;\n width: 2rem;\n}\n.status-mark svg {\n height: 1.25rem;\n width: 1.25rem;\n}\n.status-mark.ok {\n background: color-mix(in srgb, var(--ok) 14%, var(--surface));\n color: var(--ok);\n}\n.status-mark.danger {\n background: color-mix(in srgb, var(--danger) 12%, var(--surface));\n color: var(--danger);\n}\n.status-label {\n color: var(--muted);\n font-size: .875rem;\n font-weight: 600;\n}\n.status-label.ok {\n color: var(--ok);\n}\n.status-label.danger {\n color: var(--danger);\n}\n.status-page h1 {\n overflow-wrap: anywhere;\n}\n.status-copy {\n color: var(--muted);\n}\n.status-actions {\n display: flex;\n flex-wrap: wrap;\n gap: .5rem;\n margin-top: .25rem;\n}\n.status-details {\n border-top: 1px solid var(--border);\n color: var(--muted);\n font-size: .875rem;\n margin-top: .25rem;\n padding-top: .75rem;\n}\n.status-details > summary {\n cursor: pointer;\n width: fit-content;\n}\n.status-details > p {\n margin-top: .5rem;\n}\n.status-details pre {\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: calc(var(--radius) / 2);\n color: var(--text);\n font-family: var(--mono);\n font-size: .75rem;\n line-height: 1.55;\n margin: .5rem 0 0;\n max-height: 18rem;\n overflow: auto;\n overflow-wrap: anywhere;\n padding: .5rem .6rem;\n white-space: pre-wrap;\n}\n@media (max-width: 40rem) {\n :root {\n --pad: 1rem;\n }\n .status-page {\n margin-top: 0;\n padding: 1.25rem var(--pad);\n }\n}\n\n/* src/operator-ui/browser.css */\n.masthead-inner {\n min-height: calc(.9375rem * 1.5 + .7rem);\n}\ninput:not([type=checkbox], [type=radio], [type=hidden]) {\n background: var(--surface);\n border: 1px solid var(--border-strong);\n border-radius: calc(var(--radius) - 2px);\n color: var(--text);\n min-height: 2.25rem;\n padding: .3rem .6rem;\n width: 100%;\n}\ninput::placeholder {\n color: var(--muted);\n}\n.row {\n align-items: center;\n display: flex;\n flex-wrap: wrap;\n gap: .5rem;\n}\n.row input:not([type=checkbox], [type=radio]) {\n flex: 1 1 14rem;\n width: auto;\n}\ninput:disabled {\n opacity: .6;\n}\n.check {\n align-items: center;\n cursor: pointer;\n display: inline-flex;\n gap: .4rem;\n}\n.check input {\n accent-color: var(--accent);\n margin: 0;\n}\n.actions {\n display: flex;\n flex-wrap: wrap;\n gap: .5rem;\n}\n.dot {\n border-radius: 50%;\n display: inline-block;\n flex: none;\n height: .5rem;\n width: .5rem;\n background: var(--muted);\n}\n.dot.ok,\n.dot.success,\n.dot.approved {\n background: var(--ok);\n}\n.dot.auth_required,\n.dot.warn,\n.dot.paused {\n background: var(--warn);\n}\n.dot.error,\n.dot.timeout {\n background: var(--danger);\n}\n.dot.loading {\n background: var(--border-strong);\n}\n.section {\n display: grid;\n gap: .75rem;\n margin-top: 1.75rem;\n}\n.section-head {\n align-items: baseline;\n display: flex;\n gap: var(--gap);\n justify-content: space-between;\n}\n.unavailable {\n background: var(--surface);\n border: 1px dashed var(--border-strong);\n border-radius: var(--radius);\n color: var(--muted);\n padding: 1.5rem var(--pad);\n}\n.state-block {\n align-items: center;\n background: var(--surface);\n border: 1px dashed var(--border-strong);\n border-radius: var(--radius);\n color: var(--muted);\n display: flex;\n flex-direction: column;\n gap: .5rem;\n padding: 1.75rem var(--pad);\n text-align: center;\n}\n.state-block.error {\n border-color: color-mix(in srgb, var(--danger) 35%, var(--border));\n border-style: solid;\n}\n.state-title {\n color: var(--text);\n font-weight: 600;\n}\n.state-copy {\n max-width: 52ch;\n}\n.state-block .btn {\n margin-top: .25rem;\n}\n.collection {\n display: grid;\n gap: .75rem;\n}\n.msg.warn {\n background: color-mix(in srgb, var(--warn) 9%, var(--surface));\n border-color: color-mix(in srgb, var(--warn) 32%, var(--surface));\n color: var(--text);\n}\n.error-notice {\n color: var(--danger);\n}\n.notice:empty {\n block-size: 0;\n margin: 0;\n}\n.summary {\n color: var(--muted);\n font-size: .875rem;\n}\n.summary .sep {\n opacity: .5;\n}\n.summary .warn {\n color: var(--warn);\n}\n.summary .danger {\n color: var(--danger);\n}\n.endpoint {\n align-items: center;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n display: flex;\n gap: .75rem;\n justify-content: space-between;\n padding: .5rem .5rem .5rem .75rem;\n}\n.endpoint code {\n flex: 1 1 auto;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.endpoints,\n.endpoint-block {\n display: grid;\n gap: .35rem;\n}\n.endpoints {\n gap: .6rem;\n}\n.endpoint-label {\n flex: none;\n}\n.setup-list {\n display: grid;\n gap: .6rem;\n margin-top: .5rem;\n}\n.setup-item {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: calc(var(--radius) - 2px);\n display: grid;\n gap: .35rem;\n padding: .5rem .6rem .6rem .75rem;\n}\n.setup-head {\n align-items: center;\n display: flex;\n gap: .5rem;\n justify-content: space-between;\n}\n.setup-code,\n.fix-prompt-text {\n background: var(--surface-2);\n border-radius: calc(var(--radius) / 2);\n color: var(--text);\n font-family: var(--mono);\n font-size: .75rem;\n line-height: 1.55;\n margin: 0;\n overflow-wrap: anywhere;\n padding: .5rem .6rem;\n white-space: pre-wrap;\n}\n.fix-prompt {\n align-items: baseline;\n display: flex;\n flex-wrap: wrap;\n gap: .35rem .75rem;\n}\n.fix-prompt > details {\n flex: 1 1 12rem;\n}\n.fix-prompt > details[open] {\n flex-basis: 100%;\n}\n.fix-prompt-preview > .meta {\n margin-top: .4rem;\n}\n.fix-prompt-preview .fix-prompt-text {\n border: 1px solid var(--border);\n margin-top: .4rem;\n max-height: 18rem;\n overflow: auto;\n}\n.rows {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n overflow: hidden;\n}\n.rows > * + * {\n border-top: 1px solid var(--border);\n}\n.conn.open > .conn-head {\n background: var(--surface-2);\n}\n.conn-head {\n align-items: center;\n display: flex;\n gap: .75rem;\n justify-content: space-between;\n padding: .7rem var(--pad);\n position: relative;\n}\n.conn-head:hover {\n background: var(--surface-2);\n}\n.conn-main {\n align-items: center;\n display: flex;\n gap: .6rem;\n min-width: 0;\n}\n.conn-name {\n font-size: 1rem;\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.conn-toggle {\n background: none;\n border: 0;\n color: var(--text);\n cursor: pointer;\n font: inherit;\n padding: 0;\n text-align: left;\n}\n.conn-toggle::after {\n content: \"\";\n inset: 0;\n position: absolute;\n}\n.conn-toggle:focus-visible {\n outline: none;\n}\n.conn-toggle:focus-visible::after {\n border-radius: calc(var(--radius) - 2px);\n outline: 2px solid var(--link);\n outline-offset: -2px;\n}\n.conn-id {\n color: var(--muted);\n}\n.conn-badges {\n align-items: center;\n display: flex;\n flex-wrap: wrap;\n gap: .35rem;\n justify-content: flex-end;\n}\n.conn-caret {\n border-right: 1.5px solid var(--muted);\n border-bottom: 1.5px solid var(--muted);\n flex: none;\n height: .4rem;\n rotate: -45deg;\n transform-origin: center;\n width: .4rem;\n}\n.conn.open .conn-caret {\n rotate: 45deg;\n}\n.conn-body {\n background: var(--surface);\n border-top: 1px solid var(--border);\n display: grid;\n gap: .75rem;\n padding: var(--pad);\n}\n.conn-body[hidden] {\n display: none;\n}\n.conn-note {\n color: var(--muted);\n max-width: 70ch;\n}\n.subcard {\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: calc(var(--radius) - 2px);\n display: grid;\n gap: .5rem;\n padding: .75rem;\n}\n.subcard-head {\n align-items: baseline;\n display: flex;\n flex-wrap: wrap;\n gap: .5rem;\n justify-content: space-between;\n}\n.subcard-head h3 {\n font-size: .9375rem;\n}\n.confirm {\n background: color-mix(in srgb, var(--danger) 6%, var(--surface));\n border: 1px solid color-mix(in srgb, var(--danger) 28%, var(--surface));\n border-radius: calc(var(--radius) - 2px);\n display: grid;\n gap: .5rem;\n padding: .6rem .75rem;\n}\n.confirm p {\n max-width: 70ch;\n}\ndetails > .subcard,\ndetails > .tool-list {\n margin-top: .5rem;\n}\n.tool-list {\n display: grid;\n gap: .4rem;\n max-height: 22rem;\n overflow: auto;\n}\n.filter {\n flex: 0 1 18rem;\n min-width: 10rem;\n width: auto;\n}\n.connector-drift {\n display: grid;\n gap: .15rem;\n}\n.tool {\n display: grid;\n gap: .1rem;\n border-left: 2px solid var(--border-strong);\n padding-left: .6rem;\n}\n.tool code {\n font-family: var(--mono);\n font-size: .8125rem;\n overflow-wrap: anywhere;\n}\n.tool-head {\n align-items: center;\n display: flex;\n flex-wrap: wrap;\n gap: .25rem .5rem;\n}\n.tool-legend {\n margin-bottom: .15rem;\n}\n.td {\n color: var(--muted);\n font-size: .8125rem;\n}\ndetails > summary {\n cursor: pointer;\n}\n.disclosure {\n color: var(--link);\n font-size: .875rem;\n list-style: none;\n}\n.disclosure::-webkit-details-marker {\n display: none;\n}\n.disclosure::before {\n content: \"▸ \";\n}\ndetails[open] > .disclosure::before {\n content: \"▾ \";\n}\n.drift-counts {\n display: grid;\n gap: .5rem;\n grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));\n margin-top: .5rem;\n}\n.drift-count {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: calc(var(--radius) - 2px);\n display: grid;\n gap: .1rem;\n padding: .4rem .6rem;\n}\n.drift-count.flagged {\n border-color: color-mix(in srgb, var(--warn) 45%, var(--surface));\n}\n.drift-count-value {\n font-size: 1.125rem;\n font-weight: 600;\n}\n.drift-count.flagged .drift-count-value {\n color: var(--warn);\n}\n.drift-count-label {\n color: var(--muted);\n font-size: .75rem;\n}\n.credential-fields,\n.credential-field-summary {\n display: grid;\n gap: .5rem;\n}\n.credential-field {\n display: grid;\n gap: .25rem;\n}\n.credential-field label {\n color: var(--muted);\n font-size: .8125rem;\n}\n.credential-field-summary > div {\n display: flex;\n gap: .5rem;\n justify-content: space-between;\n}\n.credential-form {\n display: grid;\n gap: .5rem;\n}\n.credential-form .actions {\n justify-content: flex-end;\n}\n.activity-list {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n overflow: hidden;\n}\n.activity-list > * + * {\n border-top: 1px solid var(--border);\n}\n.activity-item {\n align-items: center;\n display: grid;\n gap: .5rem var(--gap);\n grid-template-columns: minmax(0, 14rem) minmax(0, 1fr) auto;\n padding: .6rem var(--pad);\n}\n.activity-stamp {\n align-items: center;\n display: flex;\n gap: .6rem;\n min-width: 0;\n}\n.activity-time {\n display: block;\n font-size: .8125rem;\n}\n.activity-actor {\n color: var(--muted);\n font-size: .8125rem;\n}\n.activity-actor-id {\n color: var(--muted);\n}\n.activity-address {\n font-family: var(--mono);\n font-size: .8125rem;\n overflow-wrap: anywhere;\n}\n.activity-detail {\n color: var(--muted);\n font-size: .8125rem;\n}\n.activity-result {\n display: grid;\n gap: .15rem;\n justify-items: end;\n}\n.activity-more {\n justify-self: start;\n}\n.artifact-row {\n align-items: center;\n display: grid;\n gap: .25rem var(--gap);\n grid-template-columns: minmax(0, 1fr) auto;\n padding: .6rem var(--pad);\n}\n.artifact-row .artifact-title {\n font-weight: 600;\n overflow-wrap: anywhere;\n}\n.artifact-row .artifact-meta,\n.artifact-meta {\n color: var(--muted);\n font-size: .8125rem;\n}\n.artifact-badges {\n display: flex;\n flex-wrap: wrap;\n gap: .35rem;\n justify-content: flex-end;\n}\n.artifact-head {\n display: grid;\n gap: .35rem;\n margin-bottom: .75rem;\n}\n.navlink.inline {\n font-size: inherit;\n padding: 0 .2rem;\n}\n.artifact-banner {\n background: color-mix(in srgb, var(--warn) 12%, var(--surface));\n border: 1px solid color-mix(in srgb, var(--warn) 35%, var(--border));\n border-radius: var(--radius);\n color: var(--text);\n padding: .5rem .75rem;\n}\n.artifact-frame {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n display: block;\n height: calc(100vh - 14rem);\n min-height: 28rem;\n width: 100%;\n}\n.gate-form {\n max-width: 36rem;\n}\n@media (max-width: 40rem) {\n .masthead {\n position: static;\n }\n .masthead-inner {\n flex-wrap: wrap;\n row-gap: .35rem;\n }\n #operatorNav,\n #operatorNav .mast-actions {\n display: contents;\n }\n .session-actions {\n margin-left: auto;\n }\n .page-nav {\n flex: 1 0 100%;\n flex-wrap: nowrap;\n margin-left: -.6rem;\n order: 3;\n overflow-x: auto;\n }\n .conn-head {\n align-items: flex-start;\n flex-direction: column;\n gap: .4rem;\n }\n .conn-badges {\n justify-content: flex-start;\n }\n .endpoint {\n flex-wrap: wrap;\n }\n .section-head {\n flex-wrap: wrap;\n }\n .filter {\n flex: 1 1 100%;\n }\n .activity-item {\n grid-template-columns: minmax(0, 1fr);\n }\n .activity-result {\n justify-items: start;\n }\n .artifact-row {\n grid-template-columns: minmax(0, 1fr);\n }\n .artifact-badges {\n justify-content: flex-start;\n }\n}\n"; -export const OPERATOR_UI_SCRIPT: string = "\"use strict\";\n(() => {\n // node_modules/preact/dist/preact.module.js\n var n;\n var l;\n var u;\n var t;\n var i;\n var r;\n var o;\n var e;\n var f;\n var c;\n var a;\n var s;\n var h;\n var p;\n var v;\n var y;\n var d = {};\n var w = [];\n var _ = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;\n var g = Array.isArray;\n function m(n2, l3) {\n for (var u4 in l3) n2[u4] = l3[u4];\n return n2;\n }\n function b(n2) {\n n2 && n2.parentNode && n2.parentNode.removeChild(n2);\n }\n function k(l3, u4, t3) {\n var i3, r3, o3, e3 = {};\n for (o3 in u4) \"key\" == o3 ? i3 = u4[o3] : \"ref\" == o3 ? r3 = u4[o3] : e3[o3] = u4[o3];\n if (arguments.length > 2 && (e3.children = arguments.length > 3 ? n.call(arguments, 2) : t3), \"function\" == typeof l3 && null != l3.defaultProps) for (o3 in l3.defaultProps) void 0 === e3[o3] && (e3[o3] = l3.defaultProps[o3]);\n return x(l3, e3, i3, r3, null);\n }\n function x(n2, t3, i3, r3, o3) {\n var e3 = { type: n2, props: t3, key: i3, ref: r3, __k: null, __: null, __b: 0, __e: null, __c: null, constructor: void 0, __v: null == o3 ? ++u : o3, __i: -1, __u: 0 };\n return null == o3 && null != l.vnode && l.vnode(e3), e3;\n }\n function S(n2) {\n return n2.children;\n }\n function C(n2, l3) {\n this.props = n2, this.context = l3;\n }\n function $(n2, l3) {\n if (null == l3) return n2.__ ? $(n2.__, n2.__i + 1) : null;\n for (var u4; l3 < n2.__k.length; l3++) if (null != (u4 = n2.__k[l3]) && null != u4.__e) return u4.__e;\n return \"function\" == typeof n2.type ? $(n2) : null;\n }\n function I(n2) {\n if (n2.__P && n2.__d) {\n var u4 = n2.__v, t3 = u4.__e, i3 = [], r3 = [], o3 = m({}, u4);\n o3.__v = u4.__v + 1, l.vnode && l.vnode(o3), q(n2.__P, o3, u4, n2.__n, n2.__P.namespaceURI, 32 & u4.__u ? [t3] : null, i3, null == t3 ? $(u4) : t3, !!(32 & u4.__u), r3), o3.__v = u4.__v, o3.__.__k[o3.__i] = o3, D(i3, o3, r3), u4.__e = u4.__ = null, o3.__e != t3 && P(o3);\n }\n }\n function P(n2) {\n if (null != (n2 = n2.__) && null != n2.__c) return n2.__e = n2.__c.base = null, n2.__k.some(function(l3) {\n if (null != l3 && null != l3.__e) return n2.__e = n2.__c.base = l3.__e;\n }), P(n2);\n }\n function A(n2) {\n (!n2.__d && (n2.__d = true) && i.push(n2) && !H.__r++ || r != l.debounceRendering) && ((r = l.debounceRendering) || o)(H);\n }\n function H() {\n try {\n for (var n2, l3 = 1; i.length; ) i.length > l3 && i.sort(e), n2 = i.shift(), l3 = i.length, I(n2);\n } finally {\n i.length = H.__r = 0;\n }\n }\n function L(n2, l3, u4, t3, i3, r3, o3, e3, f4, c3, a3) {\n var s3, h3, p3, v3, y3, _3, g2 = t3 && t3.__k || w, m3 = l3.length;\n for (f4 = T(u4, l3, g2, f4, m3), s3 = 0; s3 < m3; s3++) null != (p3 = u4.__k[s3]) && (h3 = -1 != p3.__i && g2[p3.__i] || d, p3.__i = s3, _3 = q(n2, p3, h3, i3, r3, o3, e3, f4, c3, a3), v3 = p3.__e, p3.ref && h3.ref != p3.ref && (h3.ref && J(h3.ref, null, p3), a3.push(p3.ref, p3.__c || v3, p3)), null == y3 && null != v3 && (y3 = v3), 4 & p3.__u ? (f4 = j(p3, f4, n2), h3.__e && (h3.__e = null)) : \"function\" == typeof p3.type && void 0 !== _3 ? f4 = _3 : v3 && (f4 = v3.nextSibling), p3.__u &= -7);\n return u4.__e = y3, f4;\n }\n function T(n2, l3, u4, t3, i3) {\n var r3, o3, e3, f4, c3, a3 = u4.length, s3 = a3, h3 = 0;\n for (n2.__k = new Array(i3), r3 = 0; r3 < i3; r3++) null != (o3 = l3[r3]) && \"boolean\" != typeof o3 && \"function\" != typeof o3 ? (\"string\" == typeof o3 || \"number\" == typeof o3 || \"bigint\" == typeof o3 || o3.constructor == String ? o3 = n2.__k[r3] = x(null, o3, null, null, null) : g(o3) ? o3 = n2.__k[r3] = x(S, { children: o3 }, null, null, null) : void 0 === o3.constructor && o3.__b > 0 ? o3 = n2.__k[r3] = x(o3.type, o3.props, o3.key, o3.ref ? o3.ref : null, o3.__v) : n2.__k[r3] = o3, f4 = r3 + h3, o3.__ = n2, o3.__b = n2.__b + 1, e3 = null, -1 != (c3 = o3.__i = O(o3, u4, f4, s3)) && (s3--, (e3 = u4[c3]) && (e3.__u |= 2)), null == e3 || null == e3.__v ? (-1 == c3 && (i3 > a3 ? h3-- : i3 < a3 && h3++), \"function\" != typeof o3.type && (o3.__u |= 4)) : c3 != f4 && (c3 == f4 - 1 ? h3-- : c3 == f4 + 1 ? h3++ : (c3 > f4 ? h3-- : h3++, o3.__u |= 4))) : n2.__k[r3] = null;\n if (s3) for (r3 = 0; r3 < a3; r3++) null != (e3 = u4[r3]) && 0 == (2 & e3.__u) && (e3.__e == t3 && (t3 = $(e3)), K(e3, e3));\n return t3;\n }\n function j(n2, l3, u4) {\n var t3, i3;\n if (\"function\" == typeof n2.type) {\n for (t3 = n2.__k, i3 = 0; t3 && i3 < t3.length; i3++) t3[i3] && (t3[i3].__ = n2, l3 = j(t3[i3], l3, u4));\n return l3;\n }\n n2.__e != l3 && (l3 && n2.type && !l3.parentNode && (l3 = $(n2)), l3 = u4.insertBefore(n2.__e, l3 || null));\n do {\n l3 = l3 && l3.nextSibling;\n } while (null != l3 && 8 == l3.nodeType);\n return l3;\n }\n function O(n2, l3, u4, t3) {\n var i3, r3, o3, e3 = n2.key, f4 = n2.type, c3 = l3[u4], a3 = null != c3 && 0 == (2 & c3.__u);\n if (null === c3 && null == e3 || a3 && e3 == c3.key && f4 == c3.type) return u4;\n if (t3 > (a3 ? 1 : 0)) {\n for (i3 = u4 - 1, r3 = u4 + 1; i3 >= 0 || r3 < l3.length; ) if (null != (c3 = l3[o3 = i3 >= 0 ? i3-- : r3++]) && 0 == (2 & c3.__u) && e3 == c3.key && f4 == c3.type) return o3;\n }\n return -1;\n }\n function z(n2, l3, u4) {\n \"-\" == l3[0] ? n2.setProperty(l3, null == u4 ? \"\" : u4) : n2[l3] = null == u4 ? \"\" : \"number\" != typeof u4 || _.test(l3) ? u4 : u4 + \"px\";\n }\n function N(n2, l3, u4, t3, i3) {\n var r3, o3;\n n: if (\"style\" == l3) if (\"string\" == typeof u4) n2.style.cssText = u4;\n else {\n if (\"string\" == typeof t3 && (n2.style.cssText = t3 = \"\"), t3) for (l3 in t3) u4 && l3 in u4 || z(n2.style, l3, \"\");\n if (u4) for (l3 in u4) t3 && u4[l3] == t3[l3] || z(n2.style, l3, u4[l3]);\n }\n else if (\"o\" == l3[0] && \"n\" == l3[1]) r3 = l3 != (l3 = l3.replace(s, \"$1\")), o3 = l3.toLowerCase(), l3 = o3 in n2 || \"onFocusOut\" == l3 || \"onFocusIn\" == l3 ? o3.slice(2) : l3.slice(2), n2.l || (n2.l = {}), n2.l[l3 + r3] = u4, u4 ? t3 ? u4[a] = t3[a] : (u4[a] = h, n2.addEventListener(l3, r3 ? v : p, r3)) : n2.removeEventListener(l3, r3 ? v : p, r3);\n else {\n if (\"http://www.w3.org/2000/svg\" == i3) l3 = l3.replace(/xlink(H|:h)/, \"h\").replace(/sName$/, \"s\");\n else if (\"width\" != l3 && \"height\" != l3 && \"href\" != l3 && \"list\" != l3 && \"form\" != l3 && \"tabIndex\" != l3 && \"download\" != l3 && \"rowSpan\" != l3 && \"colSpan\" != l3 && \"role\" != l3 && \"popover\" != l3 && l3 in n2) try {\n n2[l3] = null == u4 ? \"\" : u4;\n break n;\n } catch (n3) {\n }\n \"function\" == typeof u4 || (null == u4 || false === u4 && \"-\" != l3[4] ? n2.removeAttribute(l3) : n2.setAttribute(l3, \"popover\" == l3 && 1 == u4 ? \"\" : u4));\n }\n }\n function V(n2) {\n return function(u4) {\n if (this.l) {\n var t3 = this.l[u4.type + n2];\n if (null == u4[c]) u4[c] = h++;\n else if (u4[c] < t3[a]) return;\n return t3(l.event ? l.event(u4) : u4);\n }\n };\n }\n function q(n2, u4, t3, i3, r3, o3, e3, f4, c3, a3) {\n var s3, h3, p3, v3, y3, d3, _3, k3, x2, M, I2, P2, A3, H2, T3, j3, F = u4.type;\n if (void 0 !== u4.constructor) return null;\n 128 & t3.__u && (c3 = !!(32 & t3.__u), o3 = [f4 = u4.__e = t3.__e]), (s3 = l.__b) && s3(u4);\n n: if (\"function\" == typeof F) {\n h3 = e3.length;\n try {\n if (x2 = u4.props, M = F.prototype && F.prototype.render, I2 = (s3 = F.contextType) && i3[s3.__c], P2 = s3 ? I2 ? I2.props.value : s3.__ : i3, t3.__c ? k3 = (p3 = u4.__c = t3.__c).__ = p3.__E : (M ? u4.__c = p3 = new F(x2, P2) : (u4.__c = p3 = new C(x2, P2), p3.constructor = F, p3.render = Q), I2 && I2.sub(p3), p3.state || (p3.state = {}), p3.__n = i3, v3 = p3.__d = true, p3.__h = [], p3._sb = []), M && null == p3.__s && (p3.__s = p3.state), M && null != F.getDerivedStateFromProps && (p3.__s == p3.state && (p3.__s = m({}, p3.__s)), m(p3.__s, F.getDerivedStateFromProps(x2, p3.__s))), y3 = p3.props, d3 = p3.state, p3.__v = u4, v3) M && null == F.getDerivedStateFromProps && null != p3.componentWillMount && p3.componentWillMount(), M && null != p3.componentDidMount && p3.__h.push(p3.componentDidMount);\n else {\n if (M && null == F.getDerivedStateFromProps && x2 !== y3 && null != p3.componentWillReceiveProps && p3.componentWillReceiveProps(x2, P2), u4.__v == t3.__v || !p3.__e && null != p3.shouldComponentUpdate && false === p3.shouldComponentUpdate(x2, p3.__s, P2)) {\n u4.__v != t3.__v && (p3.props = x2, p3.state = p3.__s, p3.__d = false), u4.__e = t3.__e, u4.__k = t3.__k, u4.__k.some(function(n3) {\n n3 && (n3.__ = u4);\n }), w.push.apply(p3.__h, p3._sb), p3._sb = [], p3.__h.length && e3.push(p3), f4 = $(t3);\n break n;\n }\n null != p3.componentWillUpdate && p3.componentWillUpdate(x2, p3.__s, P2), M && null != p3.componentDidUpdate && p3.__h.push(function() {\n p3.componentDidUpdate(y3, d3, _3);\n });\n }\n if (p3.context = P2, p3.props = x2, p3.__P = n2, p3.__e = false, A3 = l.__r, H2 = 0, M) p3.state = p3.__s, p3.__d = false, A3 && A3(u4), s3 = p3.render(p3.props, p3.state, p3.context), w.push.apply(p3.__h, p3._sb), p3._sb = [];\n else do {\n p3.__d = false, A3 && A3(u4), s3 = p3.render(p3.props, p3.state, p3.context), p3.state = p3.__s;\n } while (p3.__d && ++H2 < 25);\n p3.state = p3.__s, null != p3.getChildContext && (i3 = m(m({}, i3), p3.getChildContext())), M && !v3 && null != p3.getSnapshotBeforeUpdate && (_3 = p3.getSnapshotBeforeUpdate(y3, d3)), T3 = null != s3 && s3.type === S && null == s3.key ? E(s3.props.children) : s3, f4 = L(n2, g(T3) ? T3 : [T3], u4, t3, i3, r3, o3, e3, f4, c3, a3), p3.base = u4.__e, u4.__u &= -161, p3.__h.length && e3.push(p3), k3 && (p3.__E = p3.__ = null);\n } catch (n3) {\n if (e3.length = h3, u4.__v = null, c3 || null != o3) {\n if (n3.then) {\n for (u4.__u |= c3 ? 160 : 128; f4 && 8 == f4.nodeType && f4.nextSibling; ) f4 = f4.nextSibling;\n null != o3 && (o3[o3.indexOf(f4)] = null), u4.__e = f4;\n } else if (null != o3) for (j3 = o3.length; j3--; ) b(o3[j3]);\n } else u4.__e = t3.__e;\n null == u4.__k && (u4.__k = t3.__k || []), n3.then || B(u4), l.__e(n3, u4, t3);\n }\n } else null == o3 && u4.__v == t3.__v ? (u4.__k = t3.__k, u4.__e = t3.__e) : f4 = u4.__e = G(t3.__e, u4, t3, i3, r3, o3, e3, c3, a3);\n return (s3 = l.diffed) && s3(u4), 128 & u4.__u ? void 0 : f4;\n }\n function B(n2) {\n n2 && (n2.__c && (n2.__c.__e = true), n2.__k && n2.__k.some(B));\n }\n function D(n2, u4, t3) {\n for (var i3 = 0; i3 < t3.length; i3++) J(t3[i3], t3[++i3], t3[++i3]);\n l.__c && l.__c(u4, n2), n2.some(function(u5) {\n try {\n n2 = u5.__h, u5.__h = [], n2.some(function(n3) {\n n3.call(u5);\n });\n } catch (n3) {\n l.__e(n3, u5.__v);\n }\n });\n }\n function E(n2) {\n return \"object\" != typeof n2 || null == n2 || n2.__b > 0 ? n2 : g(n2) ? n2.map(E) : void 0 !== n2.constructor ? null : m({}, n2);\n }\n function G(u4, t3, i3, r3, o3, e3, f4, c3, a3) {\n var s3, h3, p3, v3, y3, w3, _3, m3 = i3.props || d, k3 = t3.props, x2 = t3.type;\n if (\"svg\" == x2 ? o3 = \"http://www.w3.org/2000/svg\" : \"math\" == x2 ? o3 = \"http://www.w3.org/1998/Math/MathML\" : o3 || (o3 = \"http://www.w3.org/1999/xhtml\"), null != e3) {\n for (s3 = 0; s3 < e3.length; s3++) if ((y3 = e3[s3]) && \"setAttribute\" in y3 == !!x2 && (x2 ? y3.localName == x2 : 3 == y3.nodeType)) {\n u4 = y3, e3[s3] = null;\n break;\n }\n }\n if (null == u4) {\n if (null == x2) return document.createTextNode(k3);\n u4 = document.createElementNS(o3, x2, k3.is && k3), c3 && (l.__m && l.__m(t3, e3), c3 = false), e3 = null;\n }\n if (null == x2) m3 === k3 || c3 && u4.data == k3 || (u4.data = k3);\n else {\n if (e3 = \"textarea\" == x2 && null != k3.defaultValue ? null : e3 && n.call(u4.childNodes), !c3 && null != e3) for (m3 = {}, s3 = 0; s3 < u4.attributes.length; s3++) m3[(y3 = u4.attributes[s3]).name] = y3.value;\n for (s3 in m3) y3 = m3[s3], \"dangerouslySetInnerHTML\" == s3 ? p3 = y3 : \"children\" == s3 || s3 in k3 || \"value\" == s3 && \"defaultValue\" in k3 || \"checked\" == s3 && \"defaultChecked\" in k3 || N(u4, s3, null, y3, o3);\n for (s3 in k3) y3 = k3[s3], \"children\" == s3 ? v3 = y3 : \"dangerouslySetInnerHTML\" == s3 ? h3 = y3 : \"value\" == s3 ? w3 = y3 : \"checked\" == s3 ? _3 = y3 : c3 && \"function\" != typeof y3 || m3[s3] === y3 || N(u4, s3, y3, m3[s3], o3);\n if (h3) c3 || p3 && (h3.__html == p3.__html || h3.__html == u4.innerHTML) || (u4.innerHTML = h3.__html), t3.__k = [];\n else if (p3 && (u4.innerHTML = \"\"), L(\"template\" == t3.type ? u4.content : u4, g(v3) ? v3 : [v3], t3, i3, r3, \"foreignObject\" == x2 ? \"http://www.w3.org/1999/xhtml\" : o3, e3, f4, e3 ? e3[0] : i3.__k && $(i3, 0), c3, a3), null != e3) for (s3 = e3.length; s3--; ) b(e3[s3]);\n c3 && \"textarea\" != x2 || (s3 = \"value\", \"progress\" == x2 && null == w3 ? u4.removeAttribute(\"value\") : null != w3 && (w3 !== u4[s3] || \"progress\" == x2 && !w3 || \"option\" == x2 && w3 != m3[s3]) && N(u4, s3, w3, m3[s3], o3), s3 = \"checked\", null != _3 && _3 != u4[s3] && N(u4, s3, _3, m3[s3], o3));\n }\n return u4;\n }\n function J(n2, u4, t3) {\n try {\n if (\"function\" == typeof n2) {\n var i3 = \"function\" == typeof n2.__u;\n i3 && n2.__u(), i3 && null == u4 || (n2.__u = n2(u4));\n } else n2.current = u4;\n } catch (n3) {\n l.__e(n3, t3);\n }\n }\n function K(n2, u4, t3) {\n var i3, r3;\n if (l.unmount && l.unmount(n2), (i3 = n2.ref) && (i3.current && i3.current != n2.__e || J(i3, null, u4)), null != (i3 = n2.__c)) {\n if (i3.componentWillUnmount) try {\n i3.componentWillUnmount();\n } catch (n3) {\n l.__e(n3, u4);\n }\n i3.base = i3.__P = i3.__n = null;\n }\n if (i3 = n2.__k) for (r3 = 0; r3 < i3.length; r3++) i3[r3] && K(i3[r3], u4, t3 || \"function\" != typeof n2.type);\n t3 || b(n2.__e), n2.__c = n2.__ = n2.__e = void 0;\n }\n function Q(n2, l3, u4) {\n return this.constructor(n2, u4);\n }\n function R(u4, t3, i3) {\n var r3, o3, e3, f4;\n t3 == document && (t3 = document.documentElement), l.__ && l.__(u4, t3), o3 = (r3 = \"function\" == typeof i3) ? null : i3 && i3.__k || t3.__k, e3 = [], f4 = [], q(t3, u4 = (!r3 && i3 || t3).__k = k(S, null, [u4]), o3 || d, d, t3.namespaceURI, !r3 && i3 ? [i3] : o3 ? null : t3.firstChild ? n.call(t3.childNodes) : null, e3, !r3 && i3 ? i3 : o3 ? o3.__e : t3.firstChild, r3, f4), D(e3, u4, f4), u4.props.children = null;\n }\n n = w.slice, l = { __e: function(n2, l3, u4, t3) {\n for (var i3, r3, o3; l3 = l3.__; ) if ((i3 = l3.__c) && !i3.__) try {\n if ((r3 = i3.constructor) && null != r3.getDerivedStateFromError && (i3.setState(r3.getDerivedStateFromError(n2)), o3 = i3.__d), null != i3.componentDidCatch && (i3.componentDidCatch(n2, t3 || {}), o3 = i3.__d), o3) return i3.__E = i3;\n } catch (l4) {\n n2 = l4;\n }\n throw n2;\n } }, u = 0, t = function(n2) {\n return null != n2 && void 0 === n2.constructor;\n }, C.prototype.setState = function(n2, l3) {\n var u4;\n u4 = null != this.__s && this.__s != this.state ? this.__s : this.__s = m({}, this.state), \"function\" == typeof n2 && (n2 = n2(m({}, u4), this.props)), n2 && m(u4, n2), null != n2 && this.__v && (l3 && this._sb.push(l3), A(this));\n }, C.prototype.forceUpdate = function(n2) {\n this.__v && (this.__e = true, n2 && this.__h.push(n2), A(this));\n }, C.prototype.render = S, i = [], o = \"function\" == typeof Promise ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout, e = function(n2, l3) {\n return n2.__v.__b - l3.__v.__b;\n }, H.__r = 0, f = Math.random().toString(8), c = \"__d\" + f, a = \"__a\" + f, s = /(PointerCapture)$|Capture$/i, h = 0, p = V(false), v = V(true), y = 0;\n\n // node_modules/preact/hooks/dist/hooks.module.js\n var t2;\n var r2;\n var u2;\n var i2;\n var o2 = 0;\n var f2 = [];\n var c2 = l;\n var e2 = c2.__b;\n var a2 = c2.__r;\n var v2 = c2.diffed;\n var l2 = c2.__c;\n var m2 = c2.unmount;\n var p2 = c2.__;\n function s2(n2, t3) {\n c2.__h && c2.__h(r2, n2, o2 || t3), o2 = 0;\n var u4 = r2.__H || (r2.__H = { __: [], __h: [] });\n return n2 >= u4.__.length && u4.__.push({}), u4.__[n2];\n }\n function d2(n2) {\n return o2 = 1, y2(D2, n2);\n }\n function y2(n2, u4, i3) {\n var o3 = s2(t2++, 2);\n if (o3.t = n2, !o3.__c && (o3.__ = [i3 ? i3(u4) : D2(void 0, u4), function(n3) {\n var t3 = o3.__N ? o3.__N[0] : o3.__[0], r3 = o3.t(t3, n3);\n t3 !== r3 && (o3.__N = [r3, o3.__[1]], o3.__c.setState({}));\n }], o3.__c = r2, !r2.__f)) {\n var f4 = function(n3, t3, r3) {\n if (!o3.__c.__H) return true;\n var u5 = false, i4 = o3.__c.props !== n3;\n if (o3.__c.__H.__.some(function(n4) {\n if (n4.__N) {\n u5 = true;\n var t4 = n4.__[0];\n n4.__ = n4.__N, n4.__N = void 0, t4 !== n4.__[0] && (i4 = true);\n }\n }), c3) {\n var f5 = c3.call(this, n3, t3, r3);\n return u5 ? f5 || i4 : f5;\n }\n return !u5 || i4;\n };\n r2.__f = true;\n var c3 = r2.shouldComponentUpdate, e3 = r2.componentWillUpdate;\n r2.componentWillUpdate = function(n3, t3, r3) {\n if (this.__e) {\n var u5 = c3;\n c3 = void 0, f4(n3, t3, r3), c3 = u5;\n }\n e3 && e3.call(this, n3, t3, r3);\n }, r2.shouldComponentUpdate = f4;\n }\n return o3.__N || o3.__;\n }\n function h2(n2, u4) {\n var i3 = s2(t2++, 3);\n !c2.__s && C2(i3.__H, u4) && (i3.__ = n2, i3.u = u4, r2.__H.__h.push(i3));\n }\n function _2(n2, u4) {\n var i3 = s2(t2++, 4);\n !c2.__s && C2(i3.__H, u4) && (i3.__ = n2, i3.u = u4, r2.__h.push(i3));\n }\n function A2(n2) {\n return o2 = 5, T2(function() {\n return { current: n2 };\n }, []);\n }\n function T2(n2, r3) {\n var u4 = s2(t2++, 7);\n return C2(u4.__H, r3) && (u4.__ = n2(), u4.__H = r3, u4.__h = n2), u4.__;\n }\n function j2() {\n for (var n2; n2 = f2.shift(); ) {\n var t3 = n2.__H;\n if (n2.__P && t3) try {\n t3.__h.some(z2), t3.__h.some(B2), t3.__h = [];\n } catch (r3) {\n t3.__h = [], c2.__e(r3, n2.__v);\n }\n }\n }\n c2.__b = function(n2) {\n r2 = null, e2 && e2(n2);\n }, c2.__ = function(n2, t3) {\n n2 && t3.__k && t3.__k.__m && (n2.__m = t3.__k.__m), p2 && p2(n2, t3);\n }, c2.__r = function(n2) {\n a2 && a2(n2), t2 = 0;\n var i3 = (r2 = n2.__c).__H;\n i3 && (u2 === r2 ? (i3.__h = [], r2.__h = [], i3.__.some(function(n3) {\n n3.__N && (n3.__ = n3.__N), n3.u = n3.__N = void 0;\n })) : (i3.__h.some(z2), i3.__h.some(B2), i3.__h = [], t2 = 0)), u2 = r2;\n }, c2.diffed = function(n2) {\n v2 && v2(n2);\n var t3 = n2.__c;\n t3 && t3.__H && (t3.__H.__h.length && (1 !== f2.push(t3) && i2 === c2.requestAnimationFrame || ((i2 = c2.requestAnimationFrame) || w2)(j2)), t3.__H.__.some(function(n3) {\n n3.u && (n3.__H = n3.u, n3.u = void 0);\n })), u2 = r2 = null;\n }, c2.__c = function(n2, t3) {\n t3.some(function(n3) {\n try {\n n3.__h.some(z2), n3.__h = n3.__h.filter(function(n4) {\n return !n4.__ || B2(n4);\n });\n } catch (r3) {\n t3.some(function(n4) {\n n4.__h && (n4.__h = []);\n }), t3 = [], c2.__e(r3, n3.__v);\n }\n }), l2 && l2(n2, t3);\n }, c2.unmount = function(n2) {\n m2 && m2(n2);\n var t3, r3 = n2.__c;\n r3 && r3.__H && (r3.__H.__.some(function(n3) {\n try {\n z2(n3);\n } catch (n4) {\n t3 = n4;\n }\n }), r3.__H = void 0, t3 && c2.__e(t3, r3.__v));\n };\n var k2 = \"function\" == typeof requestAnimationFrame;\n function w2(n2) {\n var t3, r3 = function() {\n clearTimeout(u4), k2 && cancelAnimationFrame(t3), setTimeout(n2);\n }, u4 = setTimeout(r3, 35);\n k2 && (t3 = requestAnimationFrame(r3));\n }\n function z2(n2) {\n var t3 = r2, u4 = n2.__c;\n \"function\" == typeof u4 && (n2.__c = void 0, u4()), r2 = t3;\n }\n function B2(n2) {\n var t3 = r2;\n n2.__c = n2.__(), r2 = t3;\n }\n function C2(n2, t3) {\n return !n2 || n2.length !== t3.length || t3.some(function(t4, r3) {\n return t4 !== n2[r3];\n });\n }\n function D2(n2, t3) {\n return \"function\" == typeof t3 ? t3(n2) : t3;\n }\n\n // src/operator-ui/view.ts\n var OPERATOR_PAGES = [\n \"connections\",\n \"activity\",\n \"artifacts\"\n ];\n var PAGE_META = {\n connections: { path: \"/\", label: \"Connections\" },\n activity: { path: \"/activity\", label: \"Activity\" },\n artifacts: { path: \"/artifacts\", label: \"Artifacts\" },\n artifact: { path: \"/artifacts\", label: \"Artifact\" }\n };\n function pageDescription(page, productDescription2) {\n if (page === \"activity\") {\n return \"Every connector tool call, by who made it and how it ended. Arguments and results are never stored.\";\n }\n if (isArtifactPage(page)) {\n return \"Pages agents published for the team. Each one keeps every version.\";\n }\n return productDescription2;\n }\n function checkingCopy(page) {\n if (page === \"artifact\") return \"Loading artifact…\";\n if (page === \"artifacts\") return \"Loading artifacts…\";\n return \"Checking your session…\";\n }\n function pageForPath(path) {\n if (path.startsWith(\"/artifacts/\")) return \"artifact\";\n const match = OPERATOR_PAGES.find((page) => PAGE_META[page].path === path);\n return match ?? \"connections\";\n }\n function isArtifactPage(page) {\n return page === \"artifacts\" || page === \"artifact\";\n }\n function artifactViewRequest(pathname, search) {\n const match = /^\\/artifacts\\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)(?:\\/v\\/(\\d{1,9}))?$/.exec(pathname);\n if (!match?.[1]) return void 0;\n const params = new URLSearchParams();\n if (match[2]) {\n params.set(\"v\", match[2]);\n for (const pin of new URLSearchParams(search).getAll(\"d\")) params.append(\"d\", pin);\n }\n const query = params.toString();\n return `/artifacts/_api/view/${match[1]}${query ? `?${query}` : \"\"}`;\n }\n function info(message) {\n return { message, tone: \"info\" };\n }\n function failure(message, fix) {\n return fix ? { message, tone: \"error\", fix } : { message, tone: \"error\" };\n }\n var REFUSED_COPY = {\n oauth_disconnect: \"Disconnect didn't finish. If the service refused it, the deployment's log has its reply.\",\n oauth_reconnect: \"Authorization couldn't start. If the service refused it, the deployment's log has its reply.\",\n credential_test: \"The credential test couldn't run.\"\n };\n function oauthDoneNotice(action, answer, opened = true) {\n if (action === \"oauth_disconnect\") {\n return info(\"Disconnected. Connect again whenever you are ready.\");\n }\n if (answer?.state === \"ok\") return info(\"Connected.\");\n return info(\n opened ? \"Finish authorizing in the new tab. This page updates when you come back.\" : \"Your browser blocked the new tab. Open the authorization page from the link here.\"\n );\n }\n function credentialTestNotice(connectorId, answer) {\n return answer?.ok === true ? info(\"Credential is valid.\") : failure(\n \"Credential test failed: the service rejected the stored credential, or the test couldn't reach it. The deployment's log has the service's reply.\",\n { kind: \"credential_test_failed\", connectorId }\n );\n }\n function refusedNotice(action, connectorId, problem) {\n if (action === \"credential_test\" && (problem === \"credential_required\" || problem === \"credential_mismatch\")) {\n return failure(PROBLEM_COPY[problem], { kind: problem, connectorId });\n }\n return failure(REFUSED_COPY[action], {\n kind: action === \"credential_test\" ? \"credential_test_failed\" : \"oauth_action_failed\",\n connectorId\n });\n }\n function credentialRefusedCopy(action, status, problem) {\n if (problem === \"credential_mismatch\") return PROBLEM_COPY.credential_mismatch;\n const verb = action === \"credential_save\" ? \"saved\" : \"removed\";\n if (status === 400 || status === 413 || status === 415) {\n return action === \"credential_save\" ? \"The credential wasn't saved: a value is empty or not in the expected format. Check it and save again.\" : \"The credential wasn't removed. Refresh the page and try again.\";\n }\n if (status === 404) {\n return \"This connector no longer has a credential slot. Refresh the page to see its current setup.\";\n }\n if (status === 503) {\n return `Credential storage isn't configured on this deployment, so nothing was ${verb}.`;\n }\n return `The credential wasn't ${verb}. Try again; if it keeps failing, the deployment's log has the reason.`;\n }\n function actionFailedNotice(action, connectorId, facts, productName2) {\n if (facts.kind === \"session\") {\n return failure(\"Your session has ended. Sign in again, then retry.\");\n }\n if (facts.kind === \"forbidden\") {\n return failure(\"You don't have permission to change this connection's authentication.\");\n }\n if (facts.kind === \"network\") {\n return failure(`Couldn't reach ${productName2}. Check your connection and try again.`);\n }\n if (action === \"credential_save\" || action === \"credential_remove\") {\n return failure(credentialRefusedCopy(action, facts.status, facts.problem));\n }\n return refusedNotice(action, connectorId, facts.problem);\n }\n function connectorLoadFailureCopy(failure2, productName2) {\n return failure2 === \"session\" ? \"Your session wasn't accepted while loading this connector. Sign in again to see its status.\" : `Couldn't reach ${productName2} to load this connector. Check your connection, then refresh it.`;\n }\n function loadFailureCopy(failure2, productName2) {\n return {\n title: `Couldn't reach ${productName2}`,\n body: failure2 === \"network\" ? \"Your browser couldn't connect. Check your connection, then retry.\" : \"The deployment answered with an error. Retry in a moment; if it keeps failing, the deployment's log has the reason.\"\n };\n }\n function collectionFailureCopy(collection, facts, productName2) {\n if (facts.kind === \"network\") {\n return `Couldn't reach ${productName2}. Check your connection, then retry.`;\n }\n if (facts.kind === \"session\") return \"Your session has ended. Sign in again to see this page.\";\n if (facts.kind === \"forbidden\" || facts.status === 404) {\n if (collection === \"artifact\") return \"There is no artifact here, or this identity can't open it.\";\n return collection === \"activity\" ? \"Activity isn't available to this identity.\" : \"Artifacts aren't available to this identity.\";\n }\n return \"The deployment answered with an error. Retry in a moment; if it keeps failing, the deployment's log has the reason.\";\n }\n function confirmCopy(action, name) {\n if (action === \"oauth_disconnect\") {\n return {\n question: `Disconnect ${name}? Its stored grant and any pending authorization are removed, and its tools stop working until it is connected again.`,\n confirm: \"Disconnect\"\n };\n }\n if (action === \"oauth_restart\") {\n return {\n question: `Reconnect ${name}? Its current grant stops working until you finish authorizing in the new tab.`,\n confirm: \"Reconnect\"\n };\n }\n return {\n question: `Remove ${name}'s credential? The connector stops authenticating until a replacement is added.`,\n confirm: \"Remove\"\n };\n }\n function initialState(page) {\n return {\n page,\n generation: 0,\n session: \"loading\",\n gate: null,\n refreshing: false,\n loadFailure: null,\n pendingFocus: null,\n focusIfLost: null,\n ...identityScopedState()\n };\n }\n function identityScopedState() {\n return {\n data: null,\n connectorFilter: \"\",\n oauthNotice: null,\n oauthNoticeFor: null,\n oauthBusy: null,\n oauthBlocked: null,\n confirming: null,\n connectorFailures: {},\n credentialNotice: null,\n credentialNoticeFor: null,\n credentialEditing: null,\n credentialBusy: null,\n activityPhase: \"idle\",\n activityNotice: null,\n activityEvents: [],\n activityCursor: null,\n activitySearch: \"\",\n artifactPhase: \"idle\",\n artifactNotice: null,\n artifactRows: [],\n artifactCursor: null,\n artifactQuery: \"\",\n artifactArchived: false,\n artifactView: null\n };\n }\n function resetIdentity(state2, gate2 = null) {\n return {\n ...state2,\n generation: state2.generation + 1,\n session: \"gated\",\n gate: gate2,\n refreshing: false,\n loadFailure: null,\n pendingFocus: null,\n focusIfLost: null,\n ...identityScopedState()\n };\n }\n function withPage(state2, page) {\n return {\n ...state2,\n page,\n credentialEditing: null,\n credentialNotice: null,\n credentialNoticeFor: null,\n confirming: null\n };\n }\n function connectorStatusLabel(status, problem) {\n if (status === \"loading\") return \"Loading details\";\n if (status === \"ok\") return \"Connected\";\n if (status === \"auth_required\") {\n return problem === \"credential_required\" ? \"Credential needed\" : \"Authorization needed\";\n }\n return \"Unavailable\";\n }\n function toolCountLabel(count) {\n return `${count} ${count === 1 ? \"tool\" : \"tools\"}`;\n }\n function connectorStatusTone(status) {\n if (status === \"ok\") return \"ok\";\n if (status === \"auth_required\") return \"warn\";\n if (status === \"loading\") return \"neutral\";\n return \"danger\";\n }\n function summarizeConnectors(connectors) {\n const summary = {\n total: connectors.length,\n connected: 0,\n attention: 0,\n credentials: 0,\n unavailable: 0,\n loading: 0,\n tools: 0,\n drifting: 0\n };\n for (const connector of connectors) {\n if (connector.status === \"ok\") summary.connected += 1;\n else if (connector.status === \"auth_required\") {\n if (connector.problem === \"credential_required\") summary.credentials += 1;\n else summary.attention += 1;\n } else if (connector.status === \"loading\") summary.loading += 1;\n else summary.unavailable += 1;\n summary.tools += connector.toolCount || 0;\n if (driftState(connector.catalogDrift) === \"warning\") summary.drifting += 1;\n }\n return summary;\n }\n function connectorSummaryParts(summary) {\n if (summary.total > 0 && summary.loading === summary.total) {\n return [\n {\n text: `Checking ${summary.total} connector${summary.total === 1 ? \"\" : \"s\"}…`,\n tone: \"neutral\"\n }\n ];\n }\n return [\n { text: `${summary.connected} connected`, tone: \"neutral\" },\n ...summary.attention ? [\n {\n text: `${summary.attention} need${summary.attention === 1 ? \"s\" : \"\"} authorization`,\n tone: \"warn\"\n }\n ] : [],\n ...summary.credentials ? [\n {\n text: `${summary.credentials} need${summary.credentials === 1 ? \"s\" : \"\"} a credential`,\n tone: \"warn\"\n }\n ] : [],\n ...summary.unavailable ? [{ text: `${summary.unavailable} unavailable`, tone: \"danger\" }] : [],\n { text: toolCountLabel(summary.tools), tone: \"neutral\" },\n ...summary.loading ? [{ text: `${summary.loading} still loading`, tone: \"neutral\" }] : []\n ];\n }\n var TOOL_SAFETY_BADGE = {\n runs_in_programs: {\n label: \"runs in programs\",\n tone: \"ok\",\n title: \"Explicitly read-only: execute_code programs may call it without asking.\"\n },\n exempt: {\n label: \"exempt from approval\",\n tone: \"neutral\",\n title: \"Not read-only, but this deployment's config lets programs call it without pausing. Each call still counts against the write budget and appears in activity; call_tool still refuses it.\"\n },\n needs_approval: {\n label: \"asks for approval\",\n tone: \"warn\",\n title: \"Not explicitly read-only: a program pauses for resume_execution, and a direct call crosses call_destructive_tool — either way the host asks first.\"\n }\n };\n var PROBLEM_COPY = {\n connector_unavailable: \"Unavailable: its status check or catalog load failed, or did not finish in time. The deployment's log has the downstream error.\",\n oauth_required: \"Needs OAuth authorization: no grant is stored, or the stored grant expired or was revoked.\",\n credential_required: \"Needs a credential: nothing usable is stored in its credential slot.\",\n auth_required: \"Needs authorization. Its secret lives in deployment configuration, not on this page.\",\n credential_mismatch: \"The stored credential does not match the fields this connector declares, so it cannot be used.\",\n catalog_failed: \"Connected, but its tool catalog could not be loaded, so none of its tools are served.\"\n };\n function problemCopy(problem) {\n return problem ? PROBLEM_COPY[problem] ?? null : null;\n }\n function problemTone(problem) {\n return problem === \"oauth_required\" || problem === \"credential_required\" || problem === \"auth_required\" ? \"warn\" : \"danger\";\n }\n function authScopeLabel(scope) {\n return scope === \"personal\" ? \"personal auth\" : \"shared auth\";\n }\n function permissionLabel(connector) {\n if (connector.permissions?.manageSharedAuth) {\n return \"You can manage shared authentication for this connection.\";\n }\n if (connector.permissions?.connectPersonal) {\n return \"You can connect your own account to this connection.\";\n }\n return \"Authentication for this connection is managed by your deployment.\";\n }\n var DRIFT_CATEGORIES = [\n { key: \"unclassifiedTools\", label: \"Unclassified\" },\n { key: \"unservedTools\", label: \"Unserved\" },\n { key: \"annotationConflicts\", label: \"Annotation conflicts\" },\n { key: \"schemaChanges\", label: \"Schema changes\" }\n ];\n function driftTotal(drift) {\n if (!drift) return 0;\n return DRIFT_CATEGORIES.reduce((sum, { key }) => sum + (drift[key] || 0), 0);\n }\n function driftState(drift) {\n if (!drift) return \"unavailable\";\n return driftTotal(drift) > 0 ? \"warning\" : \"clean\";\n }\n function driftCounts(drift) {\n if (!drift) return [];\n return DRIFT_CATEGORIES.map(({ key, label }) => ({\n key,\n label,\n count: drift[key] || 0\n }));\n }\n function driftSummary(drift) {\n const state2 = driftState(drift);\n if (state2 === \"unavailable\") {\n return \"No catalog refresh observed yet in this runtime.\";\n }\n const observed = formatDate(drift?.observedAt);\n const when = observed ? ` · observed ${observed}` : \"\";\n if (state2 === \"clean\") return `Matches the reviewed manifest${when}`;\n const total = driftTotal(drift);\n return `${total} difference${total === 1 ? \"\" : \"s\"} from the reviewed manifest${when}`;\n }\n function safeHttpHref(url) {\n if (!url) return null;\n try {\n const protocol = new URL(url).protocol;\n return protocol === \"http:\" || protocol === \"https:\" ? url : null;\n } catch {\n return null;\n }\n }\n function formatDate(value) {\n if (!value) return \"\";\n const date = new Date(value);\n return Number.isNaN(date.valueOf()) ? \"\" : date.toLocaleString(void 0, { dateStyle: \"medium\", timeStyle: \"short\" });\n }\n var ACTOR_KINDS = {\n clerk: \"Clerk\",\n bearer: \"Bearer token\",\n \"cloudflare-access\": \"Cloudflare Access\",\n anonymous: \"Anonymous\"\n };\n function actorKindLabel(kind) {\n if (!kind) return \"Unknown caller\";\n return ACTOR_KINDS[kind] ?? kind;\n }\n function actorLabel(actor) {\n if (!actor?.kind) return \"Unknown caller\";\n const who = actor.label || actor.id;\n const kind = actorKindLabel(actor.kind);\n return who ? `${who} (${kind})` : kind;\n }\n function actorStableId(actor) {\n if (!actor?.id) return null;\n if (!actor.label && !actor.namespace) return null;\n return actor.namespace ? `${actor.namespace} · ${actor.id}` : actor.id;\n }\n function activityMatches(event, query) {\n const q2 = query.trim().toLowerCase();\n if (!q2) return true;\n return [\n event.address,\n event.connectorId,\n event.toolName,\n event.source,\n event.outcome,\n event.errorCode,\n event.friction,\n event.actor?.kind,\n event.actor?.id,\n event.actor?.namespace,\n event.actor?.label,\n activityOutcomeBadge(event.outcome).label,\n activityDetail(event),\n actorKindLabel(event.actor?.kind)\n ].some((value) => String(value ?? \"\").toLowerCase().includes(q2));\n }\n function filterActivity(events, query) {\n return events.filter((event) => activityMatches(event, query));\n }\n function activitySummary(events) {\n if (events.length === 0) return \"\";\n const tools = new Set(events.map((event) => event.address)).size;\n return `${events.length} loaded call${events.length === 1 ? \"\" : \"s\"} · ${tools} tool${tools === 1 ? \"\" : \"s\"}`;\n }\n var ACTIVITY_OUTCOMES = [\n \"success\",\n \"error\",\n \"timeout\",\n \"cancelled\",\n \"paused\",\n \"approved\"\n ];\n function activityOutcomeClass(outcome) {\n return ACTIVITY_OUTCOMES.includes(outcome) ? outcome : \"error\";\n }\n var OUTCOME_BADGE = {\n success: { label: \"Succeeded\", tone: \"ok\" },\n error: { label: \"Failed\", tone: \"danger\" },\n timeout: { label: \"Timed out\", tone: \"danger\" },\n cancelled: { label: \"Cancelled\", tone: \"neutral\" },\n paused: { label: \"Waiting for approval\", tone: \"warn\" },\n approved: { label: \"Approved\", tone: \"ok\" }\n };\n function activityOutcomeBadge(outcome) {\n return OUTCOME_BADGE[outcome] ?? { label: \"Failed\", tone: \"danger\" };\n }\n var SOURCE_LABELS = {\n execute_code: \"In a program\",\n call_tool: \"Direct call\",\n call_destructive_tool: \"Direct call, approved by the host\",\n resume_execution: \"Resumed program\",\n batch_call: \"Batch call\"\n };\n var REASON_LABELS = {\n tool_not_found: \"tool not found\",\n unknown_address: \"tool not found\",\n unknown_tool: \"tool not found\",\n ambiguous_tool_alias: \"ambiguous tool name\",\n schema_retry: \"arguments didn't match the schema\",\n invalid_args: \"arguments didn't match the schema\",\n destructive_reroute: \"needed host approval\",\n destructive_tool_requires_approval: \"needed host approval\",\n approval_required: \"needed approval\",\n auth_required: \"needed authorization\",\n result_too_large: \"result too large to return inline\",\n timeout: \"timed out\"\n };\n function reasonLabel(code) {\n return REASON_LABELS[code] ?? code.replaceAll(\"_\", \" \");\n }\n function activityDetail(event) {\n const parts = [SOURCE_LABELS[event.source] ?? event.source];\n if (event.approval === \"tool\") parts.push(\"approved for the rest of the run\");\n if (event.approval === \"call\") parts.push(\"approved for this call\");\n if (event.attempts > 1) parts.push(`${event.attempts} attempts`);\n const reasons = [event.friction, event.errorCode].filter((code) => Boolean(code)).map(reasonLabel);\n for (const reason of new Set(reasons)) parts.push(reason);\n return parts.join(\" · \");\n }\n function artifactRefreshBadge(last) {\n return last?.status === \"failed\" ? { label: \"Refresh failed\", tone: \"danger\" } : null;\n }\n function credentialProblemCopy(problem) {\n if (problem === \"credential_mismatch\") return PROBLEM_COPY.credential_mismatch;\n if (problem === \"credential_unreadable\") {\n return \"The stored credential can't be read, so it can't be used. Replace it, or remove it and add a new one.\";\n }\n return \"The stored credential can't be used. Replace it, or remove it and add a new one.\";\n }\n function credentialStateLabel(credential) {\n if (!credential.configured) return \"not configured\";\n const masked = credential.fields?.length ? \"configured\" : `configured · ••••${credential.lastFour ?? \"\"}`;\n return credential.updatedAt ? `${masked} · updated ${formatDate(credential.updatedAt)}` : masked;\n }\n function gateCopy(kind, signedIn) {\n if (kind === \"cloudflare-access\") {\n return \"Cloudflare Access admitted this browser, but the current identity cannot open deployment-wide operator pages.\";\n }\n if (kind !== \"clerk\") {\n return \"Paste an operator bearer token to open this page. Nothing is requested until you do.\";\n }\n return signedIn ? \"Signed in with Clerk, but this account cannot open deployment-wide operator pages.\" : \"Sign in with Clerk to open this operator page.\";\n }\n\n // src/operator-ui/app/config.ts\n var auth = AUTH;\n var mcpUrl = MCP_URL;\n var initialPage = INITIAL_PAGE;\n var homeUrl = HOME_URL;\n var titleSuffix = TITLE_SUFFIX;\n var productName = PRODUCT_NAME;\n var productDescription = PRODUCT_DESCRIPTION;\n var productOperatorLabel = PRODUCT_OPERATOR_LABEL;\n var TOKEN_KEY = \"connecta:token\";\n\n // src/fix-prompt.ts\n var CONNECTOR_ID_RE = /^[a-z0-9_-]+$/;\n function renderFixPrompt(spec, connectorId) {\n const id = connectorId !== void 0 && CONNECTOR_ID_RE.test(connectorId) ? connectorId : void 0;\n return [\n \"Diagnose and fix a problem in this connecta deployment (the @zackbart/connecta package). It is configured as code: connectors, credential slots, OAuth clients, pools, and inbound auth are declared in the deployment's source and environment, so the fix belongs there and not in the operator page.\",\n `Problem: ${spec.problem}` + (id ? `\nConnector id: ${id}` : \"\"),\n `Where to look:\n${spec.steps.map((step) => `- ${step}`).join(\"\\n\")}`,\n \"This prompt deliberately carries no error text, tokens, or URLs. Find the underlying cause in the deployment's own logs (lines prefixed [connecta]) or by reproducing the failure locally. Never put a secret in source, a log line, or your reply: secrets belong in the deployment's environment or its credential vault.\",\n \"Make the smallest change that fixes it, then verify it by redeploying and refreshing the connection on the operator Connections page. If the fix needs something you cannot do yourself — a provider console setting, a secret only the operator holds — name that exact step instead.\"\n ].join(\"\\n\\n\");\n }\n\n // src/operator-ui/fix-prompts.ts\n var CATALOGUE = {\n connector_unavailable: {\n problem: \"A connector is unavailable: its status check or catalog load failed, or did not finish before the operator page's deadline.\",\n steps: [\n \"Confirm the connector's downstream URL or API base in the deployment config, and that the deployment can reach it from where it runs.\",\n \"Check the credentials or headers the connector sends; a rejected credential often surfaces as a failed status rather than as an authorization prompt.\",\n \"If the downstream is slow rather than down, review the connector's timeouts and the deployment's discovery.probeTimeoutMs.\"\n ]\n },\n oauth_required: {\n problem: \"A downstream OAuth connector needs authorization: no grant is stored, or the stored grant expired or was revoked and could not be refreshed.\",\n steps: [\n \"Reauthorize from the operator page (Connect account or Reconnect OAuth) or with authorize_connector; this needs no code change if the grant simply lapsed.\",\n \"If it needs reauthorizing again soon after, check that the OAuth client requests offline access or a refresh token, and that the storage holding OAuth tokens persists across restarts and is shared by every instance.\",\n \"If authorization cannot start at all, check the connector's OAuth client configuration and the deployment's publicUrl, which forms the /oauth/callback/ redirect URI.\"\n ]\n },\n credential_required: {\n problem: \"A connector with an operator-managed credential slot has no usable credential stored.\",\n steps: [\n \"An operator with credential administration can add the credential on the operator page; that needs no code change.\",\n \"If nobody can, grant credential administration through the deployment's identity config (credentialAdministration for shared auth, personalConnection for personal auth), which is denied by default.\",\n \"Check that the connector's credential declaration (label and fields) matches what the downstream actually needs.\"\n ]\n },\n auth_required: {\n problem: \"A connector reports that it needs authorization, and its secret lives in deployment configuration rather than in an operator-managed slot.\",\n steps: [\n \"Find where the connector's secret is read (usually an environment variable or Worker secret passed into the connector config) and confirm it is set in the running environment.\",\n \"Rotate the secret at the provider if it expired or was revoked, then update the deployment's environment, not its source.\",\n \"If operators should manage this secret from the page instead, declare a credential slot on the connector and configure a credential vault.\"\n ]\n },\n credential_mismatch: {\n problem: \"The credential stored for a connector does not match the fields the connector currently declares, so it cannot be used.\",\n steps: [\n \"If the connector's credential fields changed on purpose, re-enter the credential on the operator page so the stored fields match.\",\n \"If they changed by accident, restore the previous field names in the connector's credential declaration.\"\n ]\n },\n credential_unreadable: {\n problem: \"A stored credential exists but could not be read or decrypted.\",\n steps: [\n \"Check that the credential vault's encryption key in the deployment environment is the one the credential was stored with; a rotated or missing key makes every stored value unreadable.\",\n \"Check the storage adapter backing the vault is reachable from the deployment.\",\n \"If the key was lost, remove and re-add the credential on the operator page; the old value cannot be recovered.\"\n ]\n },\n credential_test_failed: {\n problem: \"The test for a stored connector credential failed: the downstream rejected it, or the test could not reach the downstream.\",\n steps: [\n \"Confirm the stored value is current at the provider (not rotated, revoked, or scoped too narrowly), and replace it on the operator page if not.\",\n \"Check that the connector's credential test targets the same API base and auth scheme its tool calls use.\"\n ]\n },\n oauth_action_failed: {\n problem: \"Restarting or disconnecting a connector's downstream OAuth from the operator page failed.\",\n steps: [\n \"Check that the connector implements startAuth and disconnectAuth, and that the storage holding its OAuth state is writable.\",\n \"Check the connector's OAuth client configuration, including the authorization server it discovers or is given.\",\n \"Confirm the operator has the config-derived permission for this action (credentialAdministration for shared auth, personalConnection for personal auth).\"\n ]\n },\n catalog_failed: {\n problem: \"A connector reports itself connected, but loading its tool catalog failed, so none of its tools are being served.\",\n steps: [\n \"Check that the downstream still serves a complete tools list; connecta refuses a partial catalog rather than serving part of it.\",\n \"Check for tools the connector cannot accept: invalid schemas, missing descriptions, or names that collide.\",\n \"Pin or upgrade the @zackbart/connecta version if a maintained provider's catalog changed shape underneath it.\"\n ]\n },\n catalog_drift: {\n problem: \"A hosted provider's live catalog differs from the reviewed manifest shipped with connecta: new unclassified tools, unserved tools, annotation conflicts, or schema changes.\",\n steps: [\n \"Unclassified tools are withheld until a release classifies them. Check whether a newer @zackbart/connecta release has, and upgrade if so.\",\n \"If a tool the deployment depends on is now unserved or changed shape, review callers of it before upgrading.\",\n \"Report drift the release does not cover as an issue on the connecta repository, naming the provider and the kinds of drift but no tool data.\"\n ]\n }\n };\n function fixPrompt(kind, connectorId) {\n return renderFixPrompt(CATALOGUE[kind], connectorId);\n }\n var FIX_PROMPT_KINDS = Object.keys(CATALOGUE);\n\n // src/operator-ui/app/store.ts\n var state = initialState(initialPage);\n var listeners = /* @__PURE__ */ new Set();\n function getState() {\n return state;\n }\n function subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n }\n function set(patch) {\n state = { ...state, ...patch };\n for (const listener of listeners) listener();\n }\n function fence() {\n const generation = state.generation;\n return () => generation === state.generation;\n }\n function sessionToken() {\n if (auth.kind === \"cloudflare-access\") return Promise.resolve(void 0);\n return auth.kind === \"clerk\" ? Promise.resolve(window.Clerk?.session?.getToken() ?? null) : Promise.resolve(localStorage.getItem(TOKEN_KEY));\n }\n function requestHeaders(token, body = false) {\n return {\n ...token ? { Authorization: `Bearer ${token}` } : {},\n ...body ? { \"Content-Type\": \"application/json\" } : {}\n };\n }\n var NAV_HINT_KEY = \"connecta:nav\";\n function rememberNav(data) {\n try {\n sessionStorage.setItem(\n NAV_HINT_KEY,\n JSON.stringify({ activity: data.activityEnabled, artifacts: Boolean(data.artifactsEnabled) })\n );\n } catch {\n }\n }\n function forgetNav() {\n try {\n sessionStorage.removeItem(NAV_HINT_KEY);\n } catch {\n }\n }\n function navHint() {\n try {\n const hint = JSON.parse(sessionStorage.getItem(NAV_HINT_KEY) ?? \"null\");\n return { activity: hint?.activity === true, artifacts: hint?.artifacts === true };\n } catch {\n return { activity: false, artifacts: false };\n }\n }\n function gate(notice = null) {\n forgetNav();\n awaitingAuthorization.clear();\n state = resetIdentity(state, notice);\n for (const listener of listeners) listener();\n }\n var RequestFailure = class extends Error {\n kind;\n status;\n problem;\n constructor(kind, status, problem) {\n super(`Operator request failed: ${kind}`);\n this.kind = kind;\n this.status = status;\n this.problem = problem;\n }\n };\n function factsOf(error) {\n return error instanceof RequestFailure ? error : { kind: \"refused\" };\n }\n async function operatorRequest(path, method, current, body) {\n let token;\n try {\n token = await sessionToken();\n } catch {\n throw new RequestFailure(\"session\");\n }\n if (!current()) throw new RequestFailure(\"session\");\n if (!token && auth.kind !== \"cloudflare-access\") throw new RequestFailure(\"session\");\n let res;\n try {\n res = await fetch(path, {\n method,\n headers: requestHeaders(token, Boolean(body)),\n credentials: \"same-origin\",\n ...body ? { body: JSON.stringify(body) } : {}\n });\n } catch {\n throw new RequestFailure(\"network\");\n }\n if (res.status === 204) return null;\n let payload = {};\n try {\n payload = await res.json();\n } catch {\n }\n if (res.status === 401) throw new RequestFailure(\"session\", 401);\n if (res.status === 403) throw new RequestFailure(\"forbidden\", 403);\n if (!res.ok) throw new RequestFailure(\"refused\", res.status, payload.problem);\n return payload;\n }\n function unreachable(loadFailure) {\n set({ session: \"ready\", gate: null, refreshing: false, loadFailure });\n }\n async function loadData() {\n const current = fence();\n set(state.session === \"ready\" ? { refreshing: true, loadFailure: null } : { loadFailure: null });\n let token;\n try {\n token = await sessionToken();\n } catch {\n if (!current()) return;\n return gate(failure(\"Your sign-in session couldn't be read. Sign in again.\"));\n }\n if (!current()) return;\n if (!token && auth.kind !== \"cloudflare-access\") return gate(null);\n let res;\n try {\n res = await fetch(\"/ui/data\", {\n headers: requestHeaders(token),\n credentials: \"same-origin\"\n });\n } catch {\n if (!current()) return;\n return unreachable(\"network\");\n }\n if (!current()) return;\n if (res.status === 401 || res.status === 403) {\n if (auth.kind === \"clerk\") {\n return gate(\n failure(\n res.status === 403 ? `This Clerk account can't open ${productName}'s operator pages.` : \"Your Clerk session wasn't accepted. Sign out, then sign in again.\"\n )\n );\n }\n if (auth.kind === \"cloudflare-access\") {\n return gate(\n failure(\"Cloudflare Access let this browser in, but this identity isn't an operator here.\")\n );\n }\n localStorage.removeItem(TOKEN_KEY);\n return gate(failure(\"That token wasn't accepted. Paste a valid operator token.\"));\n }\n if (!res.ok) return unreachable(\"server\");\n let data;\n try {\n data = await res.json();\n } catch {\n if (!current()) return;\n return unreachable(\"server\");\n }\n if (!current()) return;\n if (!Array.isArray(data.connectors)) return unreachable(\"server\");\n set({ data, session: \"ready\", gate: null, refreshing: false, loadFailure: null });\n rememberNav(data);\n void loadConnectorDetails(data, current, token);\n }\n async function mutate(options) {\n const current = fence();\n if (options.reload) detailRevisions.set(options.reload, (detailRevisions.get(options.reload) ?? 0) + 1);\n set(options.busy);\n try {\n const payload = await options.request(current);\n if (!current()) return options.abandoned?.();\n set(options.done(payload));\n if (options.reload) void refreshConnector(options.reload);\n } catch (error) {\n if (!current()) return options.abandoned?.();\n set(options.failed(factsOf(error)));\n }\n }\n function focusHandled() {\n if (state.pendingFocus !== null || state.focusIfLost !== null) {\n set({ pendingFocus: null, focusIfLost: null });\n }\n }\n function setPage(page, focus = false) {\n state = withPage(state, page);\n if (focus) {\n state = {\n ...state,\n pendingFocus: state.session === \"ready\" ? `${page}Heading` : \"gateHeading\"\n };\n }\n for (const listener of listeners) listener();\n }\n function navigate(page, href) {\n history.pushState({ operatorPage: page }, \"\", href);\n setPage(page, true);\n }\n function setConnectorFilter(connectorFilter) {\n set({ connectorFilter });\n }\n function setActivitySearch(activitySearch) {\n set({ activitySearch });\n }\n function signInWithBearer(value) {\n gate(null);\n localStorage.setItem(TOKEN_KEY, value);\n void loadCurrent().then(() => {\n if (state.session === \"ready\") set({ pendingFocus: `${state.page}Heading` });\n });\n }\n function forgetBearer() {\n localStorage.removeItem(TOKEN_KEY);\n gate(null);\n set({ pendingFocus: \"token\" });\n }\n function askConfirm(connectorId, action) {\n set({ confirming: { connectorId, action }, pendingFocus: `confirm-cancel-${connectorId}` });\n }\n function cancelConfirm(returnFocusTo) {\n set({ confirming: null, pendingFocus: returnFocusTo });\n }\n function oauthStartPath(connector, mode) {\n return `/ui/oauth/${encodeURIComponent(connector)}?mode=${mode}`;\n }\n var awaitingAuthorization = /* @__PURE__ */ new Set();\n function openBlankTab() {\n let tab = null;\n try {\n tab = window.open(\"\", \"_blank\");\n } catch {\n return null;\n }\n if (!tab) return null;\n try {\n tab.document.title = \"Opening authorization…\";\n tab.document.body.textContent = \"Opening the authorization page…\";\n } catch {\n }\n return tab;\n }\n function oauthNoticePatch(connector, notice) {\n return {\n oauthBusy: null,\n oauthNotice: notice,\n oauthNoticeFor: connector,\n focusIfLost: `oauthNotice-${connector}`\n };\n }\n function startOAuth(connector, mode) {\n const tab = openBlankTab();\n return mutate({\n request: (current) => operatorRequest(oauthStartPath(connector, mode), \"POST\", current),\n busy: {\n oauthBusy: connector,\n oauthNotice: null,\n oauthNoticeFor: connector,\n oauthBlocked: null,\n confirming: null\n },\n done: (payload) => {\n const url = safeHttpHref(payload?.authorizationUrl);\n if (!url) {\n tab?.close();\n if (payload?.state === \"ok\") {\n return oauthNoticePatch(connector, oauthDoneNotice(\"oauth_reconnect\", payload));\n }\n return oauthNoticePatch(connector, refusedNotice(\"oauth_reconnect\", connector));\n }\n if (tab) {\n tab.opener = null;\n tab.location.replace(url);\n }\n awaitingAuthorization.add(connector);\n return {\n ...state.data ? {\n data: {\n ...state.data,\n connectors: state.data.connectors.map(\n (c3) => c3.id === connector ? { ...c3, status: \"auth_required\", tools: [], toolCount: 0, authorizationUrl: url } : c3\n )\n }\n } : {},\n ...oauthNoticePatch(connector, oauthDoneNotice(\"oauth_reconnect\", payload, Boolean(tab))),\n oauthBlocked: tab ? null : connector\n };\n },\n // Signed out or switched mid-request: the tab it opened goes too, rather\n // than sitting on \"Opening…\" with nothing left to send it anywhere.\n abandoned: () => tab?.close(),\n // The route's words never reach this notice (see `refusedNotice`).\n failed: (facts) => {\n tab?.close();\n return oauthNoticePatch(\n connector,\n actionFailedNotice(\"oauth_reconnect\", connector, facts, productName)\n );\n },\n reload: connector\n });\n }\n function disconnectOAuth(connector) {\n return mutate({\n request: (current) => operatorRequest(`/ui/oauth/${encodeURIComponent(connector)}`, \"DELETE\", current),\n busy: {\n oauthBusy: connector,\n oauthNotice: null,\n oauthNoticeFor: connector,\n oauthBlocked: null,\n confirming: null\n },\n done: () => ({\n ...state.data ? {\n data: {\n ...state.data,\n connectors: state.data.connectors.map((c3) => {\n if (c3.id !== connector) return c3;\n const { authorizationUrl: _old, ...rest } = c3;\n return { ...rest, status: \"auth_required\", tools: [], toolCount: 0 };\n })\n }\n } : {},\n ...oauthNoticePatch(connector, oauthDoneNotice(\"oauth_disconnect\", null))\n }),\n failed: (facts) => oauthNoticePatch(\n connector,\n actionFailedNotice(\"oauth_disconnect\", connector, facts, productName)\n ),\n reload: connector\n });\n }\n function editCredential(connector) {\n set({ credentialEditing: connector, credentialNotice: null, credentialNoticeFor: null });\n }\n function refuseCredential(connector, copy) {\n set({ credentialNotice: failure(copy), credentialNoticeFor: connector });\n }\n function credentialMutation(connector, action, request, done, reload = true) {\n const land = (patch) => ({\n credentialBusy: null,\n credentialNoticeFor: connector,\n focusIfLost: `credentialNotice-${connector}`,\n ...patch\n });\n return mutate({\n request,\n busy: {\n credentialBusy: connector,\n credentialNotice: null,\n credentialNoticeFor: connector,\n confirming: null\n },\n done: (payload) => land(done(payload)),\n failed: (facts) => land({ credentialNotice: actionFailedNotice(action, connector, facts, productName) }),\n reload: reload ? connector : void 0\n });\n }\n function saveCredential(connector, body) {\n return credentialMutation(\n connector,\n \"credential_save\",\n (current) => operatorRequest(\n `/ui/credentials/${encodeURIComponent(connector)}`,\n \"PUT\",\n current,\n body\n ),\n () => ({ credentialEditing: null, credentialNotice: info(\"Credential saved.\") })\n );\n }\n function removeCredential(connector) {\n return credentialMutation(\n connector,\n \"credential_remove\",\n (current) => operatorRequest(\n `/ui/credentials/${encodeURIComponent(connector)}`,\n \"DELETE\",\n current\n ),\n () => ({ credentialNotice: info(\"Credential removed.\") })\n );\n }\n function testCredential(connector) {\n return credentialMutation(\n connector,\n \"credential_test\",\n (current) => operatorRequest(\n `/ui/credentials/${encodeURIComponent(connector)}/test`,\n \"POST\",\n current\n ),\n (payload) => ({ credentialNotice: credentialTestNotice(connector, payload) }),\n false\n );\n }\n async function loadActivity(reset) {\n if (!state.data?.activityEnabled) return;\n const current = fence();\n set({\n activityPhase: \"loading\",\n activityNotice: null,\n ...reset ? { activityEvents: [], activityCursor: null } : {}\n });\n const params = new URLSearchParams({ limit: \"50\" });\n if (!reset && state.activityCursor) {\n params.set(\"cursor\", state.activityCursor);\n }\n try {\n const payload = await operatorRequest(\n `/ui/activity?${params}`,\n \"GET\",\n current\n );\n if (!current()) return;\n set({\n activityPhase: \"ready\",\n activityEvents: [\n ...reset ? [] : state.activityEvents,\n ...payload?.events ?? []\n ],\n activityCursor: payload?.nextCursor ?? null\n });\n } catch (error) {\n if (!current()) return;\n set({\n activityPhase: \"error\",\n activityNotice: failure(collectionFailureCopy(\"activity\", factsOf(error), productName))\n });\n }\n }\n async function artifactRead(path, current, collection) {\n let token;\n try {\n token = await sessionToken();\n } catch {\n if (current()) gate(failure(\"Your sign-in session couldn't be read. Sign in again.\"));\n return void 0;\n }\n if (!current()) return void 0;\n if (!token && auth.kind !== \"cloudflare-access\") {\n gate(null);\n return void 0;\n }\n let res;\n try {\n res = await fetch(path, { headers: requestHeaders(token), credentials: \"same-origin\" });\n } catch {\n if (current()) {\n set({\n session: \"ready\",\n gate: null,\n artifactPhase: \"error\",\n artifactNotice: failure(collectionFailureCopy(collection, { kind: \"network\" }, productName))\n });\n }\n return void 0;\n }\n if (!current()) return void 0;\n if (res.status === 401) {\n if (auth.kind !== \"clerk\" && auth.kind !== \"cloudflare-access\") {\n localStorage.removeItem(TOKEN_KEY);\n gate(failure(\"That token wasn't accepted. Paste a valid operator token.\"));\n } else {\n gate(failure(\"Your session wasn't accepted. Sign out, then sign in again.\"));\n }\n return void 0;\n }\n if (res.status === 403) {\n gate(failure(\"This deployment doesn't open artifact pages to this identity.\"));\n return void 0;\n }\n return res;\n }\n async function loadArtifacts(reset) {\n const current = fence();\n set({\n artifactPhase: \"loading\",\n artifactNotice: null,\n ...reset ? { artifactRows: [], artifactCursor: null } : {}\n });\n const params = new URLSearchParams();\n if (state.artifactQuery.trim()) params.set(\"q\", state.artifactQuery.trim());\n if (state.artifactArchived) params.set(\"archived\", \"1\");\n if (!reset && state.artifactCursor) params.set(\"cursor\", state.artifactCursor);\n const query = params.toString();\n const res = await artifactRead(`/artifacts/_api/list${query ? `?${query}` : \"\"}`, current, \"artifacts\");\n if (!res || !current()) return;\n if (!res.ok) {\n return set({\n session: \"ready\",\n gate: null,\n artifactPhase: \"error\",\n artifactNotice: failure(\n collectionFailureCopy(\"artifacts\", { kind: \"refused\", status: res.status }, productName)\n )\n });\n }\n let payload;\n try {\n payload = await res.json();\n } catch {\n payload = {};\n }\n if (!current()) return;\n set({\n session: \"ready\",\n gate: null,\n artifactPhase: \"ready\",\n artifactRows: [...reset ? [] : state.artifactRows, ...payload.artifacts ?? []],\n artifactCursor: payload.nextCursor ?? null\n });\n }\n async function loadArtifactView() {\n const current = fence();\n const request = artifactViewRequest(window.location.pathname, window.location.search);\n set({ artifactPhase: \"loading\", artifactNotice: null });\n if (!request) {\n return set({\n session: \"ready\",\n artifactPhase: \"error\",\n artifactNotice: failure(\"There is no artifact at this address.\")\n });\n }\n const res = await artifactRead(request, current, \"artifact\");\n if (!res || !current()) return;\n if (!res.ok) {\n return set({\n session: \"ready\",\n gate: null,\n artifactPhase: \"error\",\n artifactNotice: failure(\n collectionFailureCopy(\"artifact\", { kind: \"refused\", status: res.status }, productName)\n )\n });\n }\n let view = null;\n try {\n view = await res.json();\n } catch {\n view = null;\n }\n if (!current()) return;\n if (!view || typeof view.document !== \"string\") {\n return set({\n session: \"ready\",\n gate: null,\n artifactPhase: \"error\",\n artifactNotice: failure(\"The artifact couldn't be read. Retry in a moment.\")\n });\n }\n set({ session: \"ready\", gate: null, artifactPhase: \"ready\", artifactView: view });\n }\n function setArtifactQuery(artifactQuery) {\n set({ artifactQuery });\n }\n function setArtifactArchived(artifactArchived) {\n set({ artifactArchived });\n void loadArtifacts(true);\n }\n function loadCurrent() {\n if (state.page === \"artifacts\") return loadArtifacts(true);\n if (state.page === \"artifact\") return loadArtifactView();\n return loadData();\n }\n function retryLoad() {\n set({\n pendingFocus: state.page === \"connections\" ? \"connectorLedgerHeading\" : `${state.page}Heading`\n });\n return loadCurrent();\n }\n function retryCollection() {\n set({ pendingFocus: `${state.page}Heading` });\n if (state.page === \"activity\") return loadActivity(true);\n if (state.page === \"artifact\") return loadArtifactView();\n return loadArtifacts(true);\n }\n function signIn() {\n window.Clerk?.redirectToSignIn({\n signInFallbackRedirectUrl: window.location.href,\n signUpFallbackRedirectUrl: window.location.href\n });\n }\n function signOut() {\n if (auth.kind === \"cloudflare-access\") {\n gate(null);\n window.location.assign(\"/cdn-cgi/access/logout\");\n return;\n }\n const clerk = window.Clerk;\n gate(null);\n void clerk?.signOut({ redirectUrl: window.location.href });\n }\n var returnTimer;\n var lastReturnPass = 0;\n var RETURN_DEBOUNCE_MS = 150;\n var RETURN_MIN_INTERVAL_MS = 2e3;\n function onReturn() {\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\") return;\n if (returnTimer !== void 0) return;\n returnTimer = setTimeout(() => {\n returnTimer = void 0;\n const now = Date.now();\n if (now - lastReturnPass < RETURN_MIN_INTERVAL_MS) return;\n lastReturnPass = now;\n recheckAuthorization();\n }, RETURN_DEBOUNCE_MS);\n }\n function recheckAuthorization() {\n if (state.session !== \"ready\" || !state.data) return;\n for (const connector of state.data.connectors) {\n if (state.oauthBusy === connector.id) continue;\n const authorizesElsewhere = connector.status === \"auth_required\" && (connector.oauth === true || Boolean(connector.authorizationUrl));\n if (authorizesElsewhere || awaitingAuthorization.has(connector.id)) {\n void refreshConnector(connector.id, true);\n }\n }\n }\n async function boot() {\n const onPop = () => setPage(pageForPath(window.location.pathname), true);\n window.addEventListener(\"popstate\", onPop);\n window.addEventListener(\"focus\", onReturn);\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onReturn);\n }\n if (auth.kind === \"clerk\") {\n const clerk = window.Clerk;\n if (!clerk) {\n return gate(failure(\"Clerk couldn't load. Check your connection and try again.\"));\n }\n try {\n await clerk.load({\n ...auth.signInUrl ? { signInUrl: auth.signInUrl } : {},\n ...auth.signUpUrl ? { signUpUrl: auth.signUpUrl } : {},\n signInFallbackRedirectUrl: window.location.href,\n signUpFallbackRedirectUrl: window.location.href,\n afterSignOutUrl: window.location.href\n });\n let sessionId = clerk.session?.id ?? null;\n clerk.addListener((resources) => {\n const next = resources.session?.id ?? null;\n if (next === sessionId) return;\n sessionId = next;\n gate(null);\n void loadCurrent();\n });\n } catch {\n return gate(failure(\"Clerk couldn't start. Reload the page to try again.\"));\n }\n }\n await loadCurrent();\n }\n async function readConnector(id, token) {\n let response;\n try {\n response = await fetch(`/ui/connectors/${encodeURIComponent(id)}`, {\n headers: requestHeaders(token),\n credentials: \"same-origin\"\n });\n } catch {\n return { kind: \"local\", failure: \"network\" };\n }\n if (response.status === 401 || response.status === 403) {\n return { kind: \"local\", failure: \"session\" };\n }\n if (!response.ok) return { kind: \"downstream\" };\n try {\n return { kind: \"detail\", detail: await response.json() };\n } catch {\n return { kind: \"downstream\" };\n }\n }\n function withoutKey(record, key) {\n const { [key]: _gone, ...rest } = record;\n return rest;\n }\n function applyDetail(id, outcome) {\n if (!state.data) return;\n const patch = {};\n const connectors = state.data.connectors.map((c3) => {\n if (c3.id !== id) return c3;\n if (outcome.kind === \"detail\") {\n const { detail } = outcome;\n return detail.status === \"auth_required\" && c3.authorizationUrl && !detail.authorizationUrl ? { ...detail, authorizationUrl: c3.authorizationUrl } : detail;\n }\n const { problem: _problem, ...rest } = c3;\n return outcome.kind === \"downstream\" ? { ...rest, status: \"error\", problem: \"connector_unavailable\" } : { ...rest, status: \"error\" };\n });\n patch.connectorFailures = outcome.kind === \"local\" ? { ...state.connectorFailures, [id]: outcome.failure } : withoutKey(state.connectorFailures, id);\n if (outcome.kind === \"detail\" && outcome.detail.status === \"ok\" && awaitingAuthorization.delete(id)) {\n if (state.oauthNoticeFor === id) {\n patch.oauthNotice = info(\"Connected.\");\n patch.oauthBlocked = null;\n }\n }\n set({ ...patch, data: { ...state.data, connectors } });\n }\n var detailGeneration = 0;\n var detailRevisions = /* @__PURE__ */ new Map();\n async function loadConnectorDetails(data, current, token) {\n const generation = ++detailGeneration;\n let next = 0;\n const worker = async () => {\n while (next < data.connectors.length && current() && generation === detailGeneration) {\n const connector = data.connectors[next++];\n const revision = (detailRevisions.get(connector.id) ?? 0) + 1;\n detailRevisions.set(connector.id, revision);\n const outcome = await readConnector(connector.id, token);\n if (!current() || generation !== detailGeneration || !state.data) return;\n if (detailRevisions.get(connector.id) !== revision) continue;\n applyDetail(connector.id, outcome);\n }\n };\n await Promise.all(Array.from({ length: Math.min(4, data.connectors.length) }, worker));\n }\n async function refreshConnector(id, quiet = false) {\n const current = fence();\n const revision = (detailRevisions.get(id) ?? 0) + 1;\n detailRevisions.set(id, revision);\n if (!quiet && state.data) {\n set({\n data: { ...state.data, connectors: state.data.connectors.map((c3) => c3.id === id ? { ...c3, status: \"loading\" } : c3) },\n connectorFailures: withoutKey(state.connectorFailures, id)\n });\n }\n let token;\n try {\n token = await sessionToken();\n } catch {\n if (!current() || quiet || detailRevisions.get(id) !== revision) return;\n return applyDetail(id, { kind: \"local\", failure: \"session\" });\n }\n if (!current()) return;\n const outcome = await readConnector(id, token);\n if (!current() || !state.data || detailRevisions.get(id) !== revision) return;\n if (quiet && outcome.kind !== \"detail\") return;\n applyDetail(id, outcome);\n }\n\n // node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js\n var f3 = 0;\n function u3(e3, t3, n2, o3, i3, u4) {\n t3 || (t3 = {});\n var a3, c3, p3 = t3;\n if (\"ref\" in p3) for (c3 in p3 = {}, t3) \"ref\" == c3 ? a3 = t3[c3] : p3[c3] = t3[c3];\n var l3 = { type: e3, props: p3, key: n2, ref: a3, __k: null, __: null, __b: 0, __e: null, __c: null, constructor: void 0, __v: --f3, __i: -1, __u: 0, __source: i3, __self: u4 };\n if (\"function\" == typeof e3 && (a3 = e3.defaultProps)) for (c3 in a3) void 0 === p3[c3] && (p3[c3] = a3[c3]);\n return l.vnode && l.vnode(l3), l3;\n }\n\n // src/operator-ui/app/parts.tsx\n function NoticeLine({\n id,\n notice,\n className = \"meta\"\n }) {\n return /* @__PURE__ */ u3(S, { children: [\n /* @__PURE__ */ u3(\n \"p\",\n {\n id,\n class: notice?.tone === \"error\" ? `notice ${className} error-notice` : `notice ${className}`,\n role: notice?.tone === \"error\" ? \"alert\" : \"status\",\n \"aria-live\": \"polite\",\n tabIndex: -1,\n children: notice ? notice.message : null\n }\n ),\n notice?.tone === \"error\" && notice.fix ? /* @__PURE__ */ u3(FixPrompt, { kind: notice.fix.kind, connectorId: notice.fix.connectorId }) : null\n ] });\n }\n function FixPrompt({\n kind,\n connectorId,\n name\n }) {\n return /* @__PURE__ */ u3(\"div\", { class: \"fix-prompt\", \"data-fix-prompt\": kind, children: [\n /* @__PURE__ */ u3(FixPromptButton, { kind, connectorId, ...name ? { name } : {} }),\n /* @__PURE__ */ u3(FixPromptPreview, { kind, connectorId })\n ] });\n }\n function FixPromptButton({\n kind,\n connectorId,\n name\n }) {\n return /* @__PURE__ */ u3(\n CopyButton,\n {\n value: fixPrompt(kind, connectorId),\n label: \"Copy fix prompt\",\n class: \"btn quiet\",\n ariaLabel: `Copy fix prompt for ${name ?? connectorId}`\n }\n );\n }\n function FixPromptPreview({\n kind,\n connectorId,\n standalone\n }) {\n return /* @__PURE__ */ u3(\"details\", { class: \"fix-prompt-preview\", ...standalone ? { \"data-fix-prompt\": kind } : {}, children: [\n /* @__PURE__ */ u3(\"summary\", { class: \"disclosure\", children: \"Preview prompt\" }),\n /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: \"Fixed text for a coding agent working on this deployment. It carries no error details or secrets.\" }),\n /* @__PURE__ */ u3(\"pre\", { class: \"fix-prompt-text\", children: fixPrompt(kind, connectorId) })\n ] });\n }\n function Badge({\n tone = \"neutral\",\n children\n }) {\n return /* @__PURE__ */ u3(\"span\", { class: tone === \"neutral\" ? \"badge\" : `badge ${tone}`, children });\n }\n function StateBlock({\n title,\n children,\n tone = \"neutral\",\n action,\n id\n }) {\n return /* @__PURE__ */ u3(\n \"div\",\n {\n class: tone === \"error\" ? \"state-block error\" : \"state-block\",\n role: tone === \"error\" ? \"alert\" : \"status\",\n ...id ? { id } : {},\n children: [\n title ? /* @__PURE__ */ u3(\"p\", { class: \"state-title\", children: title }) : null,\n children ? /* @__PURE__ */ u3(\"p\", { class: \"state-copy\", children }) : null,\n action ? /* @__PURE__ */ u3(\n \"button\",\n {\n class: \"btn\",\n type: \"button\",\n onClick: action.onClick,\n ...action.id ? { id: action.id } : {},\n children: action.label\n }\n ) : null\n ]\n }\n );\n }\n function LoadFailure({ state: state2 }) {\n if (!state2.loadFailure) return null;\n const copy = loadFailureCopy(state2.loadFailure, productName);\n return /* @__PURE__ */ u3(\n StateBlock,\n {\n id: \"loadFailure\",\n tone: \"error\",\n title: copy.title,\n action: { label: \"Retry\", onClick: () => void retryLoad(), id: \"retryLoad\" },\n children: copy.body\n }\n );\n }\n function Empty({ children }) {\n return /* @__PURE__ */ u3(StateBlock, { children });\n }\n function Unavailable({ children }) {\n return /* @__PURE__ */ u3(\"div\", { class: \"unavailable\", children });\n }\n function ConfirmBar({\n id,\n question,\n confirm,\n onConfirm,\n onCancel\n }) {\n return /* @__PURE__ */ u3(\n \"div\",\n {\n class: \"confirm\",\n role: \"group\",\n \"aria-labelledby\": `confirm-question-${id}`,\n onKeyDown: (event) => {\n if (event.key !== \"Escape\") return;\n event.preventDefault();\n onCancel();\n },\n children: [\n /* @__PURE__ */ u3(\"p\", { id: `confirm-question-${id}`, children: question }),\n /* @__PURE__ */ u3(\"div\", { class: \"actions\", children: [\n /* @__PURE__ */ u3(\"button\", { class: \"btn danger\", type: \"button\", onClick: onConfirm, children: confirm }),\n /* @__PURE__ */ u3(\n \"button\",\n {\n id: `confirm-cancel-${id}`,\n class: \"btn quiet\",\n type: \"button\",\n onClick: onCancel,\n children: \"Cancel\"\n }\n )\n ] })\n ]\n }\n );\n }\n function focusableId(...ids) {\n for (const id of ids) {\n const element = document.getElementById(id);\n if (element && !element.disabled) return id;\n }\n return ids[ids.length - 1] ?? \"\";\n }\n function PageLink({\n page,\n class: className,\n current,\n children\n }) {\n const href = PAGE_META[page].path;\n return /* @__PURE__ */ u3(\n \"a\",\n {\n class: className,\n href,\n ...current ? { \"aria-current\": \"page\" } : {},\n onClick: (event) => {\n if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {\n return;\n }\n event.preventDefault();\n navigate(page, href);\n },\n children\n }\n );\n }\n function CopyButton({\n value,\n label,\n class: className = \"btn\",\n ariaLabel,\n id\n }) {\n const [status, setStatus] = d2(\"idle\");\n h2(() => {\n if (status === \"idle\") return;\n const timer = window.setTimeout(() => setStatus(\"idle\"), 1600);\n return () => window.clearTimeout(timer);\n }, [status]);\n return /* @__PURE__ */ u3(\n \"button\",\n {\n class: className,\n type: \"button\",\n ...id ? { id } : {},\n ...ariaLabel && status === \"idle\" ? { \"aria-label\": ariaLabel } : {},\n onClick: () => {\n const write = navigator.clipboard?.writeText(value) ?? Promise.reject(new Error(\"no clipboard\"));\n write.then(\n () => setStatus(\"copied\"),\n () => setStatus(\"failed\")\n );\n },\n children: status === \"copied\" ? \"Copied\" : status === \"failed\" ? \"Copy failed\" : label\n }\n );\n }\n\n // src/operator-ui/app/activity.tsx\n function ActivityRow({ event }) {\n const outcome = activityOutcomeClass(event.outcome);\n const badge = activityOutcomeBadge(event.outcome);\n const stableId = actorStableId(event.actor);\n return /* @__PURE__ */ u3(\"article\", { class: `activity-item ${outcome}`, children: [\n /* @__PURE__ */ u3(\"div\", { class: \"activity-stamp\", children: [\n /* @__PURE__ */ u3(\"span\", { class: `dot ${outcome}`, \"aria-hidden\": \"true\" }),\n /* @__PURE__ */ u3(\"div\", { children: [\n /* @__PURE__ */ u3(\"time\", { class: \"activity-time\", dateTime: event.occurredAt, children: formatDate(event.occurredAt) }),\n /* @__PURE__ */ u3(\"div\", { class: \"activity-actor\", children: actorLabel(event.actor) }),\n stableId ? /* @__PURE__ */ u3(\"div\", { class: \"activity-actor-id mono\", children: stableId }) : null\n ] })\n ] }),\n /* @__PURE__ */ u3(\"div\", { children: [\n /* @__PURE__ */ u3(\"div\", { class: \"activity-address\", children: event.address }),\n /* @__PURE__ */ u3(\"div\", { class: \"activity-detail\", children: activityDetail(event) })\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"activity-result\", children: [\n /* @__PURE__ */ u3(Badge, { tone: badge.tone, children: badge.label }),\n /* @__PURE__ */ u3(\"div\", { class: \"activity-detail\", children: [\n event.durationMs,\n \" ms\"\n ] })\n ] })\n ] });\n }\n function ActivityPage({ state: state2 }) {\n const data = state2.data;\n const enabled = Boolean(data?.activityEnabled);\n const loading = state2.activityPhase === \"loading\";\n const visible = filterActivity(state2.activityEvents, state2.activitySearch);\n const summary = activitySummary(state2.activityEvents);\n const failed = state2.activityPhase === \"error\";\n return /* @__PURE__ */ u3(\"section\", { id: \"activityView\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"lead\", children: [\n /* @__PURE__ */ u3(\"h1\", { id: \"activityHeading\", tabIndex: -1, children: \"Activity\" }),\n /* @__PURE__ */ u3(\"div\", { class: \"lead-copy\", children: /* @__PURE__ */ u3(\"p\", { children: pageDescription(\"activity\", productDescription) }) })\n ] }),\n !data ? (\n // Whether Activity is open to this identity is in /ui/data, which\n // has not answered yet — or could not.\n state2.loadFailure ? /* @__PURE__ */ u3(LoadFailure, { state: state2 }) : /* @__PURE__ */ u3(StateBlock, { children: \"Loading activity…\" })\n ) : !enabled ? /* @__PURE__ */ u3(Unavailable, { children: [\n \"Activity history is not configured. Add an\",\n \" \",\n /* @__PURE__ */ u3(\"span\", { class: \"mono\", children: \"activity.store\" }),\n \" with a list reader to enable this page.\"\n ] }) : /* @__PURE__ */ u3(\"div\", { id: \"activityAvailable\", class: \"collection\", children: [\n failed && state2.activityEvents.length === 0 ? null : /* @__PURE__ */ u3(\"div\", { class: \"row\", children: [\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: \"activitySearch\",\n type: \"search\",\n placeholder: \"Search user, tool, or outcome…\",\n \"aria-label\": \"Search loaded activity\",\n value: state2.activitySearch,\n onInput: (event) => setActivitySearch(event.currentTarget.value)\n }\n ),\n /* @__PURE__ */ u3(\n \"button\",\n {\n id: \"refreshActivity\",\n class: \"btn\",\n type: \"button\",\n disabled: loading,\n onClick: () => void loadActivity(true),\n children: loading ? \"Loading…\" : \"Refresh\"\n }\n )\n ] }),\n /* @__PURE__ */ u3(\"p\", { id: \"activitySummary\", class: \"meta\", \"aria-live\": \"polite\", children: summary }),\n state2.activityEvents.length === 0 ? loading ? /* @__PURE__ */ u3(StateBlock, { children: \"Loading activity…\" }) : failed ? /* @__PURE__ */ u3(\n StateBlock,\n {\n id: \"activityError\",\n tone: \"error\",\n title: \"Activity couldn't be loaded\",\n action: { label: \"Retry\", onClick: () => void retryCollection() },\n children: state2.activityNotice?.message\n }\n ) : /* @__PURE__ */ u3(StateBlock, { children: state2.activitySearch.trim() ? \"No loaded activity matches this search.\" : \"No connector tool calls recorded yet.\" }) : visible.length === 0 ? /* @__PURE__ */ u3(StateBlock, { children: \"No loaded activity matches this search.\" }) : /* @__PURE__ */ u3(\n \"div\",\n {\n id: \"activityList\",\n class: \"activity-list\",\n \"aria-busy\": loading ? \"true\" : \"false\",\n children: visible.map((event, index) => /* @__PURE__ */ u3(\n ActivityRow,\n {\n event\n },\n `${event.occurredAt}-${event.address}-${index}`\n ))\n }\n ),\n state2.activityEvents.length > 0 ? /* @__PURE__ */ u3(NoticeLine, { id: \"activityNotice\", notice: state2.activityNotice }) : null,\n state2.activityCursor ? /* @__PURE__ */ u3(\n \"button\",\n {\n id: \"moreActivity\",\n class: \"btn activity-more\",\n type: \"button\",\n disabled: loading,\n onClick: () => void loadActivity(false),\n children: loading ? \"Loading…\" : \"Load older\"\n }\n ) : null\n ] })\n ] });\n }\n\n // src/operator-ui/app/artifacts.tsx\n function ArtifactRow({ row }) {\n const refresh = artifactRefreshBadge(row.freshness?.last);\n return /* @__PURE__ */ u3(\"article\", { class: \"artifact-row\", children: [\n /* @__PURE__ */ u3(\"div\", { children: [\n /* @__PURE__ */ u3(\"a\", { class: \"artifact-title\", href: `/artifacts/${row.id}`, children: row.title }),\n /* @__PURE__ */ u3(\"div\", { class: \"artifact-meta\", children: [\n /* @__PURE__ */ u3(\"span\", { class: \"mono\", children: row.id }),\n \" · version \",\n row.viewVersion,\n \" · updated\",\n \" \",\n /* @__PURE__ */ u3(\"time\", { dateTime: row.updatedAt, children: formatDate(row.updatedAt) }),\n \" by \",\n row.updatedBy.label\n ] })\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"artifact-badges\", children: [\n /* @__PURE__ */ u3(Badge, { children: row.kind === \"markdown\" ? \"Markdown\" : \"HTML\" }),\n row.freshness?.state === \"stale\" ? /* @__PURE__ */ u3(Badge, { tone: \"warn\", children: \"Stale data\" }) : row.freshness?.state === \"current\" ? /* @__PURE__ */ u3(Badge, { children: \"Current data\" }) : null,\n refresh ? /* @__PURE__ */ u3(Badge, { tone: refresh.tone, children: refresh.label }) : null,\n row.archived ? /* @__PURE__ */ u3(Badge, { tone: \"warn\", children: \"Archived\" }) : null\n ] })\n ] });\n }\n function ArtifactsPage({ state: state2 }) {\n const loading = state2.artifactPhase === \"loading\";\n const rows = state2.artifactRows;\n return /* @__PURE__ */ u3(\"section\", { id: \"artifactsView\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"lead\", children: [\n /* @__PURE__ */ u3(\"h1\", { id: \"artifactsHeading\", tabIndex: -1, children: \"Artifacts\" }),\n /* @__PURE__ */ u3(\"div\", { class: \"lead-copy\", children: /* @__PURE__ */ u3(\"p\", { children: pageDescription(\"artifacts\", productDescription) }) })\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"collection\", children: [\n /* @__PURE__ */ u3(\n \"form\",\n {\n class: \"row\",\n onSubmit: (event) => {\n event.preventDefault();\n void loadArtifacts(true);\n },\n children: [\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: \"artifactSearch\",\n type: \"search\",\n placeholder: \"Search titles…\",\n \"aria-label\": \"Search artifact titles\",\n value: state2.artifactQuery,\n onInput: (event) => setArtifactQuery(event.currentTarget.value)\n }\n ),\n /* @__PURE__ */ u3(\"button\", { id: \"searchArtifacts\", class: \"btn\", type: \"submit\", disabled: loading, children: \"Search\" }),\n /* @__PURE__ */ u3(\"label\", { class: \"check artifact-meta\", children: [\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: \"showArchived\",\n type: \"checkbox\",\n checked: state2.artifactArchived,\n onChange: (event) => setArtifactArchived(event.currentTarget.checked)\n }\n ),\n \" \",\n \"Show archived\"\n ] })\n ]\n }\n ),\n state2.artifactPhase === \"error\" && rows.length === 0 ? /* @__PURE__ */ u3(\n StateBlock,\n {\n id: \"artifactError\",\n tone: \"error\",\n title: \"Artifacts couldn't be loaded\",\n action: { label: \"Retry\", onClick: () => void retryCollection() },\n children: state2.artifactNotice?.message\n }\n ) : rows.length === 0 ? /* @__PURE__ */ u3(StateBlock, { children: loading ? \"Loading artifacts…\" : state2.artifactQuery.trim() ? \"No artifact title matches this search.\" : \"No artifacts yet. Ask an agent to publish one.\" }) : /* @__PURE__ */ u3(\"div\", { id: \"artifactList\", class: \"activity-list\", \"aria-busy\": loading ? \"true\" : \"false\", children: rows.map((row) => /* @__PURE__ */ u3(ArtifactRow, { row }, row.id)) }),\n rows.length > 0 ? /* @__PURE__ */ u3(NoticeLine, { id: \"artifactNotice\", notice: state2.artifactNotice }) : null,\n state2.artifactCursor ? /* @__PURE__ */ u3(\n \"button\",\n {\n id: \"moreArtifacts\",\n class: \"btn activity-more\",\n type: \"button\",\n disabled: loading,\n onClick: () => void loadArtifacts(false),\n children: loading ? \"Loading…\" : \"Load more\"\n }\n ) : null\n ] })\n ] });\n }\n function ArtifactFrame({ view }) {\n const frame = A2(null);\n _2(() => {\n const element = frame.current;\n if (!element) return;\n const onMessage = (event) => {\n if (event.source !== element.contentWindow || event.origin !== \"null\") return;\n const data = event.data;\n if (!data || data.type !== \"ready\") return;\n window.removeEventListener(\"message\", onMessage);\n const policy = document.createElement(\"meta\");\n policy.httpEquiv = \"Content-Security-Policy\";\n policy.content = \"frame-src 'none'\";\n document.head.append(policy);\n element.contentWindow?.postMessage({ type: \"document\", html: view.document }, \"*\");\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [view.document]);\n return /* @__PURE__ */ u3(\n \"iframe\",\n {\n ref: frame,\n id: \"artifactFrame\",\n class: \"artifact-frame\",\n title: view.title,\n sandbox: \"allow-scripts\",\n referrerpolicy: \"no-referrer\",\n src: \"/artifacts/_frame\"\n }\n );\n }\n function ArtifactPage({ state: state2 }) {\n const view = state2.artifactView;\n const failed = state2.artifactPhase === \"error\";\n return /* @__PURE__ */ u3(\"section\", { id: \"artifactView\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"artifact-head\", children: [\n /* @__PURE__ */ u3(\"h1\", { id: \"artifactHeading\", tabIndex: -1, children: view?.title ?? \"Artifact\" }),\n view ? /* @__PURE__ */ u3(\"div\", { class: \"artifact-meta\", id: \"artifactMeta\", children: [\n view.snapshot ? \"Snapshot of \" : \"\",\n \"version \",\n view.view.version,\n view.snapshot && view.view.version !== view.latestViewVersion ? ` (latest is ${view.latestViewVersion})` : \"\",\n \" \",\n \"· updated \",\n /* @__PURE__ */ u3(\"time\", { dateTime: view.view.at, children: formatDate(view.view.at) }),\n \" by\",\n \" \",\n view.view.by.label,\n \" ·\",\n \" \",\n /* @__PURE__ */ u3(\"a\", { href: \"/artifacts\", children: \"All artifacts\" }),\n view.snapshot ? /* @__PURE__ */ u3(S, { children: [\n \" \",\n \"· \",\n /* @__PURE__ */ u3(\"a\", { href: view.url, children: \"Current version\" })\n ] }) : /* @__PURE__ */ u3(S, { children: [\n \" \",\n \"·\",\n \" \",\n /* @__PURE__ */ u3(\n CopyButton,\n {\n id: \"copySnapshot\",\n class: \"navlink inline\",\n value: view.snapshotUrl,\n label: \"Copy snapshot link\"\n }\n )\n ] })\n ] }) : null,\n view?.archived ? /* @__PURE__ */ u3(\"div\", { id: \"archivedBanner\", class: \"artifact-banner\", role: \"status\", children: \"This artifact is archived. It keeps every version, and an agent can restore it.\" }) : null,\n !view?.snapshot && view?.freshness?.state === \"stale\" ? /* @__PURE__ */ u3(\"div\", { id: \"staleBanner\", class: \"artifact-banner\", role: \"status\", children: [\n \"Data may be out of date. The last refresh \",\n view.freshness.last?.status === \"failed\" ? \"failed\" : \"is overdue\",\n \"; the last good document is still shown.\"\n ] }) : null,\n !view && !failed ? /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: pageDescription(\"artifact\", productDescription) }) : null\n ] }),\n view ? /* @__PURE__ */ u3(ArtifactFrame, { view }, view.snapshotUrl) : failed ? /* @__PURE__ */ u3(\n StateBlock,\n {\n id: \"artifactError\",\n tone: \"error\",\n title: \"This artifact couldn't be opened\",\n action: { label: \"Retry\", onClick: () => void retryCollection() },\n children: state2.artifactNotice?.message\n }\n ) : /* @__PURE__ */ u3(StateBlock, { children: \"Loading artifact…\" })\n ] });\n }\n\n // src/operator-ui/app/credentials.tsx\n function CredentialForm({\n connector,\n credential,\n busy\n }) {\n const fields = credential.fields ?? [];\n const [values, setValues] = d2({});\n const single = fields.length === 0;\n const inputId = `credential-input-${connector}`;\n const submit = () => {\n if (single) {\n const value = (values.value ?? \"\").trim();\n if (!value) return refuseCredential(connector, \"Paste a credential before saving.\");\n return void saveCredential(connector, { value });\n }\n const entries = {};\n for (const field of fields) {\n const value = (values[field.name] ?? \"\").trim();\n if (!value) {\n return refuseCredential(\n connector,\n \"Complete every credential field before saving.\"\n );\n }\n entries[field.name] = value;\n }\n void saveCredential(connector, { values: entries });\n };\n return /* @__PURE__ */ u3(\"div\", { class: \"credential-form\", \"data-credential-form\": connector, children: [\n single ? /* @__PURE__ */ u3(S, { children: [\n /* @__PURE__ */ u3(\"label\", { class: \"visually-hidden\", for: inputId, children: credential.label }),\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: inputId,\n type: \"password\",\n \"aria-label\": credential.label,\n placeholder: credential.placeholder || \"Paste credential\",\n autocomplete: \"new-password\",\n autocapitalize: \"none\",\n spellcheck: false,\n value: values.value ?? \"\",\n onInput: (event) => setValues({ value: event.currentTarget.value })\n }\n )\n ] }) : /* @__PURE__ */ u3(\"div\", { class: \"credential-fields\", children: fields.map((field, index) => {\n const id = `credential-input-${connector}-${index}`;\n return /* @__PURE__ */ u3(\"div\", { class: \"credential-field\", children: [\n /* @__PURE__ */ u3(\"label\", { for: id, children: field.label }),\n /* @__PURE__ */ u3(\n \"input\",\n {\n id,\n type: field.inputType || \"password\",\n placeholder: field.placeholder || field.label,\n autocomplete: (field.inputType ?? \"password\") === \"password\" ? \"new-password\" : \"off\",\n autocapitalize: \"none\",\n spellcheck: false,\n value: values[field.name] ?? \"\",\n onInput: (event) => setValues({\n ...values,\n [field.name]: event.currentTarget.value\n })\n }\n )\n ] }, field.name);\n }) }),\n /* @__PURE__ */ u3(\"div\", { class: \"actions\", children: [\n /* @__PURE__ */ u3(\"button\", { class: \"btn primary\", type: \"button\", disabled: busy, onClick: submit, children: busy ? \"Saving…\" : \"Save\" }),\n /* @__PURE__ */ u3(\n \"button\",\n {\n class: \"btn quiet\",\n type: \"button\",\n disabled: busy,\n onClick: () => editCredential(null),\n children: \"Cancel\"\n }\n )\n ] })\n ] });\n }\n function CredentialCard({\n connector,\n credential,\n editing,\n busy,\n confirming,\n notice\n }) {\n const name = connector.title || connector.id;\n const configured = Boolean(credential.configured);\n const removable = configured || Boolean(credential.removable);\n return /* @__PURE__ */ u3(\n \"section\",\n {\n class: \"subcard\",\n id: `credential-${connector.id}`,\n \"aria-labelledby\": `credential-title-${connector.id}`,\n children: [\n /* @__PURE__ */ u3(\"div\", { class: \"subcard-head\", children: [\n /* @__PURE__ */ u3(\"h3\", { id: `credential-title-${connector.id}`, children: credential.label }),\n /* @__PURE__ */ u3(\"span\", { class: \"meta\", children: credentialStateLabel(credential) })\n ] }),\n credential.description ? /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: credential.description }) : null,\n credential.fields?.length ? /* @__PURE__ */ u3(\"div\", { class: \"credential-field-summary\", children: credential.fields.map((field) => /* @__PURE__ */ u3(\"div\", { children: [\n /* @__PURE__ */ u3(\"span\", { children: field.label }),\n /* @__PURE__ */ u3(\"span\", { class: \"meta\", children: field.configured ? `configured · ••••${field.lastFour ?? \"\"}${field.updatedAt ? ` · updated ${formatDate(field.updatedAt)}` : \"\"}` : \"not configured\" })\n ] }, field.name)) }) : null,\n credential.error ? /* @__PURE__ */ u3(\"p\", { class: \"msg\", children: credentialProblemCopy(credential.problem) }) : null,\n credential.error && credential.problem ? /* @__PURE__ */ u3(\n FixPrompt,\n {\n kind: credential.problem,\n connectorId: connector.id,\n name: connector.title || connector.id\n }\n ) : null,\n credential.notice ? /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: credential.notice }) : null,\n /* @__PURE__ */ u3(\"div\", { class: \"actions\", children: [\n /* @__PURE__ */ u3(\n \"button\",\n {\n class: removable ? \"btn\" : \"btn primary\",\n type: \"button\",\n \"aria-expanded\": editing ? \"true\" : \"false\",\n disabled: busy,\n onClick: () => editCredential(editing ? null : connector.id),\n children: removable ? \"Replace\" : \"Add credential\"\n }\n ),\n configured && credential.testable ? /* @__PURE__ */ u3(\n \"button\",\n {\n class: \"btn\",\n type: \"button\",\n disabled: busy,\n onClick: () => void testCredential(connector.id),\n children: busy ? \"Working…\" : \"Test\"\n }\n ) : null,\n removable ? /* @__PURE__ */ u3(\n \"button\",\n {\n id: `remove-credential-${connector.id}`,\n class: \"btn danger\",\n type: \"button\",\n disabled: busy,\n onClick: () => askConfirm(connector.id, \"credential_remove\"),\n children: \"Remove\"\n }\n ) : null\n ] }),\n confirming ? /* @__PURE__ */ u3(\n ConfirmBar,\n {\n id: connector.id,\n ...confirmCopy(\"credential_remove\", name),\n onConfirm: () => void removeCredential(connector.id),\n onCancel: () => cancelConfirm(\n focusableId(`remove-credential-${connector.id}`, `conn-toggle-${connector.id}`)\n )\n }\n ) : null,\n editing ? /* @__PURE__ */ u3(\n CredentialForm,\n {\n connector: connector.id,\n credential,\n busy\n }\n ) : null,\n /* @__PURE__ */ u3(NoticeLine, { id: `credentialNotice-${connector.id}`, notice })\n ]\n }\n );\n }\n\n // src/operator-ui/model.ts\n function filterUiConnectors(connectors, query) {\n const q2 = query.trim().toLowerCase();\n const filtered = [];\n for (const connector of connectors) {\n const connectorText = [\n connector.id,\n connector.title,\n connector.description,\n connector.status\n ].join(\" \").toLowerCase();\n const connectorMatches = Boolean(q2 && connectorText.includes(q2));\n const tools = connector.tools.filter(\n (tool) => !q2 || connectorMatches || `${tool.name} ${tool.description ?? \"\"}`.toLowerCase().includes(q2)\n );\n if (q2 && tools.length === 0 && !connectorMatches) continue;\n filtered.push({ connector, tools });\n }\n return filtered;\n }\n\n // src/operator-ui/setup-commands.ts\n function clientServerName(serverName, pool) {\n const base = (serverName ?? \"\").toLowerCase().replace(/[^a-z0-9_-]+/g, \"-\").replace(/-{2,}/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 48) || \"connecta\";\n return pool ? `${base}-${pool}` : base;\n }\n function poolEndpointUrl(mcpUrl2, pool) {\n return `${mcpUrl2.replace(/\\/+$/, \"\")}/${encodeURIComponent(pool)}`;\n }\n function shellWord(value) {\n return /^[A-Za-z0-9_./:@%+=,~-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\"'\"'`)}'`;\n }\n function clientSetupCommands(name, url) {\n return [\n {\n id: \"claude\",\n label: \"Claude Code\",\n text: `claude mcp add --transport http ${shellWord(name)} ${shellWord(url)}`\n },\n {\n id: \"codex\",\n label: \"Codex\",\n text: `codex mcp add ${shellWord(name)} --url ${shellWord(url)}`\n },\n {\n id: \"json\",\n label: \"JSON config\",\n text: JSON.stringify(\n { mcpServers: { [name]: { type: \"http\", url } } },\n null,\n 2\n )\n }\n ];\n }\n\n // src/operator-ui/app/connections.tsx\n var DRIFT_HEADING = {\n clean: \"Catalog drift · none\",\n warning: \"Catalog drift · review\",\n unavailable: \"Catalog drift · not observed\"\n };\n function DriftPanel({ connector }) {\n const drift = connector.catalogDrift;\n const state2 = driftState(drift);\n return /* @__PURE__ */ u3(S, { children: [\n /* @__PURE__ */ u3(\n \"div\",\n {\n id: `drift-${connector.id}`,\n class: `connector-drift ${state2}`,\n \"data-drift\": state2,\n children: [\n /* @__PURE__ */ u3(\"p\", { class: \"cap\", children: DRIFT_HEADING[state2] }),\n /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: driftSummary(drift) }),\n state2 === \"unavailable\" ? null : /* @__PURE__ */ u3(\"ul\", { class: \"drift-counts\", children: driftCounts(drift).map(({ key, label, count }) => /* @__PURE__ */ u3(\"li\", { class: count > 0 ? \"drift-count flagged\" : \"drift-count\", children: [\n /* @__PURE__ */ u3(\"span\", { class: \"drift-count-value\", children: count }),\n /* @__PURE__ */ u3(\"span\", { class: \"drift-count-label\", children: label })\n ] }, key)) })\n ]\n }\n ),\n state2 === \"warning\" ? /* @__PURE__ */ u3(\n FixPrompt,\n {\n kind: \"catalog_drift\",\n connectorId: connector.id,\n name: connector.title || connector.id\n }\n ) : null\n ] });\n }\n function SafetyBadge({ safety }) {\n const badge = safety ? TOOL_SAFETY_BADGE[safety] : void 0;\n if (!badge) return null;\n return /* @__PURE__ */ u3(\"span\", { class: \"tool-safety\", title: badge.title, \"data-safety\": safety, children: /* @__PURE__ */ u3(Badge, { tone: badge.tone, children: badge.label }) });\n }\n function Endpoint({\n url,\n name,\n label,\n primary\n }) {\n return /* @__PURE__ */ u3(\"div\", { class: \"endpoint-block\", \"data-endpoint\": name, children: [\n /* @__PURE__ */ u3(\"div\", { class: \"endpoint\", children: [\n label ? /* @__PURE__ */ u3(\"span\", { class: \"endpoint-label cap\", children: label }) : null,\n /* @__PURE__ */ u3(\"code\", { ...primary ? { id: \"mcpUrl\" } : {}, class: \"mono\", children: url }),\n /* @__PURE__ */ u3(\n CopyButton,\n {\n value: url,\n label: \"Copy URL\",\n ...label ? { ariaLabel: `Copy URL for ${label}` } : {}\n }\n )\n ] }),\n /* @__PURE__ */ u3(\"details\", { class: \"setup\", children: [\n /* @__PURE__ */ u3(\"summary\", { class: \"disclosure\", children: [\n \"Client setup\",\n label ? ` · ${label}` : \"\"\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"setup-list\", children: [\n clientSetupCommands(name, url).map((command) => /* @__PURE__ */ u3(\"div\", { class: \"setup-item\", \"data-setup\": command.id, children: [\n /* @__PURE__ */ u3(\"div\", { class: \"setup-head\", children: [\n /* @__PURE__ */ u3(\"span\", { class: \"cap\", children: command.label }),\n /* @__PURE__ */ u3(\n CopyButton,\n {\n value: command.text,\n label: \"Copy\",\n class: \"btn quiet\",\n ariaLabel: `Copy ${command.label} setup${label ? ` for ${label}` : \"\"}`\n }\n )\n ] }),\n /* @__PURE__ */ u3(\"pre\", { class: \"setup-code\", children: command.text })\n ] }, command.id)),\n /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: \"No token is included. Clients sign in through this deployment's inbound auth.\" })\n ] })\n ] })\n ] });\n }\n function AuthActions({\n connector,\n name,\n manage,\n state: state2\n }) {\n const id = connector.id;\n const authorization = safeHttpHref(connector.authorizationUrl);\n const busy = state2.oauthBusy === id;\n if (connector.status === \"loading\") return null;\n if (!connector.oauth || !manage) {\n return authorization && connector.status !== \"ok\" ? /* @__PURE__ */ u3(\"a\", { class: \"btn primary\", href: authorization, target: \"_blank\", rel: \"noopener noreferrer\", children: \"Authorize connector\" }) : null;\n }\n if (state2.oauthBlocked === id && authorization) {\n return /* @__PURE__ */ u3(\n \"a\",\n {\n id: `authorize-${id}`,\n class: \"btn primary\",\n href: authorization,\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n children: \"Open authorization page\"\n }\n );\n }\n if (connector.status !== \"ok\") {\n const needsAuth = connector.status === \"auth_required\";\n return /* @__PURE__ */ u3(\n \"button\",\n {\n type: \"button\",\n id: `connect-${id}`,\n class: needsAuth ? \"btn primary\" : \"btn\",\n \"aria-label\": `${needsAuth ? \"Connect\" : \"Reconnect\"} ${name}`,\n disabled: busy,\n onClick: () => void startOAuth(id, \"continue\"),\n children: busy ? \"Opening…\" : needsAuth ? \"Connect account\" : \"Reconnect\"\n }\n );\n }\n const switching = connector.authScope === \"personal\";\n return /* @__PURE__ */ u3(S, { children: [\n /* @__PURE__ */ u3(\n \"button\",\n {\n type: \"button\",\n id: `reconnect-${id}`,\n class: \"btn\",\n \"aria-label\": `${switching ? \"Switch account for\" : \"Reconnect\"} ${name}`,\n disabled: busy,\n onClick: () => askConfirm(id, \"oauth_restart\"),\n children: busy ? \"Working…\" : switching ? \"Switch account\" : \"Reconnect\"\n }\n ),\n /* @__PURE__ */ u3(\n \"button\",\n {\n type: \"button\",\n id: `disconnect-${id}`,\n class: \"btn danger\",\n \"aria-label\": `Disconnect ${name}`,\n disabled: busy,\n onClick: () => askConfirm(id, \"oauth_disconnect\"),\n children: \"Disconnect\"\n }\n )\n ] });\n }\n function ConnectorRow({\n connector,\n tools,\n forceOpen,\n state: state2\n }) {\n const [open, setOpen] = d2(false);\n const shown = open || forceOpen;\n const id = connector.id;\n const name = connector.title || id;\n const drift = driftState(connector.catalogDrift);\n const manage = Boolean(\n connector.permissions?.manageSharedAuth || connector.permissions?.connectPersonal\n );\n const local = state2.connectorFailures[id];\n const problem = connector.status === \"loading\" || local ? null : problemCopy(connector.problem);\n const confirming = state2.confirming?.connectorId === id ? state2.confirming : null;\n const oauthConfirm = confirming && confirming.action !== \"credential_remove\" ? confirming : null;\n const statusLabel = local ? \"Couldn't load\" : connectorStatusLabel(connector.status, connector.problem);\n const fixKind = problem && connector.problem && problemTone(connector.problem) === \"danger\" && connector.problem !== connector.credential?.problem ? connector.problem : null;\n const statusTone = local ? \"warn\" : connectorStatusTone(connector.status);\n return /* @__PURE__ */ u3(\"div\", { class: shown ? \"conn open\" : \"conn\", \"data-connector\": id, children: [\n /* @__PURE__ */ u3(\"div\", { class: \"conn-head\", children: [\n /* @__PURE__ */ u3(\"span\", { class: \"conn-main\", children: [\n /* @__PURE__ */ u3(\"span\", { class: `dot ${local ? \"warn\" : connector.status}`, \"aria-hidden\": \"true\" }),\n /* @__PURE__ */ u3(\"h2\", { class: \"conn-name\", children: /* @__PURE__ */ u3(\n \"button\",\n {\n type: \"button\",\n id: `conn-toggle-${id}`,\n class: \"conn-toggle\",\n \"aria-expanded\": shown ? \"true\" : \"false\",\n \"aria-controls\": `conn-body-${id}`,\n \"aria-describedby\": `conn-state-${id}`,\n onClick: () => setOpen(!shown),\n children: name\n }\n ) }),\n connector.title ? /* @__PURE__ */ u3(\"span\", { class: \"conn-id mono\", children: id }) : null\n ] }),\n /* @__PURE__ */ u3(\"span\", { class: \"conn-badges\", id: `conn-state-${id}`, children: [\n drift === \"warning\" ? /* @__PURE__ */ u3(Badge, { tone: \"warn\", children: \"drift\" }) : null,\n /* @__PURE__ */ u3(Badge, { children: authScopeLabel(connector.authScope) }),\n /* @__PURE__ */ u3(Badge, { children: connector.status === \"loading\" ? \"tools not loaded\" : toolCountLabel(connector.toolCount) }),\n /* @__PURE__ */ u3(Badge, { tone: statusTone, children: statusLabel }),\n /* @__PURE__ */ u3(\"span\", { class: \"conn-caret\", \"aria-hidden\": \"true\" })\n ] })\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"conn-body\", id: `conn-body-${id}`, hidden: !shown, children: [\n connector.description ? /* @__PURE__ */ u3(\"p\", { class: \"conn-note\", children: connector.description }) : null,\n problem && connector.problem ? /* @__PURE__ */ u3(\n \"p\",\n {\n class: problemTone(connector.problem) === \"warn\" ? \"msg warn\" : \"msg\",\n \"data-problem\": connector.problem,\n children: problem\n }\n ) : null,\n local ? /* @__PURE__ */ u3(\"p\", { class: \"msg warn\", \"data-load-failure\": local, children: connectorLoadFailureCopy(local, productName) }) : null,\n connector.authorizationUrl && !safeHttpHref(connector.authorizationUrl) ? /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: [\n \"Authorization URL: \",\n connector.authorizationUrl\n ] }) : null,\n manage ? null : /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: permissionLabel(connector) }),\n /* @__PURE__ */ u3(\"div\", { class: \"actions\", children: [\n local ? null : /* @__PURE__ */ u3(AuthActions, { connector, name, manage, state: state2 }),\n fixKind ? /* @__PURE__ */ u3(FixPromptButton, { kind: fixKind, connectorId: id, name }) : null,\n /* @__PURE__ */ u3(\n \"button\",\n {\n class: local ? \"btn primary\" : \"btn quiet\",\n type: \"button\",\n \"aria-label\": `Refresh ${name}`,\n disabled: connector.status === \"loading\",\n onClick: () => void refreshConnector(id),\n children: \"Refresh\"\n }\n )\n ] }),\n fixKind ? /* @__PURE__ */ u3(FixPromptPreview, { kind: fixKind, connectorId: id, standalone: true }) : null,\n oauthConfirm ? /* @__PURE__ */ u3(\n ConfirmBar,\n {\n id,\n ...confirmCopy(oauthConfirm.action, name),\n onConfirm: () => {\n if (oauthConfirm.action === \"oauth_restart\") void startOAuth(id, \"restart\");\n else void disconnectOAuth(id);\n },\n onCancel: () => cancelConfirm(\n focusableId(\n oauthConfirm.action === \"oauth_restart\" ? `reconnect-${id}` : `disconnect-${id}`,\n `conn-toggle-${id}`\n )\n )\n }\n ) : null,\n /* @__PURE__ */ u3(\n NoticeLine,\n {\n id: `oauthNotice-${id}`,\n notice: state2.oauthNoticeFor === id ? state2.oauthNotice : null\n }\n ),\n connector.credential ? /* @__PURE__ */ u3(\n CredentialCard,\n {\n connector,\n credential: connector.credential,\n editing: state2.credentialEditing === id,\n busy: state2.credentialBusy === id,\n confirming: confirming?.action === \"credential_remove\",\n notice: state2.credentialNoticeFor === id ? state2.credentialNotice : null\n }\n ) : null,\n tools.length ? /* @__PURE__ */ u3(\"details\", { open: forceOpen, children: [\n /* @__PURE__ */ u3(\"summary\", { class: \"disclosure\", children: [\n \"Tools (\",\n tools.length,\n \")\"\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"tool-list\", children: [\n tools.some((tool) => tool.safety) ? /* @__PURE__ */ u3(\"p\", { class: \"meta tool-legend\", children: \"Read-only tools run inside execute_code programs. Everything else asks the host first: a program pauses for resume_execution, and a direct call goes through call_destructive_tool — unless this deployment's config exempts the tool, in which case programs call it unasked.\" }) : null,\n tools.map((tool) => /* @__PURE__ */ u3(\"div\", { class: \"tool\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"tool-head\", children: [\n /* @__PURE__ */ u3(\"code\", { children: tool.address }),\n /* @__PURE__ */ u3(SafetyBadge, { safety: tool.safety })\n ] }),\n tool.description ? /* @__PURE__ */ u3(\"span\", { class: \"td\", children: tool.description }) : null\n ] }, tool.address))\n ] })\n ] }) : null,\n /* @__PURE__ */ u3(\"details\", { children: [\n /* @__PURE__ */ u3(\"summary\", { class: \"disclosure\", children: \"Diagnostics\" }),\n /* @__PURE__ */ u3(\"div\", { class: \"subcard\", children: [\n /* @__PURE__ */ u3(DriftPanel, { connector }),\n connector.catalogAccess ? /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: [\n \"Agents last read its catalog\",\n \" \",\n connector.catalogAccess.state === \"stale\" ? \"from a stale cache\" : \"fresh\",\n \" · \",\n formatDate(connector.catalogAccess.observedAt)\n ] }) : null\n ] })\n ] })\n ] })\n ] });\n }\n function Endpoints({\n pools,\n serverName\n }) {\n return /* @__PURE__ */ u3(\"div\", { class: \"endpoints\", children: [\n /* @__PURE__ */ u3(\n Endpoint,\n {\n url: mcpUrl,\n name: clientServerName(serverName),\n primary: true,\n ...pools.length ? { label: \"All tools\" } : {}\n }\n ),\n pools.map((pool) => /* @__PURE__ */ u3(\n Endpoint,\n {\n url: poolEndpointUrl(mcpUrl, pool),\n name: clientServerName(serverName, pool),\n label: `Pool · ${pool}`\n },\n pool\n ))\n ] });\n }\n function SummaryLine({ connectors }) {\n const parts = connectorSummaryParts(summarizeConnectors(connectors));\n return /* @__PURE__ */ u3(\"p\", { class: \"summary\", id: \"connectorSummary\", children: parts.map((part, index) => /* @__PURE__ */ u3(\"span\", { children: [\n index > 0 ? /* @__PURE__ */ u3(\"span\", { class: \"sep\", children: \" · \" }) : null,\n /* @__PURE__ */ u3(\"span\", { class: part.tone === \"neutral\" ? \"\" : part.tone, children: part.text })\n ] }, part.text)) });\n }\n function ConnectionsPage({ state: state2 }) {\n const data = state2.data;\n const query = state2.connectorFilter.trim();\n const filtered = data ? filterUiConnectors(data.connectors, query) : [];\n return /* @__PURE__ */ u3(\"section\", { id: \"connectionsView\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"lead\", children: [\n /* @__PURE__ */ u3(\"h1\", { id: \"connectionsHeading\", tabIndex: -1, children: \"Connections\" }),\n /* @__PURE__ */ u3(\"div\", { class: \"lead-copy\", children: [\n /* @__PURE__ */ u3(\"p\", { children: \"Point an MCP client at this endpoint to reach the tools below.\" }),\n /* @__PURE__ */ u3(Endpoints, { pools: data?.pools ?? [], serverName: data?.serverInfo?.name }),\n /* @__PURE__ */ u3(\"p\", { class: \"cap\", id: \"serverInfo\", children: data ? `${data.serverInfo?.name || productName} v${data.connectaVersion || \"?\"}` : productOperatorLabel }),\n data ? /* @__PURE__ */ u3(SummaryLine, { connectors: data.connectors }) : null\n ] })\n ] }),\n /* @__PURE__ */ u3(\"section\", { class: \"section\", \"aria-labelledby\": \"connectorLedgerHeading\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"section-head\", children: [\n /* @__PURE__ */ u3(\"h2\", { id: \"connectorLedgerHeading\", tabIndex: -1, children: \"Connectors\" }),\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: \"filter\",\n type: \"search\",\n class: \"filter\",\n placeholder: \"Filter connectors or tools…\",\n \"aria-label\": \"Filter connectors or tools\",\n value: state2.connectorFilter,\n disabled: !data,\n onInput: (event) => setConnectorFilter(event.currentTarget.value)\n }\n )\n ] }),\n /* @__PURE__ */ u3(\n \"div\",\n {\n id: \"list\",\n class: !data || filtered.length === 0 ? \"\" : \"rows\",\n \"aria-busy\": state2.refreshing || !data && !state2.loadFailure ? \"true\" : \"false\",\n children: !data ? state2.loadFailure ? /* @__PURE__ */ u3(LoadFailure, { state: state2 }) : /* @__PURE__ */ u3(Empty, { children: \"Loading connectors…\" }) : filtered.length === 0 ? /* @__PURE__ */ u3(Empty, { children: query ? \"No connectors or tools match this filter.\" : \"No connectors are declared in this deployment.\" }) : filtered.map(({ connector, tools }) => /* @__PURE__ */ u3(\n ConnectorRow,\n {\n connector,\n tools,\n forceOpen: Boolean(query),\n state: state2\n },\n connector.id\n ))\n }\n )\n ] })\n ] });\n }\n\n // src/operator-ui/app/main.tsx\n function useOperatorState() {\n const [, bump] = y2((count) => count + 1, 0);\n const snapshot = getState();\n _2(() => {\n const unsubscribe = subscribe(() => bump(void 0));\n if (getState() !== snapshot) bump(void 0);\n return unsubscribe;\n }, []);\n return snapshot;\n }\n function visiblePages(state2) {\n const hint = state2.data ? null : navHint();\n return OPERATOR_PAGES.filter((page) => {\n if (page === \"activity\") return hint ? hint.activity : Boolean(state2.data?.activityEnabled);\n if (page === \"artifacts\") {\n return isArtifactPage(state2.page) || (hint ? hint.artifacts : Boolean(state2.data?.artifactsEnabled));\n }\n return true;\n });\n }\n function OperatorNav() {\n const state2 = useOperatorState();\n if (state2.session !== \"ready\") return null;\n const onArtifactPage = isArtifactPage(state2.page);\n return /* @__PURE__ */ u3(\"div\", { class: \"mast-actions\", children: [\n /* @__PURE__ */ u3(\"nav\", { class: \"page-nav\", \"aria-label\": \"Operator pages\", children: visiblePages(state2).map(\n (page) => (\n // Crossing between artifact pages and the rest is a full navigation:\n // with a dedicated artifact origin, the two live on different hosts.\n page === \"artifacts\" || onArtifactPage ? /* @__PURE__ */ u3(\n \"a\",\n {\n class: \"navlink\",\n href: page === \"artifacts\" ? PAGE_META.artifacts.path : new URL(PAGE_META[page].path, new URL(homeUrl, window.location.href)).href,\n ...state2.page === page ? { \"aria-current\": \"page\" } : {},\n children: PAGE_META[page].label\n },\n page\n ) : /* @__PURE__ */ u3(\n PageLink,\n {\n page,\n class: \"navlink\",\n current: state2.page === page,\n children: PAGE_META[page].label\n },\n page\n )\n )\n ) }),\n /* @__PURE__ */ u3(\"div\", { class: \"session-actions\", \"aria-label\": \"Session actions\", children: auth.kind === \"clerk\" || auth.kind === \"cloudflare-access\" ? /* @__PURE__ */ u3(\"button\", { class: \"navlink\", type: \"button\", onClick: signOut, children: \"Sign out\" }) : /* @__PURE__ */ u3(\"button\", { class: \"navlink\", type: \"button\", onClick: forgetBearer, children: \"Change token\" }) })\n ] });\n }\n function Gate({ state: state2 }) {\n const [token, setToken] = d2(\"\");\n const signedIn = auth.kind === \"clerk\" && Boolean(window.Clerk?.user);\n const loading = state2.session === \"loading\";\n return /* @__PURE__ */ u3(\"section\", { id: \"gate\", class: \"gate lead\", \"aria-busy\": loading ? \"true\" : \"false\", children: [\n /* @__PURE__ */ u3(\"h1\", { id: \"gateHeading\", tabIndex: -1, children: PAGE_META[state2.page].label }),\n /* @__PURE__ */ u3(\"div\", { class: \"lead-copy\", children: [\n /* @__PURE__ */ u3(\"p\", { children: pageDescription(state2.page, productDescription) }),\n loading ? /* @__PURE__ */ u3(StateBlock, { id: \"gateCopy\", children: checkingCopy(state2.page) }) : /* @__PURE__ */ u3(\"p\", { id: \"gateCopy\", class: \"meta\", children: gateCopy(auth.kind, signedIn) }),\n loading ? null : auth.kind === \"clerk\" ? /* @__PURE__ */ u3(\"div\", { id: \"clerkGate\", class: \"actions\", children: signedIn ? /* @__PURE__ */ u3(\"button\", { class: \"btn\", type: \"button\", onClick: signOut, children: \"Sign out\" }) : /* @__PURE__ */ u3(\"button\", { id: \"signin\", class: \"btn primary\", type: \"button\", onClick: signIn, children: \"Team sign in\" }) }) : auth.kind === \"cloudflare-access\" ? /* @__PURE__ */ u3(\"div\", { class: \"actions\", children: /* @__PURE__ */ u3(\"button\", { class: \"btn\", type: \"button\", onClick: signOut, children: \"Sign out of Cloudflare Access\" }) }) : /* @__PURE__ */ u3(\n \"form\",\n {\n id: \"tokenGate\",\n class: \"row gate-form\",\n onSubmit: (event) => {\n event.preventDefault();\n const value = token.trim();\n if (!value) return;\n setToken(\"\");\n signInWithBearer(value);\n },\n children: [\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: \"token\",\n type: \"password\",\n placeholder: \"Bearer token\",\n autocomplete: \"off\",\n \"aria-label\": \"Bearer token\",\n value: token,\n onInput: (event) => setToken(event.currentTarget.value)\n }\n ),\n /* @__PURE__ */ u3(\"button\", { id: \"save\", class: \"btn primary\", type: \"submit\", children: \"Open operator pages\" })\n ]\n }\n ),\n /* @__PURE__ */ u3(NoticeLine, { id: \"err\", notice: state2.gate, className: \"\" })\n ] })\n ] });\n }\n function CurrentPage({ state: state2 }) {\n if (state2.page === \"activity\") return /* @__PURE__ */ u3(ActivityPage, { state: state2 });\n if (state2.page === \"artifacts\") return /* @__PURE__ */ u3(ArtifactsPage, { state: state2 });\n if (state2.page === \"artifact\") return /* @__PURE__ */ u3(ArtifactPage, { state: state2 });\n return /* @__PURE__ */ u3(ConnectionsPage, { state: state2 });\n }\n function OperatorApp() {\n const state2 = useOperatorState();\n const ready = state2.session === \"ready\";\n h2(() => {\n const label = state2.page === \"artifact\" && state2.artifactView ? state2.artifactView.title : PAGE_META[state2.page].label;\n document.title = `${label} — ${titleSuffix}`;\n }, [state2.page, state2.artifactView]);\n h2(() => {\n if (!ready) return;\n if (state2.page === \"activity\" && state2.data?.activityEnabled && state2.activityPhase === \"idle\") {\n void loadActivity(true);\n }\n });\n h2(() => {\n if (!state2.pendingFocus && !state2.focusIfLost) return;\n if (state2.pendingFocus) {\n document.getElementById(state2.pendingFocus)?.focus();\n } else if (state2.focusIfLost) {\n const active = document.activeElement;\n const lost = !active || active === document.body || !active.isConnected || active.disabled === true;\n if (lost) document.getElementById(state2.focusIfLost)?.focus();\n }\n focusHandled();\n }, [state2.pendingFocus, state2.focusIfLost]);\n return ready ? /* @__PURE__ */ u3(\"div\", { id: \"app\", children: /* @__PURE__ */ u3(CurrentPage, { state: state2 }) }) : /* @__PURE__ */ u3(Gate, { state: state2 });\n }\n function mount(id, view) {\n const host = document.getElementById(id);\n if (!host) return;\n host.textContent = \"\";\n R(view, host);\n }\n mount(\"operatorNav\", /* @__PURE__ */ u3(OperatorNav, {}));\n mount(\"operatorContent\", /* @__PURE__ */ u3(OperatorApp, {}));\n void boot();\n})();\n"; +export const OPERATOR_UI_CSS: string = "/* src/operator-ui/tokens.css */\n:root {\n color-scheme: light;\n --accent: #2f5fe0;\n --radius: 10px;\n --sans:\n ui-sans-serif,\n system-ui,\n -apple-system,\n \"Segoe UI\",\n Roboto,\n \"Helvetica Neue\",\n Arial,\n sans-serif;\n --mono:\n ui-monospace,\n \"SF Mono\",\n Menlo,\n Monaco,\n \"Cascadia Code\",\n Consolas,\n monospace;\n --bg: #f6f7f9;\n --surface: #ffffff;\n --surface-2: #f1f3f6;\n --border: #e2e5ea;\n --border-strong: #cdd2da;\n --text: #131820;\n --muted: #5b6472;\n --ok: #12734a;\n --warn: #9a5b06;\n --danger: #bb3226;\n --on-accent: #ffffff;\n --link: var(--accent);\n --tint: color-mix(in srgb, var(--accent) 8%, var(--surface));\n --shell: 68rem;\n --pad: 1.25rem;\n --gap: 1rem;\n}\n@media (prefers-color-scheme: dark) {\n html:not([data-scheme=light]) {\n color-scheme: dark;\n }\n}\nhtml[data-scheme=dark] {\n color-scheme: dark;\n}\n@media (prefers-color-scheme: dark) {\n html:not([data-scheme=light]) {\n --bg: #0d1016;\n --surface: #151a21;\n --surface-2: #1c222c;\n --border: #262d39;\n --border-strong: #38414f;\n --text: #e6eaf1;\n --muted: #97a1b2;\n --ok: #4cc48c;\n --warn: #e2a33f;\n --danger: #f4776c;\n --link: color-mix(in srgb, var(--accent) 55%, #ffffff);\n --tint: color-mix(in srgb, var(--accent) 16%, var(--surface));\n }\n}\nhtml[data-scheme=dark] {\n --bg: #0d1016;\n --surface: #151a21;\n --surface-2: #1c222c;\n --border: #262d39;\n --border-strong: #38414f;\n --text: #e6eaf1;\n --muted: #97a1b2;\n --ok: #4cc48c;\n --warn: #e2a33f;\n --danger: #f4776c;\n --link: color-mix(in srgb, var(--accent) 55%, #ffffff);\n --tint: color-mix(in srgb, var(--accent) 16%, var(--surface));\n}\n\n/* src/operator-ui/page.css */\n* {\n box-sizing: border-box;\n}\nhtml {\n background: var(--bg);\n color: var(--text);\n font-family: var(--sans);\n font-size: 16px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n}\nbody {\n margin: 0;\n min-height: 100vh;\n}\n:is(h1, h2, h3, p, ul, ol) {\n margin: 0;\n padding: 0;\n}\n:is(h1, h2, h3) {\n font-weight: 600;\n line-height: 1.3;\n}\nh1 {\n font-size: 1.5rem;\n letter-spacing: -.01em;\n}\nh2 {\n font-size: 1rem;\n}\n:is(ul, ol) {\n list-style: none;\n}\na {\n color: var(--link);\n text-decoration-thickness: 1px;\n text-underline-offset: 2px;\n}\nbutton,\ninput {\n font: inherit;\n}\n:is(a, button, input, summary):focus-visible {\n border-radius: calc(var(--radius) / 2);\n outline: 2px solid var(--link);\n outline-offset: 2px;\n}\n.skip-link {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n left: var(--pad);\n padding: .5rem .75rem;\n position: fixed;\n top: -4rem;\n z-index: 10;\n}\n.skip-link:focus {\n top: var(--pad);\n}\n.shell {\n margin: 0 auto;\n max-width: var(--shell);\n padding-left: var(--pad);\n padding-right: var(--pad);\n}\n.cap,\n.meta {\n color: var(--muted);\n font-size: .875rem;\n}\n.mono {\n font-family: var(--mono);\n font-size: .8125rem;\n}\n.visually-hidden {\n clip-path: inset(50%);\n height: 1px;\n overflow: hidden;\n position: absolute;\n white-space: nowrap;\n width: 1px;\n}\n.masthead {\n align-items: center;\n background: var(--surface);\n border-bottom: 1px solid var(--border);\n display: flex;\n gap: var(--gap);\n justify-content: space-between;\n max-width: none;\n padding-bottom: .75rem;\n padding-top: .75rem;\n position: sticky;\n top: 0;\n z-index: 5;\n}\n.masthead-inner {\n align-items: center;\n display: flex;\n gap: var(--gap);\n justify-content: space-between;\n margin: 0 auto;\n max-width: var(--shell);\n width: 100%;\n}\n.mast-nav,\n.mast-actions,\n.page-nav,\n.session-actions {\n align-items: center;\n display: flex;\n flex-wrap: wrap;\n gap: .25rem;\n}\n.mast-nav {\n gap: var(--gap);\n}\n.mast-actions {\n gap: var(--gap);\n}\n.navlink {\n background: none;\n border: 0;\n border-radius: calc(var(--radius) - 2px);\n color: var(--muted);\n cursor: pointer;\n font-size: .9375rem;\n padding: .35rem .6rem;\n text-decoration: none;\n}\n.navlink:hover {\n background: var(--surface-2);\n color: var(--text);\n}\n.navlink[aria-current=page] {\n background: var(--tint);\n color: var(--link);\n font-weight: 500;\n}\n.brand,\n.product {\n padding-left: 0;\n padding-right: 0;\n text-decoration: none;\n}\n.brand {\n color: var(--text);\n font-weight: 600;\n}\n.brand:hover,\n.product:hover {\n background: none;\n}\n.product {\n color: var(--muted);\n}\n.page {\n padding-bottom: 4rem;\n padding-top: 2rem;\n}\n.lead {\n margin-bottom: 1.5rem;\n}\n.lead-copy {\n color: var(--muted);\n display: grid;\n gap: .5rem;\n margin-top: .35rem;\n}\n.lead-copy p {\n max-width: 60ch;\n}\n.btn {\n background: var(--surface);\n border: 1px solid var(--border-strong);\n border-radius: calc(var(--radius) - 2px);\n color: var(--text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n font-size: .875rem;\n gap: .35rem;\n padding: .35rem .7rem;\n text-decoration: none;\n white-space: nowrap;\n}\n.btn:hover {\n background: var(--surface-2);\n}\n.btn:disabled {\n cursor: not-allowed;\n opacity: .55;\n}\n.btn.primary {\n background: var(--accent);\n border-color: var(--accent);\n color: var(--on-accent);\n}\n.btn.primary:hover {\n background: color-mix(in srgb, var(--accent) 88%, black);\n}\n.btn.danger {\n color: var(--danger);\n}\n.btn.danger:hover {\n background: color-mix(in srgb, var(--danger) 10%, var(--surface));\n}\n.btn.quiet {\n background: none;\n border-color: var(--border);\n color: var(--muted);\n}\n.btn.quiet:hover {\n background: var(--surface-2);\n color: var(--text);\n}\n.badge {\n align-items: center;\n background: var(--surface-2);\n border-radius: 999px;\n color: var(--muted);\n display: inline-flex;\n font-size: .75rem;\n gap: .3rem;\n line-height: 1.6;\n padding: .1rem .5rem;\n white-space: nowrap;\n}\n.badge.ok {\n background: color-mix(in srgb, var(--ok) 12%, var(--surface));\n color: var(--ok);\n}\n.badge.warn {\n background: color-mix(in srgb, var(--warn) 14%, var(--surface));\n color: var(--warn);\n}\n.badge.danger {\n background: color-mix(in srgb, var(--danger) 12%, var(--surface));\n color: var(--danger);\n}\n.badge.accent {\n background: var(--tint);\n color: var(--link);\n}\n.msg {\n background: color-mix(in srgb, var(--danger) 8%, var(--surface));\n border: 1px solid color-mix(in srgb, var(--danger) 28%, var(--surface));\n border-radius: calc(var(--radius) - 2px);\n color: var(--danger);\n font-size: .875rem;\n padding: .5rem .75rem;\n}\n.status-page {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n display: grid;\n gap: .75rem;\n margin: 1.5rem auto 0;\n max-width: 34rem;\n padding: 1.5rem;\n}\n.status-head {\n align-items: center;\n display: flex;\n gap: .6rem;\n}\n.status-mark {\n align-items: center;\n background: var(--surface-2);\n border-radius: 50%;\n color: var(--muted);\n display: inline-flex;\n flex: none;\n height: 2rem;\n justify-content: center;\n width: 2rem;\n}\n.status-mark svg {\n height: 1.25rem;\n width: 1.25rem;\n}\n.status-mark.ok {\n background: color-mix(in srgb, var(--ok) 14%, var(--surface));\n color: var(--ok);\n}\n.status-mark.danger {\n background: color-mix(in srgb, var(--danger) 12%, var(--surface));\n color: var(--danger);\n}\n.status-label {\n color: var(--muted);\n font-size: .875rem;\n font-weight: 600;\n}\n.status-label.ok {\n color: var(--ok);\n}\n.status-label.danger {\n color: var(--danger);\n}\n.status-page h1 {\n overflow-wrap: anywhere;\n}\n.status-copy {\n color: var(--muted);\n}\n.status-actions {\n display: flex;\n flex-wrap: wrap;\n gap: .5rem;\n margin-top: .25rem;\n}\n.status-details {\n border-top: 1px solid var(--border);\n color: var(--muted);\n font-size: .875rem;\n margin-top: .25rem;\n padding-top: .75rem;\n}\n.status-details > summary {\n cursor: pointer;\n width: fit-content;\n}\n.status-details > p {\n margin-top: .5rem;\n}\n.status-details pre {\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: calc(var(--radius) / 2);\n color: var(--text);\n font-family: var(--mono);\n font-size: .75rem;\n line-height: 1.55;\n margin: .5rem 0 0;\n max-height: 18rem;\n overflow: auto;\n overflow-wrap: anywhere;\n padding: .5rem .6rem;\n white-space: pre-wrap;\n}\n@media (max-width: 40rem) {\n :root {\n --pad: 1rem;\n }\n .status-page {\n margin-top: 0;\n padding: 1.25rem var(--pad);\n }\n}\n\n/* src/operator-ui/browser.css */\n.masthead-inner {\n min-height: calc(.9375rem * 1.5 + .7rem);\n}\ninput:not([type=checkbox], [type=radio], [type=hidden]) {\n background: var(--surface);\n border: 1px solid var(--border-strong);\n border-radius: calc(var(--radius) - 2px);\n color: var(--text);\n min-height: 2.25rem;\n padding: .3rem .6rem;\n width: 100%;\n}\ninput::placeholder {\n color: var(--muted);\n}\n.row {\n align-items: center;\n display: flex;\n flex-wrap: wrap;\n gap: .5rem;\n}\n.row input:not([type=checkbox], [type=radio]) {\n flex: 1 1 14rem;\n width: auto;\n}\ninput:disabled {\n opacity: .6;\n}\n.check {\n align-items: center;\n cursor: pointer;\n display: inline-flex;\n gap: .4rem;\n}\n.check input {\n accent-color: var(--accent);\n margin: 0;\n}\n.actions {\n display: flex;\n flex-wrap: wrap;\n gap: .5rem;\n}\n.dot {\n border-radius: 50%;\n display: inline-block;\n flex: none;\n height: .5rem;\n width: .5rem;\n background: var(--muted);\n}\n.dot.ok,\n.dot.success,\n.dot.approved {\n background: var(--ok);\n}\n.dot.auth_required,\n.dot.warn,\n.dot.paused {\n background: var(--warn);\n}\n.dot.error,\n.dot.timeout {\n background: var(--danger);\n}\n.dot.loading {\n background: var(--border-strong);\n}\n.section {\n display: grid;\n gap: .75rem;\n margin-top: 1.75rem;\n}\n.section-head {\n align-items: baseline;\n display: flex;\n gap: var(--gap);\n justify-content: space-between;\n}\n.unavailable {\n background: var(--surface);\n border: 1px dashed var(--border-strong);\n border-radius: var(--radius);\n color: var(--muted);\n padding: 1.5rem var(--pad);\n}\n.state-block {\n align-items: center;\n background: var(--surface);\n border: 1px dashed var(--border-strong);\n border-radius: var(--radius);\n color: var(--muted);\n display: flex;\n flex-direction: column;\n gap: .5rem;\n padding: 1.75rem var(--pad);\n text-align: center;\n}\n.state-block.error {\n border-color: color-mix(in srgb, var(--danger) 35%, var(--border));\n border-style: solid;\n}\n.state-title {\n color: var(--text);\n font-weight: 600;\n}\n.state-copy {\n max-width: 52ch;\n}\n.state-block .btn {\n margin-top: .25rem;\n}\n.collection {\n display: grid;\n gap: .75rem;\n}\n.msg.warn {\n background: color-mix(in srgb, var(--warn) 9%, var(--surface));\n border-color: color-mix(in srgb, var(--warn) 32%, var(--surface));\n color: var(--text);\n}\n.error-notice {\n color: var(--danger);\n}\n.notice:empty {\n block-size: 0;\n margin: 0;\n}\n.summary {\n color: var(--muted);\n font-size: .875rem;\n}\n.summary .sep {\n opacity: .5;\n}\n.summary .warn {\n color: var(--warn);\n}\n.summary .danger {\n color: var(--danger);\n}\n.endpoint {\n align-items: center;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n display: flex;\n gap: .75rem;\n justify-content: space-between;\n padding: .5rem .5rem .5rem .75rem;\n}\n.endpoint code {\n flex: 1 1 auto;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.endpoints,\n.endpoint-block {\n display: grid;\n gap: .35rem;\n}\n.endpoints {\n gap: .6rem;\n}\n.endpoint-label {\n flex: none;\n}\n.setup-list {\n display: grid;\n gap: .6rem;\n margin-top: .5rem;\n}\n.setup-item {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: calc(var(--radius) - 2px);\n display: grid;\n gap: .35rem;\n padding: .5rem .6rem .6rem .75rem;\n}\n.setup-head {\n align-items: center;\n display: flex;\n gap: .5rem;\n justify-content: space-between;\n}\n.setup-code,\n.fix-prompt-text {\n background: var(--surface-2);\n border-radius: calc(var(--radius) / 2);\n color: var(--text);\n font-family: var(--mono);\n font-size: .75rem;\n line-height: 1.55;\n margin: 0;\n overflow-wrap: anywhere;\n padding: .5rem .6rem;\n white-space: pre-wrap;\n}\n.fix-prompt {\n align-items: baseline;\n display: flex;\n flex-wrap: wrap;\n gap: .35rem .75rem;\n}\n.fix-prompt > details {\n flex: 1 1 12rem;\n}\n.fix-prompt > details[open] {\n flex-basis: 100%;\n}\n.fix-prompt-preview > .meta {\n margin-top: .4rem;\n}\n.fix-prompt-preview .fix-prompt-text {\n border: 1px solid var(--border);\n margin-top: .4rem;\n max-height: 18rem;\n overflow: auto;\n}\n.rows {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n overflow: hidden;\n}\n.rows > * + * {\n border-top: 1px solid var(--border);\n}\n.conn.open > .conn-head {\n background: var(--surface-2);\n}\n.conn-head {\n align-items: center;\n display: flex;\n gap: .75rem;\n justify-content: space-between;\n padding: .7rem var(--pad);\n position: relative;\n}\n.conn-head:hover {\n background: var(--surface-2);\n}\n.conn-main {\n align-items: center;\n display: flex;\n gap: .6rem;\n min-width: 0;\n}\n.conn-name {\n font-size: 1rem;\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.conn-toggle {\n background: none;\n border: 0;\n color: var(--text);\n cursor: pointer;\n font: inherit;\n padding: 0;\n text-align: left;\n}\n.conn-toggle::after {\n content: \"\";\n inset: 0;\n position: absolute;\n}\n.conn-toggle:focus-visible {\n outline: none;\n}\n.conn-toggle:focus-visible::after {\n border-radius: calc(var(--radius) - 2px);\n outline: 2px solid var(--link);\n outline-offset: -2px;\n}\n.conn-id {\n color: var(--muted);\n}\n.conn-badges {\n align-items: center;\n display: flex;\n flex-wrap: wrap;\n gap: .35rem;\n justify-content: flex-end;\n}\n.conn-caret {\n border-right: 1.5px solid var(--muted);\n border-bottom: 1.5px solid var(--muted);\n flex: none;\n height: .4rem;\n rotate: -45deg;\n transform-origin: center;\n width: .4rem;\n}\n.conn.open .conn-caret {\n rotate: 45deg;\n}\n.conn-body {\n background: var(--surface);\n border-top: 1px solid var(--border);\n display: grid;\n gap: .75rem;\n padding: var(--pad);\n}\n.conn-body[hidden] {\n display: none;\n}\n.conn-note {\n color: var(--muted);\n max-width: 70ch;\n}\n.subcard {\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: calc(var(--radius) - 2px);\n display: grid;\n gap: .5rem;\n padding: .75rem;\n}\n.subcard-head {\n align-items: baseline;\n display: flex;\n flex-wrap: wrap;\n gap: .5rem;\n justify-content: space-between;\n}\n.subcard-head h3 {\n font-size: .9375rem;\n}\n.confirm {\n background: color-mix(in srgb, var(--danger) 6%, var(--surface));\n border: 1px solid color-mix(in srgb, var(--danger) 28%, var(--surface));\n border-radius: calc(var(--radius) - 2px);\n display: grid;\n gap: .5rem;\n padding: .6rem .75rem;\n}\n.confirm p {\n max-width: 70ch;\n}\ndetails > .subcard,\ndetails > .tool-list {\n margin-top: .5rem;\n}\n.tool-list {\n display: grid;\n gap: .4rem;\n max-height: 22rem;\n overflow: auto;\n}\n.filter {\n flex: 0 1 18rem;\n min-width: 10rem;\n width: auto;\n}\n.connector-drift {\n display: grid;\n gap: .15rem;\n}\n.tool {\n display: grid;\n gap: .1rem;\n border-left: 2px solid var(--border-strong);\n padding-left: .6rem;\n}\n.tool code {\n font-family: var(--mono);\n font-size: .8125rem;\n overflow-wrap: anywhere;\n}\n.tool-head {\n align-items: center;\n display: flex;\n flex-wrap: wrap;\n gap: .25rem .5rem;\n}\n.tool-legend {\n margin-bottom: .15rem;\n}\n.td {\n color: var(--muted);\n font-size: .8125rem;\n}\ndetails > summary {\n cursor: pointer;\n}\n.disclosure {\n color: var(--link);\n font-size: .875rem;\n list-style: none;\n}\n.disclosure::-webkit-details-marker {\n display: none;\n}\n.disclosure::before {\n content: \"▸ \";\n}\ndetails[open] > .disclosure::before {\n content: \"▾ \";\n}\n.drift-counts {\n display: grid;\n gap: .5rem;\n grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));\n margin-top: .5rem;\n}\n.drift-count {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: calc(var(--radius) - 2px);\n display: grid;\n gap: .1rem;\n padding: .4rem .6rem;\n}\n.drift-count.flagged {\n border-color: color-mix(in srgb, var(--warn) 45%, var(--surface));\n}\n.drift-count-value {\n font-size: 1.125rem;\n font-weight: 600;\n}\n.drift-count.flagged .drift-count-value {\n color: var(--warn);\n}\n.drift-count-label {\n color: var(--muted);\n font-size: .75rem;\n}\n.credential-fields,\n.credential-field-summary {\n display: grid;\n gap: .5rem;\n}\n.credential-field {\n display: grid;\n gap: .25rem;\n}\n.credential-field label {\n color: var(--muted);\n font-size: .8125rem;\n}\n.credential-field-summary > div {\n display: flex;\n gap: .5rem;\n justify-content: space-between;\n}\n.credential-form {\n display: grid;\n gap: .5rem;\n}\n.credential-form .actions {\n justify-content: flex-end;\n}\n.activity-list {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n overflow: hidden;\n}\n.activity-list > * + * {\n border-top: 1px solid var(--border);\n}\n.activity-item {\n align-items: center;\n display: grid;\n gap: .5rem var(--gap);\n grid-template-columns: minmax(0, 14rem) minmax(0, 1fr) auto;\n padding: .6rem var(--pad);\n}\n.activity-stamp {\n align-items: center;\n display: flex;\n gap: .6rem;\n min-width: 0;\n}\n.activity-time {\n display: block;\n font-size: .8125rem;\n}\n.activity-actor {\n color: var(--muted);\n font-size: .8125rem;\n}\n.activity-actor-id {\n color: var(--muted);\n}\n.activity-address {\n font-family: var(--mono);\n font-size: .8125rem;\n overflow-wrap: anywhere;\n}\n.activity-detail {\n color: var(--muted);\n font-size: .8125rem;\n}\n.activity-result {\n display: grid;\n gap: .15rem;\n justify-items: end;\n}\n.activity-more {\n justify-self: start;\n}\n.artifact-row {\n align-items: center;\n display: grid;\n gap: .25rem var(--gap);\n grid-template-columns: minmax(0, 1fr) auto;\n padding: .6rem var(--pad);\n}\n.artifact-row .artifact-title {\n font-weight: 600;\n overflow-wrap: anywhere;\n}\n.artifact-row .artifact-meta,\n.artifact-meta {\n color: var(--muted);\n font-size: .8125rem;\n}\n.artifact-badges {\n display: flex;\n flex-wrap: wrap;\n gap: .35rem;\n justify-content: flex-end;\n}\n.artifact-head {\n display: grid;\n gap: .35rem;\n margin-bottom: .75rem;\n}\n.navlink.inline {\n font-size: inherit;\n padding: 0 .2rem;\n}\n.artifact-banner {\n background: color-mix(in srgb, var(--warn) 12%, var(--surface));\n border: 1px solid color-mix(in srgb, var(--warn) 35%, var(--border));\n border-radius: var(--radius);\n color: var(--text);\n padding: .5rem .75rem;\n}\n.artifact-frame {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n display: block;\n height: calc(100vh - 14rem);\n min-height: 28rem;\n width: 100%;\n}\n.gate-form {\n max-width: 36rem;\n}\n@media (max-width: 40rem) {\n .masthead {\n position: static;\n }\n .masthead-inner {\n flex-wrap: wrap;\n row-gap: .35rem;\n }\n #operatorNav,\n #operatorNav .mast-actions {\n display: contents;\n }\n .session-actions {\n margin-left: auto;\n }\n .page-nav {\n flex: 1 0 100%;\n flex-wrap: nowrap;\n margin-left: -.6rem;\n order: 3;\n overflow-x: auto;\n }\n .conn-head {\n align-items: flex-start;\n flex-direction: column;\n gap: .4rem;\n }\n .conn-badges {\n justify-content: flex-start;\n }\n .endpoint {\n flex-wrap: wrap;\n }\n .section-head {\n flex-wrap: wrap;\n }\n .filter {\n flex: 1 1 100%;\n }\n .activity-item {\n grid-template-columns: minmax(0, 1fr);\n }\n .activity-result {\n justify-items: start;\n }\n .artifact-row {\n grid-template-columns: minmax(0, 1fr);\n }\n .artifact-badges {\n justify-content: flex-start;\n }\n}\n.token-create,\n.token-reveal {\n margin-bottom: 24px;\n}\n.token-create > label {\n display: block;\n margin-bottom: 8px;\n}\n.token-create input {\n flex: 1;\n min-width: 0;\n}\n.token-reveal {\n border: 1px solid var(--rule);\n padding: 20px;\n}\n.token-reveal-head,\n.token-card-head {\n display: flex;\n justify-content: space-between;\n gap: 16px;\n}\n.token-secret {\n overflow-wrap: anywhere;\n margin: 12px 0;\n}\n.token-card {\n border-top: 1px solid var(--rule);\n padding: 20px 0;\n}\n.token-card.revoked {\n color: var(--muted);\n}\n"; +export const OPERATOR_UI_SCRIPT: string = "\"use strict\";\n(() => {\n // node_modules/preact/dist/preact.module.js\n var n;\n var l;\n var u;\n var t;\n var i;\n var r;\n var o;\n var e;\n var f;\n var c;\n var a;\n var s;\n var h;\n var p;\n var v;\n var y;\n var d = {};\n var w = [];\n var _ = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;\n var g = Array.isArray;\n function m(n2, l3) {\n for (var u4 in l3) n2[u4] = l3[u4];\n return n2;\n }\n function b(n2) {\n n2 && n2.parentNode && n2.parentNode.removeChild(n2);\n }\n function k(l3, u4, t3) {\n var i3, r3, o3, e3 = {};\n for (o3 in u4) \"key\" == o3 ? i3 = u4[o3] : \"ref\" == o3 ? r3 = u4[o3] : e3[o3] = u4[o3];\n if (arguments.length > 2 && (e3.children = arguments.length > 3 ? n.call(arguments, 2) : t3), \"function\" == typeof l3 && null != l3.defaultProps) for (o3 in l3.defaultProps) void 0 === e3[o3] && (e3[o3] = l3.defaultProps[o3]);\n return x(l3, e3, i3, r3, null);\n }\n function x(n2, t3, i3, r3, o3) {\n var e3 = { type: n2, props: t3, key: i3, ref: r3, __k: null, __: null, __b: 0, __e: null, __c: null, constructor: void 0, __v: null == o3 ? ++u : o3, __i: -1, __u: 0 };\n return null == o3 && null != l.vnode && l.vnode(e3), e3;\n }\n function S(n2) {\n return n2.children;\n }\n function C(n2, l3) {\n this.props = n2, this.context = l3;\n }\n function $(n2, l3) {\n if (null == l3) return n2.__ ? $(n2.__, n2.__i + 1) : null;\n for (var u4; l3 < n2.__k.length; l3++) if (null != (u4 = n2.__k[l3]) && null != u4.__e) return u4.__e;\n return \"function\" == typeof n2.type ? $(n2) : null;\n }\n function I(n2) {\n if (n2.__P && n2.__d) {\n var u4 = n2.__v, t3 = u4.__e, i3 = [], r3 = [], o3 = m({}, u4);\n o3.__v = u4.__v + 1, l.vnode && l.vnode(o3), q(n2.__P, o3, u4, n2.__n, n2.__P.namespaceURI, 32 & u4.__u ? [t3] : null, i3, null == t3 ? $(u4) : t3, !!(32 & u4.__u), r3), o3.__v = u4.__v, o3.__.__k[o3.__i] = o3, D(i3, o3, r3), u4.__e = u4.__ = null, o3.__e != t3 && P(o3);\n }\n }\n function P(n2) {\n if (null != (n2 = n2.__) && null != n2.__c) return n2.__e = n2.__c.base = null, n2.__k.some(function(l3) {\n if (null != l3 && null != l3.__e) return n2.__e = n2.__c.base = l3.__e;\n }), P(n2);\n }\n function A(n2) {\n (!n2.__d && (n2.__d = true) && i.push(n2) && !H.__r++ || r != l.debounceRendering) && ((r = l.debounceRendering) || o)(H);\n }\n function H() {\n try {\n for (var n2, l3 = 1; i.length; ) i.length > l3 && i.sort(e), n2 = i.shift(), l3 = i.length, I(n2);\n } finally {\n i.length = H.__r = 0;\n }\n }\n function L(n2, l3, u4, t3, i3, r3, o3, e3, f4, c3, a3) {\n var s3, h3, p3, v3, y3, _3, g2 = t3 && t3.__k || w, m3 = l3.length;\n for (f4 = T(u4, l3, g2, f4, m3), s3 = 0; s3 < m3; s3++) null != (p3 = u4.__k[s3]) && (h3 = -1 != p3.__i && g2[p3.__i] || d, p3.__i = s3, _3 = q(n2, p3, h3, i3, r3, o3, e3, f4, c3, a3), v3 = p3.__e, p3.ref && h3.ref != p3.ref && (h3.ref && J(h3.ref, null, p3), a3.push(p3.ref, p3.__c || v3, p3)), null == y3 && null != v3 && (y3 = v3), 4 & p3.__u ? (f4 = j(p3, f4, n2), h3.__e && (h3.__e = null)) : \"function\" == typeof p3.type && void 0 !== _3 ? f4 = _3 : v3 && (f4 = v3.nextSibling), p3.__u &= -7);\n return u4.__e = y3, f4;\n }\n function T(n2, l3, u4, t3, i3) {\n var r3, o3, e3, f4, c3, a3 = u4.length, s3 = a3, h3 = 0;\n for (n2.__k = new Array(i3), r3 = 0; r3 < i3; r3++) null != (o3 = l3[r3]) && \"boolean\" != typeof o3 && \"function\" != typeof o3 ? (\"string\" == typeof o3 || \"number\" == typeof o3 || \"bigint\" == typeof o3 || o3.constructor == String ? o3 = n2.__k[r3] = x(null, o3, null, null, null) : g(o3) ? o3 = n2.__k[r3] = x(S, { children: o3 }, null, null, null) : void 0 === o3.constructor && o3.__b > 0 ? o3 = n2.__k[r3] = x(o3.type, o3.props, o3.key, o3.ref ? o3.ref : null, o3.__v) : n2.__k[r3] = o3, f4 = r3 + h3, o3.__ = n2, o3.__b = n2.__b + 1, e3 = null, -1 != (c3 = o3.__i = O(o3, u4, f4, s3)) && (s3--, (e3 = u4[c3]) && (e3.__u |= 2)), null == e3 || null == e3.__v ? (-1 == c3 && (i3 > a3 ? h3-- : i3 < a3 && h3++), \"function\" != typeof o3.type && (o3.__u |= 4)) : c3 != f4 && (c3 == f4 - 1 ? h3-- : c3 == f4 + 1 ? h3++ : (c3 > f4 ? h3-- : h3++, o3.__u |= 4))) : n2.__k[r3] = null;\n if (s3) for (r3 = 0; r3 < a3; r3++) null != (e3 = u4[r3]) && 0 == (2 & e3.__u) && (e3.__e == t3 && (t3 = $(e3)), K(e3, e3));\n return t3;\n }\n function j(n2, l3, u4) {\n var t3, i3;\n if (\"function\" == typeof n2.type) {\n for (t3 = n2.__k, i3 = 0; t3 && i3 < t3.length; i3++) t3[i3] && (t3[i3].__ = n2, l3 = j(t3[i3], l3, u4));\n return l3;\n }\n n2.__e != l3 && (l3 && n2.type && !l3.parentNode && (l3 = $(n2)), l3 = u4.insertBefore(n2.__e, l3 || null));\n do {\n l3 = l3 && l3.nextSibling;\n } while (null != l3 && 8 == l3.nodeType);\n return l3;\n }\n function O(n2, l3, u4, t3) {\n var i3, r3, o3, e3 = n2.key, f4 = n2.type, c3 = l3[u4], a3 = null != c3 && 0 == (2 & c3.__u);\n if (null === c3 && null == e3 || a3 && e3 == c3.key && f4 == c3.type) return u4;\n if (t3 > (a3 ? 1 : 0)) {\n for (i3 = u4 - 1, r3 = u4 + 1; i3 >= 0 || r3 < l3.length; ) if (null != (c3 = l3[o3 = i3 >= 0 ? i3-- : r3++]) && 0 == (2 & c3.__u) && e3 == c3.key && f4 == c3.type) return o3;\n }\n return -1;\n }\n function z(n2, l3, u4) {\n \"-\" == l3[0] ? n2.setProperty(l3, null == u4 ? \"\" : u4) : n2[l3] = null == u4 ? \"\" : \"number\" != typeof u4 || _.test(l3) ? u4 : u4 + \"px\";\n }\n function N(n2, l3, u4, t3, i3) {\n var r3, o3;\n n: if (\"style\" == l3) if (\"string\" == typeof u4) n2.style.cssText = u4;\n else {\n if (\"string\" == typeof t3 && (n2.style.cssText = t3 = \"\"), t3) for (l3 in t3) u4 && l3 in u4 || z(n2.style, l3, \"\");\n if (u4) for (l3 in u4) t3 && u4[l3] == t3[l3] || z(n2.style, l3, u4[l3]);\n }\n else if (\"o\" == l3[0] && \"n\" == l3[1]) r3 = l3 != (l3 = l3.replace(s, \"$1\")), o3 = l3.toLowerCase(), l3 = o3 in n2 || \"onFocusOut\" == l3 || \"onFocusIn\" == l3 ? o3.slice(2) : l3.slice(2), n2.l || (n2.l = {}), n2.l[l3 + r3] = u4, u4 ? t3 ? u4[a] = t3[a] : (u4[a] = h, n2.addEventListener(l3, r3 ? v : p, r3)) : n2.removeEventListener(l3, r3 ? v : p, r3);\n else {\n if (\"http://www.w3.org/2000/svg\" == i3) l3 = l3.replace(/xlink(H|:h)/, \"h\").replace(/sName$/, \"s\");\n else if (\"width\" != l3 && \"height\" != l3 && \"href\" != l3 && \"list\" != l3 && \"form\" != l3 && \"tabIndex\" != l3 && \"download\" != l3 && \"rowSpan\" != l3 && \"colSpan\" != l3 && \"role\" != l3 && \"popover\" != l3 && l3 in n2) try {\n n2[l3] = null == u4 ? \"\" : u4;\n break n;\n } catch (n3) {\n }\n \"function\" == typeof u4 || (null == u4 || false === u4 && \"-\" != l3[4] ? n2.removeAttribute(l3) : n2.setAttribute(l3, \"popover\" == l3 && 1 == u4 ? \"\" : u4));\n }\n }\n function V(n2) {\n return function(u4) {\n if (this.l) {\n var t3 = this.l[u4.type + n2];\n if (null == u4[c]) u4[c] = h++;\n else if (u4[c] < t3[a]) return;\n return t3(l.event ? l.event(u4) : u4);\n }\n };\n }\n function q(n2, u4, t3, i3, r3, o3, e3, f4, c3, a3) {\n var s3, h3, p3, v3, y3, d3, _3, k3, x2, M, I2, P2, A3, H2, T3, j3, F = u4.type;\n if (void 0 !== u4.constructor) return null;\n 128 & t3.__u && (c3 = !!(32 & t3.__u), o3 = [f4 = u4.__e = t3.__e]), (s3 = l.__b) && s3(u4);\n n: if (\"function\" == typeof F) {\n h3 = e3.length;\n try {\n if (x2 = u4.props, M = F.prototype && F.prototype.render, I2 = (s3 = F.contextType) && i3[s3.__c], P2 = s3 ? I2 ? I2.props.value : s3.__ : i3, t3.__c ? k3 = (p3 = u4.__c = t3.__c).__ = p3.__E : (M ? u4.__c = p3 = new F(x2, P2) : (u4.__c = p3 = new C(x2, P2), p3.constructor = F, p3.render = Q), I2 && I2.sub(p3), p3.state || (p3.state = {}), p3.__n = i3, v3 = p3.__d = true, p3.__h = [], p3._sb = []), M && null == p3.__s && (p3.__s = p3.state), M && null != F.getDerivedStateFromProps && (p3.__s == p3.state && (p3.__s = m({}, p3.__s)), m(p3.__s, F.getDerivedStateFromProps(x2, p3.__s))), y3 = p3.props, d3 = p3.state, p3.__v = u4, v3) M && null == F.getDerivedStateFromProps && null != p3.componentWillMount && p3.componentWillMount(), M && null != p3.componentDidMount && p3.__h.push(p3.componentDidMount);\n else {\n if (M && null == F.getDerivedStateFromProps && x2 !== y3 && null != p3.componentWillReceiveProps && p3.componentWillReceiveProps(x2, P2), u4.__v == t3.__v || !p3.__e && null != p3.shouldComponentUpdate && false === p3.shouldComponentUpdate(x2, p3.__s, P2)) {\n u4.__v != t3.__v && (p3.props = x2, p3.state = p3.__s, p3.__d = false), u4.__e = t3.__e, u4.__k = t3.__k, u4.__k.some(function(n3) {\n n3 && (n3.__ = u4);\n }), w.push.apply(p3.__h, p3._sb), p3._sb = [], p3.__h.length && e3.push(p3), f4 = $(t3);\n break n;\n }\n null != p3.componentWillUpdate && p3.componentWillUpdate(x2, p3.__s, P2), M && null != p3.componentDidUpdate && p3.__h.push(function() {\n p3.componentDidUpdate(y3, d3, _3);\n });\n }\n if (p3.context = P2, p3.props = x2, p3.__P = n2, p3.__e = false, A3 = l.__r, H2 = 0, M) p3.state = p3.__s, p3.__d = false, A3 && A3(u4), s3 = p3.render(p3.props, p3.state, p3.context), w.push.apply(p3.__h, p3._sb), p3._sb = [];\n else do {\n p3.__d = false, A3 && A3(u4), s3 = p3.render(p3.props, p3.state, p3.context), p3.state = p3.__s;\n } while (p3.__d && ++H2 < 25);\n p3.state = p3.__s, null != p3.getChildContext && (i3 = m(m({}, i3), p3.getChildContext())), M && !v3 && null != p3.getSnapshotBeforeUpdate && (_3 = p3.getSnapshotBeforeUpdate(y3, d3)), T3 = null != s3 && s3.type === S && null == s3.key ? E(s3.props.children) : s3, f4 = L(n2, g(T3) ? T3 : [T3], u4, t3, i3, r3, o3, e3, f4, c3, a3), p3.base = u4.__e, u4.__u &= -161, p3.__h.length && e3.push(p3), k3 && (p3.__E = p3.__ = null);\n } catch (n3) {\n if (e3.length = h3, u4.__v = null, c3 || null != o3) {\n if (n3.then) {\n for (u4.__u |= c3 ? 160 : 128; f4 && 8 == f4.nodeType && f4.nextSibling; ) f4 = f4.nextSibling;\n null != o3 && (o3[o3.indexOf(f4)] = null), u4.__e = f4;\n } else if (null != o3) for (j3 = o3.length; j3--; ) b(o3[j3]);\n } else u4.__e = t3.__e;\n null == u4.__k && (u4.__k = t3.__k || []), n3.then || B(u4), l.__e(n3, u4, t3);\n }\n } else null == o3 && u4.__v == t3.__v ? (u4.__k = t3.__k, u4.__e = t3.__e) : f4 = u4.__e = G(t3.__e, u4, t3, i3, r3, o3, e3, c3, a3);\n return (s3 = l.diffed) && s3(u4), 128 & u4.__u ? void 0 : f4;\n }\n function B(n2) {\n n2 && (n2.__c && (n2.__c.__e = true), n2.__k && n2.__k.some(B));\n }\n function D(n2, u4, t3) {\n for (var i3 = 0; i3 < t3.length; i3++) J(t3[i3], t3[++i3], t3[++i3]);\n l.__c && l.__c(u4, n2), n2.some(function(u5) {\n try {\n n2 = u5.__h, u5.__h = [], n2.some(function(n3) {\n n3.call(u5);\n });\n } catch (n3) {\n l.__e(n3, u5.__v);\n }\n });\n }\n function E(n2) {\n return \"object\" != typeof n2 || null == n2 || n2.__b > 0 ? n2 : g(n2) ? n2.map(E) : void 0 !== n2.constructor ? null : m({}, n2);\n }\n function G(u4, t3, i3, r3, o3, e3, f4, c3, a3) {\n var s3, h3, p3, v3, y3, w3, _3, m3 = i3.props || d, k3 = t3.props, x2 = t3.type;\n if (\"svg\" == x2 ? o3 = \"http://www.w3.org/2000/svg\" : \"math\" == x2 ? o3 = \"http://www.w3.org/1998/Math/MathML\" : o3 || (o3 = \"http://www.w3.org/1999/xhtml\"), null != e3) {\n for (s3 = 0; s3 < e3.length; s3++) if ((y3 = e3[s3]) && \"setAttribute\" in y3 == !!x2 && (x2 ? y3.localName == x2 : 3 == y3.nodeType)) {\n u4 = y3, e3[s3] = null;\n break;\n }\n }\n if (null == u4) {\n if (null == x2) return document.createTextNode(k3);\n u4 = document.createElementNS(o3, x2, k3.is && k3), c3 && (l.__m && l.__m(t3, e3), c3 = false), e3 = null;\n }\n if (null == x2) m3 === k3 || c3 && u4.data == k3 || (u4.data = k3);\n else {\n if (e3 = \"textarea\" == x2 && null != k3.defaultValue ? null : e3 && n.call(u4.childNodes), !c3 && null != e3) for (m3 = {}, s3 = 0; s3 < u4.attributes.length; s3++) m3[(y3 = u4.attributes[s3]).name] = y3.value;\n for (s3 in m3) y3 = m3[s3], \"dangerouslySetInnerHTML\" == s3 ? p3 = y3 : \"children\" == s3 || s3 in k3 || \"value\" == s3 && \"defaultValue\" in k3 || \"checked\" == s3 && \"defaultChecked\" in k3 || N(u4, s3, null, y3, o3);\n for (s3 in k3) y3 = k3[s3], \"children\" == s3 ? v3 = y3 : \"dangerouslySetInnerHTML\" == s3 ? h3 = y3 : \"value\" == s3 ? w3 = y3 : \"checked\" == s3 ? _3 = y3 : c3 && \"function\" != typeof y3 || m3[s3] === y3 || N(u4, s3, y3, m3[s3], o3);\n if (h3) c3 || p3 && (h3.__html == p3.__html || h3.__html == u4.innerHTML) || (u4.innerHTML = h3.__html), t3.__k = [];\n else if (p3 && (u4.innerHTML = \"\"), L(\"template\" == t3.type ? u4.content : u4, g(v3) ? v3 : [v3], t3, i3, r3, \"foreignObject\" == x2 ? \"http://www.w3.org/1999/xhtml\" : o3, e3, f4, e3 ? e3[0] : i3.__k && $(i3, 0), c3, a3), null != e3) for (s3 = e3.length; s3--; ) b(e3[s3]);\n c3 && \"textarea\" != x2 || (s3 = \"value\", \"progress\" == x2 && null == w3 ? u4.removeAttribute(\"value\") : null != w3 && (w3 !== u4[s3] || \"progress\" == x2 && !w3 || \"option\" == x2 && w3 != m3[s3]) && N(u4, s3, w3, m3[s3], o3), s3 = \"checked\", null != _3 && _3 != u4[s3] && N(u4, s3, _3, m3[s3], o3));\n }\n return u4;\n }\n function J(n2, u4, t3) {\n try {\n if (\"function\" == typeof n2) {\n var i3 = \"function\" == typeof n2.__u;\n i3 && n2.__u(), i3 && null == u4 || (n2.__u = n2(u4));\n } else n2.current = u4;\n } catch (n3) {\n l.__e(n3, t3);\n }\n }\n function K(n2, u4, t3) {\n var i3, r3;\n if (l.unmount && l.unmount(n2), (i3 = n2.ref) && (i3.current && i3.current != n2.__e || J(i3, null, u4)), null != (i3 = n2.__c)) {\n if (i3.componentWillUnmount) try {\n i3.componentWillUnmount();\n } catch (n3) {\n l.__e(n3, u4);\n }\n i3.base = i3.__P = i3.__n = null;\n }\n if (i3 = n2.__k) for (r3 = 0; r3 < i3.length; r3++) i3[r3] && K(i3[r3], u4, t3 || \"function\" != typeof n2.type);\n t3 || b(n2.__e), n2.__c = n2.__ = n2.__e = void 0;\n }\n function Q(n2, l3, u4) {\n return this.constructor(n2, u4);\n }\n function R(u4, t3, i3) {\n var r3, o3, e3, f4;\n t3 == document && (t3 = document.documentElement), l.__ && l.__(u4, t3), o3 = (r3 = \"function\" == typeof i3) ? null : i3 && i3.__k || t3.__k, e3 = [], f4 = [], q(t3, u4 = (!r3 && i3 || t3).__k = k(S, null, [u4]), o3 || d, d, t3.namespaceURI, !r3 && i3 ? [i3] : o3 ? null : t3.firstChild ? n.call(t3.childNodes) : null, e3, !r3 && i3 ? i3 : o3 ? o3.__e : t3.firstChild, r3, f4), D(e3, u4, f4), u4.props.children = null;\n }\n n = w.slice, l = { __e: function(n2, l3, u4, t3) {\n for (var i3, r3, o3; l3 = l3.__; ) if ((i3 = l3.__c) && !i3.__) try {\n if ((r3 = i3.constructor) && null != r3.getDerivedStateFromError && (i3.setState(r3.getDerivedStateFromError(n2)), o3 = i3.__d), null != i3.componentDidCatch && (i3.componentDidCatch(n2, t3 || {}), o3 = i3.__d), o3) return i3.__E = i3;\n } catch (l4) {\n n2 = l4;\n }\n throw n2;\n } }, u = 0, t = function(n2) {\n return null != n2 && void 0 === n2.constructor;\n }, C.prototype.setState = function(n2, l3) {\n var u4;\n u4 = null != this.__s && this.__s != this.state ? this.__s : this.__s = m({}, this.state), \"function\" == typeof n2 && (n2 = n2(m({}, u4), this.props)), n2 && m(u4, n2), null != n2 && this.__v && (l3 && this._sb.push(l3), A(this));\n }, C.prototype.forceUpdate = function(n2) {\n this.__v && (this.__e = true, n2 && this.__h.push(n2), A(this));\n }, C.prototype.render = S, i = [], o = \"function\" == typeof Promise ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout, e = function(n2, l3) {\n return n2.__v.__b - l3.__v.__b;\n }, H.__r = 0, f = Math.random().toString(8), c = \"__d\" + f, a = \"__a\" + f, s = /(PointerCapture)$|Capture$/i, h = 0, p = V(false), v = V(true), y = 0;\n\n // node_modules/preact/hooks/dist/hooks.module.js\n var t2;\n var r2;\n var u2;\n var i2;\n var o2 = 0;\n var f2 = [];\n var c2 = l;\n var e2 = c2.__b;\n var a2 = c2.__r;\n var v2 = c2.diffed;\n var l2 = c2.__c;\n var m2 = c2.unmount;\n var p2 = c2.__;\n function s2(n2, t3) {\n c2.__h && c2.__h(r2, n2, o2 || t3), o2 = 0;\n var u4 = r2.__H || (r2.__H = { __: [], __h: [] });\n return n2 >= u4.__.length && u4.__.push({}), u4.__[n2];\n }\n function d2(n2) {\n return o2 = 1, y2(D2, n2);\n }\n function y2(n2, u4, i3) {\n var o3 = s2(t2++, 2);\n if (o3.t = n2, !o3.__c && (o3.__ = [i3 ? i3(u4) : D2(void 0, u4), function(n3) {\n var t3 = o3.__N ? o3.__N[0] : o3.__[0], r3 = o3.t(t3, n3);\n t3 !== r3 && (o3.__N = [r3, o3.__[1]], o3.__c.setState({}));\n }], o3.__c = r2, !r2.__f)) {\n var f4 = function(n3, t3, r3) {\n if (!o3.__c.__H) return true;\n var u5 = false, i4 = o3.__c.props !== n3;\n if (o3.__c.__H.__.some(function(n4) {\n if (n4.__N) {\n u5 = true;\n var t4 = n4.__[0];\n n4.__ = n4.__N, n4.__N = void 0, t4 !== n4.__[0] && (i4 = true);\n }\n }), c3) {\n var f5 = c3.call(this, n3, t3, r3);\n return u5 ? f5 || i4 : f5;\n }\n return !u5 || i4;\n };\n r2.__f = true;\n var c3 = r2.shouldComponentUpdate, e3 = r2.componentWillUpdate;\n r2.componentWillUpdate = function(n3, t3, r3) {\n if (this.__e) {\n var u5 = c3;\n c3 = void 0, f4(n3, t3, r3), c3 = u5;\n }\n e3 && e3.call(this, n3, t3, r3);\n }, r2.shouldComponentUpdate = f4;\n }\n return o3.__N || o3.__;\n }\n function h2(n2, u4) {\n var i3 = s2(t2++, 3);\n !c2.__s && C2(i3.__H, u4) && (i3.__ = n2, i3.u = u4, r2.__H.__h.push(i3));\n }\n function _2(n2, u4) {\n var i3 = s2(t2++, 4);\n !c2.__s && C2(i3.__H, u4) && (i3.__ = n2, i3.u = u4, r2.__h.push(i3));\n }\n function A2(n2) {\n return o2 = 5, T2(function() {\n return { current: n2 };\n }, []);\n }\n function T2(n2, r3) {\n var u4 = s2(t2++, 7);\n return C2(u4.__H, r3) && (u4.__ = n2(), u4.__H = r3, u4.__h = n2), u4.__;\n }\n function j2() {\n for (var n2; n2 = f2.shift(); ) {\n var t3 = n2.__H;\n if (n2.__P && t3) try {\n t3.__h.some(z2), t3.__h.some(B2), t3.__h = [];\n } catch (r3) {\n t3.__h = [], c2.__e(r3, n2.__v);\n }\n }\n }\n c2.__b = function(n2) {\n r2 = null, e2 && e2(n2);\n }, c2.__ = function(n2, t3) {\n n2 && t3.__k && t3.__k.__m && (n2.__m = t3.__k.__m), p2 && p2(n2, t3);\n }, c2.__r = function(n2) {\n a2 && a2(n2), t2 = 0;\n var i3 = (r2 = n2.__c).__H;\n i3 && (u2 === r2 ? (i3.__h = [], r2.__h = [], i3.__.some(function(n3) {\n n3.__N && (n3.__ = n3.__N), n3.u = n3.__N = void 0;\n })) : (i3.__h.some(z2), i3.__h.some(B2), i3.__h = [], t2 = 0)), u2 = r2;\n }, c2.diffed = function(n2) {\n v2 && v2(n2);\n var t3 = n2.__c;\n t3 && t3.__H && (t3.__H.__h.length && (1 !== f2.push(t3) && i2 === c2.requestAnimationFrame || ((i2 = c2.requestAnimationFrame) || w2)(j2)), t3.__H.__.some(function(n3) {\n n3.u && (n3.__H = n3.u, n3.u = void 0);\n })), u2 = r2 = null;\n }, c2.__c = function(n2, t3) {\n t3.some(function(n3) {\n try {\n n3.__h.some(z2), n3.__h = n3.__h.filter(function(n4) {\n return !n4.__ || B2(n4);\n });\n } catch (r3) {\n t3.some(function(n4) {\n n4.__h && (n4.__h = []);\n }), t3 = [], c2.__e(r3, n3.__v);\n }\n }), l2 && l2(n2, t3);\n }, c2.unmount = function(n2) {\n m2 && m2(n2);\n var t3, r3 = n2.__c;\n r3 && r3.__H && (r3.__H.__.some(function(n3) {\n try {\n z2(n3);\n } catch (n4) {\n t3 = n4;\n }\n }), r3.__H = void 0, t3 && c2.__e(t3, r3.__v));\n };\n var k2 = \"function\" == typeof requestAnimationFrame;\n function w2(n2) {\n var t3, r3 = function() {\n clearTimeout(u4), k2 && cancelAnimationFrame(t3), setTimeout(n2);\n }, u4 = setTimeout(r3, 35);\n k2 && (t3 = requestAnimationFrame(r3));\n }\n function z2(n2) {\n var t3 = r2, u4 = n2.__c;\n \"function\" == typeof u4 && (n2.__c = void 0, u4()), r2 = t3;\n }\n function B2(n2) {\n var t3 = r2;\n n2.__c = n2.__(), r2 = t3;\n }\n function C2(n2, t3) {\n return !n2 || n2.length !== t3.length || t3.some(function(t4, r3) {\n return t4 !== n2[r3];\n });\n }\n function D2(n2, t3) {\n return \"function\" == typeof t3 ? t3(n2) : t3;\n }\n\n // src/operator-ui/view.ts\n var OPERATOR_PAGES = [\n \"connections\",\n \"tokens\",\n \"activity\",\n \"artifacts\"\n ];\n var PAGE_META = {\n tokens: { path: \"/tokens\", label: \"Access tokens\" },\n connections: { path: \"/\", label: \"Connections\" },\n activity: { path: \"/activity\", label: \"Activity\" },\n artifacts: { path: \"/artifacts\", label: \"Artifacts\" },\n artifact: { path: \"/artifacts\", label: \"Artifact\" }\n };\n function pageDescription(page, productDescription2) {\n if (page === \"activity\") {\n return \"Every connector tool call, by who made it and how it ended. Arguments and results are never stored.\";\n }\n if (isArtifactPage(page)) {\n return \"Pages agents published for the team. Each one keeps every version.\";\n }\n return productDescription2;\n }\n function checkingCopy(page) {\n if (page === \"artifact\") return \"Loading artifact…\";\n if (page === \"artifacts\") return \"Loading artifacts…\";\n return \"Checking your session…\";\n }\n function pageForPath(path) {\n if (path.startsWith(\"/artifacts/\")) return \"artifact\";\n const match = OPERATOR_PAGES.find((page) => PAGE_META[page].path === path);\n return match ?? \"connections\";\n }\n function isArtifactPage(page) {\n return page === \"artifacts\" || page === \"artifact\";\n }\n function artifactViewRequest(pathname, search) {\n const match = /^\\/artifacts\\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)(?:\\/v\\/(\\d{1,9}))?$/.exec(pathname);\n if (!match?.[1]) return void 0;\n const params = new URLSearchParams();\n if (match[2]) {\n params.set(\"v\", match[2]);\n for (const pin of new URLSearchParams(search).getAll(\"d\")) params.append(\"d\", pin);\n }\n const query = params.toString();\n return `/artifacts/_api/view/${match[1]}${query ? `?${query}` : \"\"}`;\n }\n function info(message) {\n return { message, tone: \"info\" };\n }\n function failure(message, fix) {\n return fix ? { message, tone: \"error\", fix } : { message, tone: \"error\" };\n }\n var REFUSED_COPY = {\n oauth_disconnect: \"Disconnect didn't finish. If the service refused it, the deployment's log has its reply.\",\n oauth_reconnect: \"Authorization couldn't start. If the service refused it, the deployment's log has its reply.\",\n credential_test: \"The credential test couldn't run.\"\n };\n function oauthDoneNotice(action, answer, opened = true) {\n if (action === \"oauth_disconnect\") {\n return info(\"Disconnected. Connect again whenever you are ready.\");\n }\n if (answer?.state === \"ok\") return info(\"Connected.\");\n return info(\n opened ? \"Finish authorizing in the new tab. This page updates when you come back.\" : \"Your browser blocked the new tab. Open the authorization page from the link here.\"\n );\n }\n function credentialTestNotice(connectorId, answer) {\n return answer?.ok === true ? info(\"Credential is valid.\") : failure(\n \"Credential test failed: the service rejected the stored credential, or the test couldn't reach it. The deployment's log has the service's reply.\",\n { kind: \"credential_test_failed\", connectorId }\n );\n }\n function refusedNotice(action, connectorId, problem) {\n if (action === \"credential_test\" && (problem === \"credential_required\" || problem === \"credential_mismatch\")) {\n return failure(PROBLEM_COPY[problem], { kind: problem, connectorId });\n }\n return failure(REFUSED_COPY[action], {\n kind: action === \"credential_test\" ? \"credential_test_failed\" : \"oauth_action_failed\",\n connectorId\n });\n }\n function credentialRefusedCopy(action, status, problem) {\n if (problem === \"credential_mismatch\") return PROBLEM_COPY.credential_mismatch;\n const verb = action === \"credential_save\" ? \"saved\" : \"removed\";\n if (status === 400 || status === 413 || status === 415) {\n return action === \"credential_save\" ? \"The credential wasn't saved: a value is empty or not in the expected format. Check it and save again.\" : \"The credential wasn't removed. Refresh the page and try again.\";\n }\n if (status === 404) {\n return \"This connector no longer has a credential slot. Refresh the page to see its current setup.\";\n }\n if (status === 503) {\n return `Credential storage isn't configured on this deployment, so nothing was ${verb}.`;\n }\n return `The credential wasn't ${verb}. Try again; if it keeps failing, the deployment's log has the reason.`;\n }\n function actionFailedNotice(action, connectorId, facts, productName2) {\n if (facts.kind === \"session\") {\n return failure(\"Your session has ended. Sign in again, then retry.\");\n }\n if (facts.kind === \"forbidden\") {\n return failure(\"You don't have permission to change this connection's authentication.\");\n }\n if (facts.kind === \"network\") {\n return failure(`Couldn't reach ${productName2}. Check your connection and try again.`);\n }\n if (action === \"credential_save\" || action === \"credential_remove\") {\n return failure(credentialRefusedCopy(action, facts.status, facts.problem));\n }\n return refusedNotice(action, connectorId, facts.problem);\n }\n function connectorLoadFailureCopy(failure2, productName2) {\n return failure2 === \"session\" ? \"Your session wasn't accepted while loading this connector. Sign in again to see its status.\" : `Couldn't reach ${productName2} to load this connector. Check your connection, then refresh it.`;\n }\n function loadFailureCopy(failure2, productName2) {\n return {\n title: `Couldn't reach ${productName2}`,\n body: failure2 === \"network\" ? \"Your browser couldn't connect. Check your connection, then retry.\" : \"The deployment answered with an error. Retry in a moment; if it keeps failing, the deployment's log has the reason.\"\n };\n }\n function collectionFailureCopy(collection, facts, productName2) {\n if (facts.kind === \"network\") {\n return `Couldn't reach ${productName2}. Check your connection, then retry.`;\n }\n if (facts.kind === \"session\") return \"Your session has ended. Sign in again to see this page.\";\n if (facts.kind === \"forbidden\" || facts.status === 404) {\n if (collection === \"artifact\") return \"There is no artifact here, or this identity can't open it.\";\n return collection === \"activity\" ? \"Activity isn't available to this identity.\" : \"Artifacts aren't available to this identity.\";\n }\n return \"The deployment answered with an error. Retry in a moment; if it keeps failing, the deployment's log has the reason.\";\n }\n function confirmCopy(action, name) {\n if (action === \"oauth_disconnect\") {\n return {\n question: `Disconnect ${name}? Its stored grant and any pending authorization are removed, and its tools stop working until it is connected again.`,\n confirm: \"Disconnect\"\n };\n }\n if (action === \"oauth_restart\") {\n return {\n question: `Reconnect ${name}? Its current grant stops working until you finish authorizing in the new tab.`,\n confirm: \"Reconnect\"\n };\n }\n return {\n question: `Remove ${name}'s credential? The connector stops authenticating until a replacement is added.`,\n confirm: \"Remove\"\n };\n }\n function accessTokenUnavailableCopy(capability) {\n return capability === void 0 ? \"Access tokens are not configured for this deployment.\" : \"Token management requires an interactive sign-in and explicit permission.\";\n }\n function initialState(page) {\n return {\n page,\n generation: 0,\n session: \"loading\",\n gate: null,\n refreshing: false,\n loadFailure: null,\n pendingFocus: null,\n focusIfLost: null,\n ...identityScopedState()\n };\n }\n function identityScopedState() {\n return {\n data: null,\n connectorFilter: \"\",\n oauthNotice: null,\n oauthNoticeFor: null,\n oauthBusy: null,\n oauthBlocked: null,\n confirming: null,\n connectorFailures: {},\n credentialNotice: null,\n credentialNoticeFor: null,\n credentialEditing: null,\n credentialBusy: null,\n tokenPhase: \"idle\",\n tokenNotice: null,\n tokens: [],\n createdToken: null,\n tokenRenaming: null,\n tokenBusy: false,\n activityPhase: \"idle\",\n activityNotice: null,\n activityEvents: [],\n activityCursor: null,\n activitySearch: \"\",\n artifactPhase: \"idle\",\n artifactNotice: null,\n artifactRows: [],\n artifactCursor: null,\n artifactQuery: \"\",\n artifactArchived: false,\n artifactView: null\n };\n }\n function resetIdentity(state2, gate2 = null) {\n return {\n ...state2,\n generation: state2.generation + 1,\n session: \"gated\",\n gate: gate2,\n refreshing: false,\n loadFailure: null,\n pendingFocus: null,\n focusIfLost: null,\n ...identityScopedState()\n };\n }\n function withPage(state2, page) {\n return {\n ...state2,\n page,\n createdToken: null,\n tokenRenaming: null,\n tokenNotice: null,\n credentialEditing: null,\n credentialNotice: null,\n credentialNoticeFor: null,\n confirming: null\n };\n }\n function connectorStatusLabel(status, problem) {\n if (status === \"loading\") return \"Loading details\";\n if (status === \"ok\") return \"Connected\";\n if (status === \"auth_required\") {\n return problem === \"credential_required\" ? \"Credential needed\" : \"Authorization needed\";\n }\n return \"Unavailable\";\n }\n function toolCountLabel(count) {\n return `${count} ${count === 1 ? \"tool\" : \"tools\"}`;\n }\n function connectorStatusTone(status) {\n if (status === \"ok\") return \"ok\";\n if (status === \"auth_required\") return \"warn\";\n if (status === \"loading\") return \"neutral\";\n return \"danger\";\n }\n function summarizeConnectors(connectors) {\n const summary = {\n total: connectors.length,\n connected: 0,\n attention: 0,\n credentials: 0,\n unavailable: 0,\n loading: 0,\n tools: 0,\n drifting: 0\n };\n for (const connector of connectors) {\n if (connector.status === \"ok\") summary.connected += 1;\n else if (connector.status === \"auth_required\") {\n if (connector.problem === \"credential_required\") summary.credentials += 1;\n else summary.attention += 1;\n } else if (connector.status === \"loading\") summary.loading += 1;\n else summary.unavailable += 1;\n summary.tools += connector.toolCount || 0;\n if (driftState(connector.catalogDrift) === \"warning\") summary.drifting += 1;\n }\n return summary;\n }\n function connectorSummaryParts(summary) {\n if (summary.total > 0 && summary.loading === summary.total) {\n return [\n {\n text: `Checking ${summary.total} connector${summary.total === 1 ? \"\" : \"s\"}…`,\n tone: \"neutral\"\n }\n ];\n }\n return [\n { text: `${summary.connected} connected`, tone: \"neutral\" },\n ...summary.attention ? [\n {\n text: `${summary.attention} need${summary.attention === 1 ? \"s\" : \"\"} authorization`,\n tone: \"warn\"\n }\n ] : [],\n ...summary.credentials ? [\n {\n text: `${summary.credentials} need${summary.credentials === 1 ? \"s\" : \"\"} a credential`,\n tone: \"warn\"\n }\n ] : [],\n ...summary.unavailable ? [{ text: `${summary.unavailable} unavailable`, tone: \"danger\" }] : [],\n { text: toolCountLabel(summary.tools), tone: \"neutral\" },\n ...summary.loading ? [{ text: `${summary.loading} still loading`, tone: \"neutral\" }] : []\n ];\n }\n var TOOL_SAFETY_BADGE = {\n runs_in_programs: {\n label: \"runs in programs\",\n tone: \"ok\",\n title: \"Explicitly read-only: execute_code programs may call it without asking.\"\n },\n exempt: {\n label: \"exempt from approval\",\n tone: \"neutral\",\n title: \"Not read-only, but this deployment's config lets programs call it without pausing. Each call still counts against the write budget and appears in activity; call_tool still refuses it.\"\n },\n needs_approval: {\n label: \"asks for approval\",\n tone: \"warn\",\n title: \"Not explicitly read-only: a program pauses for resume_execution, and a direct call crosses call_destructive_tool — either way the host asks first.\"\n }\n };\n var PROBLEM_COPY = {\n connector_unavailable: \"Unavailable: its status check or catalog load failed, or did not finish in time. The deployment's log has the downstream error.\",\n oauth_required: \"Needs OAuth authorization: no grant is stored, or the stored grant expired or was revoked.\",\n credential_required: \"Needs a credential: nothing usable is stored in its credential slot.\",\n auth_required: \"Needs authorization. Its secret lives in deployment configuration, not on this page.\",\n credential_mismatch: \"The stored credential does not match the fields this connector declares, so it cannot be used.\",\n catalog_failed: \"Connected, but its tool catalog could not be loaded, so none of its tools are served.\"\n };\n function problemCopy(problem) {\n return problem ? PROBLEM_COPY[problem] ?? null : null;\n }\n function problemTone(problem) {\n return problem === \"oauth_required\" || problem === \"credential_required\" || problem === \"auth_required\" ? \"warn\" : \"danger\";\n }\n function authScopeLabel(scope) {\n return scope === \"personal\" ? \"personal auth\" : \"shared auth\";\n }\n function permissionLabel(connector) {\n if (connector.permissions?.manageSharedAuth) {\n return \"You can manage shared authentication for this connection.\";\n }\n if (connector.permissions?.connectPersonal) {\n return \"You can connect your own account to this connection.\";\n }\n return \"Authentication for this connection is managed by your deployment.\";\n }\n var DRIFT_CATEGORIES = [\n { key: \"unclassifiedTools\", label: \"Unclassified\" },\n { key: \"unservedTools\", label: \"Unserved\" },\n { key: \"annotationConflicts\", label: \"Annotation conflicts\" },\n { key: \"schemaChanges\", label: \"Schema changes\" }\n ];\n function driftTotal(drift) {\n if (!drift) return 0;\n return DRIFT_CATEGORIES.reduce((sum, { key }) => sum + (drift[key] || 0), 0);\n }\n function driftState(drift) {\n if (!drift) return \"unavailable\";\n return driftTotal(drift) > 0 ? \"warning\" : \"clean\";\n }\n function driftCounts(drift) {\n if (!drift) return [];\n return DRIFT_CATEGORIES.map(({ key, label }) => ({\n key,\n label,\n count: drift[key] || 0\n }));\n }\n function driftSummary(drift) {\n const state2 = driftState(drift);\n if (state2 === \"unavailable\") {\n return \"No catalog refresh observed yet in this runtime.\";\n }\n const observed = formatDate(drift?.observedAt);\n const when = observed ? ` · observed ${observed}` : \"\";\n if (state2 === \"clean\") return `Matches the reviewed manifest${when}`;\n const total = driftTotal(drift);\n return `${total} difference${total === 1 ? \"\" : \"s\"} from the reviewed manifest${when}`;\n }\n function safeHttpHref(url) {\n if (!url) return null;\n try {\n const protocol = new URL(url).protocol;\n return protocol === \"http:\" || protocol === \"https:\" ? url : null;\n } catch {\n return null;\n }\n }\n function formatDate(value) {\n if (!value) return \"\";\n const date = new Date(value);\n return Number.isNaN(date.valueOf()) ? \"\" : date.toLocaleString(void 0, { dateStyle: \"medium\", timeStyle: \"short\" });\n }\n var ACTOR_KINDS = {\n clerk: \"Clerk\",\n bearer: \"Bearer token\",\n \"cloudflare-access\": \"Cloudflare Access\",\n anonymous: \"Anonymous\"\n };\n function actorKindLabel(kind) {\n if (!kind) return \"Unknown caller\";\n return ACTOR_KINDS[kind] ?? kind;\n }\n function actorLabel(actor) {\n if (!actor?.kind) return \"Unknown caller\";\n const who = actor.label || actor.id;\n const kind = actorKindLabel(actor.kind);\n return who ? `${who} (${kind})` : kind;\n }\n function actorStableId(actor) {\n if (!actor?.id) return null;\n if (!actor.label && !actor.namespace) return null;\n return actor.namespace ? `${actor.namespace} · ${actor.id}` : actor.id;\n }\n function activityMatches(event, query) {\n const q2 = query.trim().toLowerCase();\n if (!q2) return true;\n return [\n event.address,\n event.connectorId,\n event.toolName,\n event.source,\n event.outcome,\n event.errorCode,\n event.friction,\n event.actor?.kind,\n event.actor?.id,\n event.actor?.namespace,\n event.actor?.label,\n activityOutcomeBadge(event.outcome).label,\n activityDetail(event),\n actorKindLabel(event.actor?.kind)\n ].some((value) => String(value ?? \"\").toLowerCase().includes(q2));\n }\n function filterActivity(events, query) {\n return events.filter((event) => activityMatches(event, query));\n }\n function activitySummary(events) {\n if (events.length === 0) return \"\";\n const tools = new Set(events.map((event) => event.address)).size;\n return `${events.length} loaded call${events.length === 1 ? \"\" : \"s\"} · ${tools} tool${tools === 1 ? \"\" : \"s\"}`;\n }\n var ACTIVITY_OUTCOMES = [\n \"success\",\n \"error\",\n \"timeout\",\n \"cancelled\",\n \"paused\",\n \"approved\"\n ];\n function activityOutcomeClass(outcome) {\n return ACTIVITY_OUTCOMES.includes(outcome) ? outcome : \"error\";\n }\n var OUTCOME_BADGE = {\n success: { label: \"Succeeded\", tone: \"ok\" },\n error: { label: \"Failed\", tone: \"danger\" },\n timeout: { label: \"Timed out\", tone: \"danger\" },\n cancelled: { label: \"Cancelled\", tone: \"neutral\" },\n paused: { label: \"Waiting for approval\", tone: \"warn\" },\n approved: { label: \"Approved\", tone: \"ok\" }\n };\n function activityOutcomeBadge(outcome) {\n return OUTCOME_BADGE[outcome] ?? { label: \"Failed\", tone: \"danger\" };\n }\n var SOURCE_LABELS = {\n execute_code: \"In a program\",\n call_tool: \"Direct call\",\n call_destructive_tool: \"Direct call, approved by the host\",\n resume_execution: \"Resumed program\",\n batch_call: \"Batch call\"\n };\n var REASON_LABELS = {\n tool_not_found: \"tool not found\",\n unknown_address: \"tool not found\",\n unknown_tool: \"tool not found\",\n ambiguous_tool_alias: \"ambiguous tool name\",\n schema_retry: \"arguments didn't match the schema\",\n invalid_args: \"arguments didn't match the schema\",\n destructive_reroute: \"needed host approval\",\n destructive_tool_requires_approval: \"needed host approval\",\n approval_required: \"needed approval\",\n auth_required: \"needed authorization\",\n result_too_large: \"result too large to return inline\",\n timeout: \"timed out\"\n };\n function reasonLabel(code) {\n return REASON_LABELS[code] ?? code.replaceAll(\"_\", \" \");\n }\n function activityDetail(event) {\n const parts = [SOURCE_LABELS[event.source] ?? event.source];\n if (event.approval === \"tool\") parts.push(\"approved for the rest of the run\");\n if (event.approval === \"call\") parts.push(\"approved for this call\");\n if (event.attempts > 1) parts.push(`${event.attempts} attempts`);\n const reasons = [event.friction, event.errorCode].filter((code) => Boolean(code)).map(reasonLabel);\n for (const reason of new Set(reasons)) parts.push(reason);\n return parts.join(\" · \");\n }\n function artifactRefreshBadge(last) {\n return last?.status === \"failed\" ? { label: \"Refresh failed\", tone: \"danger\" } : null;\n }\n function credentialProblemCopy(problem) {\n if (problem === \"credential_mismatch\") return PROBLEM_COPY.credential_mismatch;\n if (problem === \"credential_unreadable\") {\n return \"The stored credential can't be read, so it can't be used. Replace it, or remove it and add a new one.\";\n }\n return \"The stored credential can't be used. Replace it, or remove it and add a new one.\";\n }\n function credentialStateLabel(credential) {\n if (!credential.configured) return \"not configured\";\n const masked = credential.fields?.length ? \"configured\" : `configured · ••••${credential.lastFour ?? \"\"}`;\n return credential.updatedAt ? `${masked} · updated ${formatDate(credential.updatedAt)}` : masked;\n }\n function gateCopy(kind, signedIn) {\n if (kind === \"cloudflare-access\") {\n return \"Cloudflare Access admitted this browser, but the current identity cannot open deployment-wide operator pages.\";\n }\n if (kind !== \"clerk\") {\n return \"Paste an operator bearer token to open this page. Nothing is requested until you do.\";\n }\n return signedIn ? \"Signed in with Clerk, but this account cannot open deployment-wide operator pages.\" : \"Sign in with Clerk to open this operator page.\";\n }\n\n // src/operator-ui/app/config.ts\n var auth = AUTH;\n var mcpUrl = MCP_URL;\n var initialPage = INITIAL_PAGE;\n var homeUrl = HOME_URL;\n var titleSuffix = TITLE_SUFFIX;\n var productName = PRODUCT_NAME;\n var productDescription = PRODUCT_DESCRIPTION;\n var productOperatorLabel = PRODUCT_OPERATOR_LABEL;\n var TOKEN_KEY = \"connecta:token\";\n\n // src/fix-prompt.ts\n var CONNECTOR_ID_RE = /^[a-z0-9_-]+$/;\n function renderFixPrompt(spec, connectorId) {\n const id = connectorId !== void 0 && CONNECTOR_ID_RE.test(connectorId) ? connectorId : void 0;\n return [\n \"Diagnose and fix a problem in this connecta deployment (the @zackbart/connecta package). It is configured as code: connectors, credential slots, OAuth clients, pools, and inbound auth are declared in the deployment's source and environment, so the fix belongs there and not in the operator page.\",\n `Problem: ${spec.problem}` + (id ? `\nConnector id: ${id}` : \"\"),\n `Where to look:\n${spec.steps.map((step) => `- ${step}`).join(\"\\n\")}`,\n \"This prompt deliberately carries no error text, tokens, or URLs. Find the underlying cause in the deployment's own logs (lines prefixed [connecta]) or by reproducing the failure locally. Never put a secret in source, a log line, or your reply: secrets belong in the deployment's environment or its credential vault.\",\n \"Make the smallest change that fixes it, then verify it by redeploying and refreshing the connection on the operator Connections page. If the fix needs something you cannot do yourself — a provider console setting, a secret only the operator holds — name that exact step instead.\"\n ].join(\"\\n\\n\");\n }\n\n // src/operator-ui/fix-prompts.ts\n var CATALOGUE = {\n connector_unavailable: {\n problem: \"A connector is unavailable: its status check or catalog load failed, or did not finish before the operator page's deadline.\",\n steps: [\n \"Confirm the connector's downstream URL or API base in the deployment config, and that the deployment can reach it from where it runs.\",\n \"Check the credentials or headers the connector sends; a rejected credential often surfaces as a failed status rather than as an authorization prompt.\",\n \"If the downstream is slow rather than down, review the connector's timeouts and the deployment's discovery.probeTimeoutMs.\"\n ]\n },\n oauth_required: {\n problem: \"A downstream OAuth connector needs authorization: no grant is stored, or the stored grant expired or was revoked and could not be refreshed.\",\n steps: [\n \"Reauthorize from the operator page (Connect account or Reconnect OAuth) or with authorize_connector; this needs no code change if the grant simply lapsed.\",\n \"If it needs reauthorizing again soon after, check that the OAuth client requests offline access or a refresh token, and that the storage holding OAuth tokens persists across restarts and is shared by every instance.\",\n \"If authorization cannot start at all, check the connector's OAuth client configuration and the deployment's publicUrl, which forms the /oauth/callback/ redirect URI.\"\n ]\n },\n credential_required: {\n problem: \"A connector with an operator-managed credential slot has no usable credential stored.\",\n steps: [\n \"An operator with credential administration can add the credential on the operator page; that needs no code change.\",\n \"If nobody can, grant credential administration through the deployment's identity config (credentialAdministration for shared auth, personalConnection for personal auth), which is denied by default.\",\n \"Check that the connector's credential declaration (label and fields) matches what the downstream actually needs.\"\n ]\n },\n auth_required: {\n problem: \"A connector reports that it needs authorization, and its secret lives in deployment configuration rather than in an operator-managed slot.\",\n steps: [\n \"Find where the connector's secret is read (usually an environment variable or Worker secret passed into the connector config) and confirm it is set in the running environment.\",\n \"Rotate the secret at the provider if it expired or was revoked, then update the deployment's environment, not its source.\",\n \"If operators should manage this secret from the page instead, declare a credential slot on the connector and configure a credential vault.\"\n ]\n },\n credential_mismatch: {\n problem: \"The credential stored for a connector does not match the fields the connector currently declares, so it cannot be used.\",\n steps: [\n \"If the connector's credential fields changed on purpose, re-enter the credential on the operator page so the stored fields match.\",\n \"If they changed by accident, restore the previous field names in the connector's credential declaration.\"\n ]\n },\n credential_unreadable: {\n problem: \"A stored credential exists but could not be read or decrypted.\",\n steps: [\n \"Check that the credential vault's encryption key in the deployment environment is the one the credential was stored with; a rotated or missing key makes every stored value unreadable.\",\n \"Check the storage adapter backing the vault is reachable from the deployment.\",\n \"If the key was lost, remove and re-add the credential on the operator page; the old value cannot be recovered.\"\n ]\n },\n credential_test_failed: {\n problem: \"The test for a stored connector credential failed: the downstream rejected it, or the test could not reach the downstream.\",\n steps: [\n \"Confirm the stored value is current at the provider (not rotated, revoked, or scoped too narrowly), and replace it on the operator page if not.\",\n \"Check that the connector's credential test targets the same API base and auth scheme its tool calls use.\"\n ]\n },\n oauth_action_failed: {\n problem: \"Restarting or disconnecting a connector's downstream OAuth from the operator page failed.\",\n steps: [\n \"Check that the connector implements startAuth and disconnectAuth, and that the storage holding its OAuth state is writable.\",\n \"Check the connector's OAuth client configuration, including the authorization server it discovers or is given.\",\n \"Confirm the operator has the config-derived permission for this action (credentialAdministration for shared auth, personalConnection for personal auth).\"\n ]\n },\n catalog_failed: {\n problem: \"A connector reports itself connected, but loading its tool catalog failed, so none of its tools are being served.\",\n steps: [\n \"Check that the downstream still serves a complete tools list; connecta refuses a partial catalog rather than serving part of it.\",\n \"Check for tools the connector cannot accept: invalid schemas, missing descriptions, or names that collide.\",\n \"Pin or upgrade the @zackbart/connecta version if a maintained provider's catalog changed shape underneath it.\"\n ]\n },\n catalog_drift: {\n problem: \"A hosted provider's live catalog differs from the reviewed manifest shipped with connecta: new unclassified tools, unserved tools, annotation conflicts, or schema changes.\",\n steps: [\n \"Unclassified tools are withheld until a release classifies them. Check whether a newer @zackbart/connecta release has, and upgrade if so.\",\n \"If a tool the deployment depends on is now unserved or changed shape, review callers of it before upgrading.\",\n \"Report drift the release does not cover as an issue on the connecta repository, naming the provider and the kinds of drift but no tool data.\"\n ]\n }\n };\n function fixPrompt(kind, connectorId) {\n return renderFixPrompt(CATALOGUE[kind], connectorId);\n }\n var FIX_PROMPT_KINDS = Object.keys(CATALOGUE);\n\n // src/operator-ui/app/store.ts\n var state = initialState(initialPage);\n var listeners = /* @__PURE__ */ new Set();\n function getState() {\n return state;\n }\n function subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n }\n function set(patch) {\n state = { ...state, ...patch };\n for (const listener of listeners) listener();\n }\n function fence() {\n const generation = state.generation;\n return () => generation === state.generation;\n }\n function sessionToken() {\n if (auth.kind === \"cloudflare-access\") return Promise.resolve(void 0);\n return auth.kind === \"clerk\" ? Promise.resolve(window.Clerk?.session?.getToken() ?? null) : Promise.resolve(localStorage.getItem(TOKEN_KEY));\n }\n function requestHeaders(token, body = false) {\n return {\n ...token ? { Authorization: `Bearer ${token}` } : {},\n ...body ? { \"Content-Type\": \"application/json\" } : {}\n };\n }\n var NAV_HINT_KEY = \"connecta:nav\";\n function rememberNav(data) {\n try {\n sessionStorage.setItem(\n NAV_HINT_KEY,\n JSON.stringify({ activity: data.activityEnabled, artifacts: Boolean(data.artifactsEnabled) })\n );\n } catch {\n }\n }\n function forgetNav() {\n try {\n sessionStorage.removeItem(NAV_HINT_KEY);\n } catch {\n }\n }\n function navHint() {\n try {\n const hint = JSON.parse(sessionStorage.getItem(NAV_HINT_KEY) ?? \"null\");\n return { activity: hint?.activity === true, artifacts: hint?.artifacts === true };\n } catch {\n return { activity: false, artifacts: false };\n }\n }\n function gate(notice = null) {\n forgetNav();\n awaitingAuthorization.clear();\n state = resetIdentity(state, notice);\n for (const listener of listeners) listener();\n }\n var RequestFailure = class extends Error {\n kind;\n status;\n problem;\n constructor(kind, status, problem) {\n super(`Operator request failed: ${kind}`);\n this.kind = kind;\n this.status = status;\n this.problem = problem;\n }\n };\n function factsOf(error) {\n return error instanceof RequestFailure ? error : { kind: \"refused\" };\n }\n async function operatorRequest(path, method, current, body) {\n let token;\n try {\n token = await sessionToken();\n } catch {\n throw new RequestFailure(\"session\");\n }\n if (!current()) throw new RequestFailure(\"session\");\n if (!token && auth.kind !== \"cloudflare-access\") throw new RequestFailure(\"session\");\n let res;\n try {\n res = await fetch(path, {\n method,\n headers: requestHeaders(token, Boolean(body)),\n credentials: \"same-origin\",\n ...body ? { body: JSON.stringify(body) } : {}\n });\n } catch {\n throw new RequestFailure(\"network\");\n }\n if (res.status === 204) return null;\n let payload = {};\n try {\n payload = await res.json();\n } catch {\n }\n if (res.status === 401) throw new RequestFailure(\"session\", 401);\n if (res.status === 403) throw new RequestFailure(\"forbidden\", 403);\n if (!res.ok) throw new RequestFailure(\"refused\", res.status, payload.problem);\n return payload;\n }\n function unreachable(loadFailure) {\n set({ session: \"ready\", gate: null, refreshing: false, loadFailure });\n }\n async function loadData() {\n const current = fence();\n set(state.session === \"ready\" ? { refreshing: true, loadFailure: null } : { loadFailure: null });\n let token;\n try {\n token = await sessionToken();\n } catch {\n if (!current()) return;\n return gate(failure(\"Your sign-in session couldn't be read. Sign in again.\"));\n }\n if (!current()) return;\n if (!token && auth.kind !== \"cloudflare-access\") return gate(null);\n let res;\n try {\n res = await fetch(\"/ui/data\", {\n headers: requestHeaders(token),\n credentials: \"same-origin\"\n });\n } catch {\n if (!current()) return;\n return unreachable(\"network\");\n }\n if (!current()) return;\n if (res.status === 401 || res.status === 403) {\n if (auth.kind === \"clerk\") {\n return gate(\n failure(\n res.status === 403 ? `This Clerk account can't open ${productName}'s operator pages.` : \"Your Clerk session wasn't accepted. Sign out, then sign in again.\"\n )\n );\n }\n if (auth.kind === \"cloudflare-access\") {\n return gate(\n failure(\"Cloudflare Access let this browser in, but this identity isn't an operator here.\")\n );\n }\n localStorage.removeItem(TOKEN_KEY);\n return gate(failure(\"That token wasn't accepted. Paste a valid operator token.\"));\n }\n if (!res.ok) return unreachable(\"server\");\n let data;\n try {\n data = await res.json();\n } catch {\n if (!current()) return;\n return unreachable(\"server\");\n }\n if (!current()) return;\n if (!Array.isArray(data.connectors)) return unreachable(\"server\");\n set({ data, session: \"ready\", gate: null, refreshing: false, loadFailure: null });\n rememberNav(data);\n void loadConnectorDetails(data, current, token);\n }\n async function mutate(options) {\n const current = fence();\n if (options.reload) detailRevisions.set(options.reload, (detailRevisions.get(options.reload) ?? 0) + 1);\n set(options.busy);\n try {\n const payload = await options.request(current);\n if (!current()) return options.abandoned?.();\n set(options.done(payload));\n if (options.reload) void refreshConnector(options.reload);\n } catch (error) {\n if (!current()) return options.abandoned?.();\n set(options.failed(factsOf(error)));\n }\n }\n function focusHandled() {\n if (state.pendingFocus !== null || state.focusIfLost !== null) {\n set({ pendingFocus: null, focusIfLost: null });\n }\n }\n function setPage(page, focus = false) {\n state = withPage(state, page);\n if (focus) {\n state = {\n ...state,\n pendingFocus: state.session === \"ready\" ? `${page}Heading` : \"gateHeading\"\n };\n }\n for (const listener of listeners) listener();\n }\n function navigate(page, href) {\n history.pushState({ operatorPage: page }, \"\", href);\n setPage(page, true);\n }\n function setConnectorFilter(connectorFilter) {\n set({ connectorFilter });\n }\n function setActivitySearch(activitySearch) {\n set({ activitySearch });\n }\n function signInWithBearer(value) {\n gate(null);\n localStorage.setItem(TOKEN_KEY, value);\n void loadCurrent().then(() => {\n if (state.session === \"ready\") set({ pendingFocus: `${state.page}Heading` });\n });\n }\n function forgetBearer() {\n localStorage.removeItem(TOKEN_KEY);\n gate(null);\n set({ pendingFocus: \"token\" });\n }\n function askConfirm(connectorId, action) {\n set({ confirming: { connectorId, action }, pendingFocus: `confirm-cancel-${connectorId}` });\n }\n function cancelConfirm(returnFocusTo) {\n set({ confirming: null, pendingFocus: returnFocusTo });\n }\n function oauthStartPath(connector, mode) {\n return `/ui/oauth/${encodeURIComponent(connector)}?mode=${mode}`;\n }\n var awaitingAuthorization = /* @__PURE__ */ new Set();\n function openBlankTab() {\n let tab = null;\n try {\n tab = window.open(\"\", \"_blank\");\n } catch {\n return null;\n }\n if (!tab) return null;\n try {\n tab.document.title = \"Opening authorization…\";\n tab.document.body.textContent = \"Opening the authorization page…\";\n } catch {\n }\n return tab;\n }\n function oauthNoticePatch(connector, notice) {\n return {\n oauthBusy: null,\n oauthNotice: notice,\n oauthNoticeFor: connector,\n focusIfLost: `oauthNotice-${connector}`\n };\n }\n function startOAuth(connector, mode) {\n const tab = openBlankTab();\n return mutate({\n request: (current) => operatorRequest(oauthStartPath(connector, mode), \"POST\", current),\n busy: {\n oauthBusy: connector,\n oauthNotice: null,\n oauthNoticeFor: connector,\n oauthBlocked: null,\n confirming: null\n },\n done: (payload) => {\n const url = safeHttpHref(payload?.authorizationUrl);\n if (!url) {\n tab?.close();\n if (payload?.state === \"ok\") {\n return oauthNoticePatch(connector, oauthDoneNotice(\"oauth_reconnect\", payload));\n }\n return oauthNoticePatch(connector, refusedNotice(\"oauth_reconnect\", connector));\n }\n if (tab) {\n tab.opener = null;\n tab.location.replace(url);\n }\n awaitingAuthorization.add(connector);\n return {\n ...state.data ? {\n data: {\n ...state.data,\n connectors: state.data.connectors.map(\n (c3) => c3.id === connector ? { ...c3, status: \"auth_required\", tools: [], toolCount: 0, authorizationUrl: url } : c3\n )\n }\n } : {},\n ...oauthNoticePatch(connector, oauthDoneNotice(\"oauth_reconnect\", payload, Boolean(tab))),\n oauthBlocked: tab ? null : connector\n };\n },\n // Signed out or switched mid-request: the tab it opened goes too, rather\n // than sitting on \"Opening…\" with nothing left to send it anywhere.\n abandoned: () => tab?.close(),\n // The route's words never reach this notice (see `refusedNotice`).\n failed: (facts) => {\n tab?.close();\n return oauthNoticePatch(\n connector,\n actionFailedNotice(\"oauth_reconnect\", connector, facts, productName)\n );\n },\n reload: connector\n });\n }\n function disconnectOAuth(connector) {\n return mutate({\n request: (current) => operatorRequest(`/ui/oauth/${encodeURIComponent(connector)}`, \"DELETE\", current),\n busy: {\n oauthBusy: connector,\n oauthNotice: null,\n oauthNoticeFor: connector,\n oauthBlocked: null,\n confirming: null\n },\n done: () => ({\n ...state.data ? {\n data: {\n ...state.data,\n connectors: state.data.connectors.map((c3) => {\n if (c3.id !== connector) return c3;\n const { authorizationUrl: _old, ...rest } = c3;\n return { ...rest, status: \"auth_required\", tools: [], toolCount: 0 };\n })\n }\n } : {},\n ...oauthNoticePatch(connector, oauthDoneNotice(\"oauth_disconnect\", null))\n }),\n failed: (facts) => oauthNoticePatch(\n connector,\n actionFailedNotice(\"oauth_disconnect\", connector, facts, productName)\n ),\n reload: connector\n });\n }\n function editCredential(connector) {\n set({ credentialEditing: connector, credentialNotice: null, credentialNoticeFor: null });\n }\n function refuseCredential(connector, copy) {\n set({ credentialNotice: failure(copy), credentialNoticeFor: connector });\n }\n function credentialMutation(connector, action, request, done, reload = true) {\n const land = (patch) => ({\n credentialBusy: null,\n credentialNoticeFor: connector,\n focusIfLost: `credentialNotice-${connector}`,\n ...patch\n });\n return mutate({\n request,\n busy: {\n credentialBusy: connector,\n credentialNotice: null,\n credentialNoticeFor: connector,\n confirming: null\n },\n done: (payload) => land(done(payload)),\n failed: (facts) => land({ credentialNotice: actionFailedNotice(action, connector, facts, productName) }),\n reload: reload ? connector : void 0\n });\n }\n function saveCredential(connector, body) {\n return credentialMutation(\n connector,\n \"credential_save\",\n (current) => operatorRequest(\n `/ui/credentials/${encodeURIComponent(connector)}`,\n \"PUT\",\n current,\n body\n ),\n () => ({ credentialEditing: null, credentialNotice: info(\"Credential saved.\") })\n );\n }\n function removeCredential(connector) {\n return credentialMutation(\n connector,\n \"credential_remove\",\n (current) => operatorRequest(\n `/ui/credentials/${encodeURIComponent(connector)}`,\n \"DELETE\",\n current\n ),\n () => ({ credentialNotice: info(\"Credential removed.\") })\n );\n }\n function testCredential(connector) {\n return credentialMutation(\n connector,\n \"credential_test\",\n (current) => operatorRequest(\n `/ui/credentials/${encodeURIComponent(connector)}/test`,\n \"POST\",\n current\n ),\n (payload) => ({ credentialNotice: credentialTestNotice(connector, payload) }),\n false\n );\n }\n async function loadActivity(reset) {\n if (!state.data?.activityEnabled) return;\n const current = fence();\n set({\n activityPhase: \"loading\",\n activityNotice: null,\n ...reset ? { activityEvents: [], activityCursor: null } : {}\n });\n const params = new URLSearchParams({ limit: \"50\" });\n if (!reset && state.activityCursor) {\n params.set(\"cursor\", state.activityCursor);\n }\n try {\n const payload = await operatorRequest(\n `/ui/activity?${params}`,\n \"GET\",\n current\n );\n if (!current()) return;\n set({\n activityPhase: \"ready\",\n activityEvents: [\n ...reset ? [] : state.activityEvents,\n ...payload?.events ?? []\n ],\n activityCursor: payload?.nextCursor ?? null\n });\n } catch (error) {\n if (!current()) return;\n set({\n activityPhase: \"error\",\n activityNotice: failure(collectionFailureCopy(\"activity\", factsOf(error), productName))\n });\n }\n }\n async function artifactRead(path, current, collection) {\n let token;\n try {\n token = await sessionToken();\n } catch {\n if (current()) gate(failure(\"Your sign-in session couldn't be read. Sign in again.\"));\n return void 0;\n }\n if (!current()) return void 0;\n if (!token && auth.kind !== \"cloudflare-access\") {\n gate(null);\n return void 0;\n }\n let res;\n try {\n res = await fetch(path, { headers: requestHeaders(token), credentials: \"same-origin\" });\n } catch {\n if (current()) {\n set({\n session: \"ready\",\n gate: null,\n artifactPhase: \"error\",\n artifactNotice: failure(collectionFailureCopy(collection, { kind: \"network\" }, productName))\n });\n }\n return void 0;\n }\n if (!current()) return void 0;\n if (res.status === 401) {\n if (auth.kind !== \"clerk\" && auth.kind !== \"cloudflare-access\") {\n localStorage.removeItem(TOKEN_KEY);\n gate(failure(\"That token wasn't accepted. Paste a valid operator token.\"));\n } else {\n gate(failure(\"Your session wasn't accepted. Sign out, then sign in again.\"));\n }\n return void 0;\n }\n if (res.status === 403) {\n gate(failure(\"This deployment doesn't open artifact pages to this identity.\"));\n return void 0;\n }\n return res;\n }\n async function loadArtifacts(reset) {\n const current = fence();\n set({\n artifactPhase: \"loading\",\n artifactNotice: null,\n ...reset ? { artifactRows: [], artifactCursor: null } : {}\n });\n const params = new URLSearchParams();\n if (state.artifactQuery.trim()) params.set(\"q\", state.artifactQuery.trim());\n if (state.artifactArchived) params.set(\"archived\", \"1\");\n if (!reset && state.artifactCursor) params.set(\"cursor\", state.artifactCursor);\n const query = params.toString();\n const res = await artifactRead(`/artifacts/_api/list${query ? `?${query}` : \"\"}`, current, \"artifacts\");\n if (!res || !current()) return;\n if (!res.ok) {\n return set({\n session: \"ready\",\n gate: null,\n artifactPhase: \"error\",\n artifactNotice: failure(\n collectionFailureCopy(\"artifacts\", { kind: \"refused\", status: res.status }, productName)\n )\n });\n }\n let payload;\n try {\n payload = await res.json();\n } catch {\n payload = {};\n }\n if (!current()) return;\n set({\n session: \"ready\",\n gate: null,\n artifactPhase: \"ready\",\n artifactRows: [...reset ? [] : state.artifactRows, ...payload.artifacts ?? []],\n artifactCursor: payload.nextCursor ?? null\n });\n }\n async function loadArtifactView() {\n const current = fence();\n const request = artifactViewRequest(window.location.pathname, window.location.search);\n set({ artifactPhase: \"loading\", artifactNotice: null });\n if (!request) {\n return set({\n session: \"ready\",\n artifactPhase: \"error\",\n artifactNotice: failure(\"There is no artifact at this address.\")\n });\n }\n const res = await artifactRead(request, current, \"artifact\");\n if (!res || !current()) return;\n if (!res.ok) {\n return set({\n session: \"ready\",\n gate: null,\n artifactPhase: \"error\",\n artifactNotice: failure(\n collectionFailureCopy(\"artifact\", { kind: \"refused\", status: res.status }, productName)\n )\n });\n }\n let view = null;\n try {\n view = await res.json();\n } catch {\n view = null;\n }\n if (!current()) return;\n if (!view || typeof view.document !== \"string\") {\n return set({\n session: \"ready\",\n gate: null,\n artifactPhase: \"error\",\n artifactNotice: failure(\"The artifact couldn't be read. Retry in a moment.\")\n });\n }\n set({ session: \"ready\", gate: null, artifactPhase: \"ready\", artifactView: view });\n }\n function setArtifactQuery(artifactQuery) {\n set({ artifactQuery });\n }\n function setArtifactArchived(artifactArchived) {\n set({ artifactArchived });\n void loadArtifacts(true);\n }\n function loadCurrent() {\n if (state.page === \"artifacts\") return loadArtifacts(true);\n if (state.page === \"artifact\") return loadArtifactView();\n return loadData();\n }\n function retryLoad() {\n set({\n pendingFocus: state.page === \"connections\" ? \"connectorLedgerHeading\" : `${state.page}Heading`\n });\n return loadCurrent();\n }\n function retryCollection() {\n set({ pendingFocus: `${state.page}Heading` });\n if (state.page === \"activity\") return loadActivity(true);\n if (state.page === \"artifact\") return loadArtifactView();\n return loadArtifacts(true);\n }\n function signIn() {\n window.Clerk?.redirectToSignIn({\n signInFallbackRedirectUrl: window.location.href,\n signUpFallbackRedirectUrl: window.location.href\n });\n }\n function signOut() {\n if (auth.kind === \"cloudflare-access\") {\n gate(null);\n window.location.assign(\"/cdn-cgi/access/logout\");\n return;\n }\n const clerk = window.Clerk;\n gate(null);\n void clerk?.signOut({ redirectUrl: window.location.href });\n }\n var returnTimer;\n var lastReturnPass = 0;\n var RETURN_DEBOUNCE_MS = 150;\n var RETURN_MIN_INTERVAL_MS = 2e3;\n function onReturn() {\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\") return;\n if (returnTimer !== void 0) return;\n returnTimer = setTimeout(() => {\n returnTimer = void 0;\n const now = Date.now();\n if (now - lastReturnPass < RETURN_MIN_INTERVAL_MS) return;\n lastReturnPass = now;\n recheckAuthorization();\n }, RETURN_DEBOUNCE_MS);\n }\n function recheckAuthorization() {\n if (state.session !== \"ready\" || !state.data) return;\n for (const connector of state.data.connectors) {\n if (state.oauthBusy === connector.id) continue;\n const authorizesElsewhere = connector.status === \"auth_required\" && (connector.oauth === true || Boolean(connector.authorizationUrl));\n if (authorizesElsewhere || awaitingAuthorization.has(connector.id)) {\n void refreshConnector(connector.id, true);\n }\n }\n }\n async function boot() {\n const onPop = () => setPage(pageForPath(window.location.pathname), true);\n window.addEventListener(\"popstate\", onPop);\n window.addEventListener(\"focus\", onReturn);\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onReturn);\n }\n if (auth.kind === \"clerk\") {\n const clerk = window.Clerk;\n if (!clerk) {\n return gate(failure(\"Clerk couldn't load. Check your connection and try again.\"));\n }\n try {\n await clerk.load({\n ...auth.signInUrl ? { signInUrl: auth.signInUrl } : {},\n ...auth.signUpUrl ? { signUpUrl: auth.signUpUrl } : {},\n signInFallbackRedirectUrl: window.location.href,\n signUpFallbackRedirectUrl: window.location.href,\n afterSignOutUrl: window.location.href\n });\n let sessionId = clerk.session?.id ?? null;\n clerk.addListener((resources) => {\n const next = resources.session?.id ?? null;\n if (next === sessionId) return;\n sessionId = next;\n gate(null);\n void loadCurrent();\n });\n } catch {\n return gate(failure(\"Clerk couldn't start. Reload the page to try again.\"));\n }\n }\n await loadCurrent();\n }\n async function readConnector(id, token) {\n let response;\n try {\n response = await fetch(`/ui/connectors/${encodeURIComponent(id)}`, {\n headers: requestHeaders(token),\n credentials: \"same-origin\"\n });\n } catch {\n return { kind: \"local\", failure: \"network\" };\n }\n if (response.status === 401 || response.status === 403) {\n return { kind: \"local\", failure: \"session\" };\n }\n if (!response.ok) return { kind: \"downstream\" };\n try {\n return { kind: \"detail\", detail: await response.json() };\n } catch {\n return { kind: \"downstream\" };\n }\n }\n function withoutKey(record, key) {\n const { [key]: _gone, ...rest } = record;\n return rest;\n }\n function applyDetail(id, outcome) {\n if (!state.data) return;\n const patch = {};\n const connectors = state.data.connectors.map((c3) => {\n if (c3.id !== id) return c3;\n if (outcome.kind === \"detail\") {\n const { detail } = outcome;\n return detail.status === \"auth_required\" && c3.authorizationUrl && !detail.authorizationUrl ? { ...detail, authorizationUrl: c3.authorizationUrl } : detail;\n }\n const { problem: _problem, ...rest } = c3;\n return outcome.kind === \"downstream\" ? { ...rest, status: \"error\", problem: \"connector_unavailable\" } : { ...rest, status: \"error\" };\n });\n patch.connectorFailures = outcome.kind === \"local\" ? { ...state.connectorFailures, [id]: outcome.failure } : withoutKey(state.connectorFailures, id);\n if (outcome.kind === \"detail\" && outcome.detail.status === \"ok\" && awaitingAuthorization.delete(id)) {\n if (state.oauthNoticeFor === id) {\n patch.oauthNotice = info(\"Connected.\");\n patch.oauthBlocked = null;\n }\n }\n set({ ...patch, data: { ...state.data, connectors } });\n }\n var detailGeneration = 0;\n var detailRevisions = /* @__PURE__ */ new Map();\n async function loadConnectorDetails(data, current, token) {\n const generation = ++detailGeneration;\n let next = 0;\n const worker = async () => {\n while (next < data.connectors.length && current() && generation === detailGeneration) {\n const connector = data.connectors[next++];\n const revision = (detailRevisions.get(connector.id) ?? 0) + 1;\n detailRevisions.set(connector.id, revision);\n const outcome = await readConnector(connector.id, token);\n if (!current() || generation !== detailGeneration || !state.data) return;\n if (detailRevisions.get(connector.id) !== revision) continue;\n applyDetail(connector.id, outcome);\n }\n };\n await Promise.all(Array.from({ length: Math.min(4, data.connectors.length) }, worker));\n }\n async function refreshConnector(id, quiet = false) {\n const current = fence();\n const revision = (detailRevisions.get(id) ?? 0) + 1;\n detailRevisions.set(id, revision);\n if (!quiet && state.data) {\n set({\n data: { ...state.data, connectors: state.data.connectors.map((c3) => c3.id === id ? { ...c3, status: \"loading\" } : c3) },\n connectorFailures: withoutKey(state.connectorFailures, id)\n });\n }\n let token;\n try {\n token = await sessionToken();\n } catch {\n if (!current() || quiet || detailRevisions.get(id) !== revision) return;\n return applyDetail(id, { kind: \"local\", failure: \"session\" });\n }\n if (!current()) return;\n const outcome = await readConnector(id, token);\n if (!current() || !state.data || detailRevisions.get(id) !== revision) return;\n if (quiet && outcome.kind !== \"detail\") return;\n applyDetail(id, outcome);\n }\n async function loadAccessTokens() {\n const current = fence();\n set({ tokenPhase: \"loading\", tokenNotice: null });\n try {\n const payload = await operatorRequest(\"/ui/access-tokens\", \"GET\", current);\n if (!current()) return;\n set({ tokenPhase: \"ready\", tokens: payload?.accessTokens ?? [] });\n } catch {\n if (!current()) return;\n set({\n tokenPhase: \"error\",\n tokenNotice: failure(\n \"Access tokens could not be loaded.\"\n )\n });\n }\n }\n function tokenFailure(tokenNotice) {\n return { tokenBusy: false, tokenNotice, pendingFocus: \"tokenNotice\" };\n }\n function createAccessToken(name) {\n if (state.tokenBusy) return Promise.resolve(false);\n if (!name) {\n set(tokenFailure(failure(\"Name the MCP client before creating a token.\")));\n return Promise.resolve(false);\n }\n let created = false;\n return mutate({\n request: (current) => operatorRequest(\"/ui/access-tokens\", \"POST\", current, { name }),\n busy: { tokenBusy: true, tokenNotice: null },\n done: (payload) => {\n const issued = payload?.accessToken;\n if (!payload?.token || !issued) {\n throw new Error(\"The created token was not returned.\");\n }\n created = true;\n return {\n tokenBusy: false,\n tokenPhase: \"ready\",\n tokens: [\n issued,\n ...state.tokens.filter((token) => token.id !== issued.id)\n ],\n createdToken: state.page === \"tokens\" ? payload.token : null,\n tokenNotice: info(\"Access token created.\"),\n pendingFocus: state.page === \"tokens\" ? \"tokenRevealHeading\" : null\n };\n },\n failed: () => tokenFailure(failure(\"Access token could not be created. Check the name, capacity, and storage.\"))\n }).then(() => created);\n }\n function dismissCreatedToken() {\n set({ createdToken: null });\n }\n function renameAccessToken(id) {\n set({ tokenRenaming: id });\n }\n function accessTokenMutation(id, method, body, success, fallback) {\n return mutate({\n request: (current) => operatorRequest(\n `/ui/access-tokens/${encodeURIComponent(id)}`,\n method,\n current,\n body\n ),\n busy: { tokenBusy: true, tokenNotice: null },\n done: (payload) => ({\n tokenBusy: false,\n tokenRenaming: null,\n tokenNotice: info(success),\n pendingFocus: \"tokenNotice\",\n ...payload?.accessToken ? {\n tokens: state.tokens.map(\n (token) => token.id === id ? payload.accessToken : token\n )\n } : {}\n }),\n failed: () => tokenFailure(failure(fallback))\n });\n }\n function saveAccessTokenName(id, name) {\n return accessTokenMutation(\n id,\n \"PUT\",\n { name },\n \"Access token renamed.\",\n \"Access token could not be renamed.\"\n );\n }\n function revokeAccessToken(id) {\n const named = state.tokens.find((token) => token.id === id);\n const confirmed = window.confirm(\n `Revoke ${named?.name || \"this access token\"}? Its MCP client will immediately lose access.`\n );\n if (!confirmed) return Promise.resolve();\n return accessTokenMutation(\n id,\n \"DELETE\",\n void 0,\n \"Access token revoked.\",\n \"Access token could not be revoked.\"\n );\n }\n\n // node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js\n var f3 = 0;\n function u3(e3, t3, n2, o3, i3, u4) {\n t3 || (t3 = {});\n var a3, c3, p3 = t3;\n if (\"ref\" in p3) for (c3 in p3 = {}, t3) \"ref\" == c3 ? a3 = t3[c3] : p3[c3] = t3[c3];\n var l3 = { type: e3, props: p3, key: n2, ref: a3, __k: null, __: null, __b: 0, __e: null, __c: null, constructor: void 0, __v: --f3, __i: -1, __u: 0, __source: i3, __self: u4 };\n if (\"function\" == typeof e3 && (a3 = e3.defaultProps)) for (c3 in a3) void 0 === p3[c3] && (p3[c3] = a3[c3]);\n return l.vnode && l.vnode(l3), l3;\n }\n\n // src/operator-ui/app/parts.tsx\n function NoticeLine({\n id,\n notice,\n className = \"meta\"\n }) {\n return /* @__PURE__ */ u3(S, { children: [\n /* @__PURE__ */ u3(\n \"p\",\n {\n id,\n class: notice?.tone === \"error\" ? `notice ${className} error-notice` : `notice ${className}`,\n role: notice?.tone === \"error\" ? \"alert\" : \"status\",\n \"aria-live\": \"polite\",\n tabIndex: -1,\n children: notice ? notice.message : null\n }\n ),\n notice?.tone === \"error\" && notice.fix ? /* @__PURE__ */ u3(FixPrompt, { kind: notice.fix.kind, connectorId: notice.fix.connectorId }) : null\n ] });\n }\n function FixPrompt({\n kind,\n connectorId,\n name\n }) {\n return /* @__PURE__ */ u3(\"div\", { class: \"fix-prompt\", \"data-fix-prompt\": kind, children: [\n /* @__PURE__ */ u3(FixPromptButton, { kind, connectorId, ...name ? { name } : {} }),\n /* @__PURE__ */ u3(FixPromptPreview, { kind, connectorId })\n ] });\n }\n function FixPromptButton({\n kind,\n connectorId,\n name\n }) {\n return /* @__PURE__ */ u3(\n CopyButton,\n {\n value: fixPrompt(kind, connectorId),\n label: \"Copy fix prompt\",\n class: \"btn quiet\",\n ariaLabel: `Copy fix prompt for ${name ?? connectorId}`\n }\n );\n }\n function FixPromptPreview({\n kind,\n connectorId,\n standalone\n }) {\n return /* @__PURE__ */ u3(\"details\", { class: \"fix-prompt-preview\", ...standalone ? { \"data-fix-prompt\": kind } : {}, children: [\n /* @__PURE__ */ u3(\"summary\", { class: \"disclosure\", children: \"Preview prompt\" }),\n /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: \"Fixed text for a coding agent working on this deployment. It carries no error details or secrets.\" }),\n /* @__PURE__ */ u3(\"pre\", { class: \"fix-prompt-text\", children: fixPrompt(kind, connectorId) })\n ] });\n }\n function Badge({\n tone = \"neutral\",\n children\n }) {\n return /* @__PURE__ */ u3(\"span\", { class: tone === \"neutral\" ? \"badge\" : `badge ${tone}`, children });\n }\n function StateBlock({\n title,\n children,\n tone = \"neutral\",\n action,\n id\n }) {\n return /* @__PURE__ */ u3(\n \"div\",\n {\n class: tone === \"error\" ? \"state-block error\" : \"state-block\",\n role: tone === \"error\" ? \"alert\" : \"status\",\n ...id ? { id } : {},\n children: [\n title ? /* @__PURE__ */ u3(\"p\", { class: \"state-title\", children: title }) : null,\n children ? /* @__PURE__ */ u3(\"p\", { class: \"state-copy\", children }) : null,\n action ? /* @__PURE__ */ u3(\n \"button\",\n {\n class: \"btn\",\n type: \"button\",\n onClick: action.onClick,\n ...action.id ? { id: action.id } : {},\n children: action.label\n }\n ) : null\n ]\n }\n );\n }\n function LoadFailure({ state: state2 }) {\n if (!state2.loadFailure) return null;\n const copy = loadFailureCopy(state2.loadFailure, productName);\n return /* @__PURE__ */ u3(\n StateBlock,\n {\n id: \"loadFailure\",\n tone: \"error\",\n title: copy.title,\n action: { label: \"Retry\", onClick: () => void retryLoad(), id: \"retryLoad\" },\n children: copy.body\n }\n );\n }\n function Empty({ children }) {\n return /* @__PURE__ */ u3(StateBlock, { children });\n }\n function Unavailable({ children }) {\n return /* @__PURE__ */ u3(\"div\", { class: \"unavailable\", children });\n }\n function ConfirmBar({\n id,\n question,\n confirm,\n onConfirm,\n onCancel\n }) {\n return /* @__PURE__ */ u3(\n \"div\",\n {\n class: \"confirm\",\n role: \"group\",\n \"aria-labelledby\": `confirm-question-${id}`,\n onKeyDown: (event) => {\n if (event.key !== \"Escape\") return;\n event.preventDefault();\n onCancel();\n },\n children: [\n /* @__PURE__ */ u3(\"p\", { id: `confirm-question-${id}`, children: question }),\n /* @__PURE__ */ u3(\"div\", { class: \"actions\", children: [\n /* @__PURE__ */ u3(\"button\", { class: \"btn danger\", type: \"button\", onClick: onConfirm, children: confirm }),\n /* @__PURE__ */ u3(\n \"button\",\n {\n id: `confirm-cancel-${id}`,\n class: \"btn quiet\",\n type: \"button\",\n onClick: onCancel,\n children: \"Cancel\"\n }\n )\n ] })\n ]\n }\n );\n }\n function focusableId(...ids) {\n for (const id of ids) {\n const element = document.getElementById(id);\n if (element && !element.disabled) return id;\n }\n return ids[ids.length - 1] ?? \"\";\n }\n function PageLink({\n page,\n class: className,\n current,\n children\n }) {\n const href = PAGE_META[page].path;\n return /* @__PURE__ */ u3(\n \"a\",\n {\n class: className,\n href,\n ...current ? { \"aria-current\": \"page\" } : {},\n onClick: (event) => {\n if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {\n return;\n }\n event.preventDefault();\n navigate(page, href);\n },\n children\n }\n );\n }\n function CopyButton({\n value,\n label,\n class: className = \"btn\",\n ariaLabel,\n id\n }) {\n const [status, setStatus] = d2(\"idle\");\n h2(() => {\n if (status === \"idle\") return;\n const timer = window.setTimeout(() => setStatus(\"idle\"), 1600);\n return () => window.clearTimeout(timer);\n }, [status]);\n return /* @__PURE__ */ u3(\n \"button\",\n {\n class: className,\n type: \"button\",\n ...id ? { id } : {},\n ...ariaLabel && status === \"idle\" ? { \"aria-label\": ariaLabel } : {},\n onClick: () => {\n const write = navigator.clipboard?.writeText(value) ?? Promise.reject(new Error(\"no clipboard\"));\n write.then(\n () => setStatus(\"copied\"),\n () => setStatus(\"failed\")\n );\n },\n children: status === \"copied\" ? \"Copied\" : status === \"failed\" ? \"Copy failed\" : label\n }\n );\n }\n\n // src/operator-ui/app/tokens.tsx\n function CreateForm({ busy }) {\n const [name, setName] = d2(\"\");\n return /* @__PURE__ */ u3(\n \"form\",\n {\n id: \"tokenCreateForm\",\n class: \"token-create\",\n onSubmit: (event) => {\n event.preventDefault();\n void createAccessToken(name.trim()).then((created) => {\n if (created) setName(\"\");\n });\n },\n children: [\n /* @__PURE__ */ u3(\"label\", { for: \"tokenName\", children: \"Client name\" }),\n /* @__PURE__ */ u3(\"div\", { class: \"row\", children: [\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: \"tokenName\",\n type: \"text\",\n maxLength: 80,\n placeholder: \"Claude desktop, ChatGPT production…\",\n autocomplete: \"off\",\n value: name,\n onInput: (event) => setName(event.currentTarget.value)\n }\n ),\n /* @__PURE__ */ u3(\"button\", { id: \"createToken\", class: \"btn\", type: \"submit\", disabled: busy, children: busy ? \"Creating…\" : \"Create token\" })\n ] })\n ]\n }\n );\n }\n function Reveal({ token }) {\n return /* @__PURE__ */ u3(\n \"section\",\n {\n id: \"tokenReveal\",\n class: \"token-reveal\",\n \"aria-labelledby\": \"tokenRevealHeading\",\n children: [\n /* @__PURE__ */ u3(\"div\", { class: \"token-reveal-head\", children: [\n /* @__PURE__ */ u3(\"h2\", { id: \"tokenRevealHeading\", tabIndex: -1, children: \"Copy this token now\" }),\n /* @__PURE__ */ u3(\"span\", { class: \"cap\", children: \"Shown once\" })\n ] }),\n /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: \"Store it in the MCP client before leaving this page. It cannot be displayed again.\" }),\n /* @__PURE__ */ u3(\"div\", { class: \"endpoint-row token-secret\", children: [\n /* @__PURE__ */ u3(\"code\", { id: \"createdToken\", class: \"mono\", children: token }),\n /* @__PURE__ */ u3(CopyButton, { value: token, label: \"Copy token\" })\n ] }),\n /* @__PURE__ */ u3(\"button\", { class: \"btn\", type: \"button\", onClick: dismissCreatedToken, children: \"I stored it\" })\n ]\n }\n );\n }\n function TokenCard({\n token,\n renaming,\n busy\n }) {\n const [name, setName] = d2(token.name);\n const revoked = Boolean(token.revokedAt);\n return /* @__PURE__ */ u3(\n \"section\",\n {\n class: revoked ? \"token-card revoked\" : \"token-card\",\n \"aria-labelledby\": `access-token-${token.id}`,\n children: [\n /* @__PURE__ */ u3(\"div\", { class: \"token-card-head\", children: [\n /* @__PURE__ */ u3(\"div\", { children: [\n /* @__PURE__ */ u3(\"h2\", { id: `access-token-${token.id}`, children: token.name }),\n /* @__PURE__ */ u3(\"p\", { class: \"mono\", children: [\n token.tokenPrefix,\n \"…\"\n ] })\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"cap\", children: revoked ? `Revoked ${formatDate(token.revokedAt)}` : `Created ${formatDate(token.createdAt)}` })\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"credential-actions\", children: [\n /* @__PURE__ */ u3(\n \"button\",\n {\n class: \"btn\",\n type: \"button\",\n disabled: busy,\n onClick: () => {\n setName(token.name);\n renameAccessToken(renaming ? null : token.id);\n },\n children: \"Rename\"\n }\n ),\n revoked ? null : /* @__PURE__ */ u3(\n \"button\",\n {\n class: \"btn danger\",\n type: \"button\",\n disabled: busy,\n onClick: () => void revokeAccessToken(token.id),\n children: \"Revoke\"\n }\n )\n ] }),\n renaming ? /* @__PURE__ */ u3(\n \"form\",\n {\n class: \"credential-form\",\n onSubmit: (event) => {\n event.preventDefault();\n const next = name.trim();\n if (next) void saveAccessTokenName(token.id, next);\n },\n children: [\n /* @__PURE__ */ u3(\"label\", { class: \"visually-hidden\", for: `token-name-${token.id}`, children: \"Token name\" }),\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: `token-name-${token.id}`,\n type: \"text\",\n maxLength: 80,\n autocomplete: \"off\",\n value: name,\n onInput: (event) => setName(event.currentTarget.value)\n }\n ),\n /* @__PURE__ */ u3(\"button\", { class: \"btn\", type: \"submit\", disabled: busy, children: \"Save name\" }),\n /* @__PURE__ */ u3(\n \"button\",\n {\n class: \"btn\",\n type: \"button\",\n disabled: busy,\n onClick: () => renameAccessToken(null),\n children: \"Cancel\"\n }\n )\n ]\n }\n ) : null\n ]\n }\n );\n }\n function TokensPage({ state: state2 }) {\n const available = state2.data?.accessTokenManagement === \"available\";\n return /* @__PURE__ */ u3(\"section\", { id: \"tokensView\", children: /* @__PURE__ */ u3(\"div\", { class: \"lead\", children: [\n /* @__PURE__ */ u3(\"h1\", { id: \"tokensHeading\", class: \"\", tabIndex: -1, children: \"Access tokens\" }),\n /* @__PURE__ */ u3(\"div\", { class: \"lead-copy\", children: [\n /* @__PURE__ */ u3(\"p\", { class: \"activity-copy\", children: \"Create named Bearer tokens for MCP clients. Each secret is shown once; revoke it when that client should lose access.\" }),\n /* @__PURE__ */ u3(NoticeLine, { id: \"tokenNotice\", notice: state2.tokenNotice }),\n !available ? /* @__PURE__ */ u3(Unavailable, { children: accessTokenUnavailableCopy(state2.data?.accessTokenManagement) }) : /* @__PURE__ */ u3(\"div\", { id: \"tokenAvailable\", children: [\n state2.createdToken ? /* @__PURE__ */ u3(Reveal, { token: state2.createdToken }) : /* @__PURE__ */ u3(CreateForm, { busy: state2.tokenBusy }),\n /* @__PURE__ */ u3(\n \"div\",\n {\n id: \"tokenList\",\n class: \"token-ledger\",\n \"aria-busy\": state2.tokenPhase === \"loading\" ? \"true\" : \"false\",\n children: state2.tokenPhase === \"loading\" ? /* @__PURE__ */ u3(Empty, { children: \"Loading access tokens…\" }) : state2.tokenPhase === \"error\" ? /* @__PURE__ */ u3(\"p\", { class: \"empty\", children: /* @__PURE__ */ u3(\n \"button\",\n {\n class: \"btn\",\n type: \"button\",\n onClick: () => void loadAccessTokens(),\n children: \"Try loading access tokens again\"\n }\n ) }) : state2.tokens.length === 0 ? /* @__PURE__ */ u3(Empty, { children: \"No access tokens yet. Name the first MCP client above.\" }) : state2.tokens.map((token) => /* @__PURE__ */ u3(\n TokenCard,\n {\n token,\n renaming: state2.tokenRenaming === token.id,\n busy: state2.tokenBusy\n },\n token.id\n ))\n }\n )\n ] })\n ] })\n ] }) });\n }\n\n // src/operator-ui/app/activity.tsx\n function ActivityRow({ event }) {\n const outcome = activityOutcomeClass(event.outcome);\n const badge = activityOutcomeBadge(event.outcome);\n const stableId = actorStableId(event.actor);\n return /* @__PURE__ */ u3(\"article\", { class: `activity-item ${outcome}`, children: [\n /* @__PURE__ */ u3(\"div\", { class: \"activity-stamp\", children: [\n /* @__PURE__ */ u3(\"span\", { class: `dot ${outcome}`, \"aria-hidden\": \"true\" }),\n /* @__PURE__ */ u3(\"div\", { children: [\n /* @__PURE__ */ u3(\"time\", { class: \"activity-time\", dateTime: event.occurredAt, children: formatDate(event.occurredAt) }),\n /* @__PURE__ */ u3(\"div\", { class: \"activity-actor\", children: actorLabel(event.actor) }),\n stableId ? /* @__PURE__ */ u3(\"div\", { class: \"activity-actor-id mono\", children: stableId }) : null\n ] })\n ] }),\n /* @__PURE__ */ u3(\"div\", { children: [\n /* @__PURE__ */ u3(\"div\", { class: \"activity-address\", children: event.address }),\n /* @__PURE__ */ u3(\"div\", { class: \"activity-detail\", children: activityDetail(event) })\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"activity-result\", children: [\n /* @__PURE__ */ u3(Badge, { tone: badge.tone, children: badge.label }),\n /* @__PURE__ */ u3(\"div\", { class: \"activity-detail\", children: [\n event.durationMs,\n \" ms\"\n ] })\n ] })\n ] });\n }\n function ActivityPage({ state: state2 }) {\n const data = state2.data;\n const enabled = Boolean(data?.activityEnabled);\n const loading = state2.activityPhase === \"loading\";\n const visible = filterActivity(state2.activityEvents, state2.activitySearch);\n const summary = activitySummary(state2.activityEvents);\n const failed = state2.activityPhase === \"error\";\n return /* @__PURE__ */ u3(\"section\", { id: \"activityView\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"lead\", children: [\n /* @__PURE__ */ u3(\"h1\", { id: \"activityHeading\", tabIndex: -1, children: \"Activity\" }),\n /* @__PURE__ */ u3(\"div\", { class: \"lead-copy\", children: /* @__PURE__ */ u3(\"p\", { children: pageDescription(\"activity\", productDescription) }) })\n ] }),\n !data ? (\n // Whether Activity is open to this identity is in /ui/data, which\n // has not answered yet — or could not.\n state2.loadFailure ? /* @__PURE__ */ u3(LoadFailure, { state: state2 }) : /* @__PURE__ */ u3(StateBlock, { children: \"Loading activity…\" })\n ) : !enabled ? /* @__PURE__ */ u3(Unavailable, { children: [\n \"Activity history is not configured. Add an\",\n \" \",\n /* @__PURE__ */ u3(\"span\", { class: \"mono\", children: \"activity.store\" }),\n \" with a list reader to enable this page.\"\n ] }) : /* @__PURE__ */ u3(\"div\", { id: \"activityAvailable\", class: \"collection\", children: [\n failed && state2.activityEvents.length === 0 ? null : /* @__PURE__ */ u3(\"div\", { class: \"row\", children: [\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: \"activitySearch\",\n type: \"search\",\n placeholder: \"Search user, tool, or outcome…\",\n \"aria-label\": \"Search loaded activity\",\n value: state2.activitySearch,\n onInput: (event) => setActivitySearch(event.currentTarget.value)\n }\n ),\n /* @__PURE__ */ u3(\n \"button\",\n {\n id: \"refreshActivity\",\n class: \"btn\",\n type: \"button\",\n disabled: loading,\n onClick: () => void loadActivity(true),\n children: loading ? \"Loading…\" : \"Refresh\"\n }\n )\n ] }),\n /* @__PURE__ */ u3(\"p\", { id: \"activitySummary\", class: \"meta\", \"aria-live\": \"polite\", children: summary }),\n state2.activityEvents.length === 0 ? loading ? /* @__PURE__ */ u3(StateBlock, { children: \"Loading activity…\" }) : failed ? /* @__PURE__ */ u3(\n StateBlock,\n {\n id: \"activityError\",\n tone: \"error\",\n title: \"Activity couldn't be loaded\",\n action: { label: \"Retry\", onClick: () => void retryCollection() },\n children: state2.activityNotice?.message\n }\n ) : /* @__PURE__ */ u3(StateBlock, { children: state2.activitySearch.trim() ? \"No loaded activity matches this search.\" : \"No connector tool calls recorded yet.\" }) : visible.length === 0 ? /* @__PURE__ */ u3(StateBlock, { children: \"No loaded activity matches this search.\" }) : /* @__PURE__ */ u3(\n \"div\",\n {\n id: \"activityList\",\n class: \"activity-list\",\n \"aria-busy\": loading ? \"true\" : \"false\",\n children: visible.map((event, index) => /* @__PURE__ */ u3(\n ActivityRow,\n {\n event\n },\n `${event.occurredAt}-${event.address}-${index}`\n ))\n }\n ),\n state2.activityEvents.length > 0 ? /* @__PURE__ */ u3(NoticeLine, { id: \"activityNotice\", notice: state2.activityNotice }) : null,\n state2.activityCursor ? /* @__PURE__ */ u3(\n \"button\",\n {\n id: \"moreActivity\",\n class: \"btn activity-more\",\n type: \"button\",\n disabled: loading,\n onClick: () => void loadActivity(false),\n children: loading ? \"Loading…\" : \"Load older\"\n }\n ) : null\n ] })\n ] });\n }\n\n // src/operator-ui/app/artifacts.tsx\n function ArtifactRow({ row }) {\n const refresh = artifactRefreshBadge(row.freshness?.last);\n return /* @__PURE__ */ u3(\"article\", { class: \"artifact-row\", children: [\n /* @__PURE__ */ u3(\"div\", { children: [\n /* @__PURE__ */ u3(\"a\", { class: \"artifact-title\", href: `/artifacts/${row.id}`, children: row.title }),\n /* @__PURE__ */ u3(\"div\", { class: \"artifact-meta\", children: [\n /* @__PURE__ */ u3(\"span\", { class: \"mono\", children: row.id }),\n \" · version \",\n row.viewVersion,\n \" · updated\",\n \" \",\n /* @__PURE__ */ u3(\"time\", { dateTime: row.updatedAt, children: formatDate(row.updatedAt) }),\n \" by \",\n row.updatedBy.label\n ] })\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"artifact-badges\", children: [\n /* @__PURE__ */ u3(Badge, { children: row.kind === \"markdown\" ? \"Markdown\" : \"HTML\" }),\n row.freshness?.state === \"stale\" ? /* @__PURE__ */ u3(Badge, { tone: \"warn\", children: \"Stale data\" }) : row.freshness?.state === \"current\" ? /* @__PURE__ */ u3(Badge, { children: \"Current data\" }) : null,\n refresh ? /* @__PURE__ */ u3(Badge, { tone: refresh.tone, children: refresh.label }) : null,\n row.archived ? /* @__PURE__ */ u3(Badge, { tone: \"warn\", children: \"Archived\" }) : null\n ] })\n ] });\n }\n function ArtifactsPage({ state: state2 }) {\n const loading = state2.artifactPhase === \"loading\";\n const rows = state2.artifactRows;\n return /* @__PURE__ */ u3(\"section\", { id: \"artifactsView\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"lead\", children: [\n /* @__PURE__ */ u3(\"h1\", { id: \"artifactsHeading\", tabIndex: -1, children: \"Artifacts\" }),\n /* @__PURE__ */ u3(\"div\", { class: \"lead-copy\", children: /* @__PURE__ */ u3(\"p\", { children: pageDescription(\"artifacts\", productDescription) }) })\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"collection\", children: [\n /* @__PURE__ */ u3(\n \"form\",\n {\n class: \"row\",\n onSubmit: (event) => {\n event.preventDefault();\n void loadArtifacts(true);\n },\n children: [\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: \"artifactSearch\",\n type: \"search\",\n placeholder: \"Search titles…\",\n \"aria-label\": \"Search artifact titles\",\n value: state2.artifactQuery,\n onInput: (event) => setArtifactQuery(event.currentTarget.value)\n }\n ),\n /* @__PURE__ */ u3(\"button\", { id: \"searchArtifacts\", class: \"btn\", type: \"submit\", disabled: loading, children: \"Search\" }),\n /* @__PURE__ */ u3(\"label\", { class: \"check artifact-meta\", children: [\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: \"showArchived\",\n type: \"checkbox\",\n checked: state2.artifactArchived,\n onChange: (event) => setArtifactArchived(event.currentTarget.checked)\n }\n ),\n \" \",\n \"Show archived\"\n ] })\n ]\n }\n ),\n state2.artifactPhase === \"error\" && rows.length === 0 ? /* @__PURE__ */ u3(\n StateBlock,\n {\n id: \"artifactError\",\n tone: \"error\",\n title: \"Artifacts couldn't be loaded\",\n action: { label: \"Retry\", onClick: () => void retryCollection() },\n children: state2.artifactNotice?.message\n }\n ) : rows.length === 0 ? /* @__PURE__ */ u3(StateBlock, { children: loading ? \"Loading artifacts…\" : state2.artifactQuery.trim() ? \"No artifact title matches this search.\" : \"No artifacts yet. Ask an agent to publish one.\" }) : /* @__PURE__ */ u3(\"div\", { id: \"artifactList\", class: \"activity-list\", \"aria-busy\": loading ? \"true\" : \"false\", children: rows.map((row) => /* @__PURE__ */ u3(ArtifactRow, { row }, row.id)) }),\n rows.length > 0 ? /* @__PURE__ */ u3(NoticeLine, { id: \"artifactNotice\", notice: state2.artifactNotice }) : null,\n state2.artifactCursor ? /* @__PURE__ */ u3(\n \"button\",\n {\n id: \"moreArtifacts\",\n class: \"btn activity-more\",\n type: \"button\",\n disabled: loading,\n onClick: () => void loadArtifacts(false),\n children: loading ? \"Loading…\" : \"Load more\"\n }\n ) : null\n ] })\n ] });\n }\n function ArtifactFrame({ view }) {\n const frame = A2(null);\n _2(() => {\n const element = frame.current;\n if (!element) return;\n const onMessage = (event) => {\n if (event.source !== element.contentWindow || event.origin !== \"null\") return;\n const data = event.data;\n if (!data || data.type !== \"ready\") return;\n window.removeEventListener(\"message\", onMessage);\n const policy = document.createElement(\"meta\");\n policy.httpEquiv = \"Content-Security-Policy\";\n policy.content = \"frame-src 'none'\";\n document.head.append(policy);\n element.contentWindow?.postMessage({ type: \"document\", html: view.document }, \"*\");\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [view.document]);\n return /* @__PURE__ */ u3(\n \"iframe\",\n {\n ref: frame,\n id: \"artifactFrame\",\n class: \"artifact-frame\",\n title: view.title,\n sandbox: \"allow-scripts\",\n referrerpolicy: \"no-referrer\",\n src: \"/artifacts/_frame\"\n }\n );\n }\n function ArtifactPage({ state: state2 }) {\n const view = state2.artifactView;\n const failed = state2.artifactPhase === \"error\";\n return /* @__PURE__ */ u3(\"section\", { id: \"artifactView\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"artifact-head\", children: [\n /* @__PURE__ */ u3(\"h1\", { id: \"artifactHeading\", tabIndex: -1, children: view?.title ?? \"Artifact\" }),\n view ? /* @__PURE__ */ u3(\"div\", { class: \"artifact-meta\", id: \"artifactMeta\", children: [\n view.snapshot ? \"Snapshot of \" : \"\",\n \"version \",\n view.view.version,\n view.snapshot && view.view.version !== view.latestViewVersion ? ` (latest is ${view.latestViewVersion})` : \"\",\n \" \",\n \"· updated \",\n /* @__PURE__ */ u3(\"time\", { dateTime: view.view.at, children: formatDate(view.view.at) }),\n \" by\",\n \" \",\n view.view.by.label,\n \" ·\",\n \" \",\n /* @__PURE__ */ u3(\"a\", { href: \"/artifacts\", children: \"All artifacts\" }),\n view.snapshot ? /* @__PURE__ */ u3(S, { children: [\n \" \",\n \"· \",\n /* @__PURE__ */ u3(\"a\", { href: view.url, children: \"Current version\" })\n ] }) : /* @__PURE__ */ u3(S, { children: [\n \" \",\n \"·\",\n \" \",\n /* @__PURE__ */ u3(\n CopyButton,\n {\n id: \"copySnapshot\",\n class: \"navlink inline\",\n value: view.snapshotUrl,\n label: \"Copy snapshot link\"\n }\n )\n ] })\n ] }) : null,\n view?.archived ? /* @__PURE__ */ u3(\"div\", { id: \"archivedBanner\", class: \"artifact-banner\", role: \"status\", children: \"This artifact is archived. It keeps every version, and an agent can restore it.\" }) : null,\n !view?.snapshot && view?.freshness?.state === \"stale\" ? /* @__PURE__ */ u3(\"div\", { id: \"staleBanner\", class: \"artifact-banner\", role: \"status\", children: [\n \"Data may be out of date. The last refresh \",\n view.freshness.last?.status === \"failed\" ? \"failed\" : \"is overdue\",\n \"; the last good document is still shown.\"\n ] }) : null,\n !view && !failed ? /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: pageDescription(\"artifact\", productDescription) }) : null\n ] }),\n view ? /* @__PURE__ */ u3(ArtifactFrame, { view }, view.snapshotUrl) : failed ? /* @__PURE__ */ u3(\n StateBlock,\n {\n id: \"artifactError\",\n tone: \"error\",\n title: \"This artifact couldn't be opened\",\n action: { label: \"Retry\", onClick: () => void retryCollection() },\n children: state2.artifactNotice?.message\n }\n ) : /* @__PURE__ */ u3(StateBlock, { children: \"Loading artifact…\" })\n ] });\n }\n\n // src/operator-ui/app/credentials.tsx\n function CredentialForm({\n connector,\n credential,\n busy\n }) {\n const fields = credential.fields ?? [];\n const [values, setValues] = d2({});\n const single = fields.length === 0;\n const inputId = `credential-input-${connector}`;\n const submit = () => {\n if (single) {\n const value = (values.value ?? \"\").trim();\n if (!value) return refuseCredential(connector, \"Paste a credential before saving.\");\n return void saveCredential(connector, { value });\n }\n const entries = {};\n for (const field of fields) {\n const value = (values[field.name] ?? \"\").trim();\n if (!value) {\n return refuseCredential(\n connector,\n \"Complete every credential field before saving.\"\n );\n }\n entries[field.name] = value;\n }\n void saveCredential(connector, { values: entries });\n };\n return /* @__PURE__ */ u3(\"div\", { class: \"credential-form\", \"data-credential-form\": connector, children: [\n single ? /* @__PURE__ */ u3(S, { children: [\n /* @__PURE__ */ u3(\"label\", { class: \"visually-hidden\", for: inputId, children: credential.label }),\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: inputId,\n type: \"password\",\n \"aria-label\": credential.label,\n placeholder: credential.placeholder || \"Paste credential\",\n autocomplete: \"new-password\",\n autocapitalize: \"none\",\n spellcheck: false,\n value: values.value ?? \"\",\n onInput: (event) => setValues({ value: event.currentTarget.value })\n }\n )\n ] }) : /* @__PURE__ */ u3(\"div\", { class: \"credential-fields\", children: fields.map((field, index) => {\n const id = `credential-input-${connector}-${index}`;\n return /* @__PURE__ */ u3(\"div\", { class: \"credential-field\", children: [\n /* @__PURE__ */ u3(\"label\", { for: id, children: field.label }),\n /* @__PURE__ */ u3(\n \"input\",\n {\n id,\n type: field.inputType || \"password\",\n placeholder: field.placeholder || field.label,\n autocomplete: (field.inputType ?? \"password\") === \"password\" ? \"new-password\" : \"off\",\n autocapitalize: \"none\",\n spellcheck: false,\n value: values[field.name] ?? \"\",\n onInput: (event) => setValues({\n ...values,\n [field.name]: event.currentTarget.value\n })\n }\n )\n ] }, field.name);\n }) }),\n /* @__PURE__ */ u3(\"div\", { class: \"actions\", children: [\n /* @__PURE__ */ u3(\"button\", { class: \"btn primary\", type: \"button\", disabled: busy, onClick: submit, children: busy ? \"Saving…\" : \"Save\" }),\n /* @__PURE__ */ u3(\n \"button\",\n {\n class: \"btn quiet\",\n type: \"button\",\n disabled: busy,\n onClick: () => editCredential(null),\n children: \"Cancel\"\n }\n )\n ] })\n ] });\n }\n function CredentialCard({\n connector,\n credential,\n editing,\n busy,\n confirming,\n notice\n }) {\n const name = connector.title || connector.id;\n const configured = Boolean(credential.configured);\n const removable = configured || Boolean(credential.removable);\n return /* @__PURE__ */ u3(\n \"section\",\n {\n class: \"subcard\",\n id: `credential-${connector.id}`,\n \"aria-labelledby\": `credential-title-${connector.id}`,\n children: [\n /* @__PURE__ */ u3(\"div\", { class: \"subcard-head\", children: [\n /* @__PURE__ */ u3(\"h3\", { id: `credential-title-${connector.id}`, children: credential.label }),\n /* @__PURE__ */ u3(\"span\", { class: \"meta\", children: credentialStateLabel(credential) })\n ] }),\n credential.description ? /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: credential.description }) : null,\n credential.fields?.length ? /* @__PURE__ */ u3(\"div\", { class: \"credential-field-summary\", children: credential.fields.map((field) => /* @__PURE__ */ u3(\"div\", { children: [\n /* @__PURE__ */ u3(\"span\", { children: field.label }),\n /* @__PURE__ */ u3(\"span\", { class: \"meta\", children: field.configured ? `configured · ••••${field.lastFour ?? \"\"}${field.updatedAt ? ` · updated ${formatDate(field.updatedAt)}` : \"\"}` : \"not configured\" })\n ] }, field.name)) }) : null,\n credential.error ? /* @__PURE__ */ u3(\"p\", { class: \"msg\", children: credentialProblemCopy(credential.problem) }) : null,\n credential.error && credential.problem ? /* @__PURE__ */ u3(\n FixPrompt,\n {\n kind: credential.problem,\n connectorId: connector.id,\n name: connector.title || connector.id\n }\n ) : null,\n credential.notice ? /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: credential.notice }) : null,\n /* @__PURE__ */ u3(\"div\", { class: \"actions\", children: [\n /* @__PURE__ */ u3(\n \"button\",\n {\n class: removable ? \"btn\" : \"btn primary\",\n type: \"button\",\n \"aria-expanded\": editing ? \"true\" : \"false\",\n disabled: busy,\n onClick: () => editCredential(editing ? null : connector.id),\n children: removable ? \"Replace\" : \"Add credential\"\n }\n ),\n configured && credential.testable ? /* @__PURE__ */ u3(\n \"button\",\n {\n class: \"btn\",\n type: \"button\",\n disabled: busy,\n onClick: () => void testCredential(connector.id),\n children: busy ? \"Working…\" : \"Test\"\n }\n ) : null,\n removable ? /* @__PURE__ */ u3(\n \"button\",\n {\n id: `remove-credential-${connector.id}`,\n class: \"btn danger\",\n type: \"button\",\n disabled: busy,\n onClick: () => askConfirm(connector.id, \"credential_remove\"),\n children: \"Remove\"\n }\n ) : null\n ] }),\n confirming ? /* @__PURE__ */ u3(\n ConfirmBar,\n {\n id: connector.id,\n ...confirmCopy(\"credential_remove\", name),\n onConfirm: () => void removeCredential(connector.id),\n onCancel: () => cancelConfirm(\n focusableId(`remove-credential-${connector.id}`, `conn-toggle-${connector.id}`)\n )\n }\n ) : null,\n editing ? /* @__PURE__ */ u3(\n CredentialForm,\n {\n connector: connector.id,\n credential,\n busy\n }\n ) : null,\n /* @__PURE__ */ u3(NoticeLine, { id: `credentialNotice-${connector.id}`, notice })\n ]\n }\n );\n }\n\n // src/operator-ui/model.ts\n function filterUiConnectors(connectors, query) {\n const q2 = query.trim().toLowerCase();\n const filtered = [];\n for (const connector of connectors) {\n const connectorText = [\n connector.id,\n connector.title,\n connector.description,\n connector.status\n ].join(\" \").toLowerCase();\n const connectorMatches = Boolean(q2 && connectorText.includes(q2));\n const tools = connector.tools.filter(\n (tool) => !q2 || connectorMatches || `${tool.name} ${tool.description ?? \"\"}`.toLowerCase().includes(q2)\n );\n if (q2 && tools.length === 0 && !connectorMatches) continue;\n filtered.push({ connector, tools });\n }\n return filtered;\n }\n\n // src/operator-ui/setup-commands.ts\n function clientServerName(serverName, pool) {\n const base = (serverName ?? \"\").toLowerCase().replace(/[^a-z0-9_-]+/g, \"-\").replace(/-{2,}/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 48) || \"connecta\";\n return pool ? `${base}-${pool}` : base;\n }\n function poolEndpointUrl(mcpUrl2, pool) {\n return `${mcpUrl2.replace(/\\/+$/, \"\")}/${encodeURIComponent(pool)}`;\n }\n function shellWord(value) {\n return /^[A-Za-z0-9_./:@%+=,~-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\"'\"'`)}'`;\n }\n function clientSetupCommands(name, url) {\n return [\n {\n id: \"claude\",\n label: \"Claude Code\",\n text: `claude mcp add --transport http ${shellWord(name)} ${shellWord(url)}`\n },\n {\n id: \"codex\",\n label: \"Codex\",\n text: `codex mcp add ${shellWord(name)} --url ${shellWord(url)}`\n },\n {\n id: \"json\",\n label: \"JSON config\",\n text: JSON.stringify(\n { mcpServers: { [name]: { type: \"http\", url } } },\n null,\n 2\n )\n }\n ];\n }\n\n // src/operator-ui/app/connections.tsx\n var DRIFT_HEADING = {\n clean: \"Catalog drift · none\",\n warning: \"Catalog drift · review\",\n unavailable: \"Catalog drift · not observed\"\n };\n function DriftPanel({ connector }) {\n const drift = connector.catalogDrift;\n const state2 = driftState(drift);\n return /* @__PURE__ */ u3(S, { children: [\n /* @__PURE__ */ u3(\n \"div\",\n {\n id: `drift-${connector.id}`,\n class: `connector-drift ${state2}`,\n \"data-drift\": state2,\n children: [\n /* @__PURE__ */ u3(\"p\", { class: \"cap\", children: DRIFT_HEADING[state2] }),\n /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: driftSummary(drift) }),\n state2 === \"unavailable\" ? null : /* @__PURE__ */ u3(\"ul\", { class: \"drift-counts\", children: driftCounts(drift).map(({ key, label, count }) => /* @__PURE__ */ u3(\"li\", { class: count > 0 ? \"drift-count flagged\" : \"drift-count\", children: [\n /* @__PURE__ */ u3(\"span\", { class: \"drift-count-value\", children: count }),\n /* @__PURE__ */ u3(\"span\", { class: \"drift-count-label\", children: label })\n ] }, key)) })\n ]\n }\n ),\n state2 === \"warning\" ? /* @__PURE__ */ u3(\n FixPrompt,\n {\n kind: \"catalog_drift\",\n connectorId: connector.id,\n name: connector.title || connector.id\n }\n ) : null\n ] });\n }\n function SafetyBadge({ safety }) {\n const badge = safety ? TOOL_SAFETY_BADGE[safety] : void 0;\n if (!badge) return null;\n return /* @__PURE__ */ u3(\"span\", { class: \"tool-safety\", title: badge.title, \"data-safety\": safety, children: /* @__PURE__ */ u3(Badge, { tone: badge.tone, children: badge.label }) });\n }\n function Endpoint({\n url,\n name,\n label,\n primary\n }) {\n return /* @__PURE__ */ u3(\"div\", { class: \"endpoint-block\", \"data-endpoint\": name, children: [\n /* @__PURE__ */ u3(\"div\", { class: \"endpoint\", children: [\n label ? /* @__PURE__ */ u3(\"span\", { class: \"endpoint-label cap\", children: label }) : null,\n /* @__PURE__ */ u3(\"code\", { ...primary ? { id: \"mcpUrl\" } : {}, class: \"mono\", children: url }),\n /* @__PURE__ */ u3(\n CopyButton,\n {\n value: url,\n label: \"Copy URL\",\n ...label ? { ariaLabel: `Copy URL for ${label}` } : {}\n }\n )\n ] }),\n /* @__PURE__ */ u3(\"details\", { class: \"setup\", children: [\n /* @__PURE__ */ u3(\"summary\", { class: \"disclosure\", children: [\n \"Client setup\",\n label ? ` · ${label}` : \"\"\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"setup-list\", children: [\n clientSetupCommands(name, url).map((command) => /* @__PURE__ */ u3(\"div\", { class: \"setup-item\", \"data-setup\": command.id, children: [\n /* @__PURE__ */ u3(\"div\", { class: \"setup-head\", children: [\n /* @__PURE__ */ u3(\"span\", { class: \"cap\", children: command.label }),\n /* @__PURE__ */ u3(\n CopyButton,\n {\n value: command.text,\n label: \"Copy\",\n class: \"btn quiet\",\n ariaLabel: `Copy ${command.label} setup${label ? ` for ${label}` : \"\"}`\n }\n )\n ] }),\n /* @__PURE__ */ u3(\"pre\", { class: \"setup-code\", children: command.text })\n ] }, command.id)),\n /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: \"No token is included. Clients sign in through this deployment's inbound auth.\" })\n ] })\n ] })\n ] });\n }\n function AuthActions({\n connector,\n name,\n manage,\n state: state2\n }) {\n const id = connector.id;\n const authorization = safeHttpHref(connector.authorizationUrl);\n const busy = state2.oauthBusy === id;\n if (connector.status === \"loading\") return null;\n if (!connector.oauth || !manage) {\n return authorization && connector.status !== \"ok\" ? /* @__PURE__ */ u3(\"a\", { class: \"btn primary\", href: authorization, target: \"_blank\", rel: \"noopener noreferrer\", children: \"Authorize connector\" }) : null;\n }\n if (state2.oauthBlocked === id && authorization) {\n return /* @__PURE__ */ u3(\n \"a\",\n {\n id: `authorize-${id}`,\n class: \"btn primary\",\n href: authorization,\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n children: \"Open authorization page\"\n }\n );\n }\n if (connector.status !== \"ok\") {\n const needsAuth = connector.status === \"auth_required\";\n return /* @__PURE__ */ u3(\n \"button\",\n {\n type: \"button\",\n id: `connect-${id}`,\n class: needsAuth ? \"btn primary\" : \"btn\",\n \"aria-label\": `${needsAuth ? \"Connect\" : \"Reconnect\"} ${name}`,\n disabled: busy,\n onClick: () => void startOAuth(id, \"continue\"),\n children: busy ? \"Opening…\" : needsAuth ? \"Connect account\" : \"Reconnect\"\n }\n );\n }\n const switching = connector.authScope === \"personal\";\n return /* @__PURE__ */ u3(S, { children: [\n /* @__PURE__ */ u3(\n \"button\",\n {\n type: \"button\",\n id: `reconnect-${id}`,\n class: \"btn\",\n \"aria-label\": `${switching ? \"Switch account for\" : \"Reconnect\"} ${name}`,\n disabled: busy,\n onClick: () => askConfirm(id, \"oauth_restart\"),\n children: busy ? \"Working…\" : switching ? \"Switch account\" : \"Reconnect\"\n }\n ),\n /* @__PURE__ */ u3(\n \"button\",\n {\n type: \"button\",\n id: `disconnect-${id}`,\n class: \"btn danger\",\n \"aria-label\": `Disconnect ${name}`,\n disabled: busy,\n onClick: () => askConfirm(id, \"oauth_disconnect\"),\n children: \"Disconnect\"\n }\n )\n ] });\n }\n function ConnectorRow({\n connector,\n tools,\n forceOpen,\n state: state2\n }) {\n const [open, setOpen] = d2(false);\n const shown = open || forceOpen;\n const id = connector.id;\n const name = connector.title || id;\n const drift = driftState(connector.catalogDrift);\n const manage = Boolean(\n connector.permissions?.manageSharedAuth || connector.permissions?.connectPersonal\n );\n const local = state2.connectorFailures[id];\n const problem = connector.status === \"loading\" || local ? null : problemCopy(connector.problem);\n const confirming = state2.confirming?.connectorId === id ? state2.confirming : null;\n const oauthConfirm = confirming && confirming.action !== \"credential_remove\" ? confirming : null;\n const statusLabel = local ? \"Couldn't load\" : connectorStatusLabel(connector.status, connector.problem);\n const fixKind = problem && connector.problem && problemTone(connector.problem) === \"danger\" && connector.problem !== connector.credential?.problem ? connector.problem : null;\n const statusTone = local ? \"warn\" : connectorStatusTone(connector.status);\n return /* @__PURE__ */ u3(\"div\", { class: shown ? \"conn open\" : \"conn\", \"data-connector\": id, children: [\n /* @__PURE__ */ u3(\"div\", { class: \"conn-head\", children: [\n /* @__PURE__ */ u3(\"span\", { class: \"conn-main\", children: [\n /* @__PURE__ */ u3(\"span\", { class: `dot ${local ? \"warn\" : connector.status}`, \"aria-hidden\": \"true\" }),\n /* @__PURE__ */ u3(\"h2\", { class: \"conn-name\", children: /* @__PURE__ */ u3(\n \"button\",\n {\n type: \"button\",\n id: `conn-toggle-${id}`,\n class: \"conn-toggle\",\n \"aria-expanded\": shown ? \"true\" : \"false\",\n \"aria-controls\": `conn-body-${id}`,\n \"aria-describedby\": `conn-state-${id}`,\n onClick: () => setOpen(!shown),\n children: name\n }\n ) }),\n connector.title ? /* @__PURE__ */ u3(\"span\", { class: \"conn-id mono\", children: id }) : null\n ] }),\n /* @__PURE__ */ u3(\"span\", { class: \"conn-badges\", id: `conn-state-${id}`, children: [\n drift === \"warning\" ? /* @__PURE__ */ u3(Badge, { tone: \"warn\", children: \"drift\" }) : null,\n /* @__PURE__ */ u3(Badge, { children: authScopeLabel(connector.authScope) }),\n /* @__PURE__ */ u3(Badge, { children: connector.status === \"loading\" ? \"tools not loaded\" : toolCountLabel(connector.toolCount) }),\n /* @__PURE__ */ u3(Badge, { tone: statusTone, children: statusLabel }),\n /* @__PURE__ */ u3(\"span\", { class: \"conn-caret\", \"aria-hidden\": \"true\" })\n ] })\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"conn-body\", id: `conn-body-${id}`, hidden: !shown, children: [\n connector.description ? /* @__PURE__ */ u3(\"p\", { class: \"conn-note\", children: connector.description }) : null,\n problem && connector.problem ? /* @__PURE__ */ u3(\n \"p\",\n {\n class: problemTone(connector.problem) === \"warn\" ? \"msg warn\" : \"msg\",\n \"data-problem\": connector.problem,\n children: problem\n }\n ) : null,\n local ? /* @__PURE__ */ u3(\"p\", { class: \"msg warn\", \"data-load-failure\": local, children: connectorLoadFailureCopy(local, productName) }) : null,\n connector.authorizationUrl && !safeHttpHref(connector.authorizationUrl) ? /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: [\n \"Authorization URL: \",\n connector.authorizationUrl\n ] }) : null,\n manage ? null : /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: permissionLabel(connector) }),\n /* @__PURE__ */ u3(\"div\", { class: \"actions\", children: [\n local ? null : /* @__PURE__ */ u3(AuthActions, { connector, name, manage, state: state2 }),\n fixKind ? /* @__PURE__ */ u3(FixPromptButton, { kind: fixKind, connectorId: id, name }) : null,\n /* @__PURE__ */ u3(\n \"button\",\n {\n class: local ? \"btn primary\" : \"btn quiet\",\n type: \"button\",\n \"aria-label\": `Refresh ${name}`,\n disabled: connector.status === \"loading\",\n onClick: () => void refreshConnector(id),\n children: \"Refresh\"\n }\n )\n ] }),\n fixKind ? /* @__PURE__ */ u3(FixPromptPreview, { kind: fixKind, connectorId: id, standalone: true }) : null,\n oauthConfirm ? /* @__PURE__ */ u3(\n ConfirmBar,\n {\n id,\n ...confirmCopy(oauthConfirm.action, name),\n onConfirm: () => {\n if (oauthConfirm.action === \"oauth_restart\") void startOAuth(id, \"restart\");\n else void disconnectOAuth(id);\n },\n onCancel: () => cancelConfirm(\n focusableId(\n oauthConfirm.action === \"oauth_restart\" ? `reconnect-${id}` : `disconnect-${id}`,\n `conn-toggle-${id}`\n )\n )\n }\n ) : null,\n /* @__PURE__ */ u3(\n NoticeLine,\n {\n id: `oauthNotice-${id}`,\n notice: state2.oauthNoticeFor === id ? state2.oauthNotice : null\n }\n ),\n connector.credential ? /* @__PURE__ */ u3(\n CredentialCard,\n {\n connector,\n credential: connector.credential,\n editing: state2.credentialEditing === id,\n busy: state2.credentialBusy === id,\n confirming: confirming?.action === \"credential_remove\",\n notice: state2.credentialNoticeFor === id ? state2.credentialNotice : null\n }\n ) : null,\n tools.length ? /* @__PURE__ */ u3(\"details\", { open: forceOpen, children: [\n /* @__PURE__ */ u3(\"summary\", { class: \"disclosure\", children: [\n \"Tools (\",\n tools.length,\n \")\"\n ] }),\n /* @__PURE__ */ u3(\"div\", { class: \"tool-list\", children: [\n tools.some((tool) => tool.safety) ? /* @__PURE__ */ u3(\"p\", { class: \"meta tool-legend\", children: \"Read-only tools run inside execute_code programs. Everything else asks the host first: a program pauses for resume_execution, and a direct call goes through call_destructive_tool — unless this deployment's config exempts the tool, in which case programs call it unasked.\" }) : null,\n tools.map((tool) => /* @__PURE__ */ u3(\"div\", { class: \"tool\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"tool-head\", children: [\n /* @__PURE__ */ u3(\"code\", { children: tool.address }),\n /* @__PURE__ */ u3(SafetyBadge, { safety: tool.safety })\n ] }),\n tool.description ? /* @__PURE__ */ u3(\"span\", { class: \"td\", children: tool.description }) : null\n ] }, tool.address))\n ] })\n ] }) : null,\n /* @__PURE__ */ u3(\"details\", { children: [\n /* @__PURE__ */ u3(\"summary\", { class: \"disclosure\", children: \"Diagnostics\" }),\n /* @__PURE__ */ u3(\"div\", { class: \"subcard\", children: [\n /* @__PURE__ */ u3(DriftPanel, { connector }),\n connector.catalogAccess ? /* @__PURE__ */ u3(\"p\", { class: \"meta\", children: [\n \"Agents last read its catalog\",\n \" \",\n connector.catalogAccess.state === \"stale\" ? \"from a stale cache\" : \"fresh\",\n \" · \",\n formatDate(connector.catalogAccess.observedAt)\n ] }) : null\n ] })\n ] })\n ] })\n ] });\n }\n function Endpoints({\n pools,\n serverName\n }) {\n return /* @__PURE__ */ u3(\"div\", { class: \"endpoints\", children: [\n /* @__PURE__ */ u3(\n Endpoint,\n {\n url: mcpUrl,\n name: clientServerName(serverName),\n primary: true,\n ...pools.length ? { label: \"All tools\" } : {}\n }\n ),\n pools.map((pool) => /* @__PURE__ */ u3(\n Endpoint,\n {\n url: poolEndpointUrl(mcpUrl, pool),\n name: clientServerName(serverName, pool),\n label: `Pool · ${pool}`\n },\n pool\n ))\n ] });\n }\n function SummaryLine({ connectors }) {\n const parts = connectorSummaryParts(summarizeConnectors(connectors));\n return /* @__PURE__ */ u3(\"p\", { class: \"summary\", id: \"connectorSummary\", children: parts.map((part, index) => /* @__PURE__ */ u3(\"span\", { children: [\n index > 0 ? /* @__PURE__ */ u3(\"span\", { class: \"sep\", children: \" · \" }) : null,\n /* @__PURE__ */ u3(\"span\", { class: part.tone === \"neutral\" ? \"\" : part.tone, children: part.text })\n ] }, part.text)) });\n }\n function ConnectionsPage({ state: state2 }) {\n const data = state2.data;\n const query = state2.connectorFilter.trim();\n const filtered = data ? filterUiConnectors(data.connectors, query) : [];\n return /* @__PURE__ */ u3(\"section\", { id: \"connectionsView\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"lead\", children: [\n /* @__PURE__ */ u3(\"h1\", { id: \"connectionsHeading\", tabIndex: -1, children: \"Connections\" }),\n /* @__PURE__ */ u3(\"div\", { class: \"lead-copy\", children: [\n /* @__PURE__ */ u3(\"p\", { children: \"Point an MCP client at this endpoint to reach the tools below.\" }),\n /* @__PURE__ */ u3(Endpoints, { pools: data?.pools ?? [], serverName: data?.serverInfo?.name }),\n /* @__PURE__ */ u3(\"p\", { class: \"cap\", id: \"serverInfo\", children: data ? `${data.serverInfo?.name || productName} v${data.connectaVersion || \"?\"}` : productOperatorLabel }),\n data ? /* @__PURE__ */ u3(SummaryLine, { connectors: data.connectors }) : null\n ] })\n ] }),\n /* @__PURE__ */ u3(\"section\", { class: \"section\", \"aria-labelledby\": \"connectorLedgerHeading\", children: [\n /* @__PURE__ */ u3(\"div\", { class: \"section-head\", children: [\n /* @__PURE__ */ u3(\"h2\", { id: \"connectorLedgerHeading\", tabIndex: -1, children: \"Connectors\" }),\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: \"filter\",\n type: \"search\",\n class: \"filter\",\n placeholder: \"Filter connectors or tools…\",\n \"aria-label\": \"Filter connectors or tools\",\n value: state2.connectorFilter,\n disabled: !data,\n onInput: (event) => setConnectorFilter(event.currentTarget.value)\n }\n )\n ] }),\n /* @__PURE__ */ u3(\n \"div\",\n {\n id: \"list\",\n class: !data || filtered.length === 0 ? \"\" : \"rows\",\n \"aria-busy\": state2.refreshing || !data && !state2.loadFailure ? \"true\" : \"false\",\n children: !data ? state2.loadFailure ? /* @__PURE__ */ u3(LoadFailure, { state: state2 }) : /* @__PURE__ */ u3(Empty, { children: \"Loading connectors…\" }) : filtered.length === 0 ? /* @__PURE__ */ u3(Empty, { children: query ? \"No connectors or tools match this filter.\" : \"No connectors are declared in this deployment.\" }) : filtered.map(({ connector, tools }) => /* @__PURE__ */ u3(\n ConnectorRow,\n {\n connector,\n tools,\n forceOpen: Boolean(query),\n state: state2\n },\n connector.id\n ))\n }\n )\n ] })\n ] });\n }\n\n // src/operator-ui/app/main.tsx\n function useOperatorState() {\n const [, bump] = y2((count) => count + 1, 0);\n const snapshot = getState();\n _2(() => {\n const unsubscribe = subscribe(() => bump(void 0));\n if (getState() !== snapshot) bump(void 0);\n return unsubscribe;\n }, []);\n return snapshot;\n }\n function visiblePages(state2) {\n const hint = state2.data ? null : navHint();\n return OPERATOR_PAGES.filter((page) => {\n if (page === \"tokens\") return state2.data?.accessTokenManagement === \"available\";\n if (page === \"activity\") return hint ? hint.activity : Boolean(state2.data?.activityEnabled);\n if (page === \"artifacts\") {\n return isArtifactPage(state2.page) || (hint ? hint.artifacts : Boolean(state2.data?.artifactsEnabled));\n }\n return true;\n });\n }\n function OperatorNav() {\n const state2 = useOperatorState();\n if (state2.session !== \"ready\") return null;\n const onArtifactPage = isArtifactPage(state2.page);\n return /* @__PURE__ */ u3(\"div\", { class: \"mast-actions\", children: [\n /* @__PURE__ */ u3(\"nav\", { class: \"page-nav\", \"aria-label\": \"Operator pages\", children: visiblePages(state2).map(\n (page) => (\n // Crossing between artifact pages and the rest is a full navigation:\n // with a dedicated artifact origin, the two live on different hosts.\n page === \"artifacts\" || onArtifactPage ? /* @__PURE__ */ u3(\n \"a\",\n {\n class: \"navlink\",\n href: page === \"artifacts\" ? PAGE_META.artifacts.path : new URL(PAGE_META[page].path, new URL(homeUrl, window.location.href)).href,\n ...state2.page === page ? { \"aria-current\": \"page\" } : {},\n children: PAGE_META[page].label\n },\n page\n ) : /* @__PURE__ */ u3(\n PageLink,\n {\n page,\n class: \"navlink\",\n current: state2.page === page,\n children: PAGE_META[page].label\n },\n page\n )\n )\n ) }),\n /* @__PURE__ */ u3(\"div\", { class: \"session-actions\", \"aria-label\": \"Session actions\", children: auth.kind === \"clerk\" || auth.kind === \"cloudflare-access\" ? /* @__PURE__ */ u3(\"button\", { class: \"navlink\", type: \"button\", onClick: signOut, children: \"Sign out\" }) : /* @__PURE__ */ u3(\"button\", { class: \"navlink\", type: \"button\", onClick: forgetBearer, children: \"Change token\" }) })\n ] });\n }\n function Gate({ state: state2 }) {\n const [token, setToken] = d2(\"\");\n const signedIn = auth.kind === \"clerk\" && Boolean(window.Clerk?.user);\n const loading = state2.session === \"loading\";\n return /* @__PURE__ */ u3(\"section\", { id: \"gate\", class: \"gate lead\", \"aria-busy\": loading ? \"true\" : \"false\", children: [\n /* @__PURE__ */ u3(\"h1\", { id: \"gateHeading\", tabIndex: -1, children: PAGE_META[state2.page].label }),\n /* @__PURE__ */ u3(\"div\", { class: \"lead-copy\", children: [\n /* @__PURE__ */ u3(\"p\", { children: pageDescription(state2.page, productDescription) }),\n loading ? /* @__PURE__ */ u3(StateBlock, { id: \"gateCopy\", children: checkingCopy(state2.page) }) : /* @__PURE__ */ u3(\"p\", { id: \"gateCopy\", class: \"meta\", children: gateCopy(auth.kind, signedIn) }),\n loading ? null : auth.kind === \"clerk\" ? /* @__PURE__ */ u3(\"div\", { id: \"clerkGate\", class: \"actions\", children: signedIn ? /* @__PURE__ */ u3(\"button\", { class: \"btn\", type: \"button\", onClick: signOut, children: \"Sign out\" }) : /* @__PURE__ */ u3(\"button\", { id: \"signin\", class: \"btn primary\", type: \"button\", onClick: signIn, children: \"Team sign in\" }) }) : auth.kind === \"cloudflare-access\" ? /* @__PURE__ */ u3(\"div\", { class: \"actions\", children: /* @__PURE__ */ u3(\"button\", { class: \"btn\", type: \"button\", onClick: signOut, children: \"Sign out of Cloudflare Access\" }) }) : /* @__PURE__ */ u3(\n \"form\",\n {\n id: \"tokenGate\",\n class: \"row gate-form\",\n onSubmit: (event) => {\n event.preventDefault();\n const value = token.trim();\n if (!value) return;\n setToken(\"\");\n signInWithBearer(value);\n },\n children: [\n /* @__PURE__ */ u3(\n \"input\",\n {\n id: \"token\",\n type: \"password\",\n placeholder: \"Bearer token\",\n autocomplete: \"off\",\n \"aria-label\": \"Bearer token\",\n value: token,\n onInput: (event) => setToken(event.currentTarget.value)\n }\n ),\n /* @__PURE__ */ u3(\"button\", { id: \"save\", class: \"btn primary\", type: \"submit\", children: \"Open operator pages\" })\n ]\n }\n ),\n /* @__PURE__ */ u3(NoticeLine, { id: \"err\", notice: state2.gate, className: \"\" })\n ] })\n ] });\n }\n function CurrentPage({ state: state2 }) {\n if (state2.page === \"tokens\") return /* @__PURE__ */ u3(TokensPage, { state: state2 });\n if (state2.page === \"activity\") return /* @__PURE__ */ u3(ActivityPage, { state: state2 });\n if (state2.page === \"artifacts\") return /* @__PURE__ */ u3(ArtifactsPage, { state: state2 });\n if (state2.page === \"artifact\") return /* @__PURE__ */ u3(ArtifactPage, { state: state2 });\n return /* @__PURE__ */ u3(ConnectionsPage, { state: state2 });\n }\n function OperatorApp() {\n const state2 = useOperatorState();\n const ready = state2.session === \"ready\";\n h2(() => {\n const label = state2.page === \"artifact\" && state2.artifactView ? state2.artifactView.title : PAGE_META[state2.page].label;\n document.title = `${label} — ${titleSuffix}`;\n }, [state2.page, state2.artifactView]);\n h2(() => {\n if (!ready) return;\n if (state2.page === \"tokens\" && state2.data?.accessTokenManagement === \"available\" && state2.tokenPhase === \"idle\") {\n void loadAccessTokens();\n }\n if (state2.page === \"activity\" && state2.data?.activityEnabled && state2.activityPhase === \"idle\") {\n void loadActivity(true);\n }\n });\n h2(() => {\n if (!state2.pendingFocus && !state2.focusIfLost) return;\n if (state2.pendingFocus) {\n document.getElementById(state2.pendingFocus)?.focus();\n } else if (state2.focusIfLost) {\n const active = document.activeElement;\n const lost = !active || active === document.body || !active.isConnected || active.disabled === true;\n if (lost) document.getElementById(state2.focusIfLost)?.focus();\n }\n focusHandled();\n }, [state2.pendingFocus, state2.focusIfLost]);\n return ready ? /* @__PURE__ */ u3(\"div\", { id: \"app\", children: /* @__PURE__ */ u3(CurrentPage, { state: state2 }) }) : /* @__PURE__ */ u3(Gate, { state: state2 });\n }\n function mount(id, view) {\n const host = document.getElementById(id);\n if (!host) return;\n host.textContent = \"\";\n R(view, host);\n }\n mount(\"operatorNav\", /* @__PURE__ */ u3(OperatorNav, {}));\n mount(\"operatorContent\", /* @__PURE__ */ u3(OperatorApp, {}));\n void boot();\n})();\n"; diff --git a/src/operator-ui/model.ts b/src/operator-ui/model.ts index ba6f69e8..71bc68e2 100644 --- a/src/operator-ui/model.ts +++ b/src/operator-ui/model.ts @@ -126,6 +126,7 @@ export type CredentialManagementCapability = | "no_slots"; export interface UiData { + accessTokenManagement?: "available" | "requires_operator"; serverInfo: { name: string; version: string }; /** Version of the installed @zackbart/connecta package. */ connectaVersion: string; diff --git a/src/operator-ui/view.ts b/src/operator-ui/view.ts index af54b4a0..f05686ab 100644 --- a/src/operator-ui/view.ts +++ b/src/operator-ui/view.ts @@ -20,6 +20,7 @@ import type { FixPromptKind } from "./fix-prompts.js"; */ export type OperatorPage = + | "tokens" | "connections" | "activity" | "artifacts" @@ -28,6 +29,7 @@ export type OperatorPage = /** Pages the nav lists. A single artifact is reached from the library, not the nav. */ export const OPERATOR_PAGES: readonly OperatorPage[] = [ "connections", + "tokens", "activity", "artifacts", ]; @@ -35,6 +37,7 @@ export const OPERATOR_PAGES: readonly OperatorPage[] = [ export const PAGE_META: Readonly< Record > = { + tokens: { path: "/tokens", label: "Access tokens" }, connections: { path: "/", label: "Connections" }, activity: { path: "/activity", label: "Activity" }, artifacts: { path: "/artifacts", label: "Artifacts" }, @@ -371,7 +374,27 @@ export function confirmCopy( */ type LoadPhase = "idle" | "loading" | "ready" | "error"; +export interface UiAccessToken { + id: string; + name: string; + tokenPrefix: string; + createdAt: string; + revokedAt?: string; +} + +export function accessTokenUnavailableCopy(capability?: string): string { + return capability === undefined + ? "Access tokens are not configured for this deployment." + : "Token management requires an interactive sign-in and explicit permission."; +} + export interface OperatorState { + tokenPhase: LoadPhase; + tokenNotice: Notice | null; + tokens: UiAccessToken[]; + createdToken: string | null; + tokenRenaming: string | null; + tokenBusy: boolean; page: OperatorPage; /** * Bumped by every identity change. Async work captures it before awaiting and @@ -472,6 +495,12 @@ function identityScopedState() { credentialNoticeFor: null, credentialEditing: null, credentialBusy: null, + tokenPhase: "idle" as LoadPhase, + tokenNotice: null, + tokens: [], + createdToken: null, + tokenRenaming: null, + tokenBusy: false, activityPhase: "idle" as LoadPhase, activityNotice: null, activityEvents: [], @@ -520,6 +549,9 @@ export function withPage( return { ...state, page, + createdToken: null, + tokenRenaming: null, + tokenNotice: null, credentialEditing: null, credentialNotice: null, credentialNoticeFor: null, diff --git a/src/routes/access-tokens.ts b/src/routes/access-tokens.ts new file mode 100644 index 00000000..e4162322 --- /dev/null +++ b/src/routes/access-tokens.ts @@ -0,0 +1,138 @@ +import type { AccessTokenManager } from "../access-tokens.js"; +import { + authorizeUiIdentity, + isSameOrigin, + privateJson, + type RouteContext, +} from "./shared.js"; + +async function readName( + request: Request, +): Promise< + { ok: true; name: unknown } | { ok: false; response: Response } +> { + if ( + !request.headers + .get("content-type") + ?.toLowerCase() + .startsWith("application/json") + ) { + return { + ok: false, + response: privateJson( + { error: "Content-Type must be application/json" }, + { status: 415 }, + ), + }; + } + const reader = request.body?.getReader(); + const chunks: Uint8Array[] = []; + let size = 0; + if (reader) { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > 1_000) { + reader.cancel().catch(() => {}); + return { ok: false, response: privateJson({ error: "request body is too large" }, { status: 413 }) }; + } + chunks.push(value); + } + } finally { reader.releaseLock(); } + } + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } + const raw = new TextDecoder().decode(bytes); + if (raw.length > 1_000) { + return { + ok: false, + response: privateJson( + { error: "request body is too large" }, + { status: 413 }, + ), + }; + } + try { + const body = JSON.parse(raw) as { name?: unknown }; + return { ok: true, name: body.name }; + } catch { + return { + ok: false, + response: privateJson({ error: "invalid JSON body" }, { status: 400 }), + }; + } +} + +/** + * Interactive-operator lifecycle for deployment access tokens. The token itself + * is deliberately never an administrator credential and cannot reach here. + */ +export async function routeAccessTokens( + context: RouteContext, + manager: AccessTokenManager, +): Promise { + const match = + /^\/ui\/access-tokens(?:\/([0-9a-f-]{36}))?$/.exec(context.path); + if (!match) return null; + const { request, baseUrl, opts } = context; + if (request.method === "OPTIONS") { + return privateJson({ error: "CORS is not allowed" }, { status: 403 }); + } + const mutating = request.method !== "GET"; + if (mutating && !isSameOrigin(request, baseUrl)) { + return privateJson( + { error: "same-origin request required" }, + { status: 403 }, + ); + } + const admin = await authorizeUiIdentity( + request, + baseUrl, + opts.auth, + "access token management", + context.runtimeContext, + opts.identity, + ); + if (!admin.ok) return admin.response; + if (!admin.accessTokenManagement || !admin.identity.principal) { + return privateJson({ error: "access token management permission required" }, { status: 403 }); + } + + const id = match[1]; + try { + if (!id && request.method === "GET") { + return privateJson({ accessTokens: await manager.list() }); + } + if (!id && request.method === "POST") { + const input = await readName(request); + if (!input.ok) return input.response; + return privateJson( + await manager.create( + input.name, + admin.identity.principal, + ), + { status: 201 }, + ); + } + if (id && request.method === "PUT") { + const input = await readName(request); + if (!input.ok) return input.response; + const accessToken = await manager.rename(id, input.name); + return accessToken + ? privateJson({ accessToken }) + : privateJson({ error: "unknown access token" }, { status: 404 }); + } + if (id && request.method === "DELETE") { + const accessToken = await manager.revoke(id, `${admin.identity.principal.namespace}:${admin.identity.principal.id}`); + return accessToken + ? privateJson({ accessToken }) + : privateJson({ error: "unknown access token" }, { status: 404 }); + } + return privateJson({ error: "method not allowed" }, { status: 405 }); + } catch { + return privateJson({ error: "Access token operation failed; check the name, capacity, and storage" }, { status: 400 }); + } +} diff --git a/src/routes/shared.ts b/src/routes/shared.ts index 17eb075a..5d63c4b2 100644 --- a/src/routes/shared.ts +++ b/src/routes/shared.ts @@ -1,5 +1,6 @@ import type { Implementation } from "@modelcontextprotocol/server"; import type { + AccessTokensModule, ActivityModule, ArtifactsModule, OperatorSurface, @@ -77,6 +78,7 @@ export interface ServerOptions { activityModule?: ActivityModule | undefined; /** Optional team pages; their routes mount only beside `ui`. */ artifactsModule?: ArtifactsModule | undefined; + accessTokens?: AccessTokensModule | undefined; /** Optional browser UI and OAuth result-page labels. */ branding?: ConnectaBranding | undefined; } @@ -157,6 +159,7 @@ export async function authorize( /** Granted addresses that still require the catalog's read-only hint. */ guardedToolAccess?: ToolAccess; operator: boolean; + accessTokenManagement: boolean; credentialAdministration: ConnectorPermission; personalConnection: ConnectorPermission; /** Backward-compatible name used by operator views. */ @@ -179,7 +182,7 @@ export async function authorize( response: privateJson({ error: "identity access resolution failed" }, { status: 403 }), }; } - return { ok: true, actor, identity, ...access, operator: false, credentialAdministration: "none", personalConnection: "none" }; + return { ok: true, actor, identity, ...access, operator: false, accessTokenManagement: false, credentialAdministration: "none", personalConnection: "none" }; } let lastResponse: Response | null = null; for (const provider of auth) { @@ -209,6 +212,7 @@ export async function authorize( interactive, }; let operator = interactive; + let accessTokenManagement = false; let credentialAdministration: ConnectorPermission = "none"; let personalConnection: ConnectorPermission = "none"; let access: ConnectorAccess; @@ -223,6 +227,8 @@ export async function authorize( { allowReadOnly: true }, ); if (interactive) { + accessTokenManagement = identityConfig?.accessTokenManagement ? await identityConfig.accessTokenManagement(identity) : false; + if (typeof accessTokenManagement !== "boolean") throw new Error("invalid token management permission"); credentialAdministration = identityConfig?.credentialAdministration ? await identityConfig.credentialAdministration(identity) : "none"; personalConnection = principal && identityConfig?.personalConnection ? await identityConfig.personalConnection(identity) : "none"; } @@ -250,6 +256,7 @@ export async function authorize( ? { principalKey: await identityStorageKey(principal) } : {}), ...access, + accessTokenManagement, credentialAdministration, personalConnection, operator, diff --git a/src/routes/ui.ts b/src/routes/ui.ts index e5b6246f..597e163b 100644 --- a/src/routes/ui.ts +++ b/src/routes/ui.ts @@ -100,6 +100,7 @@ export async function routeUi( if ( operatorPage && (operatorPage !== "activity" || opts.activity?.list) && + (operatorPage !== "tokens" || opts.accessTokens) && (!artifactPage || opts.artifactsModule) ) { if (request.method !== "GET" && request.method !== "HEAD") { @@ -241,6 +242,7 @@ function summary(context: RouteContext): Effect.Effect { serverInfo: opts.serverInfo, connectaVersion: CONNECTA_VERSION, activityEnabled, + ...(opts.accessTokens ? { accessTokenManagement: authz.accessTokenManagement && authz.identity.principal ? "available" : "requires_operator" } : {}), ...(opts.artifactsModule && mayViewArtifacts(authz, opts.registry) ? { artifactsEnabled: true } : {}), credentialManagement, oauthManagement: visible.some(c => mayManage(c.id)), diff --git a/src/server.ts b/src/server.ts index 511e60c0..8b356cc8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -108,6 +108,7 @@ export function createFetchHandler( "/health", ...(opts.ui?.reservedPaths ?? []), ...(opts.ui && opts.activity?.list ? ["/activity"] : []), + ...(opts.ui && opts.accessTokens ? ["/tokens"] : []), ...(opts.ui && opts.artifactsModule ? ["/artifacts", "/artifacts/*"] : []), ], }, diff --git a/src/ui.ts b/src/ui.ts index 5064be8f..cc53c091 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -50,6 +50,7 @@ function stringForInlineScript(value: string): string { } export type OperatorPage = + | "tokens" | "connections" | "activity" | "artifacts" @@ -57,6 +58,7 @@ export type OperatorPage = const OPERATOR_PAGE_LABELS: Readonly> = { connections: "Connections", + tokens: "Access tokens", activity: "Activity", artifacts: "Artifacts", artifact: "Artifact", @@ -66,6 +68,7 @@ const OPERATOR_PAGE_LABELS: Readonly> = { const ARTIFACT_PAGE = /^\/artifacts\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?(?:\/v\/\d{1,9})?$/; export function operatorPageForPath(path: string): OperatorPage | undefined { + if (path === "/tokens") return "tokens"; if (path === "/") return "connections"; if (path === "/activity") return "activity"; if (path === "/artifacts") return "artifacts"; @@ -268,7 +271,7 @@ ${OPERATOR_UI_SCRIPT}`, } function ownsOperatorPath(reserved: readonly string[], path: string): boolean { - if (path === "/activity") return true; + if (path === "/activity" || path === "/tokens") return true; return reserved.some((pattern) => pattern.endsWith("/*") ? path.startsWith(pattern.slice(0, -1)) @@ -302,6 +305,8 @@ export function operatorUi( return (await artifacts?.handle(context)) ?? notFoundResponse(context.request, context.opts); } + const tokenResponse = await context.opts.accessTokens?.handle(context); + if (tokenResponse) return tokenResponse; const routes = [ ...(context.opts.credentialVault ? [routeCredentials] : []), routeOAuthManagement, diff --git a/src/version.ts b/src/version.ts index d4e907d3..d12961f3 100644 --- a/src/version.ts +++ b/src/version.ts @@ -4,4 +4,4 @@ * a bump that forgets this file fails the build rather than shipping a stale * version to `/health` and to downstream MCP handshakes. */ -export const CONNECTA_VERSION = "0.26.1"; +export const CONNECTA_VERSION = "0.26.2"; diff --git a/templates/node/README.md b/templates/node/README.md index 6aa77d56..0b9e06a6 100644 --- a/templates/node/README.md +++ b/templates/node/README.md @@ -179,3 +179,14 @@ Doctor verifies the MCP contract. Verify UI behavior separately: sign in at `/`, confirm the visible connections and their permitted auth controls, and check Activity only when you enabled a readable history store. A missing optional feature should not leave a tab behind. + +### Existing client tokens + +Upgrading from v0.23 does not require rotating managed `cta_…` tokens. Import +`accessTokens` from `@zackbart/connecta/auth/access-tokens`, replace the old +`accessTokens: true` with `accessTokens: accessTokens(storage)`, and keep the +same persistent storage namespace and identity/tool/pool grant rules. Enable +`identity.accessTokenManagement` only for the interactive operators who should +manage tokens. New issuance needs storage with atomic `compareAndSet`; older +storage adapters can still verify existing tokens. See the package's +`documentation/auth.md` for the migration and storage requirements. diff --git a/templates/node/package.json b/templates/node/package.json index f5ff8ef9..df217f63 100644 --- a/templates/node/package.json +++ b/templates/node/package.json @@ -15,7 +15,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@zackbart/connecta": "0.26.1", + "@zackbart/connecta": "0.26.2", "quickjs-emscripten": "0.32.0" }, "devDependencies": { diff --git a/templates/node/src/index.ts b/templates/node/src/index.ts index 44cb5636..20b3d700 100644 --- a/templates/node/src/index.ts +++ b/templates/node/src/index.ts @@ -27,6 +27,7 @@ import { operatorUi } from "@zackbart/connecta/ui"; import { api, createConnecta } from "@zackbart/connecta"; import { fileStorage, listen } from "@zackbart/connecta/node"; import { quickJsExecutor } from "@zackbart/connecta/quickjs"; +// import { accessTokens } from "@zackbart/connecta/auth/access-tokens"; // import { artifacts, kvArtifactStore } from "@zackbart/connecta/artifacts"; // Operator sign-in. Needs `npm install @clerk/backend` — it is an optional // peer, so it does not install with Connecta. @@ -63,6 +64,8 @@ const storage = fileStorage(stateFile); const connecta = createConnecta({ storage, + // Reuse this storage to keep v0.23 client tokens without rotating secrets. + // accessTokens: accessTokens(storage), auth: [ bearerToken(token, { subjectId: "operator" }), // clerkAuth({ @@ -74,7 +77,7 @@ const connecta = createConnecta({ // // allowedDomains: ["acme.com"], // }), ], - // Code-owned identity resolvers. The two management permissions default to + // Code-owned identity resolvers. Connection management permissions default to // none, so the template grants them. The commented pair is the optional // member/operator split for Clerk-backed deployments: connector access is // derived from the authenticated identity and cannot be selected by an MCP @@ -86,6 +89,7 @@ const connecta = createConnecta({ // connectorAccess: ({ principal }) => // principal?.id === "user_admin" ? "all" : ["time"], // activityAccess: ({ id }) => id === "user_admin", + // accessTokenManagement: ({ principal }) => principal?.id === "user_admin", }, publicUrl, // Required: model-written programs run in a bounded QuickJS child. diff --git a/test/access-tokens.test.ts b/test/access-tokens.test.ts new file mode 100644 index 00000000..f292e6cb --- /dev/null +++ b/test/access-tokens.test.ts @@ -0,0 +1,203 @@ +import { describe, expect, it } from "vitest"; +import { accessTokens, AccessTokenManager } from "../src/access-tokens.js"; +import { CredentialVault } from "../src/credentials.js"; +import { bearerToken } from "../src/auth/bearer.js"; +import { memoryStorage } from "../src/storage/memory.js"; +import { authorize } from "../src/routes/shared.js"; +import { createTestConnecta } from "./helpers.js"; +import { calcApi, fakeClerkAuth, mcpRpc, readJsonRpc } from "./fixtures/http.js"; +import fixture from "./fixtures/access-tokens-v023.json"; +import type { KVStorage } from "../src/types.js"; + +const BASE = "https://connecta.test"; +const owner = { namespace: "clerk:test", id: "owner" }; +const human = { ...fakeClerkAuth({ userId: "owner" }), activityActorNamespace: owner.namespace }; +const recordKey = `access-token:v1:record:${fixture.bound.accessToken.id}`; +function request(token: string, path = "/mcp", method = "GET", body?: unknown) { + return new Request(BASE + path, { method, headers: { + Authorization: `Bearer ${token}`, Origin: BASE, "Content-Type": "application/json", + }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }); +} +async function legacyStorage() { + const storage = memoryStorage(); + for (const [key, value] of Object.entries(fixture.records)) await storage.set(key, value); + return storage; +} + +describe("v0.23 client-token migration", () => { + it("admits the original secrets with unchanged identity, labels and no storage rewrite", async () => { + const storage = await legacyStorage(); + const module = accessTokens(storage); + for (const entry of [fixture.bound, fixture.unbound]) { + const authz = await authorize(request(entry.token), BASE, [module.auth]); + expect(authz.ok).toBe(true); + if (!authz.ok) throw new Error("not admitted"); + expect(authz.actor).toEqual({ kind: "access_token", id: entry.accessToken.id, namespace: "connecta:access-tokens:v1" }); + expect(authz.identity.interactive).toBe(false); + expect(authz.identity.principal).toEqual(entry === fixture.bound ? owner : undefined); + expect(authz.accessTokenManagement).toBe(false); + expect(authz.credentialAdministration).toBe("none"); + expect(await module.auth.activityActorLabel?.(entry.accessToken.id)).toBe(entry.accessToken.name); + } + expect(await storage.list!("access-token:")).toEqual(Object.keys(fixture.records).sort()); + for (const [key, value] of Object.entries(fixture.records)) expect(await storage.get(key)).toBe(value); + }); + + it("verifies old tokens on adapters without compareAndSet, but refuses new issuance", async () => { + const base = await legacyStorage(); + const storage: KVStorage = { get: base.get, set: base.set, delete: base.delete, list: base.list! }; + const manager = new AccessTokenManager(storage); + expect((await manager.auth.authorize(request(fixture.bound.token), BASE)).ok).toBe(true); + await expect(manager.create("new", owner)).rejects.toThrow("compareAndSet"); + }); + + it.each([fixture.revoked.token, "cta_" + "x".repeat(43), "cta_bad", "", "other-secret"])("refuses revoked, unknown and malformed credentials: %s", async token => { + const module = accessTokens(await legacyStorage()); + expect((await module.auth.authorize(request(token), BASE)).ok).toBe(false); + }); + + it.each([ + "not json", "null", + JSON.stringify({ ...JSON.parse(fixture.records[recordKey as keyof typeof fixture.records]), id: fixture.unbound.accessToken.id }), + JSON.stringify({ ...JSON.parse(fixture.records[recordKey as keyof typeof fixture.records]), revokedAt: "" }), + JSON.stringify({ ...JSON.parse(fixture.records[recordKey as keyof typeof fixture.records]), principal: { id: "owner" } }), + JSON.stringify({ ...JSON.parse(fixture.records[recordKey as keyof typeof fixture.records]), tokenHash: "0".repeat(64) }), + ])("fails closed on corrupt records", async raw => { + const storage = await legacyStorage(); + await storage.set(recordKey, raw); + expect((await accessTokens(storage).auth.authorize(request(fixture.bound.token), BASE)).ok).toBe(false); + }); + + it("applies current tool and pool grants to the stored principal on every request", async () => { + const storage = await legacyStorage(); + let allowed = true; + const app = createTestConnecta({ connectors: [calcApi()], accessTokens: accessTokens(storage), + identity: { connectorAccess: ({ principal }) => allowed && principal?.id === "owner" ? ["calc.add"] : [] }, + pools: { desktop: { tools: ["calc.add"], grant: ({ principal }) => allowed && principal?.id === "owner" } }, + }); + try { + const list = await readJsonRpc(await mcpRpc(app, "tools/list", {}, { token: fixture.bound.token })); + expect(list.result.tools).toHaveLength(8); + const call = () => mcpRpc(app, "tools/call", { name: "call_tool", arguments: { address: "calc.add", args: { a: 2, b: 3 } } }, { token: fixture.bound.token }); + expect((await readJsonRpc(await call())).result.isError).not.toBe(true); + const poolRequest = () => new Request(BASE + "/mcp/desktop", mcpRpc("tools/list", {}, { token: fixture.bound.token })); + expect((await app.fetch(poolRequest())).status).toBe(200); + allowed = false; + expect((await readJsonRpc(await call())).result.isError).toBe(true); + expect((await app.fetch(poolRequest())).status).toBe(404); + const data = await (await app.fetch(request(fixture.bound.token, "/ui/data"))).json() as { connectors: unknown[] }; + expect(data.connectors).toEqual([]); + } finally { await app.close(); } + }); +}); + +describe("optional token lifecycle", () => { + it("creates, shows only once, renames and revokes through the operator routes", async () => { + const storage = await legacyStorage(); + const app = createTestConnecta({ connectors: [], auth: human, accessTokens: accessTokens(storage), identity: { accessTokenManagement: () => true } }); + try { + const created = await app.fetch(request("clerk-operator", "/ui/access-tokens", "POST", { name: "New desktop" })); + expect(created.status).toBe(201); + expect(created.headers.get("cache-control")).toContain("no-store"); + const result = await created.json() as { token: string; accessToken: { id: string } }; + expect(result.token).toMatch(/^cta_[A-Za-z0-9_-]{43}$/); + const manager = new AccessTokenManager(storage); + expect(await manager.auth.authorize(request(result.token), BASE)).toEqual({ ok: true, subjectId: result.accessToken.id, principal: owner }); + const path = `/ui/access-tokens/${result.accessToken.id}`; + expect((await app.fetch(request("clerk-operator", path, "PUT", { name: "Renamed" }))).status).toBe(200); + expect(await manager.auth.activityActorLabel?.(result.accessToken.id)).toBe("Renamed"); + const listed = await (await app.fetch(request("clerk-operator", "/ui/access-tokens"))).text(); + expect(listed).not.toContain(result.token); + expect(listed).not.toContain("tokenHash"); + for (const key of await storage.list!("")) expect(await storage.get(key)).not.toContain(result.token); + expect((await app.fetch(request("clerk-operator", path, "DELETE"))).status).toBe(200); + expect((await manager.auth.authorize(request(result.token), BASE)).ok).toBe(false); + expect((await app.fetch(request("clerk-operator", `/ui/access-tokens/${fixture.bound.accessToken.id}`, "DELETE"))).status).toBe(200); + expect((await manager.auth.authorize(request(fixture.bound.token), BASE)).ok).toBe(false); + } finally { await app.close(); } + }); + + it("denies bearer management, missing permission, cross-origin and oversized writes", async () => { + const storage = await legacyStorage(); + let permission = false; + const app = createTestConnecta({ connectors: [], vault: new CredentialVault(storage, btoa("x".repeat(32))), auth: [human, bearerToken("static")], accessTokens: accessTokens(storage), identity: { accessTokenManagement: () => permission } }); + try { + for (const token of ["clerk-operator", "static", fixture.bound.token]) { + const res = await app.fetch(request(token, "/ui/access-tokens", "POST", { name: "Denied" })); + expect([401, 403]).toContain(res.status); + } + permission = true; + for (const token of ["static", fixture.bound.token]) { + expect([401, 403]).toContain((await app.fetch(request(token, "/ui/access-tokens"))).status); + expect([401, 403]).toContain((await app.fetch(request(token, "/ui/credentials/missing", "PUT", { value: "x" }))).status); + } + const foreign = request("clerk-operator", "/ui/access-tokens", "POST", { name: "Denied" }); + foreign.headers.set("Origin", "https://evil.test"); + expect((await app.fetch(foreign)).status).toBe(403); + expect((await app.fetch(request("clerk-operator", "/ui/access-tokens", "OPTIONS"))).status).toBe(403); + expect((await app.fetch(request("clerk-operator", "/ui/access-tokens", "POST", { name: "x".repeat(2000) }))).status).toBe(413); + } finally { await app.close(); } + }); + + it("omitting the module leaves token authentication and management unavailable", async () => { + const app = createTestConnecta({ connectors: [], auth: human }); + try { + expect((await mcpRpc(app, "tools/list", {}, { token: fixture.bound.token })).status).toBe(401); + expect((await app.fetch(request("clerk-operator", "/tokens"))).status).toBe(404); + expect((await app.fetch(request("clerk-operator", "/ui/access-tokens"))).status).toBe(404); + } finally { await app.close(); } + }); + + it("bounds concurrent creation across managers and releases capacity on revocation", async () => { + const storage = await legacyStorage(); + const managers = Array.from({ length: 8 }, () => new AccessTokenManager(storage, { maxActive: 3 })); + const created = await Promise.allSettled(managers.map(manager => manager.create("client", owner))); + expect(created.filter(result => result.status === "fulfilled")).toHaveLength(1); + await managers[0]!.revoke(fixture.bound.accessToken.id, "owner"); + await expect(managers[1]!.create("replacement", owner)).resolves.toHaveProperty("token"); + }); +}); + + +describe("token lifecycle races", () => { + it("a stale rename cannot erase a concurrent revocation", async () => { + const base = await legacyStorage(); + let hold = true; + let enter!: () => void; + let release!: () => void; + const entered = new Promise(resolve => { enter = resolve; }); + const barrier = new Promise(resolve => { release = resolve; }); + const storage: KVStorage = { ...base, get: async key => { + const raw = await base.get(key); + if (key === recordKey && hold) { + hold = false; + enter(); + await barrier; + } + return raw; + } }; + const manager = new AccessTokenManager(storage); + const rename = manager.rename(fixture.bound.accessToken.id, "Renamed while revoking"); + await entered; + await new AccessTokenManager(base).revoke(fixture.bound.accessToken.id, "owner"); + release(); + expect((await rename)?.revokedAt).toBeTruthy(); + expect((await manager.auth.authorize(request(fixture.bound.token), BASE)).ok).toBe(false); + }); + + it("keeps capacity and revocable metadata after an uncertain lookup write", async () => { + const base = memoryStorage(); + const storage: KVStorage = { ...base, set: async (key, value) => { + await base.set(key, value); + if (key.startsWith("access-token:v1:lookup:")) throw new Error("answer lost after commit"); + } }; + const manager = new AccessTokenManager(storage, { maxActive: 1 }); + await expect(manager.create("Interrupted", owner)).rejects.toThrow("answer lost"); + await expect(manager.create("Another", owner)).rejects.toThrow("maximum"); + const [token] = await manager.list(); + expect(token).toBeDefined(); + await manager.revoke(token!.id, "owner"); + expect(await base.list!("access-token:v1:lookup:")).toEqual([]); + await expect(new AccessTokenManager(base, { maxActive: 1 }).create("Replacement", owner)).resolves.toHaveProperty("token"); + }); +}); diff --git a/test/browser/operator-ui.spec.ts b/test/browser/operator-ui.spec.ts index 6183fe10..dbef9dc8 100644 --- a/test/browser/operator-ui.spec.ts +++ b/test/browser/operator-ui.spec.ts @@ -6,6 +6,7 @@ import { renderUiHtml, type UiData, } from "../../src/ui.js"; +import { accessTokens, AccessTokenManager } from "../../src/access-tokens.js"; import { api } from "../../src/connectors/api.js"; import { CredentialVault, @@ -41,6 +42,7 @@ let clerkLoaderFails = false; let clerkLoaderRequests: string[] = []; let activityEnabled = true; let authManagement = true; +let tokenManagement = false; let detailBarriers = new Map>(); let releaseDetails: Array<() => void> = []; let pools: string[] = []; @@ -78,6 +80,7 @@ function data(): UiData { credentialManagement: "available", oauthManagement: true, activityEnabled, + ...(tokenManagement ? { accessTokenManagement: "available" as const } : {}), ...(pools.length ? { pools } : {}), connectors: emptyDeployment ? [] : [ { @@ -264,7 +267,8 @@ test.beforeAll(async () => { const real = await realRoutes.connecta.fetch( new Request(`${REAL_BASE}${url.pathname}`, { method, - headers: { Authorization: "Bearer clerk-operator", Origin: REAL_BASE }, + headers: { Authorization: "Bearer clerk-operator", Origin: REAL_BASE, "Content-Type": "application/json" }, + ...(body ? { body: JSON.stringify(body) } : {}), }), ); const text = await real.text(); @@ -409,6 +413,7 @@ test.beforeEach(() => { oauthStartUrl = undefined; leakyStatus = false; realRoutes = undefined; + tokenManagement = false; }); async function openAuthenticated( @@ -1313,3 +1318,43 @@ test("keeps loaded artifacts when loading more fails", async ({ page }) => { await expect(page.locator("#artifactError")).toHaveCount(0); await expect(page.getByRole("button", { name: "Load more" })).toBeEnabled(); }); + + +test("creates, shows once, renames and revokes client tokens through real routes", async ({ page }) => { + const storage = memoryStorage(); + const connecta = createTestConnecta({ + connectors: [], + auth: { ...fakeClerkAuth(), activityActorNamespace: "clerk:test" }, + accessTokens: accessTokens(storage), + identity: { accessTokenManagement: () => true }, + }); + const paths = new Set(["/ui/access-tokens"]); + realRoutes = { connecta, paths, answered: [] }; + tokenManagement = true; + try { + await openAuthenticated(page); + await page.getByRole("link", { name: "Access tokens", exact: true }).click(); + await page.getByLabel("Client name").fill("Desktop client"); + await page.getByRole("button", { name: "Create token", exact: true }).click(); + await expect(page.locator("#createdToken")).toContainText("cta_"); + const secret = (await page.locator("#createdToken").textContent())!; + const manager = new AccessTokenManager(storage); + const [record] = await manager.list(); + paths.add(`/ui/access-tokens/${record!.id}`); + const authenticate = () => manager.auth.authorize(new Request(REAL_BASE + "/mcp", { headers: { Authorization: `Bearer ${secret}` } }), REAL_BASE); + expect((await authenticate()).ok).toBe(true); + await page.getByRole("button", { name: "I stored it" }).click(); + await expect(page.locator("#createdToken")).toHaveCount(0); + await page.getByRole("button", { name: "Rename", exact: true }).click(); + await page.getByLabel("Token name", { exact: true }).fill("Renamed desktop"); + await page.getByRole("button", { name: "Save name", exact: true }).click(); + await expect(page.getByRole("heading", { name: "Renamed desktop", exact: true })).toBeVisible(); + await page.reload(); + await expect(page.getByRole("heading", { name: "Renamed desktop", exact: true })).toBeVisible(); + expect(await page.content()).not.toContain(secret); + page.once("dialog", dialog => dialog.accept()); + await page.getByRole("button", { name: "Revoke", exact: true }).click(); + await expect(page.locator(".token-card")).toHaveClass(/revoked/); + expect((await authenticate()).ok).toBe(false); + } finally { await connecta.close(); } +}); diff --git a/test/fixtures/access-tokens-v023.json b/test/fixtures/access-tokens-v023.json new file mode 100644 index 00000000..b6199543 --- /dev/null +++ b/test/fixtures/access-tokens-v023.json @@ -0,0 +1,37 @@ +{ + "source": "Generated by the unmodified v0.23.0 AccessTokenManager; disposable test credentials", + "bound": { + "token": "cta_h-lDNSGD3rMqeFW3Ikf8y4oGuWNlwXFAf-gE-wQRcSc", + "accessToken": { + "id": "9c9cb089-a863-491c-847f-ed0685d32469", + "name": "Legacy desktop", + "tokenPrefix": "cta_h-lDNSGD", + "createdAt": "2026-09-27T22:57:02.029Z" + } + }, + "unbound": { + "token": "cta_0fKgg1UyMzK7yXgH15IplP7FTiVQ3raJhmg4riaVwGk", + "accessToken": { + "id": "a9e57a46-e3db-470f-a800-12e4e79314b7", + "name": "Legacy service", + "tokenPrefix": "cta_0fKgg1Uy", + "createdAt": "2026-09-27T22:57:02.030Z" + } + }, + "revoked": { + "token": "cta_WaH26k1Y1uL9tRbB7JGA7FPYzGD3fMVzNb8tZ892Me0", + "accessToken": { + "id": "a6011e48-6de0-4760-9d23-320acea51dd9", + "name": "Retired client", + "tokenPrefix": "cta_WaH26k1Y", + "createdAt": "2026-09-27T22:57:02.035Z" + } + }, + "records": { + "access-token:v1:record:9c9cb089-a863-491c-847f-ed0685d32469": "{\"version\":1,\"id\":\"9c9cb089-a863-491c-847f-ed0685d32469\",\"name\":\"Legacy desktop\",\"tokenHash\":\"37e09018701a755cdff444b105b398b01f9a884f1c17afa39a867f8264efd953\",\"tokenPrefix\":\"cta_h-lDNSGD\",\"createdAt\":\"2026-09-27T22:57:02.029Z\",\"createdBy\":\"clerk:test:owner\",\"principal\":{\"namespace\":\"clerk:test\",\"id\":\"owner\"}}", + "access-token:v1:lookup:37e09018701a755cdff444b105b398b01f9a884f1c17afa39a867f8264efd953": "{\"version\":1,\"id\":\"9c9cb089-a863-491c-847f-ed0685d32469\"}", + "access-token:v1:record:a9e57a46-e3db-470f-a800-12e4e79314b7": "{\"version\":1,\"id\":\"a9e57a46-e3db-470f-a800-12e4e79314b7\",\"name\":\"Legacy service\",\"tokenHash\":\"af1a728ba73afa2bb8abfdbe6406d21b1aea7fd7dc3f7856795e68c106f1d037\",\"tokenPrefix\":\"cta_0fKgg1Uy\",\"createdAt\":\"2026-09-27T22:57:02.030Z\",\"createdBy\":\"operator\"}", + "access-token:v1:lookup:af1a728ba73afa2bb8abfdbe6406d21b1aea7fd7dc3f7856795e68c106f1d037": "{\"version\":1,\"id\":\"a9e57a46-e3db-470f-a800-12e4e79314b7\"}", + "access-token:v1:record:a6011e48-6de0-4760-9d23-320acea51dd9": "{\"version\":1,\"id\":\"a6011e48-6de0-4760-9d23-320acea51dd9\",\"name\":\"Retired client\",\"tokenHash\":\"f507c9ae97638f6d4419167926cf80db9393743eb0350a7051382c55802b0f00\",\"tokenPrefix\":\"cta_WaH26k1Y\",\"createdAt\":\"2026-09-27T22:57:02.035Z\",\"createdBy\":\"operator\",\"revokedAt\":\"2026-09-27T22:57:02.035Z\",\"revokedBy\":\"operator\"}" + } +} diff --git a/test/operator-store.test.ts b/test/operator-store.test.ts index 40a2f440..193ab2e4 100644 --- a/test/operator-store.test.ts +++ b/test/operator-store.test.ts @@ -416,3 +416,24 @@ describe("operator store load failures", () => { expect(store.getState().gate?.message).toContain("Clerk session wasn't accepted"); }); }); + + +describe("managed token secret lifetime", () => { + it("drops an issued secret when the requesting identity changes", async () => { + const { store, fetchMock, changeSession } = await loadStore({ id: "a", getToken: async () => "a" }); + fetchMock.mockResolvedValueOnce(Response.json(uiData("a"))); + await store.boot(); + const slow = deferred(); + fetchMock.mockImplementationOnce(() => slow.promise); + const pending = store.createAccessToken("desktop"); + // Let the POST capture its original session before switching identities. + await Promise.resolve(); + await Promise.resolve(); + fetchMock.mockImplementationOnce(() => new Promise(() => {})); + changeSession({ id: "b", getToken: async () => "b" }); + slow.resolve(Response.json({ token: "cta_disposable-secret", accessToken: { id: "issued", name: "desktop" } })); + expect(await pending).toBe(false); + expect(store.getState().createdToken).toBeNull(); + expect(JSON.stringify(store.getState())).not.toContain("cta_disposable-secret"); + }); +}); diff --git a/test/operator-view.test.ts b/test/operator-view.test.ts index 8c7b5fe5..3df57ab3 100644 --- a/test/operator-view.test.ts +++ b/test/operator-view.test.ts @@ -125,7 +125,7 @@ describe("operator page routing and capabilities", () => { it("maps only canonical shell paths and builds page-specific titles", () => { expect(operatorPageForPath("/")).toBe("connections"); expect(operatorPageForPath("/credentials")).toBeUndefined(); - expect(operatorPageForPath("/tokens")).toBeUndefined(); + expect(operatorPageForPath("/tokens")).toBe("tokens"); expect(operatorPageForPath("/activity")).toBe("activity"); expect(operatorPageForPath("/ui")).toBeUndefined(); expect(operatorPageForPath("/ui/data")).toBeUndefined(); diff --git a/test/package-surface.test.ts b/test/package-surface.test.ts index fdc7d646..7251aa44 100644 --- a/test/package-surface.test.ts +++ b/test/package-surface.test.ts @@ -104,7 +104,7 @@ describe("public package boundary", () => { expect(Object.keys(packageJson.exports ?? {}).sort()).toEqual( [ ".", - "./ui", "./credentials", "./activity", "./auth/bearer", "./artifacts", + "./ui", "./credentials", "./activity", "./auth/bearer", "./artifacts", "./auth/access-tokens", "./package.json", "./node", "./json-schema", diff --git a/test/purity.test.ts b/test/purity.test.ts index a8ac910e..2ca7ba9b 100644 --- a/test/purity.test.ts +++ b/test/purity.test.ts @@ -107,9 +107,9 @@ describe("src/index.ts import purity (Workers-clean entry)", () => { expect(graph.has(quickJsExecutor)).toBe(false); expect(graph.has(quickJsChild)).toBe(false); expect(graph.has(clerkAdapter)).toBe(false); - for (const file of ["ui.ts", "operator-ui/generated.ts", "credentials.ts", "activity.ts", "auth/bearer.ts", "artifacts.ts"]) expect(graph.has(join(SRC, file)), file).toBe(false); + for (const file of ["ui.ts", "operator-ui/generated.ts", "credentials.ts", "activity.ts", "auth/bearer.ts", "artifacts.ts", "access-tokens.ts", "routes/access-tokens.ts"]) expect(graph.has(join(SRC, file)), file).toBe(false); const withUi = importGraph(join(SRC, "ui.ts")); - for (const file of ["credentials.ts", "activity.ts", "routes/activity.ts", "artifacts.ts"]) expect(withUi.has(join(SRC, file)), `UI imports ${file}`).toBe(false); + for (const file of ["credentials.ts", "activity.ts", "routes/activity.ts", "artifacts.ts", "access-tokens.ts", "routes/access-tokens.ts"]) expect(withUi.has(join(SRC, file)), `UI imports ${file}`).toBe(false); // The artifacts module is one subpath: nothing of it rides the root entry, // and the operator UI reaches artifact pages only through their JSON API. const artifactFiles = readdirSync(join(SRC, "artifacts")).filter((file) => file.endsWith(".ts")); diff --git a/vitest.config.ts b/vitest.config.ts index cca75b94..f0e72053 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,6 +7,7 @@ import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; // test/suite-partition.test.ts guards the partition, including itself. export const WORKERS_SUITES = [ "test/activity.test.ts", + "test/access-tokens.test.ts", "test/api-connector.test.ts", "test/artifact-store.test.ts", "test/artifact-refresh.test.ts",