@agentconnect.md/setup 1.36.1-rc.1 → 1.37.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -36023,20 +36023,6 @@ const ZodRecord = /*@__PURE__*/ $constructor("ZodRecord", (inst, def) => {
36023
36023
  inst.keyType = def.keyType;
36024
36024
  inst.valueType = def.valueType;
36025
36025
  });
36026
- function record$1(keyType, valueType, params) {
36027
- if (!valueType || !valueType._zod) return new ZodRecord({
36028
- type: "record",
36029
- keyType: string(),
36030
- valueType: keyType,
36031
- ...normalizeParams(valueType)
36032
- });
36033
- return new ZodRecord({
36034
- type: "record",
36035
- keyType,
36036
- valueType,
36037
- ...normalizeParams(params)
36038
- });
36039
- }
36040
36026
  function partialRecord(keyType, valueType, params) {
36041
36027
  const k = clone(keyType);
36042
36028
  k._zod.values = void 0;
@@ -36293,1115 +36279,16 @@ function number(params) {
36293
36279
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
36294
36280
  }
36295
36281
  //#endregion
36296
- //#region ../protocol/dist/envelope.js
36282
+ //#region ../protocol/dist/slack-app-manifest.js
36297
36283
  var import_fastify = /* @__PURE__ */ __toESM(require_fastify(), 1);
36298
- object({
36299
- v: literal(1),
36300
- id: string().uuid(),
36301
- ts: string().datetime(),
36302
- type: string(),
36303
- corr: string().uuid().optional(),
36304
- payload: unknown()
36305
- });
36306
- object({
36307
- epoch: number$1().int(),
36308
- agentId: string().uuid().optional(),
36309
- launchId: string().uuid().optional()
36310
- });
36311
- /**
36312
- * Builds the envelope schema for one frame `type` — the `type` literal plus the
36313
- * typed payload — so a wire's discriminated union infers `payload` precisely.
36314
- * Shared by the daemon↔CP union (`frame.ts`) and the relay wire unions
36315
- * (`frames/relay-cp.ts` / `frames/relay-daemon.ts`), which are deliberately
36316
- * SEPARATE unions over the same envelope shape (shared-bot-relay.md §8).
36317
- */
36318
- function frameSchema(type, payload) {
36319
- return object({
36320
- v: literal(1),
36321
- id: string().uuid(),
36322
- ts: string().datetime(),
36323
- type: literal(type),
36324
- corr: string().uuid().optional(),
36325
- payload
36326
- });
36327
- }
36328
- const ErrorFrame = object({
36329
- code: _enum([
36330
- "UNKNOWN_FRAME",
36331
- "FRAME_TOO_LARGE",
36332
- "PROTOCOL_STATE",
36333
- "BAD_PAYLOAD",
36334
- "AUTH_FAILED",
36335
- "ATTESTATION_INVALID",
36336
- "STALE_EPOCH",
36337
- "STALE_LAUNCH",
36338
- "NO_SESSION",
36339
- "SCOPE_DENIED",
36340
- "DELEGATION_DENIED",
36341
- "INVOCATION_CONFLICT",
36342
- "LEASE_EXPIRED",
36343
- "LEASE_DENIED",
36344
- "RATE_LIMITED",
36345
- "CONFLICT",
36346
- "INTERNAL"
36347
- ]),
36348
- message: string(),
36349
- retryable: boolean(),
36350
- details: record$1(string(), unknown()).optional()
36351
- });
36352
- //#endregion
36353
- //#region ../protocol/dist/frames/route.js
36354
- const Platform = string().min(1);
36355
- const SessionKey = object({
36356
- platform: Platform,
36357
- channel: string(),
36358
- thread: string().optional()
36359
- });
36360
- /** Trigger-matching rule for a binding (protocol §5.1). */
36361
- const BindRule = object({ match: discriminatedUnion("kind", [
36362
- object({ kind: literal("mention") }),
36363
- object({ kind: literal("dm") }),
36364
- object({
36365
- kind: literal("keyword"),
36366
- value: string()
36367
- }),
36368
- object({ kind: literal("auto") })
36369
- ]) });
36370
- object({
36371
- sessionKey: SessionKey,
36372
- agentId: string().uuid(),
36373
- workspaceId: string().uuid(),
36374
- bindRules: array(BindRule).default([])
36375
- });
36376
- object({
36377
- ok: boolean(),
36378
- sessionKey: SessionKey,
36379
- reason: string().optional()
36380
- });
36381
- object({
36382
- routingEpoch: number$1().int(),
36383
- rules: array(object({
36384
- match: unknown(),
36385
- agentId: string().uuid()
36386
- }))
36387
- });
36388
- object({
36389
- scope: union([
36390
- object({
36391
- kind: literal("agent"),
36392
- agentId: string().uuid()
36393
- }),
36394
- object({ kind: literal("daemon") }),
36395
- object({
36396
- kind: literal("session"),
36397
- sessionKey: SessionKey
36398
- })
36399
- ]),
36400
- deadline: string().datetime()
36401
- });
36402
- object({
36403
- remaining: number$1().int(),
36404
- drained: array(SessionKey)
36405
- });
36406
- object({ released: array(SessionKey) });
36407
- //#endregion
36408
- //#region ../protocol/dist/frames/integration.js
36409
- /**
36410
- * Platform integration distribution (C→D) — the Slack "install" flow.
36411
- *
36412
- * The Control Plane is the source of truth for platform integrations and pushes
36413
- * them to the daemon that owns the integration's agent (`integration/upsert`, and
36414
- * the reconcile snapshot `RegisterOk.integrations[]`). The daemon opens the Socket
36415
- * Mode connection from the delivered config (see slack/connection.ts).
36416
- *
36417
- * SECURITY: `integration/upsert` and `RegisterOk.integrations[]` carry PLAINTEXT
36418
- * platform tokens (botToken/appToken/appSecret). These payloads MUST NEVER be
36419
- * logged — no body dump on decode error, no register/ok snapshot debug dump.
36420
- * CP-owned integrations live only in daemon MEMORY and are re-converged on
36421
- * reconnect; hand-authored agents keep tokens in their own `agent.json` (the
36422
- * same trust boundary as the daemon process).
36423
- *
36424
- * `signingSecret` is intentionally absent: Socket Mode authenticates with the
36425
- * app-level token, so the daemon never needs a signing secret.
36426
- */
36427
- /** Trigger match — mirrors the daemon BindRuleConfig.match (agents/agent-schema.ts). */
36428
- const BindMatch = discriminatedUnion("kind", [
36429
- object({ kind: literal("mention") }),
36430
- object({ kind: literal("dm") }),
36431
- object({
36432
- kind: literal("keyword"),
36433
- value: string()
36434
- }),
36435
- object({ kind: literal("auto") })
36436
- ]);
36437
- /** One channel/thread trigger binding — mirrors the daemon BindRuleConfig. */
36438
- const IntegrationBindRule = object({
36439
- channel: string().optional(),
36440
- thread: string().optional(),
36441
- match: BindMatch
36442
- });
36443
- object({
36444
- botToken: string(),
36445
- appToken: string().optional(),
36446
- appId: string().optional(),
36447
- shareable: boolean().default(false),
36448
- botUserId: string().optional()
36449
- });
36450
- object({ botToken: string() });
36451
- object({
36452
- botToken: string(),
36453
- applicationId: string().optional()
36454
- });
36455
- /**
36456
- * The Feishu / Lark config payload — the long-connection WebSocket
36457
- * (`@larksuiteoapi/node-sdk` `WSClient`) + REST client. A Feishu self-built app
36458
- * authenticates with an `appId` + `appSecret` PAIR — the SDK exchanges them for a
36459
- * short-lived `tenant_access_token` internally (no Slack-style app-level token, no
36460
- * signing secret). `appId` is a semi-public identifier (`cli_…`); `appSecret` is
36461
- * plaintext secret material — NEVER log it. `botOpenId` is the bot's own open_id
36462
- * for @-mention routing; lazily resolved by the daemon via `bot/info` if absent.
36463
- *
36464
- * The envelope's `mode` decides the transport: `direct` opens the SDK long
36465
- * connection on the daemon; `shared` keeps only the authenticated REST client
36466
- * (callbacks arrive through the relay, pre-addressed over rd/*).
36467
- *
36468
- * `region` selects the open-platform gateway the daemon SDK (and CP verifier)
36469
- * talk to — `'feishu'` = mainland China (`open.feishu.cn`, the SDK default) vs
36470
- * `'lark'` = international (`open.larksuite.com`). Same app model, different host;
36471
- * an app is registered in exactly one region. Defaults to `'feishu'` so existing
36472
- * installs are unaffected.
36473
- */
36474
- const FeishuRegion = _enum(["feishu", "lark"]);
36475
- object({
36476
- appId: string(),
36477
- appSecret: string(),
36478
- botOpenId: string().optional(),
36479
- region: FeishuRegion.default("feishu")
36480
- });
36481
- /**
36482
- * §6.4 core routing ENVELOPE (integration-plugin-architecture.md D4): the knobs CORE
36483
- * reads — routing, gating, ingress mode — platform-independent. This is the ONLY
36484
- * carrier of these knobs on the wire: the opaque per-platform `config` payload
36485
- * never duplicates them (the daemon reads routing exclusively from here).
36486
- */
36487
- const IntegrationCoreEnvelope = object({
36488
- mode: _enum(["direct", "shared"]).default("direct"),
36489
- bindRules: array(IntegrationBindRule).default([]),
36490
- mutedChannels: array(string()).default([]),
36491
- gated: boolean().default(false)
36492
- });
36493
- object({
36494
- integrationId: string().uuid(),
36495
- agentId: string().uuid(),
36496
- platform: Platform,
36497
- core: IntegrationCoreEnvelope,
36498
- config: unknown().optional()
36499
- });
36500
- object({ integrationId: string().uuid() });
36501
- /**
36502
- * One conversation the bot participates in (metadata only — no messages).
36503
- * `kind` distinguishes member channels from direct conversations (resource-
36504
- * visibility.md §14.3): absent = 'channel' for wire compatibility. DM rows
36505
- * (`kind: 'im'`, Slack "D…" ids) are reported for every integration on first
36506
- * inbound DM; their `name` is the counterpart's display name. Group DMs
36507
- * (`kind: 'mpim'`, Slack multi-person DMs) are reported on observation the same
36508
- * way — never enumerated, because Slack does not list them as bot membership —
36509
- * but they behave like a channel: several humans share the room, so the agent
36510
- * stays mention-gated there rather than answering every message.
36511
- *
36512
- * `spaceId`/`space` identify the container the conversation lives in — a Discord
36513
- * GUILD, which a bot in several servers needs for the channel to be identifiable at
36514
- * all (every server has a "#general"). The ID is the identity: two distinct guilds
36515
- * may carry the SAME name, so grouping on the name alone would merge them and hide
36516
- * the ambiguity it was meant to resolve. `space` is the display label only. Both are
36517
- * absent on platforms with one implicit container per bot (Slack workspace, Telegram,
36518
- * Feishu tenant) and on DM rows.
36519
- */
36520
- const IntegrationChannel = object({
36521
- id: string(),
36522
- name: string().optional(),
36523
- spaceId: string().optional(),
36524
- space: string().optional(),
36525
- isPrivate: boolean().optional(),
36526
- kind: _enum([
36527
- "channel",
36528
- "im",
36529
- "mpim"
36530
- ]).optional()
36531
- });
36532
- object({
36533
- integrationId: string().uuid(),
36534
- channels: array(IntegrationChannel),
36535
- authoritative: boolean().optional(),
36536
- removed: array(string()).optional()
36537
- });
36538
- /**
36539
- * What a leave targets. Platforms disagree about what a bot can withdraw from, and
36540
- * the difference is not cosmetic — so the caller has to say which it means rather
36541
- * than the daemon guessing from an id:
36542
- *
36543
- * - `conversation` — one channel/group the bot is a member of (Slack
36544
- * `conversations.leave`, Telegram `leaveChat`).
36545
- * - `space` — the whole container. Discord has no per-channel membership for a
36546
- * bot at all: it is in a GUILD and sees that guild's channels through
36547
- * permissions, so the only thing it can leave is the entire server. That is a
36548
- * much larger action than leaving one channel and must be requested as such.
36549
- */
36550
- const IntegrationLeaveTarget = discriminatedUnion("kind", [object({
36551
- kind: literal("conversation"),
36552
- channel: string().min(1)
36553
- }), object({
36554
- kind: literal("space"),
36555
- spaceId: string().min(1)
36556
- })]);
36557
- object({
36558
- integrationId: string().uuid(),
36559
- target: IntegrationLeaveTarget
36560
- });
36561
- object({
36562
- integrationId: string().uuid(),
36563
- channels: array(string()).min(1)
36564
- });
36565
- object({
36566
- ok: boolean(),
36567
- error: string().optional()
36568
- });
36569
- //#endregion
36570
- //#region ../protocol/dist/frames/cron.js
36571
- /**
36572
- * Cron sinks to the daemon (D5) — protocol §5.4.
36573
- *
36574
- * A cron periodically triggers ONE AGENT with a synthetic prompt (`trigger`) to
36575
- * carry out some work. The CP owns the definition; the daemon owns firing +
36576
- * last-run persistence, so crons fire even when the CP is down. On receipt the
36577
- * daemon persists the def into the owning agent's `agent.json` `crons[]` (the
36578
- * single source of truth, same model as integrations) — surviving a restart
36579
- * with the CP down.
36580
- *
36581
- * `target` is OPTIONAL output routing: when present, the daemon posts the
36582
- * trigger as a real message in that channel and the agent's session replies in
36583
- * its thread; when absent the fire is headless (the agent works with no
36584
- * platform output).
36585
- */
36586
- const CronTarget = object({
36587
- platform: Platform.default("slack"),
36588
- channel: string(),
36589
- integrationId: string().uuid().optional()
36590
- });
36591
- object({
36592
- cronId: string().uuid(),
36593
- agentId: string().uuid(),
36594
- schedule: string(),
36595
- timezone: string().min(1),
36596
- target: CronTarget.optional(),
36597
- trigger: string(),
36598
- enabled: boolean().default(true)
36599
- });
36600
- object({ cronId: string().uuid() });
36601
- /**
36602
- * `cron/report` (D→C EVT, fire-and-forget) — one CP-owned cron fired. The
36603
- * daemon stamps the fire into its local store first (it stays authoritative,
36604
- * §5.4) and reports it here so the console's `lastRunAt` converges; the CP
36605
- * upsert is latest-wins, so the daemon re-asserting its stored stamps on
36606
- * reconnect (fires while the CP was unreachable) is idempotent and can never
36607
- * regress the value. Hand-authored (no-origin) crons are never reported.
36608
- *
36609
- * Reports are keyed by `(cronId, firedAt)`: the FIRE report (no `status`)
36610
- * opens the run, an optional SESSION report attaches the ACP session as soon
36611
- * as it is initialized, and the COMPLETION report (with `status` + outcome
36612
- * fields) closes the run once the dispatched turn ends. A completion without
36613
- * a prior fire report (CP was down at fire time) still creates the run row.
36614
- */
36615
- const CronRunStatus = _enum(["success", "failed"]);
36616
- object({
36617
- cronId: string().uuid(),
36618
- agentId: string().uuid(),
36619
- firedAt: string().datetime(),
36620
- status: CronRunStatus.optional(),
36621
- durationMs: number$1().int().nonnegative().optional(),
36622
- sessionId: string().optional(),
36623
- reason: string().optional()
36624
- });
36625
- object({ cronId: string().uuid() });
36626
- //#endregion
36627
- //#region ../protocol/dist/frames/collab.js
36628
- /**
36629
- * Bot-AGNOSTIC agent-collaboration routing snapshot (agent-collaboration §2.3 / §6.2 / §6.5).
36630
- *
36631
- * The existing shared-bot `members` table is keyed by botId (`BotAssignment`) and
36632
- * CANNOT address an agent on a DIFFERENT bot / arbitrary channel. This snapshot is
36633
- * the fix: it maps a channel — `(orgId, platform, channelId)` — to the per-agent
36634
- * placement + call policy the relay needs to route a cross-daemon `rd/agentmsg` and
36635
- * the target daemon needs to terminal-verify a remote caller.
36636
- *
36637
- * It carries NO message body — pure routing/policy metadata, like the rest of the
36638
- * control plane. The SAME shape is distributed two ways (§6.5):
36639
- * - CP→relay over the `rc/*` wire (`rc/collab-routes`) — the relay routes
36640
- * `toAgentId` → owning `daemonId` and authorizes the caller/target policy.
36641
- * - CP→daemon over the daemon↔CP wire — as a `register/ok` field (reconnect
36642
- * baseline) + a `collaboration/routes` EVT (hot push) — so the OWNING daemon of
36643
- * the target can terminal-verify (defense in depth, §2.5 #4) the remote caller's
36644
- * org/channel/placement against its OWN copy, never trusting the relay's claim
36645
- * blindly.
36646
- *
36647
- * FOLLOW-UP (scoped down in P2, see PR description): the full versioned lifecycle
36648
- * of §6.5 — per-entry tombstones, TTL/expiry after a CP disconnect, and
36649
- * fail-closed-on-stale — is NOT fully implemented here. `generation` is present as
36650
- * the version hook and the snapshot is FULL-REPLACE (converge-don't-diff, same as
36651
- * `register/ok`), which is enough to route + authorize on a live CP. TTL-expiry and
36652
- * tombstone semantics are a follow-up within this phase.
36653
- */
36654
- /** One agent's placement + call policy within a channel. `daemonId` is the owning
36655
- * daemon the relay forwards to; `integrationId` is the DEFINITE reply integration
36656
- * (§6.2 — no fallback to "first connection"). */
36657
- const CollabAgentPlacement = object({
36658
- agentId: string().uuid(),
36659
- daemonId: string().uuid(),
36660
- integrationId: string().uuid().optional(),
36661
- /** Public Slack app id (`A…`) for this agent's bot. Receivers use it to recognize
36662
- * AgentConnect-authored platform messages — the first half of verifying an
36663
- * authorship claim (send-message-routing-rework.md §4). */
36664
- botAppId: string().optional(),
36665
- /** Public platform member id this agent's bot posts as (Slack `U…`) — the id a
36666
- * `<@…>` mention resolves to. With {@link botShared} and `name` it is the complete
36667
- * input for the conversation-specific mention ADDRESS
36668
- * (send-message-routing-rework.md §8.5), which both directions need: rendering an
36669
- * exact address to post, and resolving an inbound mention back to an agent.
36670
- * Public metadata, never a credential. */
36671
- botUserId: string().optional(),
36672
- /** True when {@link botUserId} backs MORE THAN ONE agent (a shared bot). The bot user
36673
- * id alone then cannot identify an agent, so its address carries the agent slug too
36674
- * (`<@U_SHARED> reviewer`) and a BARE mention of it addresses no specific agent —
36675
- * which is why a shared bot with no exact author claim fails closed at ingress (§4)
36676
- * and why a bare shared-bot mention from an agent must not fall back to the channel
36677
- * default (§6). */
36678
- botShared: boolean().optional(),
36679
- callPolicy: _enum(["all", "selected"]).default("all"),
36680
- allowedCallerAgentIds: array(string()).default([]),
36681
- /** Caller-side authorization. Effective A→B access requires A's outbound
36682
- * policy to admit B and B's inbound call policy to admit A. */
36683
- outboundPolicy: _enum(["all", "selected"]).default("all"),
36684
- allowedTargetAgentIds: array(string()).default([]),
36685
- name: string().optional(),
36686
- displayName: string().optional()
36687
- });
36688
- /**
36689
- * One agent's placement + call policy carried OUTSIDE any channel — the org-scoped
36690
- * peer directory entry.
36691
- *
36692
- * Every structure on the CP→daemon and CP→relay wires is channel-keyed, so an agent
36693
- * with NO IM integration (webchat, hook, dream, memory-only) never appears in any
36694
- * `channels[]` entry at all. The channel-keyed snapshot structurally cannot express
36695
- * "which agents exist in this org", which is precisely the input channel-free
36696
- * authorization needs: discovery and A2A authorization depend only on the directional
36697
- * call policy (`outboundPolicy`/`allowedTargetAgentIds` on the caller,
36698
- * `callPolicy`/`allowedCallerAgentIds` on the target), org-scoped, with channel
36699
- * demoted to an optional filter. Hence the flat list below.
36700
- */
36701
- const CollabOrgAgent = CollabAgentPlacement.extend({ orgId: string().min(1) });
36702
- /** All agents present in one channel, across daemons. `orgId` scopes routing +
36703
- * authorization: a cross-org caller/target pair never resolves (§2.5 — cross-org
36704
- * rejected). */
36705
- const CollabChannelRoute = object({
36706
- orgId: string().min(1),
36707
- platform: Platform,
36708
- channelId: string().min(1),
36709
- agents: array(CollabAgentPlacement)
36710
- });
36711
- /**
36712
- * The full collaboration snapshot — FULL-REPLACE (converge-don't-diff): the
36713
- * recipient replaces its whole table with `channels`. `generation` monotonically
36714
- * increases per source so a recipient can ignore a stale re-order (version hook for
36715
- * the §6.5 lifecycle follow-up).
36716
- */
36717
- const CollabRoutesSnapshot = object({
36718
- generation: number$1().int().nonnegative().default(0),
36719
- channels: array(CollabChannelRoute).default([]),
36720
- /**
36721
- * FLAT org-scoped directory, alongside (not instead of) `channels`. It is the only
36722
- * place an integration-less agent can appear — see `CollabOrgAgent` — and therefore
36723
- * the authorization input for channel-free A2A. `default([])` keeps a snapshot from
36724
- * an older CP (which advertises no `agent-directory-org-scope-v1`) decodable.
36725
- */
36726
- agents: array(CollabOrgAgent).default([]),
36727
- /**
36728
- * §6.1 origin-kind classification for the platform ids the EMITTER knows — how an
36729
- * older peer classifies an id a newer CP introduces (its own built-in seed only
36730
- * covers ids its build shipped with). Consumers overlay these entries on their
36731
- * seed; an id classified by neither defaults to `'chat'`, the fail-closed answer
36732
- * wherever placements matter (`coordsDecision`). Both fields read as open strings
36733
- * per the S1a rule. `default([])` keeps a pre-S1b snapshot decodable.
36734
- */
36735
- platformKinds: array(object({
36736
- platformId: string().min(1),
36737
- originKind: string().min(1)
36738
- })).default([])
36739
- });
36740
- //#endregion
36741
- //#region ../protocol/dist/frames/hook.js
36742
- /** Decimal wire form for Prisma/GitHub bigint values. */
36743
- const HookBigIntString = string().regex(/^(?:0|[1-9]\d*)$/);
36744
- /** The highest formal-review event one hook permits. */
36745
- const HookReviewPolicy = _enum([
36746
- "off",
36747
- "comment",
36748
- "request_changes",
36749
- "full"
36750
- ]);
36751
- /** External run-reporting transport. `status` is reserved for the later R3. */
36752
- const HookReportingMode = _enum([
36753
- "off",
36754
- "check",
36755
- "status"
36756
- ]);
36757
- /** R2a only admits informational; `required` remains a fail-closed future mode. */
36758
- const HookGateMode = _enum(["informational", "required"]);
36759
- /**
36760
- * Exact config + dispatch identity captured when the CP compiles a hook fire.
36761
- *
36762
- * The relay-facing frames expose these fields individually at top level so an
36763
- * older CP/relay can omit them without making the whole fire undecodable. A
36764
- * consumer MUST have every field before enabling review/reporting; absence is
36765
- * not filled with a revision and therefore fails closed. Action-time RPCs use
36766
- * this complete object so a partially rolled-out tuple cannot authorize an
36767
- * effect.
36768
- */
36769
- const HookConfigSnapshot = object({
36770
- configRevision: HookBigIntString,
36771
- dispatchRevision: HookBigIntString,
36772
- dispatchDaemonId: string().uuid(),
36773
- reviewPolicy: HookReviewPolicy,
36774
- reportingMode: HookReportingMode,
36775
- gateMode: HookGateMode
36776
- });
36777
- /** Optional top-level form used only on rolling-compatible delivery frames. */
36778
- const OptionalHookConfigSnapshot = HookConfigSnapshot.partial();
36779
- /**
36780
- * Trusted, body-free GitHub subject/revision metadata. It is deliberately
36781
- * separate from HookContext: HookContext contains attacker-authored prompt
36782
- * excerpts, whereas this object is cut from a signature-verified payload and
36783
- * fenced to the CP-compiled numeric repo/installation rule.
36784
- *
36785
- * `headSha` is absent for PR issue_comment deliveries because GitHub omits the
36786
- * revision there; the daemon resolves it before `hook/start`. R2a informational
36787
- * checks use `reportSha=headSha` once known. Review-comment deliveries may also
36788
- * identify the triggering comment and its normalized thread root; both remain
36789
- * optional so mixed-version relay/daemon rollouts continue to decode.
36790
- */
36791
- const GithubHookMetadata = object({
36792
- repoId: HookBigIntString,
36793
- repoFullName: string().min(1),
36794
- sourceInstallationId: HookBigIntString,
36795
- subjectKind: _enum(["issue", "pull_request"]),
36796
- pullNumber: number$1().int().positive().optional(),
36797
- headSha: string().min(1).optional(),
36798
- baseSha: string().min(1).optional(),
36799
- reportSha: string().min(1).optional(),
36800
- headRepoFullName: string().min(1).optional(),
36801
- mergeCommitSha: string().min(1).optional(),
36802
- isDraft: boolean().optional(),
36803
- baseChanged: boolean().optional(),
36804
- explicitReviewRequest: boolean().optional(),
36805
- reviewCommentId: HookBigIntString.optional(),
36806
- reviewThreadRootCommentId: HookBigIntString.optional()
36807
- }).superRefine((value, ctx) => {
36808
- if (value.subjectKind === "pull_request" && value.pullNumber === void 0) ctx.addIssue({
36809
- code: "custom",
36810
- path: ["pullNumber"],
36811
- message: "pullNumber is required for a pull_request subject"
36812
- });
36813
- if (value.reportSha !== void 0 && value.headSha === void 0) ctx.addIssue({
36814
- code: "custom",
36815
- path: ["reportSha"],
36816
- message: "reportSha requires an authoritative headSha"
36817
- });
36818
- if (value.reportSha !== void 0 && value.headSha !== void 0 && value.reportSha !== value.headSha) ctx.addIssue({
36819
- code: "custom",
36820
- path: ["reportSha"],
36821
- message: "R2a reports only the authoritative pull head SHA"
36822
- });
36823
- });
36824
- const HookReviewEvent = _enum([
36825
- "COMMENT",
36826
- "REQUEST_CHANGES",
36827
- "APPROVE"
36828
- ]);
36829
- const HookReviewVerdict = _enum([
36830
- "pass",
36831
- "fail",
36832
- "neutral"
36833
- ]);
36834
- /** Short machine code only; raw GitHub/runtime exception text never crosses to reporting. */
36835
- const HookReviewResultCode = string().min(1).max(100).regex(/^[a-z0-9_:-]+$/);
36836
- /**
36837
- * Body-free outcome of one reserved formal-review attempt.
36838
- *
36839
- * `not_submitted` is only legal after a deterministic no-effect failure and
36840
- * lets the CP release the reservation. An uncertain POST remains `ambiguous`
36841
- * until marker reconciliation proves whether GitHub created it.
36842
- */
36843
- const HookReviewResult = discriminatedUnion("state", [
36844
- object({
36845
- state: literal("submitted"),
36846
- reviewId: string().min(1),
36847
- event: HookReviewEvent,
36848
- verdict: HookReviewVerdict,
36849
- commitId: string().min(1)
36850
- }),
36851
- object({
36852
- state: literal("not_submitted"),
36853
- code: HookReviewResultCode
36854
- }),
36855
- object({
36856
- state: literal("ambiguous"),
36857
- code: HookReviewResultCode
36858
- })
36859
- ]);
36860
- /** Body-free identity of the daemon's one fallback GitHub comment. */
36861
- const GithubPublishedComment = object({
36862
- kind: _enum(["issue_comment", "review_comment"]),
36863
- commentId: HookBigIntString
36864
- });
36865
- object({
36866
- source: _enum(["webhook", "github"]),
36867
- event: string().optional(),
36868
- action: string().optional(),
36869
- repo: string().optional(),
36870
- number: number$1().int().optional(),
36871
- title: string().optional(),
36872
- senderLogin: string().optional(),
36873
- senderAvatarUrl: string().url().optional(),
36874
- authorAssociation: string().optional(),
36875
- labels: array(string()).optional(),
36876
- htmlUrl: string().optional(),
36877
- bodyExcerpt: string().optional(),
36878
- body: string().optional(),
36879
- truncated: boolean().optional()
36880
- });
36881
- object({
36882
- hookId: string().uuid(),
36883
- agentId: string().uuid(),
36884
- deliveryKey: string().min(1),
36885
- ...OptionalHookConfigSnapshot.shape,
36886
- event: string().min(1).optional(),
36887
- github: GithubHookMetadata.optional(),
36888
- status: _enum(["success", "failed"]),
36889
- durationMs: number$1().int().nonnegative().optional(),
36890
- sessionId: string().optional(),
36891
- reason: string().optional(),
36892
- reviewAttemptId: string().uuid().optional(),
36893
- reviewResult: HookReviewResult.optional(),
36894
- publishedComment: GithubPublishedComment.optional()
36895
- }).superRefine((report, ctx) => {
36896
- if (report.reviewAttemptId === void 0 !== (report.reviewResult === void 0)) ctx.addIssue({
36897
- code: "custom",
36898
- path: report.reviewAttemptId === void 0 ? ["reviewAttemptId"] : ["reviewResult"],
36899
- message: "reviewAttemptId and reviewResult must be reported together"
36900
- });
36901
- if (report.publishedComment && report.reviewResult?.state === "submitted") ctx.addIssue({
36902
- code: "custom",
36903
- path: ["publishedComment"],
36904
- message: "a submitted formal review and fallback comment are mutually exclusive"
36905
- });
36906
- });
36907
- object({
36908
- hookId: string().uuid(),
36909
- agentId: string().uuid(),
36910
- deliveryKey: string().min(1),
36911
- sessionId: string().min(1).optional(),
36912
- event: string().min(1).optional(),
36913
- github: GithubHookMetadata,
36914
- ...HookConfigSnapshot.shape
36915
- });
36916
- object({ accepted: literal(true) });
36917
- object({
36918
- hookId: string().uuid(),
36919
- deliveryKey: string().min(1),
36920
- attemptId: string().uuid(),
36921
- requestedEvent: HookReviewEvent,
36922
- requestedVerdict: HookReviewVerdict,
36923
- snapshot: HookConfigSnapshot
36924
- });
36925
- object({
36926
- attemptId: string().uuid(),
36927
- token: string().min(1),
36928
- ttlSec: number$1().int().positive(),
36929
- expiresAt: string().datetime(),
36930
- repoId: HookBigIntString,
36931
- repoFullName: string().min(1),
36932
- pullNumber: number$1().int().positive(),
36933
- expectedHeadSha: string().min(1),
36934
- expectedBaseSha: string().min(1)
36935
- });
36936
- object({
36937
- hookId: string().uuid(),
36938
- deliveryKey: string().min(1),
36939
- attemptId: string().uuid(),
36940
- snapshot: HookConfigSnapshot,
36941
- result: HookReviewResult
36942
- });
36943
- object({ accepted: literal(true) });
36944
- //#endregion
36945
- //#region ../protocol/dist/frames/remote-mcp.js
36946
- /** Non-secret entitlement carried CP → relay → daemon after webchat verification. */
36947
- const WebchatRemoteMcpEntitlement = object({
36948
- authorityId: string().uuid(),
36949
- authorityGeneration: number$1().int().positive(),
36950
- expiresAt: string().datetime()
36951
- }).strict();
36952
- const DescriptorFence = object({
36953
- authorityId: string().uuid(),
36954
- authorityGeneration: number$1().int().positive(),
36955
- conversationId: string().uuid(),
36956
- descriptorInstanceId: string().uuid(),
36957
- grantRevision: number$1().int().positive()
36958
- }).strict();
36959
- object({
36960
- authorityId: string().uuid(),
36961
- authorityGeneration: number$1().int().positive(),
36962
- conversationId: string().uuid(),
36963
- descriptorInstanceId: string().uuid()
36964
- }).strict();
36965
- DescriptorFence.extend({
36966
- grantId: string().uuid(),
36967
- token: string().min(32),
36968
- expiresAt: string().datetime(),
36969
- mcpUrl: string().url()
36970
- }).strict();
36971
- DescriptorFence.extend({ grantId: string().uuid() }).strict().extend({ activated: boolean() }).strict();
36972
- object({
36973
- authorityId: string().uuid(),
36974
- authorityGeneration: number$1().int().positive(),
36975
- conversationId: string().uuid(),
36976
- reason: _enum([
36977
- "session_closed",
36978
- "session_expired",
36979
- "agent_detached",
36980
- "feature_disabled",
36981
- "security"
36982
- ])
36983
- }).strict().pick({
36984
- authorityId: true,
36985
- authorityGeneration: true,
36986
- conversationId: true
36987
- }).extend({ revoked: boolean() }).strict();
36988
- //#endregion
36989
- //#region ../protocol/dist/frames/relay-cp.js
36990
- const RcAuth = object({
36991
- method: _enum(["token", "apikey"]),
36992
- credential: string().min(1)
36993
- });
36994
- const RcDeploymentConfig = object({
36995
- revision: number$1().int().nonnegative(),
36996
- githubWebhookSecret: string().min(1).optional()
36997
- });
36998
- const RcAuthOk = object({
36999
- heartbeatSec: number$1().int(),
37000
- serverTime: string().datetime(),
37001
- deploymentConfig: RcDeploymentConfig.optional()
37002
- });
37003
- const RcRegister = object({
37004
- name: string().min(1),
37005
- daemonUrl: string().min(1)
37006
- });
37007
- const RcRegistered = object({ relayId: string().uuid() });
37008
- const RcHeartbeat = object({});
37009
- const RcVerify = discriminatedUnion("kind", [object({
37010
- kind: literal("daemon-key"),
37011
- credential: string().min(1)
37012
- }), object({
37013
- kind: literal("webchat-token"),
37014
- credential: string().min(1),
37015
- conversationBinding: literal("v1").optional()
37016
- })]);
37017
- const RcWebchatParticipant = object({
37018
- agentId: string().uuid(),
37019
- daemonId: string().uuid().optional(),
37020
- primary: boolean().optional()
37021
- });
37022
- const RcVerifyResult = object({
37023
- ok: boolean(),
37024
- reason: string().optional(),
37025
- orgId: string().optional(),
37026
- daemonId: string().uuid().optional(),
37027
- userId: string().optional(),
37028
- user: string().optional(),
37029
- agentId: string().uuid().optional(),
37030
- conversationId: string().uuid().optional(),
37031
- participants: array(RcWebchatParticipant).max(16).optional(),
37032
- remoteMcp: WebchatRemoteMcpEntitlement.optional()
37033
- });
37034
- const RcGithubHookFence = object({
37035
- hookId: string().uuid(),
37036
- configRevision: HookBigIntString,
37037
- dispatchRevision: HookBigIntString
37038
- }).strict();
37039
- const RcGithubCommentAuthz = object({
37040
- hookId: string().uuid(),
37041
- installationId: string().regex(/^[1-9]\d*$/),
37042
- repoId: string().regex(/^[1-9]\d*$/),
37043
- repoFullName: string().regex(/^[^/\s]+\/[^/\s]+$/),
37044
- senderLogin: string().min(1),
37045
- subjectAuthorLogin: string().min(1).optional(),
37046
- configRevision: HookBigIntString,
37047
- dispatchRevision: HookBigIntString,
37048
- siblingFences: array(RcGithubHookFence).min(1).optional()
37049
- }).strict();
37050
- const RcGithubCommentAuthzResult = object({ allowed: boolean() }).strict();
37051
- const GithubNumericId = string().regex(/^[1-9]\d*$/);
37052
- const RcGithubRerequest = union([object({
37053
- checkRunId: GithubNumericId,
37054
- repoId: GithubNumericId,
37055
- headSha: string().min(1),
37056
- deliveryKey: string().min(1).max(200),
37057
- includeBaseSha: literal(true).optional()
37058
- }).strict(), object({
37059
- scope: literal("suite"),
37060
- appId: GithubNumericId,
37061
- installationId: GithubNumericId,
37062
- repoId: GithubNumericId,
37063
- headSha: string().min(1),
37064
- deliveryKey: string().min(1).max(200)
37065
- }).strict()]);
37066
- const RcGithubRerequestTarget = object({
37067
- hookId: string().uuid(),
37068
- pullNumber: number$1().int().positive(),
37069
- baseSha: string().min(1),
37070
- configRevision: HookBigIntString,
37071
- dispatchRevision: HookBigIntString
37072
- }).strict();
37073
- const RcGithubRerequestResult = union([
37074
- object({ allowed: literal(false) }).strict(),
37075
- object({
37076
- allowed: literal(true),
37077
- hookId: string().uuid(),
37078
- pullNumber: number$1().int().positive(),
37079
- baseSha: string().min(1).optional(),
37080
- configRevision: HookBigIntString,
37081
- dispatchRevision: HookBigIntString
37082
- }).strict(),
37083
- object({
37084
- allowed: literal(true),
37085
- targets: array(RcGithubRerequestTarget).min(1)
37086
- }).strict()
37087
- ]);
37088
- const RcHookAssign = object({
37089
- hookId: string().uuid(),
37090
- kind: _enum(["webhook", "github"]),
37091
- agentId: string().uuid(),
37092
- daemonId: string().uuid(),
37093
- ...OptionalHookConfigSnapshot.shape,
37094
- sessionMode: _enum([
37095
- "perDelivery",
37096
- "perThread",
37097
- "shared"
37098
- ]),
37099
- target: CronTarget.optional(),
37100
- webhook: object({
37101
- urlToken: string().min(1),
37102
- hmacSecret: string().optional()
37103
- }).optional(),
37104
- github: object({
37105
- repoId: string(),
37106
- repoFullName: string(),
37107
- sessionKeyPrefix: string().min(1).optional(),
37108
- events: array(string()),
37109
- labelFilter: array(string()),
37110
- commentFamilies: array(_enum(["issues", "pull_request"])).optional(),
37111
- mentionOnly: boolean(),
37112
- appSlug: string().optional(),
37113
- agentName: string().optional(),
37114
- installationIds: array(string())
37115
- }).optional()
37116
- }).superRefine((rule, ctx) => {
37117
- if (rule.dispatchDaemonId !== void 0 && rule.dispatchDaemonId !== rule.daemonId) ctx.addIssue({
37118
- code: "custom",
37119
- path: ["dispatchDaemonId"],
37120
- message: "dispatchDaemonId must equal daemonId on a compiled rule"
37121
- });
37122
- });
37123
- const RcHookRemove = object({ hookId: string().uuid() });
37124
- const RcRunReport = object({
37125
- hookId: string().uuid(),
37126
- deliveryKey: string().min(1),
37127
- firedAt: string().datetime(),
37128
- agentId: string().uuid(),
37129
- daemonId: string().uuid().optional(),
37130
- ...OptionalHookConfigSnapshot.shape,
37131
- event: string().min(1).optional(),
37132
- github: GithubHookMetadata.optional(),
37133
- status: _enum(["accepted", "failed"]),
37134
- reason: string().optional()
37135
- }).superRefine((report, ctx) => {
37136
- if (report.daemonId !== void 0 && report.dispatchDaemonId !== void 0 && report.daemonId !== report.dispatchDaemonId) ctx.addIssue({
37137
- code: "custom",
37138
- path: ["dispatchDaemonId"],
37139
- message: "dispatchDaemonId must equal daemonId on a delivery report"
37140
- });
37141
- });
37142
- const RcGithubInstallation = object({
37143
- installationId: string().min(1),
37144
- action: string().min(1)
37145
- });
37146
- const RcDaemonRevoke = object({ daemonId: string().uuid() });
37147
36284
  /** App-level Slack message shortcut for opening the controls of the session that
37148
36285
  * owns the selected message's conversation. Direct apps receive it over Socket
37149
- * Mode; shared apps receive the same callback through the relay HTTP edge. */
36286
+ * Mode; shared apps receive the same callback through the relay HTTP edge.
36287
+ *
36288
+ * Declared to Slack by {@link buildSlackAppManifest} below, which is why it lives
36289
+ * here rather than beside the runtime Block Kit action ids in `frames/relay-cp.ts`:
36290
+ * those name controls on messages we post, this one names a manifest feature. */
37150
36291
  const SLACK_MANAGE_SESSION_SHORTCUT_CALLBACK_ID = "ac_manage_session";
37151
- /** One short choice from the compact Slack status overflow. Slack caps option
37152
- * values at 150 characters; the longer session target rides in `block_id`. */
37153
- const SlackStatusOverflowAction = _enum([
37154
- "switch-agent",
37155
- "manage",
37156
- "cancel"
37157
- ]);
37158
- object({
37159
- v: literal(1),
37160
- action: SlackStatusOverflowAction,
37161
- target: string().min(1)
37162
- }).strict();
37163
- object({
37164
- v: literal(1),
37165
- agentId: string().min(1),
37166
- integrationId: string().min(1),
37167
- sessionKey: string().min(1)
37168
- }).strict();
37169
- const AttributedRoute = object({
37170
- agentId: string().uuid(),
37171
- daemonId: string().uuid(),
37172
- integrationId: string().uuid(),
37173
- scope: object({
37174
- channel: string().optional(),
37175
- thread: string().optional()
37176
- }).optional(),
37177
- match: BindMatch
37178
- });
37179
- const RcBotSecrets = union([
37180
- object({
37181
- botToken: string(),
37182
- signingSecret: string()
37183
- }).catchall(unknown()),
37184
- object({
37185
- verificationToken: string(),
37186
- encryptKey: string().optional()
37187
- }).catchall(unknown()),
37188
- record$1(string(), unknown())
37189
- ]);
37190
- const RcAgentDirEntry = object({
37191
- agentId: string().uuid(),
37192
- name: string(),
37193
- daemonId: string().uuid(),
37194
- integrationId: string().uuid().optional()
37195
- });
37196
- const RcBotAssign = object({
37197
- botId: string().uuid(),
37198
- platform: Platform,
37199
- originKind: string().min(1).optional(),
37200
- credentialRevision: number$1().int().nonnegative().optional(),
37201
- secrets: RcBotSecrets,
37202
- ingress: unknown().optional(),
37203
- members: array(object({
37204
- daemonId: string().uuid(),
37205
- agentIds: array(string().uuid())
37206
- })),
37207
- agents: array(RcAgentDirEntry).default([]),
37208
- routes: array(AttributedRoute),
37209
- defaultAgentId: string().uuid().optional(),
37210
- defaultDaemonId: string().uuid().optional(),
37211
- gatedAgentIds: array(string().uuid()).default([]),
37212
- mutedChannels: array(string()).default([]),
37213
- gatedOffChannels: array(string()).default([]),
37214
- noticeAuthority: string().uuid().optional(),
37215
- noticedDmConversations: array(string()).default([])
37216
- });
37217
- const RcBotUnassign = object({
37218
- botId: string().uuid(),
37219
- credentialRevision: number$1().int().nonnegative().optional()
37220
- });
37221
- const RcRoutes = object({
37222
- botId: string().uuid(),
37223
- members: array(object({
37224
- daemonId: string().uuid(),
37225
- agentIds: array(string().uuid())
37226
- })),
37227
- agents: array(RcAgentDirEntry).default([]),
37228
- routes: array(AttributedRoute),
37229
- defaultAgentId: string().uuid().optional(),
37230
- defaultDaemonId: string().uuid().optional(),
37231
- gatedAgentIds: array(string().uuid()).default([]),
37232
- mutedChannels: array(string()).default([]),
37233
- gatedOffChannels: array(string()).default([]),
37234
- noticeAuthority: string().uuid().optional(),
37235
- noticedDmConversations: array(string()).default([])
37236
- });
37237
- const RcAssign = object({
37238
- botId: string().uuid(),
37239
- sessionKey: string().min(1),
37240
- agentId: string().uuid(),
37241
- daemonId: string().uuid()
37242
- });
37243
- const RcParticipantAssign = object({
37244
- botId: string().uuid(),
37245
- sessionKey: string().min(1),
37246
- agentId: string().uuid(),
37247
- daemonId: string().uuid()
37248
- });
37249
- const RcSetChannelAgent = object({
37250
- botId: string().uuid(),
37251
- channelId: string().min(1),
37252
- agentId: string().uuid()
37253
- });
37254
- const RcBotChannels = object({
37255
- botId: string().uuid(),
37256
- channels: array(IntegrationChannel)
37257
- });
37258
- const RcNoticePosted = object({
37259
- botId: string().uuid(),
37260
- channel: string().min(1)
37261
- });
37262
- const RcBotRevoked = object({
37263
- botId: string().uuid(),
37264
- reason: _enum(["app_uninstalled", "tokens_revoked"]),
37265
- credentialRevision: number$1().int().nonnegative().optional(),
37266
- eventAtMs: number$1().int().nonnegative().optional()
37267
- });
37268
- const RcBotRevokedOk = object({
37269
- botId: string().uuid(),
37270
- applied: boolean()
37271
- });
37272
- /** `type` string → payload schema for the relay↔CP wire. */
37273
- const RELAY_CP_SCHEMAS = {
37274
- "rc/auth": RcAuth,
37275
- "rc/auth/ok": RcAuthOk,
37276
- "rc/register": RcRegister,
37277
- "rc/registered": RcRegistered,
37278
- "rc/heartbeat": RcHeartbeat,
37279
- "rc/verify": RcVerify,
37280
- "rc/verify/ok": RcVerifyResult,
37281
- "rc/github-comment-authz": RcGithubCommentAuthz,
37282
- "rc/github-comment-authz/ok": RcGithubCommentAuthzResult,
37283
- "rc/github-rerequest": RcGithubRerequest,
37284
- "rc/github-rerequest/ok": RcGithubRerequestResult,
37285
- "rc/hook-assign": RcHookAssign,
37286
- "rc/hook-remove": RcHookRemove,
37287
- "rc/run-report": RcRunReport,
37288
- "rc/github-installation": RcGithubInstallation,
37289
- "rc/daemon-revoke": RcDaemonRevoke,
37290
- "rc/bot-assign": RcBotAssign,
37291
- "rc/bot-unassign": RcBotUnassign,
37292
- "rc/routes": RcRoutes,
37293
- "rc/assign": RcAssign,
37294
- "rc/participant-assign": RcParticipantAssign,
37295
- "rc/set-channel-agent": RcSetChannelAgent,
37296
- "rc/bot-channels": RcBotChannels,
37297
- "rc/bot-conversation": object({
37298
- botId: string().uuid(),
37299
- conversation: IntegrationChannel
37300
- }),
37301
- "rc/bot-revoked": RcBotRevoked,
37302
- "rc/bot-revoked/ok": RcBotRevokedOk,
37303
- "rc/notice-posted": RcNoticePosted,
37304
- "rc/thread-assign": object({
37305
- botId: string().uuid(),
37306
- sessionKey: string().min(1),
37307
- agentId: string().uuid(),
37308
- daemonId: string().uuid()
37309
- }),
37310
- "rc/thread-participant": object({
37311
- botId: string().uuid(),
37312
- sessionKey: string().min(1),
37313
- agentId: string().uuid(),
37314
- daemonId: string().uuid()
37315
- }),
37316
- "rc/thread-lookup": object({
37317
- botId: string().uuid(),
37318
- sessionKey: string().min(1)
37319
- }),
37320
- "rc/thread-lookup/ok": object({
37321
- botId: string().uuid(),
37322
- sessionKey: string().min(1),
37323
- target: object({
37324
- agentId: string().uuid(),
37325
- daemonId: string().uuid()
37326
- }).nullable(),
37327
- participants: array(object({
37328
- agentId: string().uuid(),
37329
- daemonId: string().uuid()
37330
- })).default([])
37331
- }),
37332
- "rc/collab-routes": CollabRoutesSnapshot,
37333
- "rc/mcp-assign": object({
37334
- providerId: string().uuid(),
37335
- upstreamUrl: string(),
37336
- headers: array(object({
37337
- name: string(),
37338
- value: string()
37339
- })).default([]),
37340
- grantKeyHashes: array(string().min(1)).min(1)
37341
- }),
37342
- "rc/mcp-unassign": object({
37343
- providerId: string().uuid(),
37344
- grantKeyHash: string().optional()
37345
- }),
37346
- "rc/memoryconnection-assign": object({
37347
- connectionId: string().uuid(),
37348
- revision: number$1().int().positive(),
37349
- upstreamUrl: string().url().max(2048),
37350
- headers: array(object({
37351
- name: string().min(1).max(128),
37352
- value: string().max(16 * 1024)
37353
- })).max(64).default([]),
37354
- grantKeyHashes: array(string().regex(/^[a-f0-9]{64}$/)).min(1).max(64)
37355
- }),
37356
- "rc/memoryconnection-unassign": object({
37357
- connectionId: string().uuid(),
37358
- revision: number$1().int().positive(),
37359
- grantKeyHash: string().regex(/^[a-f0-9]{64}$/).optional()
37360
- }),
37361
- error: ErrorFrame
37362
- };
37363
- Object.keys(RELAY_CP_SCHEMAS);
37364
- discriminatedUnion("type", [
37365
- frameSchema("rc/auth", RELAY_CP_SCHEMAS["rc/auth"]),
37366
- frameSchema("rc/auth/ok", RELAY_CP_SCHEMAS["rc/auth/ok"]),
37367
- frameSchema("rc/register", RELAY_CP_SCHEMAS["rc/register"]),
37368
- frameSchema("rc/registered", RELAY_CP_SCHEMAS["rc/registered"]),
37369
- frameSchema("rc/heartbeat", RELAY_CP_SCHEMAS["rc/heartbeat"]),
37370
- frameSchema("rc/verify", RELAY_CP_SCHEMAS["rc/verify"]),
37371
- frameSchema("rc/verify/ok", RELAY_CP_SCHEMAS["rc/verify/ok"]),
37372
- frameSchema("rc/github-comment-authz", RELAY_CP_SCHEMAS["rc/github-comment-authz"]),
37373
- frameSchema("rc/github-comment-authz/ok", RELAY_CP_SCHEMAS["rc/github-comment-authz/ok"]),
37374
- frameSchema("rc/github-rerequest", RELAY_CP_SCHEMAS["rc/github-rerequest"]),
37375
- frameSchema("rc/github-rerequest/ok", RELAY_CP_SCHEMAS["rc/github-rerequest/ok"]),
37376
- frameSchema("rc/hook-assign", RELAY_CP_SCHEMAS["rc/hook-assign"]),
37377
- frameSchema("rc/hook-remove", RELAY_CP_SCHEMAS["rc/hook-remove"]),
37378
- frameSchema("rc/run-report", RELAY_CP_SCHEMAS["rc/run-report"]),
37379
- frameSchema("rc/github-installation", RELAY_CP_SCHEMAS["rc/github-installation"]),
37380
- frameSchema("rc/daemon-revoke", RELAY_CP_SCHEMAS["rc/daemon-revoke"]),
37381
- frameSchema("rc/bot-assign", RELAY_CP_SCHEMAS["rc/bot-assign"]),
37382
- frameSchema("rc/bot-unassign", RELAY_CP_SCHEMAS["rc/bot-unassign"]),
37383
- frameSchema("rc/routes", RELAY_CP_SCHEMAS["rc/routes"]),
37384
- frameSchema("rc/assign", RELAY_CP_SCHEMAS["rc/assign"]),
37385
- frameSchema("rc/participant-assign", RELAY_CP_SCHEMAS["rc/participant-assign"]),
37386
- frameSchema("rc/set-channel-agent", RELAY_CP_SCHEMAS["rc/set-channel-agent"]),
37387
- frameSchema("rc/bot-channels", RELAY_CP_SCHEMAS["rc/bot-channels"]),
37388
- frameSchema("rc/bot-conversation", RELAY_CP_SCHEMAS["rc/bot-conversation"]),
37389
- frameSchema("rc/bot-revoked", RELAY_CP_SCHEMAS["rc/bot-revoked"]),
37390
- frameSchema("rc/bot-revoked/ok", RELAY_CP_SCHEMAS["rc/bot-revoked/ok"]),
37391
- frameSchema("rc/notice-posted", RELAY_CP_SCHEMAS["rc/notice-posted"]),
37392
- frameSchema("rc/thread-assign", RELAY_CP_SCHEMAS["rc/thread-assign"]),
37393
- frameSchema("rc/thread-participant", RELAY_CP_SCHEMAS["rc/thread-participant"]),
37394
- frameSchema("rc/thread-lookup", RELAY_CP_SCHEMAS["rc/thread-lookup"]),
37395
- frameSchema("rc/thread-lookup/ok", RELAY_CP_SCHEMAS["rc/thread-lookup/ok"]),
37396
- frameSchema("rc/collab-routes", RELAY_CP_SCHEMAS["rc/collab-routes"]),
37397
- frameSchema("rc/mcp-assign", RELAY_CP_SCHEMAS["rc/mcp-assign"]),
37398
- frameSchema("rc/mcp-unassign", RELAY_CP_SCHEMAS["rc/mcp-unassign"]),
37399
- frameSchema("rc/memoryconnection-assign", RELAY_CP_SCHEMAS["rc/memoryconnection-assign"]),
37400
- frameSchema("rc/memoryconnection-unassign", RELAY_CP_SCHEMAS["rc/memoryconnection-unassign"]),
37401
- frameSchema("error", RELAY_CP_SCHEMAS["error"])
37402
- ]);
37403
- //#endregion
37404
- //#region ../protocol/dist/slack-app-manifest.js
37405
36292
  /** Public platform profile copy. Never derive this from an Agent description. */
37406
36293
  const PLATFORM_APP_DESCRIPTION = "AI agent powered by AgentConnect.";
37407
36294
  const SLACK_BOT_SCOPES = [
@@ -37445,7 +36332,7 @@ function slackInteractionsRequestUrl(relayUrl) {
37445
36332
  function normalizeSlackRelayUrl(relayUrl) {
37446
36333
  return relayUrl.replace(/^ws(s?):\/\//i, "http$1://").replace(/\/+$/, "");
37447
36334
  }
37448
- /** One canonical Slack manifest shared by browser, Control Plane, and Tenant Admin. */
36335
+ /** One canonical Slack manifest shared by browser, Control Plane, and Setup Server. */
37449
36336
  function buildSlackAppManifest(name, options = {}) {
37450
36337
  const appName = name.trim() || "agentconnect";
37451
36338
  const displayName = options.displayName?.trim() || appName;
@@ -58307,7 +57194,7 @@ function makeSecretCipher(config) {
58307
57194
  /**
58308
57195
  * Safe, side-effect-free deployment-config subpath for operator tooling.
58309
57196
  *
58310
- * Importing the package root starts the Control Plane process, so Tenant Admin
57197
+ * Importing the package root starts the Control Plane process, so Setup Server
58311
57198
  * uses this narrow composition facade instead. The typed service remains the
58312
57199
  * only place that validates the document, seals secrets, computes fingerprints,
58313
57200
  * and applies an atomic replacement.
@@ -58391,7 +57278,7 @@ const LOGTO_GOOGLE_CONNECTOR_ID = "agentconnect-google";
58391
57278
  const LOGTO_SLACK_CONNECTOR_ID = "agentconnect-slack";
58392
57279
  //#endregion
58393
57280
  //#region src/deployment-config-client.ts
58394
- /** Typed deployment mutations used by the Tenant Admin provider workflows. */
57281
+ /** Typed deployment mutations used by the Setup Server provider workflows. */
58395
57282
  const DeploymentConfigPutSchema = strictObject({
58396
57283
  values: DeploymentConfigValuesV1Schema,
58397
57284
  secrets: DeploymentSecretPatchSchema.optional()
@@ -62046,14 +60933,14 @@ function createRemoteJWKSet(url, options) {
62046
60933
  return remoteJWKSet;
62047
60934
  }
62048
60935
  const ADMIN_ROLE = "ADMIN";
62049
- var TenantAdminAuthError = class extends Error {
60936
+ var SetupAuthError = class extends Error {
62050
60937
  statusCode;
62051
60938
  code;
62052
60939
  constructor(statusCode, code, message) {
62053
60940
  super(message);
62054
60941
  this.statusCode = statusCode;
62055
60942
  this.code = code;
62056
- this.name = "TenantAdminAuthError";
60943
+ this.name = "SetupAuthError";
62057
60944
  }
62058
60945
  };
62059
60946
  function urlAtOrigin(value, origin) {
@@ -62098,7 +60985,7 @@ function extractAdminRoles(claim) {
62098
60985
  if (typeof claim === "string") return claim.split(/[\s,]+/).filter(Boolean);
62099
60986
  return [];
62100
60987
  }
62101
- var TenantAdminAuthenticator = class {
60988
+ var SetupAuthenticator = class {
62102
60989
  config;
62103
60990
  verifyToken;
62104
60991
  constructor(config, verifyToken = createOidcTokenVerifier()) {
@@ -62106,19 +60993,19 @@ var TenantAdminAuthenticator = class {
62106
60993
  this.verifyToken = verifyToken;
62107
60994
  }
62108
60995
  /** Verify an OIDC identity. Only the one-time local bootstrap flow may call
62109
- * this with `requireAdminRole=false`. Ordinary admin routes always require it. */
60996
+ * this with `requireAdminRole=false`. Ordinary Setup routes always require it. */
62110
60997
  async authenticate(authorization, requireAdminRole = true) {
62111
60998
  const oidc = await this.config.get();
62112
- if (!oidc) throw new TenantAdminAuthError(503, "ADMIN_OIDC_NOT_CONFIGURED", "tenant admin OIDC is not configured");
62113
- if (!authorization?.startsWith("Bearer ")) throw new TenantAdminAuthError(401, "INVALID_TOKEN", "missing bearer token");
60999
+ if (!oidc) throw new SetupAuthError(503, "ADMIN_OIDC_NOT_CONFIGURED", "Setup Server OIDC is not configured");
61000
+ if (!authorization?.startsWith("Bearer ")) throw new SetupAuthError(401, "INVALID_TOKEN", "missing bearer token");
62114
61001
  let payload;
62115
61002
  try {
62116
61003
  payload = await this.verifyToken(authorization.slice(7), oidc);
62117
61004
  } catch {
62118
- throw new TenantAdminAuthError(401, "INVALID_TOKEN", "invalid bearer token");
61005
+ throw new SetupAuthError(401, "INVALID_TOKEN", "invalid bearer token");
62119
61006
  }
62120
- if (typeof payload.sub !== "string" || payload.sub.length === 0) throw new TenantAdminAuthError(401, "INVALID_TOKEN", "token is missing its subject");
62121
- if (requireAdminRole && !extractAdminRoles(payload["roles"]).includes("ADMIN")) throw new TenantAdminAuthError(403, "ADMIN_ROLE_REQUIRED", "ADMIN role required");
61007
+ if (typeof payload.sub !== "string" || payload.sub.length === 0) throw new SetupAuthError(401, "INVALID_TOKEN", "token is missing its subject");
61008
+ if (requireAdminRole && !extractAdminRoles(payload["roles"]).includes("ADMIN")) throw new SetupAuthError(403, "ADMIN_ROLE_REQUIRED", "ADMIN role required");
62122
61009
  return {
62123
61010
  subject: payload.sub,
62124
61011
  ...typeof payload.email === "string" ? { email: payload.email } : {}
@@ -62126,9 +61013,9 @@ var TenantAdminAuthenticator = class {
62126
61013
  }
62127
61014
  };
62128
61015
  //#endregion
62129
- //#region src/admin/config.ts
61016
+ //#region src/server/config.ts
62130
61017
  /**
62131
- * Process bootstrap configuration for the Tenant Admin server.
61018
+ * Process bootstrap configuration for the Setup Server.
62132
61019
  *
62133
61020
  * Product/provider settings intentionally do not live here: they are loaded
62134
61021
  * from the deployment config singleton after initialization, or held only in
@@ -62156,7 +61043,7 @@ const SecureOriginSchema = HttpOriginSchema.superRefine((value, ctx) => {
62156
61043
  message: "must use HTTPS unless it is loopback"
62157
61044
  });
62158
61045
  });
62159
- const TenantAdminProcessConfigSchema = object({
61046
+ const SetupServerProcessConfigSchema = object({
62160
61047
  NODE_ENV: _enum([
62161
61048
  "development",
62162
61049
  "test",
@@ -62164,8 +61051,8 @@ const TenantAdminProcessConfigSchema = object({
62164
61051
  ]).default("development"),
62165
61052
  HOST: string().default("127.0.0.1"),
62166
61053
  PORT: number().int().min(1).max(65535).default(8091),
62167
- TENANT_ADMIN_URL: SecureOriginSchema.default("http://localhost:8091"),
62168
- TENANT_ADMIN_ALLOW_CONTAINER_PROXY: stringbool().default(false),
61054
+ SETUP_SERVER_URL: SecureOriginSchema.default("http://localhost:8091"),
61055
+ SETUP_SERVER_ALLOW_CONTAINER_PROXY: stringbool().default(false),
62169
61056
  LOGTO_ADMIN_ENDPOINT: SecureOriginSchema.default("http://localhost:3002"),
62170
61057
  DATABASE_URL: string().url().default("postgresql://agentconnect:agentconnect-local-only@127.0.0.1:5432/agentconnect?schema=public"),
62171
61058
  SECRET_CIPHER: _enum(["none", "vault-transit"]).default("none"),
@@ -62190,8 +61077,8 @@ const TenantAdminProcessConfigSchema = object({
62190
61077
  message: "SECRET_CIPHER=vault-transit requires exactly one of VAULT_TOKEN or VAULT_JWT_ROLE"
62191
61078
  });
62192
61079
  });
62193
- function loadTenantAdminProcessConfig(env = process.env) {
62194
- return TenantAdminProcessConfigSchema.parse(env);
61080
+ function loadSetupServerProcessConfig(env = process.env) {
61081
+ return SetupServerProcessConfigSchema.parse(env);
62195
61082
  }
62196
61083
  //#endregion
62197
61084
  //#region \0@oxc-project+runtime@0.138.0/helpers/esm/taggedTemplateLiteral.js
@@ -62199,12 +61086,12 @@ function _taggedTemplateLiteral(e, t) {
62199
61086
  return t || (t = e.slice(0)), Object.freeze(Object.defineProperties(e, { raw: { value: Object.freeze(t) } }));
62200
61087
  }
62201
61088
  //#endregion
62202
- //#region src/admin/html.ts
61089
+ //#region src/server/html.ts
62203
61090
  var _templateObject;
62204
61091
  /** One deliberately small, dependency-free deployment administration page. */
62205
- const TENANT_ADMIN_HTML = String.raw(_templateObject || (_templateObject = _taggedTemplateLiteral(["<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>AgentConnect deployment settings</title>\n <style>\n :root { color-scheme: light dark; font: 15px/1.5 system-ui, sans-serif; }\n body { max-width: 1280px; margin: 48px auto; padding: 0 20px 60px; }\n h1 { margin-bottom: 4px; } h2 { margin-top: 32px; } h3 { margin: 0 0 8px; }\n .muted { color: #777; } .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }\n button, .button { padding: 8px 13px; cursor: pointer; }\n .button { border: 1px solid #8888; border-radius: 4px; color: inherit; text-decoration: none; display: inline-block; }\n input, select { width: min(520px, 100%); padding: 7px; box-sizing: border-box; }\n input[type=\"checkbox\"] { width: auto; padding: 0; }\n .field { display: grid; gap: 4px; margin: 10px 0; }\n .provider-stack { display: grid; gap: 16px; }\n .panel { border: 1px solid #8886; border-radius: 8px; padding: 16px; }\n .section-head, .provider-head { display: flex; justify-content: space-between; gap: 16px; align-items: start; }\n .section-head h2, .provider-head h3 { margin: 0; }\n .provider-head p { margin: 4px 0 0; }\n .badge { flex: none; border: 1px solid #8886; border-radius: 999px; padding: 3px 9px; font-size: 13px; }\n .badge.pass { color: #198754; border-color: #19875466; background: #19875412; }\n .badge.warn { color: #a66b00; border-color: #d99b1366; background: #d99b1315; }\n .badge.fail { color: #c33; border-color: #c333; background: #c3331111; }\n .credentials { display: grid; grid-template-columns: minmax(160px, 220px) minmax(0, 1fr); gap: 7px 16px; margin: 16px 0; }\n .credentials dt { color: #777; }\n .credentials dd { margin: 0; overflow-wrap: anywhere; }\n .credentials code { user-select: all; }\n .redacted { font-family: ui-monospace, monospace; letter-spacing: .08em; }\n .secret-line, .value-line { display: flex; gap: 10px; align-items: center; min-height: 30px; }\n .edit-secret { padding: 2px 8px; font-size: 13px; }\n .secret-editor { display: flex; gap: 8px; flex-wrap: wrap; width: 100%; }\n .secret-editor input { width: min(420px, 100%); }\n .edit-configuration { padding: 2px 8px; font-size: 13px; }\n .startup-owned { color: #777; font-size: 13px; }\n .danger { color: #c33; border-color: #c336; }\n .subsection { margin-top: 16px; padding-top: 14px; border-top: 1px solid #8883; }\n textarea { width: min(720px, 100%); min-height: 100px; padding: 7px; box-sizing: border-box; }\n .uris { margin: 8px 0; padding-left: 20px; } .uris code { user-select: all; }\n .notice { border-left: 4px solid #d99b13; padding: 8px 12px; background: #d99b1315; }\n .diff-title { display: block; margin-bottom: 8px; }\n .config-diff { display: grid; gap: 1px; border: 1px solid #8884; border-radius: 6px; overflow: hidden; background: #8884; }\n .diff-row { display: grid; grid-template-columns: minmax(140px, .8fr) minmax(0, 1.2fr) minmax(0, 1.2fr); background: Canvas; }\n .diff-row > * { min-width: 0; padding: 7px 9px; overflow-wrap: anywhere; white-space: pre-wrap; }\n .diff-head { font-size: 12px; font-weight: 600; color: #777; }\n .diff-field { font-weight: 600; }\n .diff-value { font-family: ui-monospace, monospace; font-size: 13px; }\n pre { padding: 12px; border-radius: 6px; background: #8881; overflow-x: auto; user-select: all; }\n #message { white-space: pre-wrap; padding: 10px 0; min-height: 1.5em; }\n .error { color: #c33; } .ok { color: #198754; } .warn { color: #a66b00; }\n code { overflow-wrap: anywhere; }\n .admin-layout { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 32px; align-items: start; }\n .admin-nav { position: sticky; top: 24px; display: grid; gap: 3px; padding: 10px; border: 1px solid #8886; border-radius: 8px; background: Canvas; }\n .admin-nav a { padding: 7px 9px; border-radius: 5px; color: inherit; text-decoration: none; white-space: nowrap; }\n .admin-nav a:hover { background: #8882; }\n .admin-section { scroll-margin-top: 24px; }\n details.environment { margin: 0 0 24px; } details.environment summary { cursor: pointer; font-weight: 600; }\n @media (max-width: 760px) {\n body { margin-top: 24px; }\n .admin-layout { grid-template-columns: 1fr; gap: 18px; }\n .admin-nav { position: static; display: flex; overflow-x: auto; }\n .credentials { grid-template-columns: 1fr; gap: 2px; }\n .credentials dd { margin-bottom: 8px; }\n .diff-row { grid-template-columns: 1fr; }\n .diff-head { display: none; }\n .diff-value::before { display: block; margin-bottom: 2px; color: #777; font: 11px/1.4 system-ui, sans-serif; }\n .diff-current::before { content: 'Current'; }\n .diff-expected::before { content: 'Expected'; }\n }\n [hidden] { display: none !important; }\n </style>\n</head>\n<body>\n <section id=\"access\">\n <h1>AgentConnect Tenant Admin</h1>\n <p id=\"access-message\" class=\"muted\" aria-live=\"polite\">Checking Logto sign-in…</p>\n <div class=\"row\">\n <button id=\"login\" hidden>Sign in with Logto</button>\n <a id=\"open-logto\" class=\"button\" href=\"http://localhost:3002\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n <button id=\"show-bootstrap\" hidden>Continue setup</button>\n </div>\n\n <section id=\"bootstrap\" hidden>\n <h2>Set up sign-in</h2>\n <p id=\"bootstrap-progress\" class=\"muted\">Step 1 of 2</p>\n <div id=\"bootstrap-logto-step\" class=\"panel\">\n <h3>Connect Logto</h3>\n <p class=\"muted\">Enter the one-time Logto Management API credential. It is sealed in the deployment database and verified before continuing.</p>\n <label class=\"field\">Logto M2M App ID<input id=\"logto-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Logto M2M App Secret<input id=\"logto-app-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-management-api-resource\" type=\"url\" autocomplete=\"off\"></label>\n <p class=\"muted\">Logto Cloud uses <code>https://&lt;tenant-id&gt;.logto.app/api</code>. Keep <code>https://default.logto.app/api</code> for Logto OSS.</p>\n <button id=\"bootstrap-logto-submit\">Save Logto and continue</button>\n </div>\n <div id=\"bootstrap-provider-step\" hidden>\n <h3>Choose a sign-in provider</h3>\n <p class=\"muted\">Logto Management API access is ready. Configure one provider to enable sign-in.</p>\n <label class=\"field\">Sign-in provider\n <select id=\"bootstrap-provider\"><option value=\"google\">Google (works on localhost)</option><option value=\"github\">GitHub integration App</option><option id=\"bootstrap-slack-option\" value=\"slack\">Slack integration App</option></select>\n </label>\n <div id=\"bootstrap-google\" class=\"panel\">\n <h3>Google OAuth client</h3>\n <p class=\"muted\">Create a Web application client in Google Auth Platform, then paste its credentials here.</p>\n <p>Authorized JavaScript origin:</p><ul id=\"bootstrap-google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"bootstrap-google-redirects\" class=\"uris\"></ul>\n <a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a>\n <label class=\"field\">Client ID<input id=\"bootstrap-google-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client Secret<input id=\"bootstrap-google-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-google-submit\">Save Google OAuth and configure Logto</button>\n </div>\n <div id=\"bootstrap-github\" class=\"panel\" hidden>\n <h3>GitHub integration App</h3>\n <p id=\"bootstrap-github-note\" class=\"muted\">This creates one complete App for both GitHub sign-in and repository integration.</p>\n <label class=\"field\">Owner\n <select id=\"bootstrap-github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"bootstrap-github-org-field\" class=\"field\" hidden>Organization login<input id=\"bootstrap-github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"bootstrap-github-name\" value=\"AgentConnect\"></label>\n <button id=\"bootstrap-github-submit\">Create GitHub App and configure Logto</button>\n </div>\n <div id=\"bootstrap-slack\" class=\"panel\" hidden>\n <h3>Slack integration App</h3>\n <p id=\"bootstrap-slack-note\" class=\"muted\">Creates one complete Slack App for workspace installation and a separate Sign in with Slack OIDC flow.</p>\n <p>Logto redirect URI:</p><ul id=\"bootstrap-slack-redirects\" class=\"uris\"></ul>\n <label class=\"field\">App name<input id=\"bootstrap-slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"bootstrap-slack-token\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-slack-submit\">Create Slack App and configure Logto</button>\n </div>\n <button id=\"bootstrap-back\">Back to Logto credentials</button>\n </div>\n </section>\n </section>\n\n <main id=\"admin\" hidden>\n <div class=\"admin-layout\">\n <nav class=\"admin-nav\" aria-label=\"Deployment settings\">\n <a href=\"#startup-section\">Startup</a>\n <a href=\"#logto-section\">Logto</a>\n <a href=\"#github-section\">GitHub</a>\n <a href=\"#slack-section\">Slack</a>\n <a href=\"#google-section\">Google</a>\n <a href=\"#feishu-section\">Feishu</a>\n <a href=\"#lark-section\">Lark</a>\n <a href=\"#options-section\">Options</a>\n </nav>\n <div class=\"admin-content\">\n <h1>AgentConnect deployment settings</h1>\n <p class=\"muted\">Saved settings take effect after the stack is restarted.</p>\n <div class=\"row\">\n <button id=\"logout\">Log out</button>\n </div>\n <div id=\"message\" aria-live=\"polite\"></div>\n\n <section id=\"editor\" hidden>\n <details id=\"startup-section\" class=\"environment admin-section\" open>\n <summary>Startup environment</summary>\n <p class=\"notice\">Public service URLs come from <code>.env</code>. Provider callbacks below are derived from these values.</p>\n <pre id=\"startup-environment\"></pre>\n </details>\n\n <section id=\"logto-section\" class=\"admin-section\" aria-labelledby=\"logto-heading\">\n <div class=\"section-head\">\n <div><h2 id=\"logto-heading\">Logto</h2><p class=\"muted\">Authentication and Tenant Admin access.</p></div>\n <span id=\"logto-match\" class=\"badge\">Not checked</span>\n </div>\n <div class=\"panel\">\n <dl class=\"credentials\">\n <dt>Management endpoint</dt><dd class=\"value-line\"><code id=\"logto-management-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>Management App ID</dt><dd class=\"value-line\"><code id=\"logto-management-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management API resource</dt><dd class=\"value-line\"><code id=\"logto-management-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management App secret</dt><dd class=\"secret-line\"><span id=\"logto-management-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.managementAppSecret\" data-secret-display=\"logto-management-secret-display\">Edit</button></dd>\n <dt>Sign-in endpoint</dt><dd class=\"value-line\"><code id=\"logto-browser-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>SPA App ID</dt><dd class=\"value-line\"><code id=\"logto-browser-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Browser API resource</dt><dd class=\"value-line\"><code id=\"logto-browser-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n </dl>\n <div id=\"logto-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Logto configuration</h3>\n <label class=\"field\">Management App ID<input id=\"logto-edit-management-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-edit-management-resource\" autocomplete=\"off\"></label>\n <label class=\"field\">New Management App secret<input id=\"logto-edit-management-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when management identity changes\"></label>\n <label class=\"field\">SPA App ID<input id=\"logto-edit-browser-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Browser API resource<input id=\"logto-edit-browser-resource\" autocomplete=\"off\" placeholder=\"Optional\"></label>\n <div class=\"row\"><button id=\"save-logto-configuration\">Save configuration</button><button id=\"cancel-logto-configuration\">Cancel</button></div>\n </div>\n <p id=\"logto-status\" class=\"muted\">Checking redirects and sign-in settings…</p>\n <div id=\"logto-drift\" class=\"notice\" hidden></div>\n <div class=\"row\">\n <button id=\"check-logto\">Check match</button>\n <button id=\"reconcile-logto\">Apply expected settings</button>\n <a id=\"logto-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n </div>\n </div>\n </section>\n\n <h2>Providers</h2>\n <div class=\"provider-stack\">\n <section id=\"github-section\" class=\"panel admin-section\" aria-labelledby=\"github-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"github-heading\">GitHub</h3><p class=\"muted\">Repository integration and optional Logto sign-in.</p></div>\n <span id=\"github-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"github-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>App slug</dt><dd class=\"value-line\"><code id=\"github-app-slug\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"github-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Webhook delivery</dt><dd class=\"value-line\"><code id=\"github-webhook-enabled\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"github-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.clientSecret\" data-secret-display=\"github-client-secret-display\">Edit</button></dd>\n <dt>Private key</dt><dd class=\"secret-line\"><span id=\"github-private-key-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.privateKeyB64\" data-secret-display=\"github-private-key-display\">Edit</button></dd>\n <dt>Webhook secret</dt><dd class=\"secret-line\"><span id=\"github-webhook-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.webhookSecret\" data-secret-display=\"github-webhook-secret-display\">Edit</button></dd>\n <dt>Logto connector secret</dt><dd class=\"secret-line\"><span id=\"github-logto-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.githubConnectorClientSecret\" data-secret-display=\"github-logto-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"github-status\" class=\"muted\"></p>\n <div id=\"github-drift\" class=\"notice\" hidden></div>\n <div id=\"github-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit GitHub App identity</h3>\n <label class=\"field\">App ID<input id=\"github-edit-app-id\" inputmode=\"numeric\" autocomplete=\"off\"></label>\n <label class=\"field\">App slug<input id=\"github-edit-slug\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"github-edit-client-id\" autocomplete=\"off\"></label>\n <label><input id=\"github-edit-webhook-enabled\" type=\"checkbox\"> Enable Relay webhook delivery</label>\n <label class=\"field\">New client secret<input id=\"github-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when App or Client ID changes\"></label>\n <label class=\"field\">New private key (base64)<textarea id=\"github-edit-private-key\" autocomplete=\"off\" placeholder=\"Required when App ID changes\"></textarea></label>\n <label class=\"field\">New webhook secret<input id=\"github-edit-webhook-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when an active webhook App ID changes\"></label>\n <label id=\"github-edit-logto-secret-field\" class=\"field\" hidden>New Logto connector client secret<input id=\"github-edit-logto-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when this App is also used for sign-in\"></label>\n <div class=\"row\"><button id=\"save-github-configuration\">Save configuration</button><button id=\"cancel-github-configuration\">Cancel</button></div>\n </div>\n <div id=\"github-create-controls\" class=\"subsection\">\n <label class=\"field\">Owner\n <select id=\"github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"github-org-field\" class=\"field\" hidden>Organization login<input id=\"github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"github-name\" value=\"AgentConnect\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-github\">Create GitHub App</button>\n <button id=\"connect-github-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-github\" hidden>Check match</button>\n <button id=\"clear-github\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"github-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open GitHub settings</a>\n </div>\n </section>\n\n <section id=\"slack-section\" class=\"panel admin-section\" aria-labelledby=\"slack-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"slack-heading\">Slack</h3><p class=\"muted\">One App for workspace integration and Logto sign-in.</p></div>\n <span id=\"slack-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"slack-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"slack-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"slack-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.clientSecret\" data-secret-display=\"slack-client-secret-display\">Edit</button></dd>\n <dt>Signing secret</dt><dd class=\"secret-line\"><span id=\"slack-signing-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.signingSecret\" data-secret-display=\"slack-signing-secret-display\">Edit</button></dd>\n <dt>Logto sign-in</dt><dd id=\"slack-logto-status\">Not configured</dd>\n </dl>\n <p id=\"slack-status\" class=\"muted\"></p>\n <div id=\"slack-drift\" class=\"notice\" hidden></div>\n <div id=\"slack-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Slack App identity</h3>\n <label class=\"field\">App ID<input id=\"slack-edit-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"slack-edit-client-id\" autocomplete=\"off\"></label>\n <label class=\"field\">New client secret<input id=\"slack-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <label class=\"field\">New signing secret<input id=\"slack-edit-signing-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <div class=\"row\"><button id=\"save-slack-configuration\">Save configuration</button><button id=\"cancel-slack-configuration\">Cancel</button></div>\n </div>\n <div class=\"subsection\">\n <label id=\"slack-name-field\" class=\"field\">App name<input id=\"slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"slack-token\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Used only for create or check\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-slack\">Create Slack App</button>\n <button id=\"connect-slack-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-slack\" hidden>Check match</button>\n <button id=\"clear-slack\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"slack-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Slack settings</a>\n </div>\n </section>\n\n <section id=\"google-section\" class=\"panel admin-section\" aria-labelledby=\"google-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"google-heading\">Google</h3><p class=\"muted\">OAuth client used by the Logto Google connector.</p></div>\n <span id=\"google-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"google-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"google\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"google-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.googleConnectorClientSecret\" data-secret-display=\"google-client-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"google-status\" class=\"muted\"></p>\n <div id=\"google-drift\" class=\"notice\" hidden></div>\n <p>Authorized JavaScript origin:</p><ul id=\"google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"google-redirects\" class=\"uris\"></ul>\n <p class=\"muted\">Google does not expose OAuth client redirect settings through an API. Copy these required values into Google Auth Platform; Tenant Admin can verify only the Logto connector.</p>\n <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a></div>\n <div id=\"google-config-controls\" class=\"subsection\">\n <label class=\"field\">Client ID<input id=\"google-id\" autocomplete=\"off\"></label>\n <label id=\"google-initial-secret-field\" class=\"field\">Client secret<input id=\"google-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when Client ID changes\"></label>\n </div>\n <div class=\"row\"><button id=\"save-google\">Save Google client</button><button id=\"cancel-google-configuration\" hidden>Cancel</button><button id=\"check-google\" hidden>Check match</button><button id=\"clear-google\" class=\"danger\" hidden>Clear configuration</button></div>\n </section>\n\n <section id=\"feishu-section\" class=\"panel admin-section\" aria-labelledby=\"feishu-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"feishu-heading\">Feishu</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Feishu, OAuth callbacks, and published App setup.</p></div>\n <span id=\"feishu-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"feishu-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"feishu\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"feishu-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"feishu.loginAppSecret\" data-secret-display=\"feishu-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"feishu-login-status\" class=\"muted\"></p>\n <div id=\"feishu-drift\" class=\"notice\" hidden></div>\n <div id=\"feishu-config-controls\" class=\"subsection\">\n <label id=\"feishu-create-name-field\" class=\"field\">New App name<input id=\"feishu-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"feishu-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"feishu-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-feishu-login-app\">Create Feishu App</button>\n <button id=\"save-feishu-login-app\">Save existing App</button>\n <button id=\"cancel-feishu-configuration\" hidden>Cancel</button>\n <button id=\"check-feishu-login-app\" hidden>Check setup</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"feishu-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Feishu settings</a>\n </div>\n </section>\n\n <section id=\"lark-section\" class=\"panel admin-section\" aria-labelledby=\"lark-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"lark-heading\">Lark</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Lark, OAuth callbacks, and published App setup.</p></div>\n <span id=\"lark-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"lark-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"lark\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"lark-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"lark.loginAppSecret\" data-secret-display=\"lark-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"lark-login-status\" class=\"muted\"></p>\n <div id=\"lark-drift\" class=\"notice\" hidden></div>\n <div id=\"lark-config-controls\" class=\"subsection\">\n <label id=\"lark-create-name-field\" class=\"field\">New App name<input id=\"lark-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"lark-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"lark-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-lark-login-app\">Create Lark App</button>\n <button id=\"save-lark-login-app\">Save existing App</button>\n <button id=\"cancel-lark-configuration\" hidden>Cancel</button>\n <button id=\"check-lark-login-app\" hidden>Check setup</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"lark-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Lark settings</a>\n </div>\n </section>\n </div>\n\n <section id=\"options-section\" class=\"admin-section\">\n <h2>Deployment options</h2>\n <div class=\"panel\">\n <label class=\"field\"><span><input id=\"preset-agents-enabled\" type=\"checkbox\"> Enable preset Agents</span></label>\n <div class=\"row\"><button id=\"save-options\">Save options</button></div>\n </div>\n </section>\n </section>\n </div>\n </div>\n </main>\n\n <script>\n const api = '/api/v1';\n const tokenKey = 'agentconnect.tenant-admin.token';\n const verifierKey = 'agentconnect.tenant-admin.pkce';\n const stateKey = 'agentconnect.tenant-admin.state';\n let currentRevision = 0;\n let currentStatus = null;\n let bootstrapInfo = null;\n const el = (id) => document.getElementById(id);\n const message = (text, error = false) => {\n const target = el('admin').hidden ? el('access-message') : el('message');\n target.textContent = text;\n target.className = error ? 'error' : 'ok';\n };\n const bearer = () => {\n const token = sessionStorage.getItem(tokenKey);\n return token ? { authorization: 'Bearer ' + token } : {};\n };\n const json = async (response) => {\n const body = await response.json().catch(() => ({}));\n if (!response.ok) throw Object.assign(new Error(body.message || ('HTTP ' + response.status)), { status: response.status, code: body.code });\n return body;\n };\n const base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/+/g, '-').replace(///g, '_').replace(/=+$/, '');\n const random = () => base64url(crypto.getRandomValues(new Uint8Array(32)));\n const configured = (byKey, key) => Boolean(byKey.get(key) && byKey.get(key).configured);\n\n function showIdentityEditors(provider, show) {\n for (const button of document.querySelectorAll('.edit-configuration[data-provider=\"' + provider + '\"]')) {\n button.hidden = !show;\n }\n }\n\n function text(id, value) {\n el(id).textContent = value === null || value === undefined || value === '' ? 'Not configured' : String(value);\n }\n\n function secretText(id, byKey, key, editable) {\n const stored = configured(byKey, key);\n const display = el(id);\n const button = document.querySelector('[data-secret-display=\"' + id + '\"]');\n const row = display.closest('.secret-line');\n const editor = row && row.querySelector('.secret-editor');\n if (editor) editor.remove();\n display.hidden = false;\n display.textContent = stored ? '***' : 'Not configured';\n display.className = stored ? 'redacted' : 'muted';\n button.hidden = !editable;\n button.textContent = stored ? 'Edit' : 'Set';\n }\n\n function match(id, state, label) {\n const target = el(id);\n target.textContent = label;\n target.className = 'badge' + (state ? ' ' + state : '');\n }\n\n async function authConfig() { return json(await fetch(api + '/auth-config')); }\n\n async function signIn() {\n const config = await authConfig();\n if (config.mode !== 'oidc') throw new Error('Save an OIDC configuration first');\n const verifier = random();\n const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));\n const state = random();\n sessionStorage.setItem(verifierKey, verifier);\n sessionStorage.setItem(stateKey, state);\n const url = new URL(config.authorizationEndpoint);\n url.searchParams.set('client_id', config.appId);\n url.searchParams.set('redirect_uri', config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', 'openid profile email roles');\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n if (config.resource) url.searchParams.set('resource', config.resource);\n location.assign(url);\n }\n\n async function finishSignIn() {\n const url = new URL(location.href);\n const code = url.searchParams.get('code');\n if (!code) return;\n const state = url.searchParams.get('state');\n if (!state || state !== sessionStorage.getItem(stateKey)) throw new Error('OIDC state mismatch');\n const verifier = sessionStorage.getItem(verifierKey);\n if (!verifier) throw new Error('PKCE verifier is missing; start sign-in again');\n const config = await authConfig();\n const body = new URLSearchParams({\n grant_type: 'authorization_code', code, client_id: config.appId,\n redirect_uri: config.redirectUri, code_verifier: verifier\n });\n if (config.resource) body.set('resource', config.resource);\n const tokens = await json(await fetch(config.tokenEndpoint, {\n method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body\n }));\n if (typeof tokens.id_token !== 'string') throw new Error('Logto did not return an ID token');\n sessionStorage.setItem(tokenKey, tokens.id_token);\n sessionStorage.removeItem(verifierKey);\n sessionStorage.removeItem(stateKey);\n history.replaceState({}, '', '/');\n }\n\n function renderUriList(id, values) {\n el(id).replaceChildren(...(values || []).map((value) => {\n const item = document.createElement('li');\n const code = document.createElement('code'); code.textContent = value;\n item.append(code); return item;\n }));\n }\n\n function updateBootstrapProvider() {\n const provider = el('bootstrap-provider').value;\n el('bootstrap-google').hidden = provider !== 'google';\n el('bootstrap-github').hidden = provider !== 'github';\n el('bootstrap-slack').hidden = provider !== 'slack';\n }\n\n function showBootstrapStep(step) {\n const logto = step === 'logto';\n el('bootstrap-logto-step').hidden = !logto;\n el('bootstrap-provider-step').hidden = logto;\n el('bootstrap-progress').textContent = logto ? 'Step 1 of 2' : 'Step 2 of 2';\n }\n\n function updateOwner(prefix) {\n el(prefix + '-org-field').hidden = el(prefix + '-owner').value !== 'organization';\n }\n\n function ownership(prefix) {\n if (el(prefix + '-owner').value === 'personal') return { owner: 'personal', organization: null };\n const organization = el(prefix + '-org').value.trim();\n if (!organization) throw new Error('Enter the GitHub organization login');\n return { owner: 'organization', organization };\n }\n\n function formatDiffValue(value) {\n if (value === null || value === undefined || value === '') return 'Not configured';\n if (Array.isArray(value)) return value.length ? value.map(formatDiffValue).join('\n') : '[]';\n if (typeof value === 'object') {\n return Object.entries(value).map(([key, item]) => key + ': ' + (Array.isArray(item) ? item.join(', ') : formatDiffValue(item))).join('\n');\n }\n return String(value);\n }\n\n function showDiff(id, rows, label = 'Update required') {\n const box = el(id);\n box.hidden = rows.length === 0;\n box.replaceChildren();\n if (rows.length === 0) return;\n const title = document.createElement('strong');\n title.className = 'diff-title';\n title.textContent = label;\n const grid = document.createElement('div');\n grid.className = 'config-diff';\n const head = document.createElement('div');\n head.className = 'diff-row diff-head';\n for (const text of ['Field', 'Current', 'Expected']) {\n const cell = document.createElement('span'); cell.textContent = text; head.append(cell);\n }\n grid.append(head);\n for (const item of rows) {\n const row = document.createElement('div'); row.className = 'diff-row';\n const field = document.createElement('span'); field.className = 'diff-field'; field.textContent = item.field;\n const current = document.createElement('span'); current.className = 'diff-value diff-current'; current.textContent = formatDiffValue(item.current);\n const expected = document.createElement('span'); expected.className = 'diff-value diff-expected'; expected.textContent = formatDiffValue(item.expected);\n row.append(field, current, expected); grid.append(row);\n }\n box.append(title, grid);\n }\n\n function renderStartupEnvironment() {\n const services = bootstrapInfo.services;\n const environment = [\n ['AGENTCONNECT_PUBLIC_CP_URL', services.controlPlane],\n ['AGENTCONNECT_PUBLIC_RELAY_URL', services.relay],\n ['AGENTCONNECT_PUBLIC_WEB_URL', services.web],\n ['LOGTO_ENDPOINT', bootstrapInfo.logtoEndpoint],\n ['LOGTO_MGMT_ENDPOINT', bootstrapInfo.logtoManagementEndpoint]\n ];\n el('startup-environment').textContent = environment\n .filter(([, value]) => value)\n .map(([key, value]) => key + '=' + value)\n .join('\n');\n }\n\n function renderApps(status) {\n currentStatus = status;\n const byKey = new Map((status.secrets || []).map((item) => [item.key, item]));\n const values = status.values;\n const expected = status.providerExpectations || { github: null, slack: null, google: { origins: [], redirects: [] } };\n renderStartupEnvironment();\n\n const logto = values.logto;\n text('logto-management-endpoint', logto && bootstrapInfo.logtoManagementEndpoint);\n text('logto-management-id', logto && logto.managementAppId);\n text('logto-management-resource', logto && logto.managementResource);\n secretText('logto-management-secret-display', byKey, 'logto.managementAppSecret', Boolean(logto));\n text('logto-browser-endpoint', logto && logto.browser && bootstrapInfo.logtoEndpoint);\n text('logto-browser-id', values.auth.mode === 'oidc' ? values.auth.browserClient.appId : null);\n text('logto-browser-resource', logto && logto.browser && logto.browser.apiResource);\n el('logto-edit-controls').hidden = true;\n showIdentityEditors('logto', Boolean(logto && logto.browser && values.auth.mode === 'oidc'));\n match(\n 'logto-match',\n logto && configured(byKey, 'logto.managementAppSecret') ? '' : 'warn',\n logto && configured(byKey, 'logto.managementAppSecret') ? 'Ready to check' : 'Not configured'\n );\n\n el('preset-agents-enabled').checked = values.features.presetAgentsEnabled;\n\n const github = values.github;\n const webhookStored = byKey.get('github.webhookSecret') && byKey.get('github.webhookSecret').configured;\n const webhookInactive = github && github.webhookEnabled === false;\n text('github-app-id', github && github.appId);\n text('github-app-slug', github && github.slug);\n text('github-client-id', github && github.clientId);\n text('github-webhook-enabled', github && (github.webhookEnabled === false ? 'Disabled' : 'Enabled'));\n el('github-edit-controls').hidden = true;\n showIdentityEditors('github', Boolean(github));\n secretText('github-client-secret-display', byKey, 'github.clientSecret', Boolean(github));\n secretText('github-private-key-display', byKey, 'github.privateKeyB64', Boolean(github));\n secretText('github-webhook-secret-display', byKey, 'github.webhookSecret', Boolean(github));\n secretText('github-logto-secret-display', byKey, 'logto.githubConnectorClientSecret', Boolean(values.logto && values.logto.githubConnector));\n el('github-status').textContent = github\n ? github.slug + ' is configured. Webhook secret: ' + (webhookStored ? 'stored' : webhookInactive ? 'not required yet' : 'missing') + '.' +\n (webhookInactive ? ' Relay webhook delivery is disabled.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github\n ? expected.github\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'Valid Web, API, and ingress URLs' }]\n : [];\n match('github-match', !github ? '' : githubDrift.length ? 'warn' : '', !github ? 'Not configured' : githubDrift.length ? 'Expected URLs changed' : 'Ready to check');\n el('github-create-controls').hidden = Boolean(github);\n el('create-github').hidden = Boolean(github);\n el('clear-github').hidden = !github;\n el('connect-github-login').hidden = !github || !values.logto || Boolean(values.logto.githubConnector);\n el('check-github').hidden = !github;\n if (github) showDiff('github-drift', githubDrift, 'Expected URLs changed since App creation');\n else el('github-drift').hidden = true;\n\n const slack = values.slack;\n text('slack-app-id', slack && slack.appId);\n text('slack-client-id', slack && slack.clientId);\n el('slack-edit-controls').hidden = true;\n showIdentityEditors('slack', Boolean(slack));\n secretText('slack-client-secret-display', byKey, 'slack.clientSecret', Boolean(slack));\n secretText('slack-signing-secret-display', byKey, 'slack.signingSecret', Boolean(slack));\n el('slack-logto-status').textContent = values.logto && values.logto.slackConnector\n ? 'Enabled; reuses the Slack client secret above.'\n : 'Not enabled.';\n el('slack-status').textContent = slack ? slack.appId + ' is configured.' : 'Creates the default AgentConnect integration manifest.';\n const slackDrift = slack\n ? expected.slack\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'HTTPS Web, API, and ingress URLs' }]\n : [];\n match('slack-match', !slack ? '' : slackDrift.length ? 'warn' : '', !slack ? 'Not configured' : slackDrift.length ? 'Update required' : 'Ready to check');\n el('slack-name-field').hidden = Boolean(slack);\n el('create-slack').hidden = Boolean(slack);\n el('connect-slack-login').hidden = !slack || !values.logto || Boolean(values.logto.slackConnector) || !bootstrapInfo?.slackAvailable;\n el('clear-slack').hidden = !slack;\n el('check-slack').hidden = !slack;\n el('slack-settings').hidden = !slack;\n if (slack) {\n el('slack-settings').href = 'https://api.slack.com/apps/' + encodeURIComponent(slack.appId);\n showDiff('slack-drift', slackDrift);\n } else el('slack-drift').hidden = true;\n\n const google = values.logto && values.logto.googleConnector;\n const googleSecret = configured(byKey, 'logto.googleConnectorClientSecret');\n const expectedGoogle = expected.google;\n renderUriList('google-origins', expectedGoogle.origins);\n renderUriList('google-redirects', expectedGoogle.redirects);\n text('google-client-id', google && google.clientId);\n secretText('google-client-secret-display', byKey, 'logto.googleConnectorClientSecret', Boolean(google));\n showIdentityEditors('google', Boolean(google));\n el('google-id').value = google ? google.clientId : '';\n el('google-status').textContent = google ? 'Google OAuth client is configured.' : 'Create a Web application OAuth client manually, then save it here.';\n showDiff('google-drift', []);\n match('google-match', google && googleSecret ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : \"Can't verify automatically\");\n el('google-initial-secret-field').hidden = googleSecret;\n el('save-google').textContent = google ? 'Confirm callback settings' : 'Save Google client';\n el('google-config-controls').hidden = Boolean(google);\n el('save-google').hidden = Boolean(google);\n el('cancel-google-configuration').hidden = true;\n el('check-google').hidden = !google || !googleSecret;\n el('clear-google').hidden = !google;\n\n const feishuSecret = byKey.get('feishu.loginAppSecret');\n const larkSecret = byKey.get('lark.loginAppSecret');\n el('feishu-login-id').value = values.feishu ? values.feishu.loginAppId : '';\n el('lark-login-id').value = values.lark ? values.lark.loginAppId : '';\n text('feishu-app-id', values.feishu && values.feishu.loginAppId);\n text('lark-app-id', values.lark && values.lark.loginAppId);\n showIdentityEditors('feishu', Boolean(values.feishu));\n showIdentityEditors('lark', Boolean(values.lark));\n secretText('feishu-app-secret-display', byKey, 'feishu.loginAppSecret', Boolean(values.feishu));\n secretText('lark-app-secret-display', byKey, 'lark.loginAppSecret', Boolean(values.lark));\n el('feishu-login-status').textContent = values.feishu && feishuSecret && feishuSecret.configured\n ? values.feishu.loginAppId + ' is configured.'\n : 'No Feishu tenant App is configured.';\n el('lark-login-status').textContent = values.lark && larkSecret && larkSecret.configured\n ? values.lark.loginAppId + ' is configured.'\n : 'No Lark tenant App is configured.';\n match('feishu-match', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? '' : '', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? 'Ready to check' : 'Not configured');\n match('lark-match', values.lark && configured(byKey, 'lark.loginAppSecret') ? '' : '', values.lark && configured(byKey, 'lark.loginAppSecret') ? 'Ready to check' : 'Not configured');\n showDiff('feishu-drift', []);\n showDiff('lark-drift', []);\n el('feishu-config-controls').hidden = Boolean(values.feishu);\n el('lark-config-controls').hidden = Boolean(values.lark);\n el('feishu-create-name-field').hidden = Boolean(values.feishu);\n el('lark-create-name-field').hidden = Boolean(values.lark);\n el('create-feishu-login-app').hidden = Boolean(values.feishu);\n el('create-lark-login-app').hidden = Boolean(values.lark);\n el('save-feishu-login-app').hidden = Boolean(values.feishu);\n el('save-lark-login-app').hidden = Boolean(values.lark);\n el('cancel-feishu-configuration').hidden = true;\n el('cancel-lark-configuration').hidden = true;\n el('clear-feishu').hidden = !values.feishu;\n el('clear-lark').hidden = !values.lark;\n el('check-feishu-login-app').hidden = !values.feishu || !configured(byKey, 'feishu.loginAppSecret');\n el('check-lark-login-app').hidden = !values.lark || !configured(byKey, 'lark.loginAppSecret');\n el('feishu-settings').hidden = !values.feishu;\n el('lark-settings').hidden = !values.lark;\n if (values.feishu) el('feishu-settings').href = 'https://open.feishu.cn/app/' + encodeURIComponent(values.feishu.loginAppId);\n if (values.lark) el('lark-settings').href = 'https://open.larksuite.com/app/' + encodeURIComponent(values.lark.loginAppId);\n }\n\n function requiredInput(id, label) {\n const value = el(id).value.trim();\n if (!value) throw new Error('Enter ' + label);\n return value;\n }\n\n function githubConnectorUsesDeployment(values) {\n const github = values.github;\n const connector = values.logto && values.logto.githubConnector;\n return Boolean(\n github && connector &&\n connector.appId === github.appId &&\n connector.slug === github.slug &&\n connector.clientId === github.clientId\n );\n }\n\n function beginConfigurationEdit(provider) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const values = currentStatus.values;\n if (provider === 'logto') {\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') return;\n el('logto-edit-management-id').value = logto.managementAppId;\n el('logto-edit-management-resource').value = logto.managementResource;\n el('logto-edit-management-secret').value = '';\n el('logto-edit-browser-id').value = values.auth.browserClient.appId;\n el('logto-edit-browser-resource').value = logto.browser.apiResource || '';\n el('logto-edit-controls').hidden = false;\n el('logto-edit-management-id').focus();\n } else if (provider === 'github') {\n const github = values.github;\n if (!github) return;\n el('github-edit-app-id').value = String(github.appId);\n el('github-edit-slug').value = github.slug;\n el('github-edit-client-id').value = github.clientId || '';\n el('github-edit-webhook-enabled').checked = github.webhookEnabled !== false;\n for (const id of ['github-edit-client-secret', 'github-edit-private-key', 'github-edit-webhook-secret', 'github-edit-logto-secret']) el(id).value = '';\n el('github-edit-logto-secret-field').hidden = !githubConnectorUsesDeployment(values);\n el('github-edit-controls').hidden = false;\n el('clear-github').hidden = true;\n el('github-edit-app-id').focus();\n } else if (provider === 'slack') {\n const slack = values.slack;\n if (!slack) return;\n el('slack-edit-app-id').value = slack.appId;\n el('slack-edit-client-id').value = slack.clientId;\n el('slack-edit-client-secret').value = '';\n el('slack-edit-signing-secret').value = '';\n el('slack-edit-controls').hidden = false;\n el('clear-slack').hidden = true;\n el('slack-edit-app-id').focus();\n } else if (provider === 'google') {\n const google = values.logto && values.logto.googleConnector;\n if (!google) return;\n el('google-id').value = google.clientId;\n el('google-secret').value = '';\n el('google-config-controls').hidden = false;\n el('google-initial-secret-field').hidden = false;\n el('save-google').hidden = false;\n el('save-google').textContent = 'Save Google client';\n el('cancel-google-configuration').hidden = false;\n el('clear-google').hidden = true;\n el('google-id').focus();\n } else if (provider === 'feishu' || provider === 'lark') {\n el(provider + '-config-controls').hidden = false;\n el(provider + '-create-name-field').hidden = true;\n el('create-' + provider + '-login-app').hidden = true;\n el('save-' + provider + '-login-app').hidden = false;\n el('cancel-' + provider + '-configuration').hidden = false;\n el('clear-' + provider).hidden = true;\n el(provider + '-login-secret').value = '';\n el(provider + '-login-id').focus();\n }\n showIdentityEditors(provider, false);\n }\n\n function cancelConfigurationEdit() {\n if (currentStatus) renderApps(currentStatus);\n }\n\n async function replaceConfiguration(values, secrets, successMessage) {\n const response = await json(await fetch(api + '/deployment-config', {\n method: 'PUT',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values, ...(secrets ? { secrets } : {}) })\n }));\n await load();\n message(successMessage || 'Configuration saved. Restart AgentConnect to apply it.');\n return response;\n }\n\n async function saveLogtoConfiguration() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = currentStatus.values;\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') throw new Error('Logto is not configured');\n const managementAppId = requiredInput('logto-edit-management-id', 'the Management App ID');\n const managementResource = requiredInput('logto-edit-management-resource', 'the Management API resource');\n const browserAppId = requiredInput('logto-edit-browser-id', 'the SPA App ID');\n const browserResource = el('logto-edit-browser-resource').value.trim() || null;\n const managementIdentityChanged = managementAppId !== logto.managementAppId;\n const managementSecret = el('logto-edit-management-secret').value;\n if (managementIdentityChanged && !managementSecret) throw new Error('Enter the new Management App secret');\n const browser = { ...logto.browser, apiResource: browserResource };\n const auth = {\n ...values.auth,\n audience: browserResource || browserAppId,\n browserClient: { appId: browserAppId, apiResource: browserResource }\n };\n await replaceConfiguration(\n {\n ...values,\n auth,\n logto: { ...logto, managementAppId, managementResource, browser }\n },\n managementSecret ? { 'logto.managementAppSecret': managementSecret } : undefined,\n 'Logto configuration saved. Sign in again if the SPA identity changed, then restart AgentConnect.'\n );\n }\n\n async function saveGithubConfiguration() {\n if (!currentStatus || !currentStatus.values.github) throw new Error('GitHub is not configured');\n const values = currentStatus.values;\n const previous = values.github;\n const appId = Number(requiredInput('github-edit-app-id', 'the GitHub App ID'));\n if (!Number.isSafeInteger(appId) || appId <= 0) throw new Error('GitHub App ID must be a positive integer');\n const slug = requiredInput('github-edit-slug', 'the GitHub App slug');\n const clientId = requiredInput('github-edit-client-id', 'the GitHub Client ID');\n const appChanged = appId !== previous.appId;\n const clientChanged = clientId !== previous.clientId;\n const webhookEnabled = el('github-edit-webhook-enabled').checked;\n const webhookEnabling = webhookEnabled && previous.webhookEnabled === false;\n const connectorReused = githubConnectorUsesDeployment(values);\n const clientSecret = el('github-edit-client-secret').value;\n const privateKey = el('github-edit-private-key').value.trim();\n const webhookSecret = el('github-edit-webhook-secret').value;\n const connectorSecret = el('github-edit-logto-secret').value;\n const webhookSecretStored = currentStatus.secrets.some((secret) => secret.key === 'github.webhookSecret' && secret.configured);\n if ((appChanged || clientChanged) && !clientSecret) throw new Error('Enter the new GitHub client secret');\n if (appChanged && !privateKey) throw new Error('Enter the new GitHub private key as base64');\n if (webhookEnabled && ((appChanged && !webhookSecret) || (webhookEnabling && !webhookSecret && !webhookSecretStored))) throw new Error('Enter the new GitHub webhook secret');\n if (connectorReused && (appChanged || clientChanged) && !connectorSecret) throw new Error('Enter the new Logto connector client secret');\n const secrets = {};\n if (clientSecret) secrets['github.clientSecret'] = clientSecret;\n if (privateKey) secrets['github.privateKeyB64'] = privateKey;\n if (webhookSecret) secrets['github.webhookSecret'] = webhookSecret;\n if (connectorSecret) secrets['logto.githubConnectorClientSecret'] = connectorSecret;\n const nextLogto = connectorReused && values.logto\n ? { ...values.logto, githubConnector: { ...values.logto.githubConnector, appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, webhookEnabled },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\n );\n if (connectorSecret) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function saveSlackConfiguration() {\n if (!currentStatus || !currentStatus.values.slack) throw new Error('Slack is not configured');\n const values = currentStatus.values;\n const previous = values.slack;\n const appId = requiredInput('slack-edit-app-id', 'the Slack App ID');\n const clientId = requiredInput('slack-edit-client-id', 'the Slack Client ID');\n const changed = appId !== previous.appId || clientId !== previous.clientId;\n const clientSecret = el('slack-edit-client-secret').value;\n const signingSecret = el('slack-edit-signing-secret').value;\n if (changed && (!clientSecret || !signingSecret)) throw new Error('Enter both the new Slack client secret and signing secret');\n const secrets = {};\n if (clientSecret) secrets['slack.clientSecret'] = clientSecret;\n if (signingSecret) secrets['slack.signingSecret'] = signingSecret;\n const nextLogto = values.logto && values.logto.slackConnector\n ? { ...values.logto, slackConnector: { ...values.logto.slackConnector, appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\n );\n if (clientSecret && nextLogto && nextLogto.slackConnector) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function clearProvider(provider) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const label = provider === 'github' ? 'GitHub' : provider === 'slack' ? 'Slack' : provider === 'google' ? 'Google' : provider === 'feishu' ? 'Feishu' : 'Lark';\n if (!window.confirm('Clear the saved ' + label + ' configuration and secrets?')) return;\n const values = currentStatus.values;\n let next = values;\n let secrets = {};\n if (provider === 'github') {\n const connectorReused = githubConnectorUsesDeployment(values);\n const logto = connectorReused && values.logto\n ? {\n ...values.logto,\n githubConnector: null\n }\n : values.logto;\n next = { ...values, github: null, ...(logto ? { logto } : {}) };\n secrets = {\n 'github.clientSecret': null,\n 'github.privateKeyB64': null,\n 'github.webhookSecret': null,\n ...(connectorReused ? { 'logto.githubConnectorClientSecret': null } : {})\n };\n } else if (provider === 'slack') {\n const logto = values.logto\n ? {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'slack') }\n : values.logto.browser,\n slackConnector: null\n }\n : values.logto;\n next = { ...values, slack: null, ...(logto ? { logto } : {}) };\n secrets = { 'slack.clientSecret': null, 'slack.signingSecret': null };\n } else if (provider === 'google') {\n if (!values.logto) return;\n next = {\n ...values,\n logto: {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'google') }\n : values.logto.browser,\n googleConnector: null\n }\n };\n secrets = { 'logto.googleConnectorClientSecret': null };\n } else if (provider === 'feishu' || provider === 'lark') {\n next = { ...values, [provider]: null };\n secrets = { [provider + '.loginAppSecret']: null };\n }\n await replaceConfiguration(next, secrets, label + ' configuration cleared. Its setup controls are available again.');\n }\n\n async function startGithub(prefix) {\n const result = await json(await fetch(api + '/create/github/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n purpose: 'deployment',\n name: el(prefix + '-name').value,\n ownership: ownership(prefix),\n connectLogto: prefix === 'bootstrap-github'\n })\n }));\n const form = document.createElement('form');\n form.method = 'post'; form.action = result.action;\n const manifest = document.createElement('input');\n manifest.type = 'hidden'; manifest.name = 'manifest'; manifest.value = JSON.stringify(result.manifest);\n form.append(manifest); document.body.append(form);\n message('Opening GitHub to review the complete integration App.');\n form.submit();\n }\n\n async function loadBootstrapInfo() {\n bootstrapInfo = await json(await fetch(api + '/bootstrap-info'));\n el('open-logto').href = bootstrapInfo.logtoAdminEndpoint;\n el('logto-settings').href = bootstrapInfo.logtoAdminEndpoint;\n if (!el('logto-app-id').value && bootstrapInfo.logtoManagementAppId) {\n el('logto-app-id').value = bootstrapInfo.logtoManagementAppId;\n }\n renderUriList('bootstrap-google-origins', bootstrapInfo.google.javascriptOrigins);\n renderUriList('bootstrap-google-redirects', bootstrapInfo.google.redirectUris);\n renderUriList('bootstrap-slack-redirects', [bootstrapInfo.slackLoginRedirectUrl]);\n el('logto-management-api-resource').value = bootstrapInfo.logtoManagementResource;\n el('bootstrap-github-submit').disabled = !bootstrapInfo.githubAvailable;\n if (!bootstrapInfo.githubAvailable) {\n el('bootstrap-github-note').textContent = 'GitHub App creation needs valid saved Web, API, and ingress URLs.';\n } else if (!bootstrapInfo.githubWebhookActive) {\n el('bootstrap-github-note').textContent = 'Creates the complete GitHub App now without submitting the localhost webhook URL. Add it after saving reachable HTTPS ingress.';\n } else {\n el('bootstrap-github-note').textContent = 'This creates one complete App for both GitHub sign-in and repository integration.';\n }\n el('bootstrap-slack-option').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-submit').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-note').textContent = bootstrapInfo.slackAvailable\n ? 'Creates one complete Slack App. Sign-in uses a separate openid profile email flow from workspace installation.'\n : 'Slack sign-in needs all public service URLs to use HTTPS. Use Google locally or expose the stack through trusted HTTPS endpoints.';\n if (!bootstrapInfo.slackAvailable && el('bootstrap-provider').value === 'slack') {\n el('bootstrap-provider').value = 'google';\n updateBootstrapProvider();\n }\n return bootstrapInfo;\n }\n\n async function logtoBootstrapFinding() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n for (const id of ['logto.client_credentials', 'logto.roles_read']) {\n const finding = report.findings.find((candidate) => candidate.id === id);\n if (!finding || finding.status !== 'pass') {\n return finding || { status: 'fail', message: 'Logto Management API permissions could not be verified.' };\n }\n }\n return { status: 'pass', message: 'Logto Management API access is ready.' };\n }\n\n async function showBootstrap() {\n el('bootstrap').hidden = false;\n el('show-bootstrap').hidden = true;\n const info = bootstrapInfo || await loadBootstrapInfo();\n if (!info.logtoConfigured) {\n showBootstrapStep('logto');\n return;\n }\n const finding = await logtoBootstrapFinding();\n if (finding && finding.status === 'pass') {\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n return;\n }\n showBootstrapStep('logto');\n message(finding?.message || 'Logto Management API credentials could not be verified.', true);\n }\n\n async function bootstrapLogto() {\n await json(await fetch(api + '/bootstrap/logto', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n managementAppId: el('logto-app-id').value,\n managementAppSecret: el('logto-app-secret').value,\n managementResource: el('logto-management-api-resource').value\n })\n }));\n el('logto-app-secret').value = '';\n await loadBootstrapInfo();\n const finding = await logtoBootstrapFinding();\n if (!finding || finding.status !== 'pass') {\n throw new Error(finding?.message || 'Logto Management API credentials could not be verified.');\n }\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n }\n\n async function bootstrapGoogle() {\n const secret = el('bootstrap-google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ clientId: el('bootstrap-google-id').value, clientSecret: secret })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto(true);\n await load();\n }\n\n async function bootstrapGithub() {\n if (!bootstrapInfo || !bootstrapInfo.githubAvailable) throw new Error('GitHub App creation needs valid saved Web, API, and ingress URLs.');\n await startGithub('bootstrap-github');\n }\n\n async function bootstrapSlack() {\n if (!bootstrapInfo || !bootstrapInfo.slackAvailable) throw new Error('Slack sign-in requires all public service URLs to use HTTPS.');\n await createSlack('bootstrap-slack', true);\n await reconcileLogto();\n await load();\n }\n\n async function createSlack(prefix = 'slack', connectLogto = false) {\n const result = await json(await fetch(api + '/create/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n name: el(prefix + '-name').value,\n configToken: el(prefix + '-token').value,\n connectLogto\n })\n }));\n el(prefix + '-token').value = '';\n if (!connectLogto) await load();\n message('Slack App ' + result.app.id + ' was created with the default integration manifest. Restart AgentConnect to apply it.');\n }\n\n async function checkGithub() {\n const result = await json(await fetch(api + '/check/github', { headers: bearer() }));\n el('github-settings').href = result.settingsUrl;\n el('github-settings').hidden = false;\n const label = result.missing.length ? 'Update required' : \"Can't verify automatically\";\n showDiff('github-drift', result.diff || [], label);\n match('github-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : label);\n message(result.status === 'pass' ? 'GitHub App matches the expected integration manifest.' : result.missing.length ? 'GitHub App settings need an update.' : \"GitHub can't expose callback, setup, or webhook-active settings through its API; check them in GitHub.\", result.status === 'fail');\n }\n\n async function connectGithubLogin() {\n await json(await fetch(api + '/configure/github-login', { method: 'POST', headers: bearer() }));\n await reconcileLogto();\n await load();\n message('The deployment GitHub App is now also used for Logto sign-in. Update its displayed callback URLs.');\n }\n\n async function connectSlackLogin() {\n if (!currentStatus || !currentStatus.values.slack || !currentStatus.values.logto) {\n throw new Error('Save the Slack App and Logto configuration first');\n }\n const values = currentStatus.values;\n const slack = values.slack;\n const logto = values.logto;\n await replaceConfiguration(\n {\n ...values,\n logto: {\n ...logto,\n browser: logto.browser\n ? { ...logto.browser, socialProviders: [...new Set([...logto.browser.socialProviders, 'slack'])] }\n : logto.browser,\n slackConnector: {\n appId: slack.appId,\n clientId: slack.clientId\n }\n }\n },\n undefined,\n 'Slack App is ready to connect to Logto.'\n );\n await reconcileLogto();\n await load();\n message('The deployment Slack App is now also used for Logto sign-in.');\n }\n\n async function checkSlack() {\n const token = requiredInput('slack-token', 'the temporary Slack App configuration token');\n const result = await json(await fetch(api + '/check/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ configToken: token })\n }));\n el('slack-token').value = '';\n showDiff('slack-drift', result.diff || []);\n match('slack-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : 'Update required');\n message(result.status === 'pass' ? 'Slack App matches the default integration manifest.' : 'Slack App settings need an update.', result.status !== 'pass');\n }\n\n async function checkGoogle() {\n const report = await checkLogto();\n const connector = report.findings.find((finding) => finding.id === 'logto.connectors');\n const google = currentStatus && currentStatus.values.logto && currentStatus.values.logto.googleConnector;\n const connectorDiff = connector && connector.diff\n ? connector.diff.filter((item) => item.field.toLowerCase().startsWith('google'))\n : [];\n const passed = Boolean(google && connector && connectorDiff.length === 0);\n showDiff('google-drift', connectorDiff);\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Logto matches' : 'Update required');\n message(\n passed\n ? \"The Logto Google connector matches. Google OAuth redirect settings can't be verified automatically.\"\n : 'The Logto Google connector needs an update.',\n !passed\n );\n }\n\n async function checkRegionalLoginApp(region) {\n const result = await json(await fetch(api + '/check/regional-login-app/' + region, { headers: bearer() }));\n const label = region === 'feishu' ? 'Feishu' : 'Lark';\n showDiff(region + '-drift', result.diff || []);\n match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Matches' : result.status === 'fail' ? 'Update required' : 'Could not check');\n el(region + '-login-status').textContent = result.message;\n el(region + '-login-status').className = result.status === 'pass' ? 'ok' : result.status === 'fail' ? 'warn' : 'muted';\n message(result.message || (label + ' credential check completed.'), result.status === 'fail');\n }\n\n async function saveGoogle() {\n const secret = el('google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n clientId: el('google-id').value,\n ...(secret ? { clientSecret: secret } : {})\n })\n }));\n el('google-secret').value = '';\n await reconcileLogto(Boolean(secret));\n await load();\n message('Google OAuth client and Logto connector are configured. Restart AgentConnect to apply the saved settings.');\n }\n\n function regionalLoginApp(prefix) {\n const appId = el(prefix + '-login-id').value.trim();\n const appSecret = el(prefix + '-login-secret').value;\n return appId ? { appId, ...(appSecret ? { appSecret } : {}) } : null;\n }\n\n async function saveRegionalLoginApp(region) {\n const app = regionalLoginApp(region);\n if (!app) throw new Error('Enter the ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App ID');\n await json(await fetch(api + '/configure/regional-login-app', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, app })\n }));\n el(region + '-login-secret').value = '';\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' tenant App is saved. Restart AgentConnect services to apply it.');\n }\n\n async function createRegionalLoginApp(region) {\n const popup = window.open('about:blank', '_blank');\n try {\n const started = await json(await fetch(api + '/create/regional-login-app/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, name: el(region + '-create-name').value })\n }));\n if (popup) popup.location.replace(started.authorizationUrl);\n else window.open(started.authorizationUrl, '_blank', 'noopener');\n message('Approve the new ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App in the opened page. This page will finish saving it automatically.');\n while (true) {\n const result = await json(await fetch(api + '/create/regional-login-app/' + encodeURIComponent(started.id), { headers: bearer() }));\n if (result.status === 'completed') {\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' App ' + result.appId + ' was created and saved. Restart AgentConnect services to apply it.');\n return;\n }\n if (result.status === 'failed') throw new Error('Regional App creation ' + result.reason + '. Start it again.');\n await new Promise((resolve) => setTimeout(resolve, Math.max(500, Math.min(result.retryAfterMs || 2000, 5000))));\n }\n } catch (error) {\n if (popup) popup.close();\n throw error;\n }\n }\n\n async function reconcileLogto(refreshConnectorSecrets) {\n return json(await fetch(api + '/reconcile/logto', {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ refreshConnectorSecrets: Boolean(refreshConnectorSecrets) })\n }));\n }\n\n async function refreshDeploymentConfig() {\n const status = await json(await fetch(api + '/deployment-config', { headers: bearer() }));\n el('access').hidden = true;\n el('admin').hidden = false;\n el('editor').hidden = false;\n currentRevision = status.revision;\n renderApps(status);\n return status;\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n await refreshDeploymentConfig();\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, connectors, and social-only sign-in match.'\n : failures.map((finding) => finding.message).join(' ');\n el('logto-status').className = failures.length === 0 ? 'ok' : 'warn';\n match('logto-match', failures.length === 0 ? 'pass' : 'warn', failures.length === 0 ? 'Matches' : 'Update required');\n showDiff(\n 'logto-drift',\n failures.flatMap((finding) => finding.diff && finding.diff.length\n ? finding.diff\n : [{ field: finding.id.replace(/^logto./, '').replaceAll('_', ' '), current: finding.message, expected: 'Matches expected configuration' }])\n );\n el('logto-settings').hidden = failures.length === 0;\n return report;\n }\n\n function githubNotice(value) {\n if (value === 'deployment-created') return 'GitHub integration App created. Its private key and webhook secret are stored.';\n if (value === 'deployment-login-created') return 'GitHub integration App created and connected to Logto sign-in.';\n if (value === 'cancelled') return 'GitHub App creation was cancelled.';\n if (value === 'expired') return 'GitHub App creation expired. Start it again.';\n if (value === 'invalid-callback') return 'GitHub returned an invalid App creation callback.';\n if (value === 'conversion-failed') return 'GitHub may have created the App, but did not return complete credentials. Delete the orphaned App and retry.';\n if (value === 'save-failed') return 'GitHub created the App, but its credentials could not be saved. Delete the orphaned App and retry.';\n return '';\n }\n\n async function load() {\n let publicAuth = await authConfig();\n const currentUrl = new URL(location.href);\n const githubResult = currentUrl.searchParams.get('github');\n const notice = githubNotice(githubResult);\n if (githubResult === 'deployment-login-created' && publicAuth.mode === 'none') {\n await reconcileLogto();\n publicAuth = await authConfig();\n }\n if (githubResult) history.replaceState({}, '', '/');\n const hasToken = Boolean(sessionStorage.getItem(tokenKey));\n el('access').hidden = false;\n el('admin').hidden = true;\n el('login').hidden = true;\n el('open-logto').hidden = true;\n el('show-bootstrap').hidden = true;\n el('editor').hidden = true;\n el('bootstrap').hidden = true;\n if (publicAuth.logtoAdminEndpoint) {\n el('open-logto').href = publicAuth.logtoAdminEndpoint;\n el('logto-settings').href = publicAuth.logtoAdminEndpoint;\n }\n if (publicAuth.mode === 'none') {\n sessionStorage.removeItem(tokenKey);\n await loadBootstrapInfo();\n el('open-logto').hidden = false;\n el('show-bootstrap').hidden = false;\n message(notice || 'Logto sign-in is not configured. Set up the initial Logto administrator first.');\n return;\n }\n if (publicAuth.mode === 'unavailable') {\n el('open-logto').hidden = false;\n message(publicAuth.message || 'Logto sign-in is unavailable. Open Logto Console to review its settings.', true);\n return;\n }\n if (!hasToken) {\n el('login').hidden = false;\n message(notice || 'Sign in with Logto to continue.');\n return;\n }\n if (publicAuth.claimAvailable) {\n try {\n await json(await fetch(api + '/bootstrap/claim', { method: 'POST', headers: bearer() }));\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('This first user is now an ADMIN. Sign in again to refresh the role claim.');\n return;\n } catch (error) {\n if (error.status !== 401 && error.status !== 403) throw error;\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with Logto to continue.', true);\n return;\n }\n }\n try {\n await loadBootstrapInfo();\n await refreshDeploymentConfig();\n message(notice);\n checkLogto().catch((error) => {\n el('logto-status').textContent = error.message;\n el('logto-status').className = 'warn';\n el('logto-settings').hidden = false;\n });\n } catch (error) {\n if (error.status === 401 || error.status === 403) {\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with a Logto ADMIN account.', true);\n }\n else throw error;\n }\n }\n\n async function saveSecretReplacement(key, value) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n if (!value) throw new Error('Enter the replacement secret');\n const refreshLogto =\n (key === 'logto.githubConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.githubConnector) ||\n (key === 'logto.googleConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.googleConnector) ||\n (key === 'slack.clientSecret' && currentStatus.values.logto && currentStatus.values.logto.slackConnector);\n await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values: currentStatus.values, secrets: { [key]: value } })\n }));\n try {\n if (refreshLogto) await reconcileLogto(true);\n } finally {\n await load();\n }\n if (refreshLogto) {\n message('Secret replaced and applied to the Logto connector. Restart AgentConnect to apply deployment changes.');\n } else {\n message('Secret replaced. Restart AgentConnect to apply it.');\n }\n }\n\n function editSecret(button) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const row = button.closest('.secret-line');\n const display = el(button.dataset.secretDisplay);\n if (!row || !display || row.querySelector('.secret-editor')) return;\n const editor = document.createElement('span'); editor.className = 'secret-editor';\n const input = document.createElement('input');\n input.type = 'password'; input.autocomplete = 'new-password'; input.placeholder = 'Enter replacement secret';\n const save = document.createElement('button'); save.textContent = 'Save';\n const cancel = document.createElement('button'); cancel.textContent = 'Cancel';\n const close = () => { editor.remove(); display.hidden = false; button.hidden = false; };\n save.onclick = async () => {\n save.disabled = true;\n try { await saveSecretReplacement(button.dataset.secretKey, input.value); }\n catch (error) { save.disabled = false; message(error.message, true); }\n };\n cancel.onclick = close;\n input.onkeydown = (event) => { if (event.key === 'Enter') save.click(); if (event.key === 'Escape') close(); };\n display.hidden = true; button.hidden = true;\n editor.append(input, save, cancel); row.append(editor); input.focus();\n }\n\n async function saveOptions() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = {\n ...currentStatus.values,\n features: {\n presetAgentsEnabled: el('preset-agents-enabled').checked\n }\n };\n const saved = await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values })\n }));\n message('Saved deployment options. Restart AgentConnect to apply them.');\n await load();\n }\n\n el('login').onclick = () => signIn().catch((error) => message(error.message, true));\n el('show-bootstrap').onclick = () => showBootstrap().catch((error) => message(error.message, true));\n el('bootstrap-provider').onchange = updateBootstrapProvider;\n el('bootstrap-github-owner').onchange = () => updateOwner('bootstrap-github');\n el('github-owner').onchange = () => updateOwner('github');\n el('bootstrap-logto-submit').onclick = () => bootstrapLogto().catch((error) => message(error.message, true));\n el('bootstrap-back').onclick = () => showBootstrapStep('logto');\n el('bootstrap-google-submit').onclick = () => bootstrapGoogle().catch((error) => message(error.message, true));\n el('bootstrap-github-submit').onclick = () => bootstrapGithub().catch((error) => message(error.message, true));\n el('bootstrap-slack-submit').onclick = () => bootstrapSlack().catch((error) => message(error.message, true));\n el('create-github').onclick = () => startGithub('github').catch((error) => message(error.message, true));\n el('save-logto-configuration').onclick = () => saveLogtoConfiguration().catch((error) => message(error.message, true));\n el('cancel-logto-configuration').onclick = cancelConfigurationEdit;\n el('save-github-configuration').onclick = () => saveGithubConfiguration().catch((error) => message(error.message, true));\n el('cancel-github-configuration').onclick = cancelConfigurationEdit;\n el('clear-github').onclick = () => clearProvider('github').catch((error) => message(error.message, true));\n el('connect-github-login').onclick = () => connectGithubLogin().catch((error) => message(error.message, true));\n el('check-github').onclick = () => checkGithub().catch((error) => message(error.message, true));\n el('create-slack').onclick = () => createSlack('slack').catch((error) => message(error.message, true));\n el('connect-slack-login').onclick = () => connectSlackLogin().catch((error) => message(error.message, true));\n el('save-slack-configuration').onclick = () => saveSlackConfiguration().catch((error) => message(error.message, true));\n el('cancel-slack-configuration').onclick = cancelConfigurationEdit;\n el('clear-slack').onclick = () => clearProvider('slack').catch((error) => message(error.message, true));\n el('check-slack').onclick = () => checkSlack().catch((error) => message(error.message, true));\n el('save-google').onclick = () => saveGoogle().catch((error) => message(error.message, true));\n el('cancel-google-configuration').onclick = cancelConfigurationEdit;\n el('clear-google').onclick = () => clearProvider('google').catch((error) => message(error.message, true));\n el('check-google').onclick = () => checkGoogle().catch((error) => message(error.message, true));\n el('create-feishu-login-app').onclick = () => createRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('save-feishu-login-app').onclick = () => saveRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('cancel-feishu-configuration').onclick = cancelConfigurationEdit;\n el('clear-feishu').onclick = () => clearProvider('feishu').catch((error) => message(error.message, true));\n el('check-feishu-login-app').onclick = () => checkRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('create-lark-login-app').onclick = () => createRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('save-lark-login-app').onclick = () => saveRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('cancel-lark-configuration').onclick = cancelConfigurationEdit;\n el('clear-lark').onclick = () => clearProvider('lark').catch((error) => message(error.message, true));\n el('check-lark-login-app').onclick = () => checkRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('check-logto').onclick = () => checkLogto().catch((error) => message(error.message, true));\n el('reconcile-logto').onclick = () => reconcileLogto().then(load).catch((error) => message(error.message, true));\n el('logout').onclick = () => { sessionStorage.removeItem(tokenKey); load().catch((error) => message(error.message, true)); };\n el('save-options').onclick = () => saveOptions().catch((error) => message(error.message, true));\n for (const button of document.querySelectorAll('.edit-secret')) button.onclick = () => editSecret(button);\n for (const button of document.querySelectorAll('.edit-configuration')) button.onclick = () => beginConfigurationEdit(button.dataset.provider);\n updateBootstrapProvider();\n finishSignIn().then(load).catch((error) => message(error.message, true));\n <\/script>\n</body>\n</html>"], ["<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>AgentConnect deployment settings</title>\n <style>\n :root { color-scheme: light dark; font: 15px/1.5 system-ui, sans-serif; }\n body { max-width: 1280px; margin: 48px auto; padding: 0 20px 60px; }\n h1 { margin-bottom: 4px; } h2 { margin-top: 32px; } h3 { margin: 0 0 8px; }\n .muted { color: #777; } .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }\n button, .button { padding: 8px 13px; cursor: pointer; }\n .button { border: 1px solid #8888; border-radius: 4px; color: inherit; text-decoration: none; display: inline-block; }\n input, select { width: min(520px, 100%); padding: 7px; box-sizing: border-box; }\n input[type=\"checkbox\"] { width: auto; padding: 0; }\n .field { display: grid; gap: 4px; margin: 10px 0; }\n .provider-stack { display: grid; gap: 16px; }\n .panel { border: 1px solid #8886; border-radius: 8px; padding: 16px; }\n .section-head, .provider-head { display: flex; justify-content: space-between; gap: 16px; align-items: start; }\n .section-head h2, .provider-head h3 { margin: 0; }\n .provider-head p { margin: 4px 0 0; }\n .badge { flex: none; border: 1px solid #8886; border-radius: 999px; padding: 3px 9px; font-size: 13px; }\n .badge.pass { color: #198754; border-color: #19875466; background: #19875412; }\n .badge.warn { color: #a66b00; border-color: #d99b1366; background: #d99b1315; }\n .badge.fail { color: #c33; border-color: #c333; background: #c3331111; }\n .credentials { display: grid; grid-template-columns: minmax(160px, 220px) minmax(0, 1fr); gap: 7px 16px; margin: 16px 0; }\n .credentials dt { color: #777; }\n .credentials dd { margin: 0; overflow-wrap: anywhere; }\n .credentials code { user-select: all; }\n .redacted { font-family: ui-monospace, monospace; letter-spacing: .08em; }\n .secret-line, .value-line { display: flex; gap: 10px; align-items: center; min-height: 30px; }\n .edit-secret { padding: 2px 8px; font-size: 13px; }\n .secret-editor { display: flex; gap: 8px; flex-wrap: wrap; width: 100%; }\n .secret-editor input { width: min(420px, 100%); }\n .edit-configuration { padding: 2px 8px; font-size: 13px; }\n .startup-owned { color: #777; font-size: 13px; }\n .danger { color: #c33; border-color: #c336; }\n .subsection { margin-top: 16px; padding-top: 14px; border-top: 1px solid #8883; }\n textarea { width: min(720px, 100%); min-height: 100px; padding: 7px; box-sizing: border-box; }\n .uris { margin: 8px 0; padding-left: 20px; } .uris code { user-select: all; }\n .notice { border-left: 4px solid #d99b13; padding: 8px 12px; background: #d99b1315; }\n .diff-title { display: block; margin-bottom: 8px; }\n .config-diff { display: grid; gap: 1px; border: 1px solid #8884; border-radius: 6px; overflow: hidden; background: #8884; }\n .diff-row { display: grid; grid-template-columns: minmax(140px, .8fr) minmax(0, 1.2fr) minmax(0, 1.2fr); background: Canvas; }\n .diff-row > * { min-width: 0; padding: 7px 9px; overflow-wrap: anywhere; white-space: pre-wrap; }\n .diff-head { font-size: 12px; font-weight: 600; color: #777; }\n .diff-field { font-weight: 600; }\n .diff-value { font-family: ui-monospace, monospace; font-size: 13px; }\n pre { padding: 12px; border-radius: 6px; background: #8881; overflow-x: auto; user-select: all; }\n #message { white-space: pre-wrap; padding: 10px 0; min-height: 1.5em; }\n .error { color: #c33; } .ok { color: #198754; } .warn { color: #a66b00; }\n code { overflow-wrap: anywhere; }\n .admin-layout { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 32px; align-items: start; }\n .admin-nav { position: sticky; top: 24px; display: grid; gap: 3px; padding: 10px; border: 1px solid #8886; border-radius: 8px; background: Canvas; }\n .admin-nav a { padding: 7px 9px; border-radius: 5px; color: inherit; text-decoration: none; white-space: nowrap; }\n .admin-nav a:hover { background: #8882; }\n .admin-section { scroll-margin-top: 24px; }\n details.environment { margin: 0 0 24px; } details.environment summary { cursor: pointer; font-weight: 600; }\n @media (max-width: 760px) {\n body { margin-top: 24px; }\n .admin-layout { grid-template-columns: 1fr; gap: 18px; }\n .admin-nav { position: static; display: flex; overflow-x: auto; }\n .credentials { grid-template-columns: 1fr; gap: 2px; }\n .credentials dd { margin-bottom: 8px; }\n .diff-row { grid-template-columns: 1fr; }\n .diff-head { display: none; }\n .diff-value::before { display: block; margin-bottom: 2px; color: #777; font: 11px/1.4 system-ui, sans-serif; }\n .diff-current::before { content: 'Current'; }\n .diff-expected::before { content: 'Expected'; }\n }\n [hidden] { display: none !important; }\n </style>\n</head>\n<body>\n <section id=\"access\">\n <h1>AgentConnect Tenant Admin</h1>\n <p id=\"access-message\" class=\"muted\" aria-live=\"polite\">Checking Logto sign-in…</p>\n <div class=\"row\">\n <button id=\"login\" hidden>Sign in with Logto</button>\n <a id=\"open-logto\" class=\"button\" href=\"http://localhost:3002\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n <button id=\"show-bootstrap\" hidden>Continue setup</button>\n </div>\n\n <section id=\"bootstrap\" hidden>\n <h2>Set up sign-in</h2>\n <p id=\"bootstrap-progress\" class=\"muted\">Step 1 of 2</p>\n <div id=\"bootstrap-logto-step\" class=\"panel\">\n <h3>Connect Logto</h3>\n <p class=\"muted\">Enter the one-time Logto Management API credential. It is sealed in the deployment database and verified before continuing.</p>\n <label class=\"field\">Logto M2M App ID<input id=\"logto-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Logto M2M App Secret<input id=\"logto-app-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-management-api-resource\" type=\"url\" autocomplete=\"off\"></label>\n <p class=\"muted\">Logto Cloud uses <code>https://&lt;tenant-id&gt;.logto.app/api</code>. Keep <code>https://default.logto.app/api</code> for Logto OSS.</p>\n <button id=\"bootstrap-logto-submit\">Save Logto and continue</button>\n </div>\n <div id=\"bootstrap-provider-step\" hidden>\n <h3>Choose a sign-in provider</h3>\n <p class=\"muted\">Logto Management API access is ready. Configure one provider to enable sign-in.</p>\n <label class=\"field\">Sign-in provider\n <select id=\"bootstrap-provider\"><option value=\"google\">Google (works on localhost)</option><option value=\"github\">GitHub integration App</option><option id=\"bootstrap-slack-option\" value=\"slack\">Slack integration App</option></select>\n </label>\n <div id=\"bootstrap-google\" class=\"panel\">\n <h3>Google OAuth client</h3>\n <p class=\"muted\">Create a Web application client in Google Auth Platform, then paste its credentials here.</p>\n <p>Authorized JavaScript origin:</p><ul id=\"bootstrap-google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"bootstrap-google-redirects\" class=\"uris\"></ul>\n <a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a>\n <label class=\"field\">Client ID<input id=\"bootstrap-google-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client Secret<input id=\"bootstrap-google-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-google-submit\">Save Google OAuth and configure Logto</button>\n </div>\n <div id=\"bootstrap-github\" class=\"panel\" hidden>\n <h3>GitHub integration App</h3>\n <p id=\"bootstrap-github-note\" class=\"muted\">This creates one complete App for both GitHub sign-in and repository integration.</p>\n <label class=\"field\">Owner\n <select id=\"bootstrap-github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"bootstrap-github-org-field\" class=\"field\" hidden>Organization login<input id=\"bootstrap-github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"bootstrap-github-name\" value=\"AgentConnect\"></label>\n <button id=\"bootstrap-github-submit\">Create GitHub App and configure Logto</button>\n </div>\n <div id=\"bootstrap-slack\" class=\"panel\" hidden>\n <h3>Slack integration App</h3>\n <p id=\"bootstrap-slack-note\" class=\"muted\">Creates one complete Slack App for workspace installation and a separate Sign in with Slack OIDC flow.</p>\n <p>Logto redirect URI:</p><ul id=\"bootstrap-slack-redirects\" class=\"uris\"></ul>\n <label class=\"field\">App name<input id=\"bootstrap-slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"bootstrap-slack-token\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-slack-submit\">Create Slack App and configure Logto</button>\n </div>\n <button id=\"bootstrap-back\">Back to Logto credentials</button>\n </div>\n </section>\n </section>\n\n <main id=\"admin\" hidden>\n <div class=\"admin-layout\">\n <nav class=\"admin-nav\" aria-label=\"Deployment settings\">\n <a href=\"#startup-section\">Startup</a>\n <a href=\"#logto-section\">Logto</a>\n <a href=\"#github-section\">GitHub</a>\n <a href=\"#slack-section\">Slack</a>\n <a href=\"#google-section\">Google</a>\n <a href=\"#feishu-section\">Feishu</a>\n <a href=\"#lark-section\">Lark</a>\n <a href=\"#options-section\">Options</a>\n </nav>\n <div class=\"admin-content\">\n <h1>AgentConnect deployment settings</h1>\n <p class=\"muted\">Saved settings take effect after the stack is restarted.</p>\n <div class=\"row\">\n <button id=\"logout\">Log out</button>\n </div>\n <div id=\"message\" aria-live=\"polite\"></div>\n\n <section id=\"editor\" hidden>\n <details id=\"startup-section\" class=\"environment admin-section\" open>\n <summary>Startup environment</summary>\n <p class=\"notice\">Public service URLs come from <code>.env</code>. Provider callbacks below are derived from these values.</p>\n <pre id=\"startup-environment\"></pre>\n </details>\n\n <section id=\"logto-section\" class=\"admin-section\" aria-labelledby=\"logto-heading\">\n <div class=\"section-head\">\n <div><h2 id=\"logto-heading\">Logto</h2><p class=\"muted\">Authentication and Tenant Admin access.</p></div>\n <span id=\"logto-match\" class=\"badge\">Not checked</span>\n </div>\n <div class=\"panel\">\n <dl class=\"credentials\">\n <dt>Management endpoint</dt><dd class=\"value-line\"><code id=\"logto-management-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>Management App ID</dt><dd class=\"value-line\"><code id=\"logto-management-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management API resource</dt><dd class=\"value-line\"><code id=\"logto-management-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management App secret</dt><dd class=\"secret-line\"><span id=\"logto-management-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.managementAppSecret\" data-secret-display=\"logto-management-secret-display\">Edit</button></dd>\n <dt>Sign-in endpoint</dt><dd class=\"value-line\"><code id=\"logto-browser-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>SPA App ID</dt><dd class=\"value-line\"><code id=\"logto-browser-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Browser API resource</dt><dd class=\"value-line\"><code id=\"logto-browser-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n </dl>\n <div id=\"logto-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Logto configuration</h3>\n <label class=\"field\">Management App ID<input id=\"logto-edit-management-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-edit-management-resource\" autocomplete=\"off\"></label>\n <label class=\"field\">New Management App secret<input id=\"logto-edit-management-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when management identity changes\"></label>\n <label class=\"field\">SPA App ID<input id=\"logto-edit-browser-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Browser API resource<input id=\"logto-edit-browser-resource\" autocomplete=\"off\" placeholder=\"Optional\"></label>\n <div class=\"row\"><button id=\"save-logto-configuration\">Save configuration</button><button id=\"cancel-logto-configuration\">Cancel</button></div>\n </div>\n <p id=\"logto-status\" class=\"muted\">Checking redirects and sign-in settings…</p>\n <div id=\"logto-drift\" class=\"notice\" hidden></div>\n <div class=\"row\">\n <button id=\"check-logto\">Check match</button>\n <button id=\"reconcile-logto\">Apply expected settings</button>\n <a id=\"logto-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n </div>\n </div>\n </section>\n\n <h2>Providers</h2>\n <div class=\"provider-stack\">\n <section id=\"github-section\" class=\"panel admin-section\" aria-labelledby=\"github-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"github-heading\">GitHub</h3><p class=\"muted\">Repository integration and optional Logto sign-in.</p></div>\n <span id=\"github-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"github-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>App slug</dt><dd class=\"value-line\"><code id=\"github-app-slug\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"github-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Webhook delivery</dt><dd class=\"value-line\"><code id=\"github-webhook-enabled\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"github-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.clientSecret\" data-secret-display=\"github-client-secret-display\">Edit</button></dd>\n <dt>Private key</dt><dd class=\"secret-line\"><span id=\"github-private-key-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.privateKeyB64\" data-secret-display=\"github-private-key-display\">Edit</button></dd>\n <dt>Webhook secret</dt><dd class=\"secret-line\"><span id=\"github-webhook-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.webhookSecret\" data-secret-display=\"github-webhook-secret-display\">Edit</button></dd>\n <dt>Logto connector secret</dt><dd class=\"secret-line\"><span id=\"github-logto-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.githubConnectorClientSecret\" data-secret-display=\"github-logto-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"github-status\" class=\"muted\"></p>\n <div id=\"github-drift\" class=\"notice\" hidden></div>\n <div id=\"github-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit GitHub App identity</h3>\n <label class=\"field\">App ID<input id=\"github-edit-app-id\" inputmode=\"numeric\" autocomplete=\"off\"></label>\n <label class=\"field\">App slug<input id=\"github-edit-slug\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"github-edit-client-id\" autocomplete=\"off\"></label>\n <label><input id=\"github-edit-webhook-enabled\" type=\"checkbox\"> Enable Relay webhook delivery</label>\n <label class=\"field\">New client secret<input id=\"github-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when App or Client ID changes\"></label>\n <label class=\"field\">New private key (base64)<textarea id=\"github-edit-private-key\" autocomplete=\"off\" placeholder=\"Required when App ID changes\"></textarea></label>\n <label class=\"field\">New webhook secret<input id=\"github-edit-webhook-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when an active webhook App ID changes\"></label>\n <label id=\"github-edit-logto-secret-field\" class=\"field\" hidden>New Logto connector client secret<input id=\"github-edit-logto-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when this App is also used for sign-in\"></label>\n <div class=\"row\"><button id=\"save-github-configuration\">Save configuration</button><button id=\"cancel-github-configuration\">Cancel</button></div>\n </div>\n <div id=\"github-create-controls\" class=\"subsection\">\n <label class=\"field\">Owner\n <select id=\"github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"github-org-field\" class=\"field\" hidden>Organization login<input id=\"github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"github-name\" value=\"AgentConnect\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-github\">Create GitHub App</button>\n <button id=\"connect-github-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-github\" hidden>Check match</button>\n <button id=\"clear-github\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"github-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open GitHub settings</a>\n </div>\n </section>\n\n <section id=\"slack-section\" class=\"panel admin-section\" aria-labelledby=\"slack-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"slack-heading\">Slack</h3><p class=\"muted\">One App for workspace integration and Logto sign-in.</p></div>\n <span id=\"slack-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"slack-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"slack-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"slack-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.clientSecret\" data-secret-display=\"slack-client-secret-display\">Edit</button></dd>\n <dt>Signing secret</dt><dd class=\"secret-line\"><span id=\"slack-signing-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.signingSecret\" data-secret-display=\"slack-signing-secret-display\">Edit</button></dd>\n <dt>Logto sign-in</dt><dd id=\"slack-logto-status\">Not configured</dd>\n </dl>\n <p id=\"slack-status\" class=\"muted\"></p>\n <div id=\"slack-drift\" class=\"notice\" hidden></div>\n <div id=\"slack-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Slack App identity</h3>\n <label class=\"field\">App ID<input id=\"slack-edit-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"slack-edit-client-id\" autocomplete=\"off\"></label>\n <label class=\"field\">New client secret<input id=\"slack-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <label class=\"field\">New signing secret<input id=\"slack-edit-signing-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <div class=\"row\"><button id=\"save-slack-configuration\">Save configuration</button><button id=\"cancel-slack-configuration\">Cancel</button></div>\n </div>\n <div class=\"subsection\">\n <label id=\"slack-name-field\" class=\"field\">App name<input id=\"slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"slack-token\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Used only for create or check\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-slack\">Create Slack App</button>\n <button id=\"connect-slack-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-slack\" hidden>Check match</button>\n <button id=\"clear-slack\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"slack-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Slack settings</a>\n </div>\n </section>\n\n <section id=\"google-section\" class=\"panel admin-section\" aria-labelledby=\"google-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"google-heading\">Google</h3><p class=\"muted\">OAuth client used by the Logto Google connector.</p></div>\n <span id=\"google-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"google-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"google\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"google-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.googleConnectorClientSecret\" data-secret-display=\"google-client-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"google-status\" class=\"muted\"></p>\n <div id=\"google-drift\" class=\"notice\" hidden></div>\n <p>Authorized JavaScript origin:</p><ul id=\"google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"google-redirects\" class=\"uris\"></ul>\n <p class=\"muted\">Google does not expose OAuth client redirect settings through an API. Copy these required values into Google Auth Platform; Tenant Admin can verify only the Logto connector.</p>\n <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a></div>\n <div id=\"google-config-controls\" class=\"subsection\">\n <label class=\"field\">Client ID<input id=\"google-id\" autocomplete=\"off\"></label>\n <label id=\"google-initial-secret-field\" class=\"field\">Client secret<input id=\"google-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when Client ID changes\"></label>\n </div>\n <div class=\"row\"><button id=\"save-google\">Save Google client</button><button id=\"cancel-google-configuration\" hidden>Cancel</button><button id=\"check-google\" hidden>Check match</button><button id=\"clear-google\" class=\"danger\" hidden>Clear configuration</button></div>\n </section>\n\n <section id=\"feishu-section\" class=\"panel admin-section\" aria-labelledby=\"feishu-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"feishu-heading\">Feishu</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Feishu, OAuth callbacks, and published App setup.</p></div>\n <span id=\"feishu-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"feishu-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"feishu\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"feishu-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"feishu.loginAppSecret\" data-secret-display=\"feishu-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"feishu-login-status\" class=\"muted\"></p>\n <div id=\"feishu-drift\" class=\"notice\" hidden></div>\n <div id=\"feishu-config-controls\" class=\"subsection\">\n <label id=\"feishu-create-name-field\" class=\"field\">New App name<input id=\"feishu-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"feishu-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"feishu-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-feishu-login-app\">Create Feishu App</button>\n <button id=\"save-feishu-login-app\">Save existing App</button>\n <button id=\"cancel-feishu-configuration\" hidden>Cancel</button>\n <button id=\"check-feishu-login-app\" hidden>Check setup</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"feishu-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Feishu settings</a>\n </div>\n </section>\n\n <section id=\"lark-section\" class=\"panel admin-section\" aria-labelledby=\"lark-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"lark-heading\">Lark</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Lark, OAuth callbacks, and published App setup.</p></div>\n <span id=\"lark-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"lark-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"lark\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"lark-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"lark.loginAppSecret\" data-secret-display=\"lark-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"lark-login-status\" class=\"muted\"></p>\n <div id=\"lark-drift\" class=\"notice\" hidden></div>\n <div id=\"lark-config-controls\" class=\"subsection\">\n <label id=\"lark-create-name-field\" class=\"field\">New App name<input id=\"lark-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"lark-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"lark-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-lark-login-app\">Create Lark App</button>\n <button id=\"save-lark-login-app\">Save existing App</button>\n <button id=\"cancel-lark-configuration\" hidden>Cancel</button>\n <button id=\"check-lark-login-app\" hidden>Check setup</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"lark-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Lark settings</a>\n </div>\n </section>\n </div>\n\n <section id=\"options-section\" class=\"admin-section\">\n <h2>Deployment options</h2>\n <div class=\"panel\">\n <label class=\"field\"><span><input id=\"preset-agents-enabled\" type=\"checkbox\"> Enable preset Agents</span></label>\n <div class=\"row\"><button id=\"save-options\">Save options</button></div>\n </div>\n </section>\n </section>\n </div>\n </div>\n </main>\n\n <script>\n const api = '/api/v1';\n const tokenKey = 'agentconnect.tenant-admin.token';\n const verifierKey = 'agentconnect.tenant-admin.pkce';\n const stateKey = 'agentconnect.tenant-admin.state';\n let currentRevision = 0;\n let currentStatus = null;\n let bootstrapInfo = null;\n const el = (id) => document.getElementById(id);\n const message = (text, error = false) => {\n const target = el('admin').hidden ? el('access-message') : el('message');\n target.textContent = text;\n target.className = error ? 'error' : 'ok';\n };\n const bearer = () => {\n const token = sessionStorage.getItem(tokenKey);\n return token ? { authorization: 'Bearer ' + token } : {};\n };\n const json = async (response) => {\n const body = await response.json().catch(() => ({}));\n if (!response.ok) throw Object.assign(new Error(body.message || ('HTTP ' + response.status)), { status: response.status, code: body.code });\n return body;\n };\n const base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n const random = () => base64url(crypto.getRandomValues(new Uint8Array(32)));\n const configured = (byKey, key) => Boolean(byKey.get(key) && byKey.get(key).configured);\n\n function showIdentityEditors(provider, show) {\n for (const button of document.querySelectorAll('.edit-configuration[data-provider=\"' + provider + '\"]')) {\n button.hidden = !show;\n }\n }\n\n function text(id, value) {\n el(id).textContent = value === null || value === undefined || value === '' ? 'Not configured' : String(value);\n }\n\n function secretText(id, byKey, key, editable) {\n const stored = configured(byKey, key);\n const display = el(id);\n const button = document.querySelector('[data-secret-display=\"' + id + '\"]');\n const row = display.closest('.secret-line');\n const editor = row && row.querySelector('.secret-editor');\n if (editor) editor.remove();\n display.hidden = false;\n display.textContent = stored ? '***' : 'Not configured';\n display.className = stored ? 'redacted' : 'muted';\n button.hidden = !editable;\n button.textContent = stored ? 'Edit' : 'Set';\n }\n\n function match(id, state, label) {\n const target = el(id);\n target.textContent = label;\n target.className = 'badge' + (state ? ' ' + state : '');\n }\n\n async function authConfig() { return json(await fetch(api + '/auth-config')); }\n\n async function signIn() {\n const config = await authConfig();\n if (config.mode !== 'oidc') throw new Error('Save an OIDC configuration first');\n const verifier = random();\n const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));\n const state = random();\n sessionStorage.setItem(verifierKey, verifier);\n sessionStorage.setItem(stateKey, state);\n const url = new URL(config.authorizationEndpoint);\n url.searchParams.set('client_id', config.appId);\n url.searchParams.set('redirect_uri', config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', 'openid profile email roles');\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n if (config.resource) url.searchParams.set('resource', config.resource);\n location.assign(url);\n }\n\n async function finishSignIn() {\n const url = new URL(location.href);\n const code = url.searchParams.get('code');\n if (!code) return;\n const state = url.searchParams.get('state');\n if (!state || state !== sessionStorage.getItem(stateKey)) throw new Error('OIDC state mismatch');\n const verifier = sessionStorage.getItem(verifierKey);\n if (!verifier) throw new Error('PKCE verifier is missing; start sign-in again');\n const config = await authConfig();\n const body = new URLSearchParams({\n grant_type: 'authorization_code', code, client_id: config.appId,\n redirect_uri: config.redirectUri, code_verifier: verifier\n });\n if (config.resource) body.set('resource', config.resource);\n const tokens = await json(await fetch(config.tokenEndpoint, {\n method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body\n }));\n if (typeof tokens.id_token !== 'string') throw new Error('Logto did not return an ID token');\n sessionStorage.setItem(tokenKey, tokens.id_token);\n sessionStorage.removeItem(verifierKey);\n sessionStorage.removeItem(stateKey);\n history.replaceState({}, '', '/');\n }\n\n function renderUriList(id, values) {\n el(id).replaceChildren(...(values || []).map((value) => {\n const item = document.createElement('li');\n const code = document.createElement('code'); code.textContent = value;\n item.append(code); return item;\n }));\n }\n\n function updateBootstrapProvider() {\n const provider = el('bootstrap-provider').value;\n el('bootstrap-google').hidden = provider !== 'google';\n el('bootstrap-github').hidden = provider !== 'github';\n el('bootstrap-slack').hidden = provider !== 'slack';\n }\n\n function showBootstrapStep(step) {\n const logto = step === 'logto';\n el('bootstrap-logto-step').hidden = !logto;\n el('bootstrap-provider-step').hidden = logto;\n el('bootstrap-progress').textContent = logto ? 'Step 1 of 2' : 'Step 2 of 2';\n }\n\n function updateOwner(prefix) {\n el(prefix + '-org-field').hidden = el(prefix + '-owner').value !== 'organization';\n }\n\n function ownership(prefix) {\n if (el(prefix + '-owner').value === 'personal') return { owner: 'personal', organization: null };\n const organization = el(prefix + '-org').value.trim();\n if (!organization) throw new Error('Enter the GitHub organization login');\n return { owner: 'organization', organization };\n }\n\n function formatDiffValue(value) {\n if (value === null || value === undefined || value === '') return 'Not configured';\n if (Array.isArray(value)) return value.length ? value.map(formatDiffValue).join('\\n') : '[]';\n if (typeof value === 'object') {\n return Object.entries(value).map(([key, item]) => key + ': ' + (Array.isArray(item) ? item.join(', ') : formatDiffValue(item))).join('\\n');\n }\n return String(value);\n }\n\n function showDiff(id, rows, label = 'Update required') {\n const box = el(id);\n box.hidden = rows.length === 0;\n box.replaceChildren();\n if (rows.length === 0) return;\n const title = document.createElement('strong');\n title.className = 'diff-title';\n title.textContent = label;\n const grid = document.createElement('div');\n grid.className = 'config-diff';\n const head = document.createElement('div');\n head.className = 'diff-row diff-head';\n for (const text of ['Field', 'Current', 'Expected']) {\n const cell = document.createElement('span'); cell.textContent = text; head.append(cell);\n }\n grid.append(head);\n for (const item of rows) {\n const row = document.createElement('div'); row.className = 'diff-row';\n const field = document.createElement('span'); field.className = 'diff-field'; field.textContent = item.field;\n const current = document.createElement('span'); current.className = 'diff-value diff-current'; current.textContent = formatDiffValue(item.current);\n const expected = document.createElement('span'); expected.className = 'diff-value diff-expected'; expected.textContent = formatDiffValue(item.expected);\n row.append(field, current, expected); grid.append(row);\n }\n box.append(title, grid);\n }\n\n function renderStartupEnvironment() {\n const services = bootstrapInfo.services;\n const environment = [\n ['AGENTCONNECT_PUBLIC_CP_URL', services.controlPlane],\n ['AGENTCONNECT_PUBLIC_RELAY_URL', services.relay],\n ['AGENTCONNECT_PUBLIC_WEB_URL', services.web],\n ['LOGTO_ENDPOINT', bootstrapInfo.logtoEndpoint],\n ['LOGTO_MGMT_ENDPOINT', bootstrapInfo.logtoManagementEndpoint]\n ];\n el('startup-environment').textContent = environment\n .filter(([, value]) => value)\n .map(([key, value]) => key + '=' + value)\n .join('\\n');\n }\n\n function renderApps(status) {\n currentStatus = status;\n const byKey = new Map((status.secrets || []).map((item) => [item.key, item]));\n const values = status.values;\n const expected = status.providerExpectations || { github: null, slack: null, google: { origins: [], redirects: [] } };\n renderStartupEnvironment();\n\n const logto = values.logto;\n text('logto-management-endpoint', logto && bootstrapInfo.logtoManagementEndpoint);\n text('logto-management-id', logto && logto.managementAppId);\n text('logto-management-resource', logto && logto.managementResource);\n secretText('logto-management-secret-display', byKey, 'logto.managementAppSecret', Boolean(logto));\n text('logto-browser-endpoint', logto && logto.browser && bootstrapInfo.logtoEndpoint);\n text('logto-browser-id', values.auth.mode === 'oidc' ? values.auth.browserClient.appId : null);\n text('logto-browser-resource', logto && logto.browser && logto.browser.apiResource);\n el('logto-edit-controls').hidden = true;\n showIdentityEditors('logto', Boolean(logto && logto.browser && values.auth.mode === 'oidc'));\n match(\n 'logto-match',\n logto && configured(byKey, 'logto.managementAppSecret') ? '' : 'warn',\n logto && configured(byKey, 'logto.managementAppSecret') ? 'Ready to check' : 'Not configured'\n );\n\n el('preset-agents-enabled').checked = values.features.presetAgentsEnabled;\n\n const github = values.github;\n const webhookStored = byKey.get('github.webhookSecret') && byKey.get('github.webhookSecret').configured;\n const webhookInactive = github && github.webhookEnabled === false;\n text('github-app-id', github && github.appId);\n text('github-app-slug', github && github.slug);\n text('github-client-id', github && github.clientId);\n text('github-webhook-enabled', github && (github.webhookEnabled === false ? 'Disabled' : 'Enabled'));\n el('github-edit-controls').hidden = true;\n showIdentityEditors('github', Boolean(github));\n secretText('github-client-secret-display', byKey, 'github.clientSecret', Boolean(github));\n secretText('github-private-key-display', byKey, 'github.privateKeyB64', Boolean(github));\n secretText('github-webhook-secret-display', byKey, 'github.webhookSecret', Boolean(github));\n secretText('github-logto-secret-display', byKey, 'logto.githubConnectorClientSecret', Boolean(values.logto && values.logto.githubConnector));\n el('github-status').textContent = github\n ? github.slug + ' is configured. Webhook secret: ' + (webhookStored ? 'stored' : webhookInactive ? 'not required yet' : 'missing') + '.' +\n (webhookInactive ? ' Relay webhook delivery is disabled.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github\n ? expected.github\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'Valid Web, API, and ingress URLs' }]\n : [];\n match('github-match', !github ? '' : githubDrift.length ? 'warn' : '', !github ? 'Not configured' : githubDrift.length ? 'Expected URLs changed' : 'Ready to check');\n el('github-create-controls').hidden = Boolean(github);\n el('create-github').hidden = Boolean(github);\n el('clear-github').hidden = !github;\n el('connect-github-login').hidden = !github || !values.logto || Boolean(values.logto.githubConnector);\n el('check-github').hidden = !github;\n if (github) showDiff('github-drift', githubDrift, 'Expected URLs changed since App creation');\n else el('github-drift').hidden = true;\n\n const slack = values.slack;\n text('slack-app-id', slack && slack.appId);\n text('slack-client-id', slack && slack.clientId);\n el('slack-edit-controls').hidden = true;\n showIdentityEditors('slack', Boolean(slack));\n secretText('slack-client-secret-display', byKey, 'slack.clientSecret', Boolean(slack));\n secretText('slack-signing-secret-display', byKey, 'slack.signingSecret', Boolean(slack));\n el('slack-logto-status').textContent = values.logto && values.logto.slackConnector\n ? 'Enabled; reuses the Slack client secret above.'\n : 'Not enabled.';\n el('slack-status').textContent = slack ? slack.appId + ' is configured.' : 'Creates the default AgentConnect integration manifest.';\n const slackDrift = slack\n ? expected.slack\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'HTTPS Web, API, and ingress URLs' }]\n : [];\n match('slack-match', !slack ? '' : slackDrift.length ? 'warn' : '', !slack ? 'Not configured' : slackDrift.length ? 'Update required' : 'Ready to check');\n el('slack-name-field').hidden = Boolean(slack);\n el('create-slack').hidden = Boolean(slack);\n el('connect-slack-login').hidden = !slack || !values.logto || Boolean(values.logto.slackConnector) || !bootstrapInfo?.slackAvailable;\n el('clear-slack').hidden = !slack;\n el('check-slack').hidden = !slack;\n el('slack-settings').hidden = !slack;\n if (slack) {\n el('slack-settings').href = 'https://api.slack.com/apps/' + encodeURIComponent(slack.appId);\n showDiff('slack-drift', slackDrift);\n } else el('slack-drift').hidden = true;\n\n const google = values.logto && values.logto.googleConnector;\n const googleSecret = configured(byKey, 'logto.googleConnectorClientSecret');\n const expectedGoogle = expected.google;\n renderUriList('google-origins', expectedGoogle.origins);\n renderUriList('google-redirects', expectedGoogle.redirects);\n text('google-client-id', google && google.clientId);\n secretText('google-client-secret-display', byKey, 'logto.googleConnectorClientSecret', Boolean(google));\n showIdentityEditors('google', Boolean(google));\n el('google-id').value = google ? google.clientId : '';\n el('google-status').textContent = google ? 'Google OAuth client is configured.' : 'Create a Web application OAuth client manually, then save it here.';\n showDiff('google-drift', []);\n match('google-match', google && googleSecret ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : \"Can't verify automatically\");\n el('google-initial-secret-field').hidden = googleSecret;\n el('save-google').textContent = google ? 'Confirm callback settings' : 'Save Google client';\n el('google-config-controls').hidden = Boolean(google);\n el('save-google').hidden = Boolean(google);\n el('cancel-google-configuration').hidden = true;\n el('check-google').hidden = !google || !googleSecret;\n el('clear-google').hidden = !google;\n\n const feishuSecret = byKey.get('feishu.loginAppSecret');\n const larkSecret = byKey.get('lark.loginAppSecret');\n el('feishu-login-id').value = values.feishu ? values.feishu.loginAppId : '';\n el('lark-login-id').value = values.lark ? values.lark.loginAppId : '';\n text('feishu-app-id', values.feishu && values.feishu.loginAppId);\n text('lark-app-id', values.lark && values.lark.loginAppId);\n showIdentityEditors('feishu', Boolean(values.feishu));\n showIdentityEditors('lark', Boolean(values.lark));\n secretText('feishu-app-secret-display', byKey, 'feishu.loginAppSecret', Boolean(values.feishu));\n secretText('lark-app-secret-display', byKey, 'lark.loginAppSecret', Boolean(values.lark));\n el('feishu-login-status').textContent = values.feishu && feishuSecret && feishuSecret.configured\n ? values.feishu.loginAppId + ' is configured.'\n : 'No Feishu tenant App is configured.';\n el('lark-login-status').textContent = values.lark && larkSecret && larkSecret.configured\n ? values.lark.loginAppId + ' is configured.'\n : 'No Lark tenant App is configured.';\n match('feishu-match', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? '' : '', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? 'Ready to check' : 'Not configured');\n match('lark-match', values.lark && configured(byKey, 'lark.loginAppSecret') ? '' : '', values.lark && configured(byKey, 'lark.loginAppSecret') ? 'Ready to check' : 'Not configured');\n showDiff('feishu-drift', []);\n showDiff('lark-drift', []);\n el('feishu-config-controls').hidden = Boolean(values.feishu);\n el('lark-config-controls').hidden = Boolean(values.lark);\n el('feishu-create-name-field').hidden = Boolean(values.feishu);\n el('lark-create-name-field').hidden = Boolean(values.lark);\n el('create-feishu-login-app').hidden = Boolean(values.feishu);\n el('create-lark-login-app').hidden = Boolean(values.lark);\n el('save-feishu-login-app').hidden = Boolean(values.feishu);\n el('save-lark-login-app').hidden = Boolean(values.lark);\n el('cancel-feishu-configuration').hidden = true;\n el('cancel-lark-configuration').hidden = true;\n el('clear-feishu').hidden = !values.feishu;\n el('clear-lark').hidden = !values.lark;\n el('check-feishu-login-app').hidden = !values.feishu || !configured(byKey, 'feishu.loginAppSecret');\n el('check-lark-login-app').hidden = !values.lark || !configured(byKey, 'lark.loginAppSecret');\n el('feishu-settings').hidden = !values.feishu;\n el('lark-settings').hidden = !values.lark;\n if (values.feishu) el('feishu-settings').href = 'https://open.feishu.cn/app/' + encodeURIComponent(values.feishu.loginAppId);\n if (values.lark) el('lark-settings').href = 'https://open.larksuite.com/app/' + encodeURIComponent(values.lark.loginAppId);\n }\n\n function requiredInput(id, label) {\n const value = el(id).value.trim();\n if (!value) throw new Error('Enter ' + label);\n return value;\n }\n\n function githubConnectorUsesDeployment(values) {\n const github = values.github;\n const connector = values.logto && values.logto.githubConnector;\n return Boolean(\n github && connector &&\n connector.appId === github.appId &&\n connector.slug === github.slug &&\n connector.clientId === github.clientId\n );\n }\n\n function beginConfigurationEdit(provider) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const values = currentStatus.values;\n if (provider === 'logto') {\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') return;\n el('logto-edit-management-id').value = logto.managementAppId;\n el('logto-edit-management-resource').value = logto.managementResource;\n el('logto-edit-management-secret').value = '';\n el('logto-edit-browser-id').value = values.auth.browserClient.appId;\n el('logto-edit-browser-resource').value = logto.browser.apiResource || '';\n el('logto-edit-controls').hidden = false;\n el('logto-edit-management-id').focus();\n } else if (provider === 'github') {\n const github = values.github;\n if (!github) return;\n el('github-edit-app-id').value = String(github.appId);\n el('github-edit-slug').value = github.slug;\n el('github-edit-client-id').value = github.clientId || '';\n el('github-edit-webhook-enabled').checked = github.webhookEnabled !== false;\n for (const id of ['github-edit-client-secret', 'github-edit-private-key', 'github-edit-webhook-secret', 'github-edit-logto-secret']) el(id).value = '';\n el('github-edit-logto-secret-field').hidden = !githubConnectorUsesDeployment(values);\n el('github-edit-controls').hidden = false;\n el('clear-github').hidden = true;\n el('github-edit-app-id').focus();\n } else if (provider === 'slack') {\n const slack = values.slack;\n if (!slack) return;\n el('slack-edit-app-id').value = slack.appId;\n el('slack-edit-client-id').value = slack.clientId;\n el('slack-edit-client-secret').value = '';\n el('slack-edit-signing-secret').value = '';\n el('slack-edit-controls').hidden = false;\n el('clear-slack').hidden = true;\n el('slack-edit-app-id').focus();\n } else if (provider === 'google') {\n const google = values.logto && values.logto.googleConnector;\n if (!google) return;\n el('google-id').value = google.clientId;\n el('google-secret').value = '';\n el('google-config-controls').hidden = false;\n el('google-initial-secret-field').hidden = false;\n el('save-google').hidden = false;\n el('save-google').textContent = 'Save Google client';\n el('cancel-google-configuration').hidden = false;\n el('clear-google').hidden = true;\n el('google-id').focus();\n } else if (provider === 'feishu' || provider === 'lark') {\n el(provider + '-config-controls').hidden = false;\n el(provider + '-create-name-field').hidden = true;\n el('create-' + provider + '-login-app').hidden = true;\n el('save-' + provider + '-login-app').hidden = false;\n el('cancel-' + provider + '-configuration').hidden = false;\n el('clear-' + provider).hidden = true;\n el(provider + '-login-secret').value = '';\n el(provider + '-login-id').focus();\n }\n showIdentityEditors(provider, false);\n }\n\n function cancelConfigurationEdit() {\n if (currentStatus) renderApps(currentStatus);\n }\n\n async function replaceConfiguration(values, secrets, successMessage) {\n const response = await json(await fetch(api + '/deployment-config', {\n method: 'PUT',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values, ...(secrets ? { secrets } : {}) })\n }));\n await load();\n message(successMessage || 'Configuration saved. Restart AgentConnect to apply it.');\n return response;\n }\n\n async function saveLogtoConfiguration() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = currentStatus.values;\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') throw new Error('Logto is not configured');\n const managementAppId = requiredInput('logto-edit-management-id', 'the Management App ID');\n const managementResource = requiredInput('logto-edit-management-resource', 'the Management API resource');\n const browserAppId = requiredInput('logto-edit-browser-id', 'the SPA App ID');\n const browserResource = el('logto-edit-browser-resource').value.trim() || null;\n const managementIdentityChanged = managementAppId !== logto.managementAppId;\n const managementSecret = el('logto-edit-management-secret').value;\n if (managementIdentityChanged && !managementSecret) throw new Error('Enter the new Management App secret');\n const browser = { ...logto.browser, apiResource: browserResource };\n const auth = {\n ...values.auth,\n audience: browserResource || browserAppId,\n browserClient: { appId: browserAppId, apiResource: browserResource }\n };\n await replaceConfiguration(\n {\n ...values,\n auth,\n logto: { ...logto, managementAppId, managementResource, browser }\n },\n managementSecret ? { 'logto.managementAppSecret': managementSecret } : undefined,\n 'Logto configuration saved. Sign in again if the SPA identity changed, then restart AgentConnect.'\n );\n }\n\n async function saveGithubConfiguration() {\n if (!currentStatus || !currentStatus.values.github) throw new Error('GitHub is not configured');\n const values = currentStatus.values;\n const previous = values.github;\n const appId = Number(requiredInput('github-edit-app-id', 'the GitHub App ID'));\n if (!Number.isSafeInteger(appId) || appId <= 0) throw new Error('GitHub App ID must be a positive integer');\n const slug = requiredInput('github-edit-slug', 'the GitHub App slug');\n const clientId = requiredInput('github-edit-client-id', 'the GitHub Client ID');\n const appChanged = appId !== previous.appId;\n const clientChanged = clientId !== previous.clientId;\n const webhookEnabled = el('github-edit-webhook-enabled').checked;\n const webhookEnabling = webhookEnabled && previous.webhookEnabled === false;\n const connectorReused = githubConnectorUsesDeployment(values);\n const clientSecret = el('github-edit-client-secret').value;\n const privateKey = el('github-edit-private-key').value.trim();\n const webhookSecret = el('github-edit-webhook-secret').value;\n const connectorSecret = el('github-edit-logto-secret').value;\n const webhookSecretStored = currentStatus.secrets.some((secret) => secret.key === 'github.webhookSecret' && secret.configured);\n if ((appChanged || clientChanged) && !clientSecret) throw new Error('Enter the new GitHub client secret');\n if (appChanged && !privateKey) throw new Error('Enter the new GitHub private key as base64');\n if (webhookEnabled && ((appChanged && !webhookSecret) || (webhookEnabling && !webhookSecret && !webhookSecretStored))) throw new Error('Enter the new GitHub webhook secret');\n if (connectorReused && (appChanged || clientChanged) && !connectorSecret) throw new Error('Enter the new Logto connector client secret');\n const secrets = {};\n if (clientSecret) secrets['github.clientSecret'] = clientSecret;\n if (privateKey) secrets['github.privateKeyB64'] = privateKey;\n if (webhookSecret) secrets['github.webhookSecret'] = webhookSecret;\n if (connectorSecret) secrets['logto.githubConnectorClientSecret'] = connectorSecret;\n const nextLogto = connectorReused && values.logto\n ? { ...values.logto, githubConnector: { ...values.logto.githubConnector, appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, webhookEnabled },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\n );\n if (connectorSecret) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function saveSlackConfiguration() {\n if (!currentStatus || !currentStatus.values.slack) throw new Error('Slack is not configured');\n const values = currentStatus.values;\n const previous = values.slack;\n const appId = requiredInput('slack-edit-app-id', 'the Slack App ID');\n const clientId = requiredInput('slack-edit-client-id', 'the Slack Client ID');\n const changed = appId !== previous.appId || clientId !== previous.clientId;\n const clientSecret = el('slack-edit-client-secret').value;\n const signingSecret = el('slack-edit-signing-secret').value;\n if (changed && (!clientSecret || !signingSecret)) throw new Error('Enter both the new Slack client secret and signing secret');\n const secrets = {};\n if (clientSecret) secrets['slack.clientSecret'] = clientSecret;\n if (signingSecret) secrets['slack.signingSecret'] = signingSecret;\n const nextLogto = values.logto && values.logto.slackConnector\n ? { ...values.logto, slackConnector: { ...values.logto.slackConnector, appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\n );\n if (clientSecret && nextLogto && nextLogto.slackConnector) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function clearProvider(provider) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const label = provider === 'github' ? 'GitHub' : provider === 'slack' ? 'Slack' : provider === 'google' ? 'Google' : provider === 'feishu' ? 'Feishu' : 'Lark';\n if (!window.confirm('Clear the saved ' + label + ' configuration and secrets?')) return;\n const values = currentStatus.values;\n let next = values;\n let secrets = {};\n if (provider === 'github') {\n const connectorReused = githubConnectorUsesDeployment(values);\n const logto = connectorReused && values.logto\n ? {\n ...values.logto,\n githubConnector: null\n }\n : values.logto;\n next = { ...values, github: null, ...(logto ? { logto } : {}) };\n secrets = {\n 'github.clientSecret': null,\n 'github.privateKeyB64': null,\n 'github.webhookSecret': null,\n ...(connectorReused ? { 'logto.githubConnectorClientSecret': null } : {})\n };\n } else if (provider === 'slack') {\n const logto = values.logto\n ? {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'slack') }\n : values.logto.browser,\n slackConnector: null\n }\n : values.logto;\n next = { ...values, slack: null, ...(logto ? { logto } : {}) };\n secrets = { 'slack.clientSecret': null, 'slack.signingSecret': null };\n } else if (provider === 'google') {\n if (!values.logto) return;\n next = {\n ...values,\n logto: {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'google') }\n : values.logto.browser,\n googleConnector: null\n }\n };\n secrets = { 'logto.googleConnectorClientSecret': null };\n } else if (provider === 'feishu' || provider === 'lark') {\n next = { ...values, [provider]: null };\n secrets = { [provider + '.loginAppSecret']: null };\n }\n await replaceConfiguration(next, secrets, label + ' configuration cleared. Its setup controls are available again.');\n }\n\n async function startGithub(prefix) {\n const result = await json(await fetch(api + '/create/github/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n purpose: 'deployment',\n name: el(prefix + '-name').value,\n ownership: ownership(prefix),\n connectLogto: prefix === 'bootstrap-github'\n })\n }));\n const form = document.createElement('form');\n form.method = 'post'; form.action = result.action;\n const manifest = document.createElement('input');\n manifest.type = 'hidden'; manifest.name = 'manifest'; manifest.value = JSON.stringify(result.manifest);\n form.append(manifest); document.body.append(form);\n message('Opening GitHub to review the complete integration App.');\n form.submit();\n }\n\n async function loadBootstrapInfo() {\n bootstrapInfo = await json(await fetch(api + '/bootstrap-info'));\n el('open-logto').href = bootstrapInfo.logtoAdminEndpoint;\n el('logto-settings').href = bootstrapInfo.logtoAdminEndpoint;\n if (!el('logto-app-id').value && bootstrapInfo.logtoManagementAppId) {\n el('logto-app-id').value = bootstrapInfo.logtoManagementAppId;\n }\n renderUriList('bootstrap-google-origins', bootstrapInfo.google.javascriptOrigins);\n renderUriList('bootstrap-google-redirects', bootstrapInfo.google.redirectUris);\n renderUriList('bootstrap-slack-redirects', [bootstrapInfo.slackLoginRedirectUrl]);\n el('logto-management-api-resource').value = bootstrapInfo.logtoManagementResource;\n el('bootstrap-github-submit').disabled = !bootstrapInfo.githubAvailable;\n if (!bootstrapInfo.githubAvailable) {\n el('bootstrap-github-note').textContent = 'GitHub App creation needs valid saved Web, API, and ingress URLs.';\n } else if (!bootstrapInfo.githubWebhookActive) {\n el('bootstrap-github-note').textContent = 'Creates the complete GitHub App now without submitting the localhost webhook URL. Add it after saving reachable HTTPS ingress.';\n } else {\n el('bootstrap-github-note').textContent = 'This creates one complete App for both GitHub sign-in and repository integration.';\n }\n el('bootstrap-slack-option').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-submit').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-note').textContent = bootstrapInfo.slackAvailable\n ? 'Creates one complete Slack App. Sign-in uses a separate openid profile email flow from workspace installation.'\n : 'Slack sign-in needs all public service URLs to use HTTPS. Use Google locally or expose the stack through trusted HTTPS endpoints.';\n if (!bootstrapInfo.slackAvailable && el('bootstrap-provider').value === 'slack') {\n el('bootstrap-provider').value = 'google';\n updateBootstrapProvider();\n }\n return bootstrapInfo;\n }\n\n async function logtoBootstrapFinding() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n for (const id of ['logto.client_credentials', 'logto.roles_read']) {\n const finding = report.findings.find((candidate) => candidate.id === id);\n if (!finding || finding.status !== 'pass') {\n return finding || { status: 'fail', message: 'Logto Management API permissions could not be verified.' };\n }\n }\n return { status: 'pass', message: 'Logto Management API access is ready.' };\n }\n\n async function showBootstrap() {\n el('bootstrap').hidden = false;\n el('show-bootstrap').hidden = true;\n const info = bootstrapInfo || await loadBootstrapInfo();\n if (!info.logtoConfigured) {\n showBootstrapStep('logto');\n return;\n }\n const finding = await logtoBootstrapFinding();\n if (finding && finding.status === 'pass') {\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n return;\n }\n showBootstrapStep('logto');\n message(finding?.message || 'Logto Management API credentials could not be verified.', true);\n }\n\n async function bootstrapLogto() {\n await json(await fetch(api + '/bootstrap/logto', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n managementAppId: el('logto-app-id').value,\n managementAppSecret: el('logto-app-secret').value,\n managementResource: el('logto-management-api-resource').value\n })\n }));\n el('logto-app-secret').value = '';\n await loadBootstrapInfo();\n const finding = await logtoBootstrapFinding();\n if (!finding || finding.status !== 'pass') {\n throw new Error(finding?.message || 'Logto Management API credentials could not be verified.');\n }\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n }\n\n async function bootstrapGoogle() {\n const secret = el('bootstrap-google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ clientId: el('bootstrap-google-id').value, clientSecret: secret })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto(true);\n await load();\n }\n\n async function bootstrapGithub() {\n if (!bootstrapInfo || !bootstrapInfo.githubAvailable) throw new Error('GitHub App creation needs valid saved Web, API, and ingress URLs.');\n await startGithub('bootstrap-github');\n }\n\n async function bootstrapSlack() {\n if (!bootstrapInfo || !bootstrapInfo.slackAvailable) throw new Error('Slack sign-in requires all public service URLs to use HTTPS.');\n await createSlack('bootstrap-slack', true);\n await reconcileLogto();\n await load();\n }\n\n async function createSlack(prefix = 'slack', connectLogto = false) {\n const result = await json(await fetch(api + '/create/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n name: el(prefix + '-name').value,\n configToken: el(prefix + '-token').value,\n connectLogto\n })\n }));\n el(prefix + '-token').value = '';\n if (!connectLogto) await load();\n message('Slack App ' + result.app.id + ' was created with the default integration manifest. Restart AgentConnect to apply it.');\n }\n\n async function checkGithub() {\n const result = await json(await fetch(api + '/check/github', { headers: bearer() }));\n el('github-settings').href = result.settingsUrl;\n el('github-settings').hidden = false;\n const label = result.missing.length ? 'Update required' : \"Can't verify automatically\";\n showDiff('github-drift', result.diff || [], label);\n match('github-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : label);\n message(result.status === 'pass' ? 'GitHub App matches the expected integration manifest.' : result.missing.length ? 'GitHub App settings need an update.' : \"GitHub can't expose callback, setup, or webhook-active settings through its API; check them in GitHub.\", result.status === 'fail');\n }\n\n async function connectGithubLogin() {\n await json(await fetch(api + '/configure/github-login', { method: 'POST', headers: bearer() }));\n await reconcileLogto();\n await load();\n message('The deployment GitHub App is now also used for Logto sign-in. Update its displayed callback URLs.');\n }\n\n async function connectSlackLogin() {\n if (!currentStatus || !currentStatus.values.slack || !currentStatus.values.logto) {\n throw new Error('Save the Slack App and Logto configuration first');\n }\n const values = currentStatus.values;\n const slack = values.slack;\n const logto = values.logto;\n await replaceConfiguration(\n {\n ...values,\n logto: {\n ...logto,\n browser: logto.browser\n ? { ...logto.browser, socialProviders: [...new Set([...logto.browser.socialProviders, 'slack'])] }\n : logto.browser,\n slackConnector: {\n appId: slack.appId,\n clientId: slack.clientId\n }\n }\n },\n undefined,\n 'Slack App is ready to connect to Logto.'\n );\n await reconcileLogto();\n await load();\n message('The deployment Slack App is now also used for Logto sign-in.');\n }\n\n async function checkSlack() {\n const token = requiredInput('slack-token', 'the temporary Slack App configuration token');\n const result = await json(await fetch(api + '/check/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ configToken: token })\n }));\n el('slack-token').value = '';\n showDiff('slack-drift', result.diff || []);\n match('slack-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : 'Update required');\n message(result.status === 'pass' ? 'Slack App matches the default integration manifest.' : 'Slack App settings need an update.', result.status !== 'pass');\n }\n\n async function checkGoogle() {\n const report = await checkLogto();\n const connector = report.findings.find((finding) => finding.id === 'logto.connectors');\n const google = currentStatus && currentStatus.values.logto && currentStatus.values.logto.googleConnector;\n const connectorDiff = connector && connector.diff\n ? connector.diff.filter((item) => item.field.toLowerCase().startsWith('google'))\n : [];\n const passed = Boolean(google && connector && connectorDiff.length === 0);\n showDiff('google-drift', connectorDiff);\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Logto matches' : 'Update required');\n message(\n passed\n ? \"The Logto Google connector matches. Google OAuth redirect settings can't be verified automatically.\"\n : 'The Logto Google connector needs an update.',\n !passed\n );\n }\n\n async function checkRegionalLoginApp(region) {\n const result = await json(await fetch(api + '/check/regional-login-app/' + region, { headers: bearer() }));\n const label = region === 'feishu' ? 'Feishu' : 'Lark';\n showDiff(region + '-drift', result.diff || []);\n match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Matches' : result.status === 'fail' ? 'Update required' : 'Could not check');\n el(region + '-login-status').textContent = result.message;\n el(region + '-login-status').className = result.status === 'pass' ? 'ok' : result.status === 'fail' ? 'warn' : 'muted';\n message(result.message || (label + ' credential check completed.'), result.status === 'fail');\n }\n\n async function saveGoogle() {\n const secret = el('google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n clientId: el('google-id').value,\n ...(secret ? { clientSecret: secret } : {})\n })\n }));\n el('google-secret').value = '';\n await reconcileLogto(Boolean(secret));\n await load();\n message('Google OAuth client and Logto connector are configured. Restart AgentConnect to apply the saved settings.');\n }\n\n function regionalLoginApp(prefix) {\n const appId = el(prefix + '-login-id').value.trim();\n const appSecret = el(prefix + '-login-secret').value;\n return appId ? { appId, ...(appSecret ? { appSecret } : {}) } : null;\n }\n\n async function saveRegionalLoginApp(region) {\n const app = regionalLoginApp(region);\n if (!app) throw new Error('Enter the ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App ID');\n await json(await fetch(api + '/configure/regional-login-app', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, app })\n }));\n el(region + '-login-secret').value = '';\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' tenant App is saved. Restart AgentConnect services to apply it.');\n }\n\n async function createRegionalLoginApp(region) {\n const popup = window.open('about:blank', '_blank');\n try {\n const started = await json(await fetch(api + '/create/regional-login-app/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, name: el(region + '-create-name').value })\n }));\n if (popup) popup.location.replace(started.authorizationUrl);\n else window.open(started.authorizationUrl, '_blank', 'noopener');\n message('Approve the new ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App in the opened page. This page will finish saving it automatically.');\n while (true) {\n const result = await json(await fetch(api + '/create/regional-login-app/' + encodeURIComponent(started.id), { headers: bearer() }));\n if (result.status === 'completed') {\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' App ' + result.appId + ' was created and saved. Restart AgentConnect services to apply it.');\n return;\n }\n if (result.status === 'failed') throw new Error('Regional App creation ' + result.reason + '. Start it again.');\n await new Promise((resolve) => setTimeout(resolve, Math.max(500, Math.min(result.retryAfterMs || 2000, 5000))));\n }\n } catch (error) {\n if (popup) popup.close();\n throw error;\n }\n }\n\n async function reconcileLogto(refreshConnectorSecrets) {\n return json(await fetch(api + '/reconcile/logto', {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ refreshConnectorSecrets: Boolean(refreshConnectorSecrets) })\n }));\n }\n\n async function refreshDeploymentConfig() {\n const status = await json(await fetch(api + '/deployment-config', { headers: bearer() }));\n el('access').hidden = true;\n el('admin').hidden = false;\n el('editor').hidden = false;\n currentRevision = status.revision;\n renderApps(status);\n return status;\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n await refreshDeploymentConfig();\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, connectors, and social-only sign-in match.'\n : failures.map((finding) => finding.message).join(' ');\n el('logto-status').className = failures.length === 0 ? 'ok' : 'warn';\n match('logto-match', failures.length === 0 ? 'pass' : 'warn', failures.length === 0 ? 'Matches' : 'Update required');\n showDiff(\n 'logto-drift',\n failures.flatMap((finding) => finding.diff && finding.diff.length\n ? finding.diff\n : [{ field: finding.id.replace(/^logto\\./, '').replaceAll('_', ' '), current: finding.message, expected: 'Matches expected configuration' }])\n );\n el('logto-settings').hidden = failures.length === 0;\n return report;\n }\n\n function githubNotice(value) {\n if (value === 'deployment-created') return 'GitHub integration App created. Its private key and webhook secret are stored.';\n if (value === 'deployment-login-created') return 'GitHub integration App created and connected to Logto sign-in.';\n if (value === 'cancelled') return 'GitHub App creation was cancelled.';\n if (value === 'expired') return 'GitHub App creation expired. Start it again.';\n if (value === 'invalid-callback') return 'GitHub returned an invalid App creation callback.';\n if (value === 'conversion-failed') return 'GitHub may have created the App, but did not return complete credentials. Delete the orphaned App and retry.';\n if (value === 'save-failed') return 'GitHub created the App, but its credentials could not be saved. Delete the orphaned App and retry.';\n return '';\n }\n\n async function load() {\n let publicAuth = await authConfig();\n const currentUrl = new URL(location.href);\n const githubResult = currentUrl.searchParams.get('github');\n const notice = githubNotice(githubResult);\n if (githubResult === 'deployment-login-created' && publicAuth.mode === 'none') {\n await reconcileLogto();\n publicAuth = await authConfig();\n }\n if (githubResult) history.replaceState({}, '', '/');\n const hasToken = Boolean(sessionStorage.getItem(tokenKey));\n el('access').hidden = false;\n el('admin').hidden = true;\n el('login').hidden = true;\n el('open-logto').hidden = true;\n el('show-bootstrap').hidden = true;\n el('editor').hidden = true;\n el('bootstrap').hidden = true;\n if (publicAuth.logtoAdminEndpoint) {\n el('open-logto').href = publicAuth.logtoAdminEndpoint;\n el('logto-settings').href = publicAuth.logtoAdminEndpoint;\n }\n if (publicAuth.mode === 'none') {\n sessionStorage.removeItem(tokenKey);\n await loadBootstrapInfo();\n el('open-logto').hidden = false;\n el('show-bootstrap').hidden = false;\n message(notice || 'Logto sign-in is not configured. Set up the initial Logto administrator first.');\n return;\n }\n if (publicAuth.mode === 'unavailable') {\n el('open-logto').hidden = false;\n message(publicAuth.message || 'Logto sign-in is unavailable. Open Logto Console to review its settings.', true);\n return;\n }\n if (!hasToken) {\n el('login').hidden = false;\n message(notice || 'Sign in with Logto to continue.');\n return;\n }\n if (publicAuth.claimAvailable) {\n try {\n await json(await fetch(api + '/bootstrap/claim', { method: 'POST', headers: bearer() }));\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('This first user is now an ADMIN. Sign in again to refresh the role claim.');\n return;\n } catch (error) {\n if (error.status !== 401 && error.status !== 403) throw error;\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with Logto to continue.', true);\n return;\n }\n }\n try {\n await loadBootstrapInfo();\n await refreshDeploymentConfig();\n message(notice);\n checkLogto().catch((error) => {\n el('logto-status').textContent = error.message;\n el('logto-status').className = 'warn';\n el('logto-settings').hidden = false;\n });\n } catch (error) {\n if (error.status === 401 || error.status === 403) {\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with a Logto ADMIN account.', true);\n }\n else throw error;\n }\n }\n\n async function saveSecretReplacement(key, value) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n if (!value) throw new Error('Enter the replacement secret');\n const refreshLogto =\n (key === 'logto.githubConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.githubConnector) ||\n (key === 'logto.googleConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.googleConnector) ||\n (key === 'slack.clientSecret' && currentStatus.values.logto && currentStatus.values.logto.slackConnector);\n await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values: currentStatus.values, secrets: { [key]: value } })\n }));\n try {\n if (refreshLogto) await reconcileLogto(true);\n } finally {\n await load();\n }\n if (refreshLogto) {\n message('Secret replaced and applied to the Logto connector. Restart AgentConnect to apply deployment changes.');\n } else {\n message('Secret replaced. Restart AgentConnect to apply it.');\n }\n }\n\n function editSecret(button) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const row = button.closest('.secret-line');\n const display = el(button.dataset.secretDisplay);\n if (!row || !display || row.querySelector('.secret-editor')) return;\n const editor = document.createElement('span'); editor.className = 'secret-editor';\n const input = document.createElement('input');\n input.type = 'password'; input.autocomplete = 'new-password'; input.placeholder = 'Enter replacement secret';\n const save = document.createElement('button'); save.textContent = 'Save';\n const cancel = document.createElement('button'); cancel.textContent = 'Cancel';\n const close = () => { editor.remove(); display.hidden = false; button.hidden = false; };\n save.onclick = async () => {\n save.disabled = true;\n try { await saveSecretReplacement(button.dataset.secretKey, input.value); }\n catch (error) { save.disabled = false; message(error.message, true); }\n };\n cancel.onclick = close;\n input.onkeydown = (event) => { if (event.key === 'Enter') save.click(); if (event.key === 'Escape') close(); };\n display.hidden = true; button.hidden = true;\n editor.append(input, save, cancel); row.append(editor); input.focus();\n }\n\n async function saveOptions() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = {\n ...currentStatus.values,\n features: {\n presetAgentsEnabled: el('preset-agents-enabled').checked\n }\n };\n const saved = await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values })\n }));\n message('Saved deployment options. Restart AgentConnect to apply them.');\n await load();\n }\n\n el('login').onclick = () => signIn().catch((error) => message(error.message, true));\n el('show-bootstrap').onclick = () => showBootstrap().catch((error) => message(error.message, true));\n el('bootstrap-provider').onchange = updateBootstrapProvider;\n el('bootstrap-github-owner').onchange = () => updateOwner('bootstrap-github');\n el('github-owner').onchange = () => updateOwner('github');\n el('bootstrap-logto-submit').onclick = () => bootstrapLogto().catch((error) => message(error.message, true));\n el('bootstrap-back').onclick = () => showBootstrapStep('logto');\n el('bootstrap-google-submit').onclick = () => bootstrapGoogle().catch((error) => message(error.message, true));\n el('bootstrap-github-submit').onclick = () => bootstrapGithub().catch((error) => message(error.message, true));\n el('bootstrap-slack-submit').onclick = () => bootstrapSlack().catch((error) => message(error.message, true));\n el('create-github').onclick = () => startGithub('github').catch((error) => message(error.message, true));\n el('save-logto-configuration').onclick = () => saveLogtoConfiguration().catch((error) => message(error.message, true));\n el('cancel-logto-configuration').onclick = cancelConfigurationEdit;\n el('save-github-configuration').onclick = () => saveGithubConfiguration().catch((error) => message(error.message, true));\n el('cancel-github-configuration').onclick = cancelConfigurationEdit;\n el('clear-github').onclick = () => clearProvider('github').catch((error) => message(error.message, true));\n el('connect-github-login').onclick = () => connectGithubLogin().catch((error) => message(error.message, true));\n el('check-github').onclick = () => checkGithub().catch((error) => message(error.message, true));\n el('create-slack').onclick = () => createSlack('slack').catch((error) => message(error.message, true));\n el('connect-slack-login').onclick = () => connectSlackLogin().catch((error) => message(error.message, true));\n el('save-slack-configuration').onclick = () => saveSlackConfiguration().catch((error) => message(error.message, true));\n el('cancel-slack-configuration').onclick = cancelConfigurationEdit;\n el('clear-slack').onclick = () => clearProvider('slack').catch((error) => message(error.message, true));\n el('check-slack').onclick = () => checkSlack().catch((error) => message(error.message, true));\n el('save-google').onclick = () => saveGoogle().catch((error) => message(error.message, true));\n el('cancel-google-configuration').onclick = cancelConfigurationEdit;\n el('clear-google').onclick = () => clearProvider('google').catch((error) => message(error.message, true));\n el('check-google').onclick = () => checkGoogle().catch((error) => message(error.message, true));\n el('create-feishu-login-app').onclick = () => createRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('save-feishu-login-app').onclick = () => saveRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('cancel-feishu-configuration').onclick = cancelConfigurationEdit;\n el('clear-feishu').onclick = () => clearProvider('feishu').catch((error) => message(error.message, true));\n el('check-feishu-login-app').onclick = () => checkRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('create-lark-login-app').onclick = () => createRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('save-lark-login-app').onclick = () => saveRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('cancel-lark-configuration').onclick = cancelConfigurationEdit;\n el('clear-lark').onclick = () => clearProvider('lark').catch((error) => message(error.message, true));\n el('check-lark-login-app').onclick = () => checkRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('check-logto').onclick = () => checkLogto().catch((error) => message(error.message, true));\n el('reconcile-logto').onclick = () => reconcileLogto().then(load).catch((error) => message(error.message, true));\n el('logout').onclick = () => { sessionStorage.removeItem(tokenKey); load().catch((error) => message(error.message, true)); };\n el('save-options').onclick = () => saveOptions().catch((error) => message(error.message, true));\n for (const button of document.querySelectorAll('.edit-secret')) button.onclick = () => editSecret(button);\n for (const button of document.querySelectorAll('.edit-configuration')) button.onclick = () => beginConfigurationEdit(button.dataset.provider);\n updateBootstrapProvider();\n finishSignIn().then(load).catch((error) => message(error.message, true));\n <\/script>\n</body>\n</html>"])));
61092
+ const SETUP_HTML = String.raw(_templateObject || (_templateObject = _taggedTemplateLiteral(["<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>AgentConnect deployment settings</title>\n <style>\n :root { color-scheme: light dark; font: 15px/1.5 system-ui, sans-serif; }\n body { max-width: 1280px; margin: 48px auto; padding: 0 20px 60px; }\n h1 { margin-bottom: 4px; } h2 { margin-top: 32px; } h3 { margin: 0 0 8px; }\n .muted { color: #777; } .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }\n button, .button { padding: 8px 13px; cursor: pointer; }\n .button { border: 1px solid #8888; border-radius: 4px; color: inherit; text-decoration: none; display: inline-block; }\n input, select { width: min(520px, 100%); padding: 7px; box-sizing: border-box; }\n input[type=\"checkbox\"] { width: auto; padding: 0; }\n .field { display: grid; gap: 4px; margin: 10px 0; }\n .provider-stack { display: grid; gap: 16px; }\n .panel { border: 1px solid #8886; border-radius: 8px; padding: 16px; }\n .section-head, .provider-head { display: flex; justify-content: space-between; gap: 16px; align-items: start; }\n .section-head h2, .provider-head h3 { margin: 0; }\n .provider-head p { margin: 4px 0 0; }\n .badge { flex: none; border: 1px solid #8886; border-radius: 999px; padding: 3px 9px; font-size: 13px; }\n .badge.pass { color: #198754; border-color: #19875466; background: #19875412; }\n .badge.warn { color: #a66b00; border-color: #d99b1366; background: #d99b1315; }\n .badge.fail { color: #c33; border-color: #c333; background: #c3331111; }\n .credentials { display: grid; grid-template-columns: minmax(160px, 220px) minmax(0, 1fr); gap: 7px 16px; margin: 16px 0; }\n .credentials dt { color: #777; }\n .credentials dd { margin: 0; overflow-wrap: anywhere; }\n .credentials code { user-select: all; }\n .redacted { font-family: ui-monospace, monospace; letter-spacing: .08em; }\n .secret-line, .value-line { display: flex; gap: 10px; align-items: center; min-height: 30px; }\n .edit-secret { padding: 2px 8px; font-size: 13px; }\n .secret-editor { display: flex; gap: 8px; flex-wrap: wrap; width: 100%; }\n .secret-editor input { width: min(420px, 100%); }\n .edit-configuration { padding: 2px 8px; font-size: 13px; }\n .startup-owned { color: #777; font-size: 13px; }\n .danger { color: #c33; border-color: #c336; }\n .subsection { margin-top: 16px; padding-top: 14px; border-top: 1px solid #8883; }\n textarea { width: min(720px, 100%); min-height: 100px; padding: 7px; box-sizing: border-box; }\n .uris { margin: 8px 0; padding-left: 20px; } .uris code { user-select: all; }\n .notice { border-left: 4px solid #d99b13; padding: 8px 12px; background: #d99b1315; }\n .diff-title { display: block; margin-bottom: 8px; }\n .config-diff { display: grid; gap: 1px; border: 1px solid #8884; border-radius: 6px; overflow: hidden; background: #8884; }\n .diff-row { display: grid; grid-template-columns: minmax(140px, .8fr) minmax(0, 1.2fr) minmax(0, 1.2fr); background: Canvas; }\n .diff-row > * { min-width: 0; padding: 7px 9px; overflow-wrap: anywhere; white-space: pre-wrap; }\n .diff-head { font-size: 12px; font-weight: 600; color: #777; }\n .diff-field { font-weight: 600; }\n .diff-value { font-family: ui-monospace, monospace; font-size: 13px; }\n pre { padding: 12px; border-radius: 6px; background: #8881; overflow-x: auto; user-select: all; }\n #message { white-space: pre-wrap; padding: 10px 0; min-height: 1.5em; }\n .error { color: #c33; } .ok { color: #198754; } .warn { color: #a66b00; }\n code { overflow-wrap: anywhere; }\n .setup-layout { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 32px; align-items: start; }\n .setup-nav { position: sticky; top: 24px; display: grid; gap: 3px; padding: 10px; border: 1px solid #8886; border-radius: 8px; background: Canvas; }\n .setup-nav a { padding: 7px 9px; border-radius: 5px; color: inherit; text-decoration: none; white-space: nowrap; }\n .setup-nav a:hover { background: #8882; }\n .setup-section { scroll-margin-top: 24px; }\n details.environment { margin: 0 0 24px; } details.environment summary { cursor: pointer; font-weight: 600; }\n @media (max-width: 760px) {\n body { margin-top: 24px; }\n .setup-layout { grid-template-columns: 1fr; gap: 18px; }\n .setup-nav { position: static; display: flex; overflow-x: auto; }\n .credentials { grid-template-columns: 1fr; gap: 2px; }\n .credentials dd { margin-bottom: 8px; }\n .diff-row { grid-template-columns: 1fr; }\n .diff-head { display: none; }\n .diff-value::before { display: block; margin-bottom: 2px; color: #777; font: 11px/1.4 system-ui, sans-serif; }\n .diff-current::before { content: 'Current'; }\n .diff-expected::before { content: 'Expected'; }\n }\n [hidden] { display: none !important; }\n </style>\n</head>\n<body>\n <section id=\"access\">\n <h1>AgentConnect Setup</h1>\n <p id=\"access-message\" class=\"muted\" aria-live=\"polite\">Checking Logto sign-in…</p>\n <div class=\"row\">\n <button id=\"login\" hidden>Sign in with Logto</button>\n <a id=\"open-logto\" class=\"button\" href=\"http://localhost:3002\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n <button id=\"show-bootstrap\" hidden>Continue setup</button>\n </div>\n\n <section id=\"bootstrap\" hidden>\n <h2>Set up sign-in</h2>\n <p id=\"bootstrap-progress\" class=\"muted\">Step 1 of 2</p>\n <div id=\"bootstrap-logto-step\" class=\"panel\">\n <h3>Connect Logto</h3>\n <p class=\"muted\">Enter the one-time Logto Management API credential. It is sealed in the deployment database and verified before continuing.</p>\n <label class=\"field\">Logto M2M App ID<input id=\"logto-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Logto M2M App Secret<input id=\"logto-app-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-management-api-resource\" type=\"url\" autocomplete=\"off\"></label>\n <p class=\"muted\">Logto Cloud uses <code>https://&lt;tenant-id&gt;.logto.app/api</code>. Keep <code>https://default.logto.app/api</code> for Logto OSS.</p>\n <button id=\"bootstrap-logto-submit\">Save Logto and continue</button>\n </div>\n <div id=\"bootstrap-provider-step\" hidden>\n <h3>Choose a sign-in provider</h3>\n <p class=\"muted\">Logto Management API access is ready. Configure one provider to enable sign-in.</p>\n <label class=\"field\">Sign-in provider\n <select id=\"bootstrap-provider\"><option value=\"google\">Google (works on localhost)</option><option value=\"github\">GitHub integration App</option><option id=\"bootstrap-slack-option\" value=\"slack\">Slack integration App</option></select>\n </label>\n <div id=\"bootstrap-google\" class=\"panel\">\n <h3>Google OAuth client</h3>\n <p class=\"muted\">Create a Web application client in Google Auth Platform, then paste its credentials here.</p>\n <p>Authorized JavaScript origin:</p><ul id=\"bootstrap-google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"bootstrap-google-redirects\" class=\"uris\"></ul>\n <a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a>\n <label class=\"field\">Client ID<input id=\"bootstrap-google-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client Secret<input id=\"bootstrap-google-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-google-submit\">Save Google OAuth and configure Logto</button>\n </div>\n <div id=\"bootstrap-github\" class=\"panel\" hidden>\n <h3>GitHub integration App</h3>\n <p id=\"bootstrap-github-note\" class=\"muted\">This creates one complete App for both GitHub sign-in and repository integration.</p>\n <label class=\"field\">Owner\n <select id=\"bootstrap-github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"bootstrap-github-org-field\" class=\"field\" hidden>Organization login<input id=\"bootstrap-github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"bootstrap-github-name\" value=\"AgentConnect\"></label>\n <button id=\"bootstrap-github-submit\">Create GitHub App and configure Logto</button>\n </div>\n <div id=\"bootstrap-slack\" class=\"panel\" hidden>\n <h3>Slack integration App</h3>\n <p id=\"bootstrap-slack-note\" class=\"muted\">Creates one complete Slack App for workspace installation and a separate Sign in with Slack OIDC flow.</p>\n <p>Logto redirect URI:</p><ul id=\"bootstrap-slack-redirects\" class=\"uris\"></ul>\n <label class=\"field\">App name<input id=\"bootstrap-slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"bootstrap-slack-token\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-slack-submit\">Create Slack App and configure Logto</button>\n </div>\n <button id=\"bootstrap-back\">Back to Logto credentials</button>\n </div>\n </section>\n </section>\n\n <main id=\"setup\" hidden>\n <div class=\"setup-layout\">\n <nav class=\"setup-nav\" aria-label=\"Deployment settings\">\n <a href=\"#startup-section\">Startup</a>\n <a href=\"#logto-section\">Logto</a>\n <a href=\"#github-section\">GitHub</a>\n <a href=\"#slack-section\">Slack</a>\n <a href=\"#google-section\">Google</a>\n <a href=\"#feishu-section\">Feishu</a>\n <a href=\"#lark-section\">Lark</a>\n <a href=\"#options-section\">Options</a>\n </nav>\n <div class=\"setup-content\">\n <h1>AgentConnect deployment settings</h1>\n <p class=\"muted\">Saved settings take effect after the stack is restarted.</p>\n <div class=\"row\">\n <button id=\"logout\">Log out</button>\n </div>\n <div id=\"message\" aria-live=\"polite\"></div>\n\n <section id=\"editor\" hidden>\n <details id=\"startup-section\" class=\"environment setup-section\" open>\n <summary>Startup environment</summary>\n <p class=\"notice\">Public service URLs come from <code>.env</code>. Provider callbacks below are derived from these values.</p>\n <pre id=\"startup-environment\"></pre>\n </details>\n\n <section id=\"logto-section\" class=\"setup-section\" aria-labelledby=\"logto-heading\">\n <div class=\"section-head\">\n <div><h2 id=\"logto-heading\">Logto</h2><p class=\"muted\">Authentication and administrator access.</p></div>\n <span id=\"logto-match\" class=\"badge\">Not checked</span>\n </div>\n <div class=\"panel\">\n <dl class=\"credentials\">\n <dt>Management endpoint</dt><dd class=\"value-line\"><code id=\"logto-management-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>Management App ID</dt><dd class=\"value-line\"><code id=\"logto-management-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management API resource</dt><dd class=\"value-line\"><code id=\"logto-management-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management App secret</dt><dd class=\"secret-line\"><span id=\"logto-management-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.managementAppSecret\" data-secret-display=\"logto-management-secret-display\">Edit</button></dd>\n <dt>Sign-in endpoint</dt><dd class=\"value-line\"><code id=\"logto-browser-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>SPA App ID</dt><dd class=\"value-line\"><code id=\"logto-browser-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Browser API resource</dt><dd class=\"value-line\"><code id=\"logto-browser-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n </dl>\n <div id=\"logto-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Logto configuration</h3>\n <label class=\"field\">Management App ID<input id=\"logto-edit-management-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-edit-management-resource\" autocomplete=\"off\"></label>\n <label class=\"field\">New Management App secret<input id=\"logto-edit-management-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when management identity changes\"></label>\n <label class=\"field\">SPA App ID<input id=\"logto-edit-browser-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Browser API resource<input id=\"logto-edit-browser-resource\" autocomplete=\"off\" placeholder=\"Optional\"></label>\n <div class=\"row\"><button id=\"save-logto-configuration\">Save configuration</button><button id=\"cancel-logto-configuration\">Cancel</button></div>\n </div>\n <p id=\"logto-status\" class=\"muted\">Checking redirects and sign-in settings…</p>\n <div id=\"logto-drift\" class=\"notice\" hidden></div>\n <div class=\"row\">\n <button id=\"check-logto\">Check match</button>\n <button id=\"reconcile-logto\">Apply expected settings</button>\n <a id=\"logto-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n </div>\n </div>\n </section>\n\n <h2>Providers</h2>\n <div class=\"provider-stack\">\n <section id=\"github-section\" class=\"panel setup-section\" aria-labelledby=\"github-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"github-heading\">GitHub</h3><p class=\"muted\">Repository integration and optional Logto sign-in.</p></div>\n <span id=\"github-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"github-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>App slug</dt><dd class=\"value-line\"><code id=\"github-app-slug\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"github-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Webhook delivery</dt><dd class=\"value-line\"><code id=\"github-webhook-enabled\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"github-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.clientSecret\" data-secret-display=\"github-client-secret-display\">Edit</button></dd>\n <dt>Private key</dt><dd class=\"secret-line\"><span id=\"github-private-key-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.privateKeyB64\" data-secret-display=\"github-private-key-display\">Edit</button></dd>\n <dt>Webhook secret</dt><dd class=\"secret-line\"><span id=\"github-webhook-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.webhookSecret\" data-secret-display=\"github-webhook-secret-display\">Edit</button></dd>\n <dt>Logto connector secret</dt><dd class=\"secret-line\"><span id=\"github-logto-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.githubConnectorClientSecret\" data-secret-display=\"github-logto-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"github-status\" class=\"muted\"></p>\n <div id=\"github-drift\" class=\"notice\" hidden></div>\n <div id=\"github-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit GitHub App identity</h3>\n <label class=\"field\">App ID<input id=\"github-edit-app-id\" inputmode=\"numeric\" autocomplete=\"off\"></label>\n <label class=\"field\">App slug<input id=\"github-edit-slug\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"github-edit-client-id\" autocomplete=\"off\"></label>\n <label><input id=\"github-edit-webhook-enabled\" type=\"checkbox\"> Enable Relay webhook delivery</label>\n <label class=\"field\">New client secret<input id=\"github-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when App or Client ID changes\"></label>\n <label class=\"field\">New private key (base64)<textarea id=\"github-edit-private-key\" autocomplete=\"off\" placeholder=\"Required when App ID changes\"></textarea></label>\n <label class=\"field\">New webhook secret<input id=\"github-edit-webhook-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when an active webhook App ID changes\"></label>\n <label id=\"github-edit-logto-secret-field\" class=\"field\" hidden>New Logto connector client secret<input id=\"github-edit-logto-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when this App is also used for sign-in\"></label>\n <div class=\"row\"><button id=\"save-github-configuration\">Save configuration</button><button id=\"cancel-github-configuration\">Cancel</button></div>\n </div>\n <div id=\"github-create-controls\" class=\"subsection\">\n <label class=\"field\">Owner\n <select id=\"github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"github-org-field\" class=\"field\" hidden>Organization login<input id=\"github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"github-name\" value=\"AgentConnect\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-github\">Create GitHub App</button>\n <button id=\"connect-github-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-github\" hidden>Check match</button>\n <button id=\"clear-github\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"github-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open GitHub settings</a>\n </div>\n </section>\n\n <section id=\"slack-section\" class=\"panel setup-section\" aria-labelledby=\"slack-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"slack-heading\">Slack</h3><p class=\"muted\">One App for workspace integration and Logto sign-in.</p></div>\n <span id=\"slack-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"slack-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"slack-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"slack-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.clientSecret\" data-secret-display=\"slack-client-secret-display\">Edit</button></dd>\n <dt>Signing secret</dt><dd class=\"secret-line\"><span id=\"slack-signing-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.signingSecret\" data-secret-display=\"slack-signing-secret-display\">Edit</button></dd>\n <dt>Logto sign-in</dt><dd id=\"slack-logto-status\">Not configured</dd>\n </dl>\n <p id=\"slack-status\" class=\"muted\"></p>\n <div id=\"slack-drift\" class=\"notice\" hidden></div>\n <div id=\"slack-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Slack App identity</h3>\n <label class=\"field\">App ID<input id=\"slack-edit-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"slack-edit-client-id\" autocomplete=\"off\"></label>\n <label class=\"field\">New client secret<input id=\"slack-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <label class=\"field\">New signing secret<input id=\"slack-edit-signing-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <div class=\"row\"><button id=\"save-slack-configuration\">Save configuration</button><button id=\"cancel-slack-configuration\">Cancel</button></div>\n </div>\n <div class=\"subsection\">\n <label id=\"slack-name-field\" class=\"field\">App name<input id=\"slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"slack-token\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Used only for create or check\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-slack\">Create Slack App</button>\n <button id=\"connect-slack-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-slack\" hidden>Check match</button>\n <button id=\"clear-slack\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"slack-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Slack settings</a>\n </div>\n </section>\n\n <section id=\"google-section\" class=\"panel setup-section\" aria-labelledby=\"google-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"google-heading\">Google</h3><p class=\"muted\">OAuth client used by the Logto Google connector.</p></div>\n <span id=\"google-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"google-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"google\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"google-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.googleConnectorClientSecret\" data-secret-display=\"google-client-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"google-status\" class=\"muted\"></p>\n <div id=\"google-drift\" class=\"notice\" hidden></div>\n <p>Authorized JavaScript origin:</p><ul id=\"google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"google-redirects\" class=\"uris\"></ul>\n <p class=\"muted\">Google does not expose OAuth client redirect settings through an API. Copy these required values into Google Auth Platform; Setup can verify only the Logto connector.</p>\n <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a></div>\n <div id=\"google-config-controls\" class=\"subsection\">\n <label class=\"field\">Client ID<input id=\"google-id\" autocomplete=\"off\"></label>\n <label id=\"google-initial-secret-field\" class=\"field\">Client secret<input id=\"google-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when Client ID changes\"></label>\n </div>\n <div class=\"row\"><button id=\"save-google\">Save Google client</button><button id=\"cancel-google-configuration\" hidden>Cancel</button><button id=\"check-google\" hidden>Check match</button><button id=\"clear-google\" class=\"danger\" hidden>Clear configuration</button></div>\n </section>\n\n <section id=\"feishu-section\" class=\"panel setup-section\" aria-labelledby=\"feishu-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"feishu-heading\">Feishu</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Feishu, OAuth callbacks, and published App setup.</p></div>\n <span id=\"feishu-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"feishu-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"feishu\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"feishu-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"feishu.loginAppSecret\" data-secret-display=\"feishu-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"feishu-login-status\" class=\"muted\"></p>\n <div id=\"feishu-drift\" class=\"notice\" hidden></div>\n <div id=\"feishu-config-controls\" class=\"subsection\">\n <label id=\"feishu-create-name-field\" class=\"field\">New App name<input id=\"feishu-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"feishu-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"feishu-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-feishu-login-app\">Create Feishu App</button>\n <button id=\"save-feishu-login-app\">Save existing App</button>\n <button id=\"cancel-feishu-configuration\" hidden>Cancel</button>\n <button id=\"check-feishu-login-app\" hidden>Check setup</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"feishu-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Feishu settings</a>\n </div>\n </section>\n\n <section id=\"lark-section\" class=\"panel setup-section\" aria-labelledby=\"lark-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"lark-heading\">Lark</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Lark, OAuth callbacks, and published App setup.</p></div>\n <span id=\"lark-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"lark-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"lark\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"lark-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"lark.loginAppSecret\" data-secret-display=\"lark-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"lark-login-status\" class=\"muted\"></p>\n <div id=\"lark-drift\" class=\"notice\" hidden></div>\n <div id=\"lark-config-controls\" class=\"subsection\">\n <label id=\"lark-create-name-field\" class=\"field\">New App name<input id=\"lark-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"lark-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"lark-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-lark-login-app\">Create Lark App</button>\n <button id=\"save-lark-login-app\">Save existing App</button>\n <button id=\"cancel-lark-configuration\" hidden>Cancel</button>\n <button id=\"check-lark-login-app\" hidden>Check setup</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"lark-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Lark settings</a>\n </div>\n </section>\n </div>\n\n <section id=\"options-section\" class=\"setup-section\">\n <h2>Deployment options</h2>\n <div class=\"panel\">\n <label class=\"field\"><span><input id=\"preset-agents-enabled\" type=\"checkbox\"> Enable preset Agents</span></label>\n <div class=\"row\"><button id=\"save-options\">Save options</button></div>\n </div>\n </section>\n </section>\n </div>\n </div>\n </main>\n\n <script>\n const api = '/api/v1';\n const tokenKey = 'agentconnect.setup.token';\n const verifierKey = 'agentconnect.setup.pkce';\n const stateKey = 'agentconnect.setup.state';\n let currentRevision = 0;\n let currentStatus = null;\n let bootstrapInfo = null;\n const el = (id) => document.getElementById(id);\n const message = (text, error = false) => {\n const target = el('setup').hidden ? el('access-message') : el('message');\n target.textContent = text;\n target.className = error ? 'error' : 'ok';\n };\n const bearer = () => {\n const token = sessionStorage.getItem(tokenKey);\n return token ? { authorization: 'Bearer ' + token } : {};\n };\n const json = async (response) => {\n const body = await response.json().catch(() => ({}));\n if (!response.ok) throw Object.assign(new Error(body.message || ('HTTP ' + response.status)), { status: response.status, code: body.code });\n return body;\n };\n const base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/+/g, '-').replace(///g, '_').replace(/=+$/, '');\n const random = () => base64url(crypto.getRandomValues(new Uint8Array(32)));\n const configured = (byKey, key) => Boolean(byKey.get(key) && byKey.get(key).configured);\n\n function showIdentityEditors(provider, show) {\n for (const button of document.querySelectorAll('.edit-configuration[data-provider=\"' + provider + '\"]')) {\n button.hidden = !show;\n }\n }\n\n function text(id, value) {\n el(id).textContent = value === null || value === undefined || value === '' ? 'Not configured' : String(value);\n }\n\n function secretText(id, byKey, key, editable) {\n const stored = configured(byKey, key);\n const display = el(id);\n const button = document.querySelector('[data-secret-display=\"' + id + '\"]');\n const row = display.closest('.secret-line');\n const editor = row && row.querySelector('.secret-editor');\n if (editor) editor.remove();\n display.hidden = false;\n display.textContent = stored ? '***' : 'Not configured';\n display.className = stored ? 'redacted' : 'muted';\n button.hidden = !editable;\n button.textContent = stored ? 'Edit' : 'Set';\n }\n\n function match(id, state, label) {\n const target = el(id);\n target.textContent = label;\n target.className = 'badge' + (state ? ' ' + state : '');\n }\n\n async function authConfig() { return json(await fetch(api + '/auth-config')); }\n\n async function signIn() {\n const config = await authConfig();\n if (config.mode !== 'oidc') throw new Error('Save an OIDC configuration first');\n const verifier = random();\n const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));\n const state = random();\n sessionStorage.setItem(verifierKey, verifier);\n sessionStorage.setItem(stateKey, state);\n const url = new URL(config.authorizationEndpoint);\n url.searchParams.set('client_id', config.appId);\n url.searchParams.set('redirect_uri', config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', 'openid profile email roles');\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n if (config.resource) url.searchParams.set('resource', config.resource);\n location.assign(url);\n }\n\n async function finishSignIn() {\n const url = new URL(location.href);\n const code = url.searchParams.get('code');\n if (!code) return;\n const state = url.searchParams.get('state');\n if (!state || state !== sessionStorage.getItem(stateKey)) throw new Error('OIDC state mismatch');\n const verifier = sessionStorage.getItem(verifierKey);\n if (!verifier) throw new Error('PKCE verifier is missing; start sign-in again');\n const config = await authConfig();\n const body = new URLSearchParams({\n grant_type: 'authorization_code', code, client_id: config.appId,\n redirect_uri: config.redirectUri, code_verifier: verifier\n });\n if (config.resource) body.set('resource', config.resource);\n const tokens = await json(await fetch(config.tokenEndpoint, {\n method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body\n }));\n if (typeof tokens.id_token !== 'string') throw new Error('Logto did not return an ID token');\n sessionStorage.setItem(tokenKey, tokens.id_token);\n sessionStorage.removeItem(verifierKey);\n sessionStorage.removeItem(stateKey);\n history.replaceState({}, '', '/');\n }\n\n function renderUriList(id, values) {\n el(id).replaceChildren(...(values || []).map((value) => {\n const item = document.createElement('li');\n const code = document.createElement('code'); code.textContent = value;\n item.append(code); return item;\n }));\n }\n\n function updateBootstrapProvider() {\n const provider = el('bootstrap-provider').value;\n el('bootstrap-google').hidden = provider !== 'google';\n el('bootstrap-github').hidden = provider !== 'github';\n el('bootstrap-slack').hidden = provider !== 'slack';\n }\n\n function showBootstrapStep(step) {\n const logto = step === 'logto';\n el('bootstrap-logto-step').hidden = !logto;\n el('bootstrap-provider-step').hidden = logto;\n el('bootstrap-progress').textContent = logto ? 'Step 1 of 2' : 'Step 2 of 2';\n }\n\n function updateOwner(prefix) {\n el(prefix + '-org-field').hidden = el(prefix + '-owner').value !== 'organization';\n }\n\n function ownership(prefix) {\n if (el(prefix + '-owner').value === 'personal') return { owner: 'personal', organization: null };\n const organization = el(prefix + '-org').value.trim();\n if (!organization) throw new Error('Enter the GitHub organization login');\n return { owner: 'organization', organization };\n }\n\n function formatDiffValue(value) {\n if (value === null || value === undefined || value === '') return 'Not configured';\n if (Array.isArray(value)) return value.length ? value.map(formatDiffValue).join('\n') : '[]';\n if (typeof value === 'object') {\n return Object.entries(value).map(([key, item]) => key + ': ' + (Array.isArray(item) ? item.join(', ') : formatDiffValue(item))).join('\n');\n }\n return String(value);\n }\n\n function showDiff(id, rows, label = 'Update required') {\n const box = el(id);\n box.hidden = rows.length === 0;\n box.replaceChildren();\n if (rows.length === 0) return;\n const title = document.createElement('strong');\n title.className = 'diff-title';\n title.textContent = label;\n const grid = document.createElement('div');\n grid.className = 'config-diff';\n const head = document.createElement('div');\n head.className = 'diff-row diff-head';\n for (const text of ['Field', 'Current', 'Expected']) {\n const cell = document.createElement('span'); cell.textContent = text; head.append(cell);\n }\n grid.append(head);\n for (const item of rows) {\n const row = document.createElement('div'); row.className = 'diff-row';\n const field = document.createElement('span'); field.className = 'diff-field'; field.textContent = item.field;\n const current = document.createElement('span'); current.className = 'diff-value diff-current'; current.textContent = formatDiffValue(item.current);\n const expected = document.createElement('span'); expected.className = 'diff-value diff-expected'; expected.textContent = formatDiffValue(item.expected);\n row.append(field, current, expected); grid.append(row);\n }\n box.append(title, grid);\n }\n\n function renderStartupEnvironment() {\n const services = bootstrapInfo.services;\n const environment = [\n ['AGENTCONNECT_PUBLIC_CP_URL', services.controlPlane],\n ['AGENTCONNECT_PUBLIC_RELAY_URL', services.relay],\n ['AGENTCONNECT_PUBLIC_WEB_URL', services.web],\n ['LOGTO_ENDPOINT', bootstrapInfo.logtoEndpoint],\n ['LOGTO_MGMT_ENDPOINT', bootstrapInfo.logtoManagementEndpoint]\n ];\n el('startup-environment').textContent = environment\n .filter(([, value]) => value)\n .map(([key, value]) => key + '=' + value)\n .join('\n');\n }\n\n function renderApps(status) {\n currentStatus = status;\n const byKey = new Map((status.secrets || []).map((item) => [item.key, item]));\n const values = status.values;\n const expected = status.providerExpectations || { github: null, slack: null, google: { origins: [], redirects: [] } };\n renderStartupEnvironment();\n\n const logto = values.logto;\n text('logto-management-endpoint', logto && bootstrapInfo.logtoManagementEndpoint);\n text('logto-management-id', logto && logto.managementAppId);\n text('logto-management-resource', logto && logto.managementResource);\n secretText('logto-management-secret-display', byKey, 'logto.managementAppSecret', Boolean(logto));\n text('logto-browser-endpoint', logto && logto.browser && bootstrapInfo.logtoEndpoint);\n text('logto-browser-id', values.auth.mode === 'oidc' ? values.auth.browserClient.appId : null);\n text('logto-browser-resource', logto && logto.browser && logto.browser.apiResource);\n el('logto-edit-controls').hidden = true;\n showIdentityEditors('logto', Boolean(logto && logto.browser && values.auth.mode === 'oidc'));\n match(\n 'logto-match',\n logto && configured(byKey, 'logto.managementAppSecret') ? '' : 'warn',\n logto && configured(byKey, 'logto.managementAppSecret') ? 'Ready to check' : 'Not configured'\n );\n\n el('preset-agents-enabled').checked = values.features.presetAgentsEnabled;\n\n const github = values.github;\n const webhookStored = byKey.get('github.webhookSecret') && byKey.get('github.webhookSecret').configured;\n const webhookInactive = github && github.webhookEnabled === false;\n text('github-app-id', github && github.appId);\n text('github-app-slug', github && github.slug);\n text('github-client-id', github && github.clientId);\n text('github-webhook-enabled', github && (github.webhookEnabled === false ? 'Disabled' : 'Enabled'));\n el('github-edit-controls').hidden = true;\n showIdentityEditors('github', Boolean(github));\n secretText('github-client-secret-display', byKey, 'github.clientSecret', Boolean(github));\n secretText('github-private-key-display', byKey, 'github.privateKeyB64', Boolean(github));\n secretText('github-webhook-secret-display', byKey, 'github.webhookSecret', Boolean(github));\n secretText('github-logto-secret-display', byKey, 'logto.githubConnectorClientSecret', Boolean(values.logto && values.logto.githubConnector));\n el('github-status').textContent = github\n ? github.slug + ' is configured. Webhook secret: ' + (webhookStored ? 'stored' : webhookInactive ? 'not required yet' : 'missing') + '.' +\n (webhookInactive ? ' Relay webhook delivery is disabled.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github\n ? expected.github\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'Valid Web, API, and ingress URLs' }]\n : [];\n match('github-match', !github ? '' : githubDrift.length ? 'warn' : '', !github ? 'Not configured' : githubDrift.length ? 'Expected URLs changed' : 'Ready to check');\n el('github-create-controls').hidden = Boolean(github);\n el('create-github').hidden = Boolean(github);\n el('clear-github').hidden = !github;\n el('connect-github-login').hidden = !github || !values.logto || Boolean(values.logto.githubConnector);\n el('check-github').hidden = !github;\n if (github) showDiff('github-drift', githubDrift, 'Expected URLs changed since App creation');\n else el('github-drift').hidden = true;\n\n const slack = values.slack;\n text('slack-app-id', slack && slack.appId);\n text('slack-client-id', slack && slack.clientId);\n el('slack-edit-controls').hidden = true;\n showIdentityEditors('slack', Boolean(slack));\n secretText('slack-client-secret-display', byKey, 'slack.clientSecret', Boolean(slack));\n secretText('slack-signing-secret-display', byKey, 'slack.signingSecret', Boolean(slack));\n el('slack-logto-status').textContent = values.logto && values.logto.slackConnector\n ? 'Enabled; reuses the Slack client secret above.'\n : 'Not enabled.';\n el('slack-status').textContent = slack ? slack.appId + ' is configured.' : 'Creates the default AgentConnect integration manifest.';\n const slackDrift = slack\n ? expected.slack\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'HTTPS Web, API, and ingress URLs' }]\n : [];\n match('slack-match', !slack ? '' : slackDrift.length ? 'warn' : '', !slack ? 'Not configured' : slackDrift.length ? 'Update required' : 'Ready to check');\n el('slack-name-field').hidden = Boolean(slack);\n el('create-slack').hidden = Boolean(slack);\n el('connect-slack-login').hidden = !slack || !values.logto || Boolean(values.logto.slackConnector) || !bootstrapInfo?.slackAvailable;\n el('clear-slack').hidden = !slack;\n el('check-slack').hidden = !slack;\n el('slack-settings').hidden = !slack;\n if (slack) {\n el('slack-settings').href = 'https://api.slack.com/apps/' + encodeURIComponent(slack.appId);\n showDiff('slack-drift', slackDrift);\n } else el('slack-drift').hidden = true;\n\n const google = values.logto && values.logto.googleConnector;\n const googleSecret = configured(byKey, 'logto.googleConnectorClientSecret');\n const expectedGoogle = expected.google;\n renderUriList('google-origins', expectedGoogle.origins);\n renderUriList('google-redirects', expectedGoogle.redirects);\n text('google-client-id', google && google.clientId);\n secretText('google-client-secret-display', byKey, 'logto.googleConnectorClientSecret', Boolean(google));\n showIdentityEditors('google', Boolean(google));\n el('google-id').value = google ? google.clientId : '';\n el('google-status').textContent = google ? 'Google OAuth client is configured.' : 'Create a Web application OAuth client manually, then save it here.';\n showDiff('google-drift', []);\n match('google-match', google && googleSecret ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : \"Can't verify automatically\");\n el('google-initial-secret-field').hidden = googleSecret;\n el('save-google').textContent = google ? 'Confirm callback settings' : 'Save Google client';\n el('google-config-controls').hidden = Boolean(google);\n el('save-google').hidden = Boolean(google);\n el('cancel-google-configuration').hidden = true;\n el('check-google').hidden = !google || !googleSecret;\n el('clear-google').hidden = !google;\n\n const feishuSecret = byKey.get('feishu.loginAppSecret');\n const larkSecret = byKey.get('lark.loginAppSecret');\n el('feishu-login-id').value = values.feishu ? values.feishu.loginAppId : '';\n el('lark-login-id').value = values.lark ? values.lark.loginAppId : '';\n text('feishu-app-id', values.feishu && values.feishu.loginAppId);\n text('lark-app-id', values.lark && values.lark.loginAppId);\n showIdentityEditors('feishu', Boolean(values.feishu));\n showIdentityEditors('lark', Boolean(values.lark));\n secretText('feishu-app-secret-display', byKey, 'feishu.loginAppSecret', Boolean(values.feishu));\n secretText('lark-app-secret-display', byKey, 'lark.loginAppSecret', Boolean(values.lark));\n el('feishu-login-status').textContent = values.feishu && feishuSecret && feishuSecret.configured\n ? values.feishu.loginAppId + ' is configured.'\n : 'No Feishu tenant App is configured.';\n el('lark-login-status').textContent = values.lark && larkSecret && larkSecret.configured\n ? values.lark.loginAppId + ' is configured.'\n : 'No Lark tenant App is configured.';\n match('feishu-match', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? '' : '', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? 'Ready to check' : 'Not configured');\n match('lark-match', values.lark && configured(byKey, 'lark.loginAppSecret') ? '' : '', values.lark && configured(byKey, 'lark.loginAppSecret') ? 'Ready to check' : 'Not configured');\n showDiff('feishu-drift', []);\n showDiff('lark-drift', []);\n el('feishu-config-controls').hidden = Boolean(values.feishu);\n el('lark-config-controls').hidden = Boolean(values.lark);\n el('feishu-create-name-field').hidden = Boolean(values.feishu);\n el('lark-create-name-field').hidden = Boolean(values.lark);\n el('create-feishu-login-app').hidden = Boolean(values.feishu);\n el('create-lark-login-app').hidden = Boolean(values.lark);\n el('save-feishu-login-app').hidden = Boolean(values.feishu);\n el('save-lark-login-app').hidden = Boolean(values.lark);\n el('cancel-feishu-configuration').hidden = true;\n el('cancel-lark-configuration').hidden = true;\n el('clear-feishu').hidden = !values.feishu;\n el('clear-lark').hidden = !values.lark;\n el('check-feishu-login-app').hidden = !values.feishu || !configured(byKey, 'feishu.loginAppSecret');\n el('check-lark-login-app').hidden = !values.lark || !configured(byKey, 'lark.loginAppSecret');\n el('feishu-settings').hidden = !values.feishu;\n el('lark-settings').hidden = !values.lark;\n if (values.feishu) el('feishu-settings').href = 'https://open.feishu.cn/app/' + encodeURIComponent(values.feishu.loginAppId);\n if (values.lark) el('lark-settings').href = 'https://open.larksuite.com/app/' + encodeURIComponent(values.lark.loginAppId);\n }\n\n function requiredInput(id, label) {\n const value = el(id).value.trim();\n if (!value) throw new Error('Enter ' + label);\n return value;\n }\n\n function githubConnectorUsesDeployment(values) {\n const github = values.github;\n const connector = values.logto && values.logto.githubConnector;\n return Boolean(\n github && connector &&\n connector.appId === github.appId &&\n connector.slug === github.slug &&\n connector.clientId === github.clientId\n );\n }\n\n function beginConfigurationEdit(provider) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const values = currentStatus.values;\n if (provider === 'logto') {\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') return;\n el('logto-edit-management-id').value = logto.managementAppId;\n el('logto-edit-management-resource').value = logto.managementResource;\n el('logto-edit-management-secret').value = '';\n el('logto-edit-browser-id').value = values.auth.browserClient.appId;\n el('logto-edit-browser-resource').value = logto.browser.apiResource || '';\n el('logto-edit-controls').hidden = false;\n el('logto-edit-management-id').focus();\n } else if (provider === 'github') {\n const github = values.github;\n if (!github) return;\n el('github-edit-app-id').value = String(github.appId);\n el('github-edit-slug').value = github.slug;\n el('github-edit-client-id').value = github.clientId || '';\n el('github-edit-webhook-enabled').checked = github.webhookEnabled !== false;\n for (const id of ['github-edit-client-secret', 'github-edit-private-key', 'github-edit-webhook-secret', 'github-edit-logto-secret']) el(id).value = '';\n el('github-edit-logto-secret-field').hidden = !githubConnectorUsesDeployment(values);\n el('github-edit-controls').hidden = false;\n el('clear-github').hidden = true;\n el('github-edit-app-id').focus();\n } else if (provider === 'slack') {\n const slack = values.slack;\n if (!slack) return;\n el('slack-edit-app-id').value = slack.appId;\n el('slack-edit-client-id').value = slack.clientId;\n el('slack-edit-client-secret').value = '';\n el('slack-edit-signing-secret').value = '';\n el('slack-edit-controls').hidden = false;\n el('clear-slack').hidden = true;\n el('slack-edit-app-id').focus();\n } else if (provider === 'google') {\n const google = values.logto && values.logto.googleConnector;\n if (!google) return;\n el('google-id').value = google.clientId;\n el('google-secret').value = '';\n el('google-config-controls').hidden = false;\n el('google-initial-secret-field').hidden = false;\n el('save-google').hidden = false;\n el('save-google').textContent = 'Save Google client';\n el('cancel-google-configuration').hidden = false;\n el('clear-google').hidden = true;\n el('google-id').focus();\n } else if (provider === 'feishu' || provider === 'lark') {\n el(provider + '-config-controls').hidden = false;\n el(provider + '-create-name-field').hidden = true;\n el('create-' + provider + '-login-app').hidden = true;\n el('save-' + provider + '-login-app').hidden = false;\n el('cancel-' + provider + '-configuration').hidden = false;\n el('clear-' + provider).hidden = true;\n el(provider + '-login-secret').value = '';\n el(provider + '-login-id').focus();\n }\n showIdentityEditors(provider, false);\n }\n\n function cancelConfigurationEdit() {\n if (currentStatus) renderApps(currentStatus);\n }\n\n async function replaceConfiguration(values, secrets, successMessage) {\n const response = await json(await fetch(api + '/deployment-config', {\n method: 'PUT',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values, ...(secrets ? { secrets } : {}) })\n }));\n await load();\n message(successMessage || 'Configuration saved. Restart AgentConnect to apply it.');\n return response;\n }\n\n async function saveLogtoConfiguration() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = currentStatus.values;\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') throw new Error('Logto is not configured');\n const managementAppId = requiredInput('logto-edit-management-id', 'the Management App ID');\n const managementResource = requiredInput('logto-edit-management-resource', 'the Management API resource');\n const browserAppId = requiredInput('logto-edit-browser-id', 'the SPA App ID');\n const browserResource = el('logto-edit-browser-resource').value.trim() || null;\n const managementIdentityChanged = managementAppId !== logto.managementAppId;\n const managementSecret = el('logto-edit-management-secret').value;\n if (managementIdentityChanged && !managementSecret) throw new Error('Enter the new Management App secret');\n const browser = { ...logto.browser, apiResource: browserResource };\n const auth = {\n ...values.auth,\n audience: browserResource || browserAppId,\n browserClient: { appId: browserAppId, apiResource: browserResource }\n };\n await replaceConfiguration(\n {\n ...values,\n auth,\n logto: { ...logto, managementAppId, managementResource, browser }\n },\n managementSecret ? { 'logto.managementAppSecret': managementSecret } : undefined,\n 'Logto configuration saved. Sign in again if the SPA identity changed, then restart AgentConnect.'\n );\n }\n\n async function saveGithubConfiguration() {\n if (!currentStatus || !currentStatus.values.github) throw new Error('GitHub is not configured');\n const values = currentStatus.values;\n const previous = values.github;\n const appId = Number(requiredInput('github-edit-app-id', 'the GitHub App ID'));\n if (!Number.isSafeInteger(appId) || appId <= 0) throw new Error('GitHub App ID must be a positive integer');\n const slug = requiredInput('github-edit-slug', 'the GitHub App slug');\n const clientId = requiredInput('github-edit-client-id', 'the GitHub Client ID');\n const appChanged = appId !== previous.appId;\n const clientChanged = clientId !== previous.clientId;\n const webhookEnabled = el('github-edit-webhook-enabled').checked;\n const webhookEnabling = webhookEnabled && previous.webhookEnabled === false;\n const connectorReused = githubConnectorUsesDeployment(values);\n const clientSecret = el('github-edit-client-secret').value;\n const privateKey = el('github-edit-private-key').value.trim();\n const webhookSecret = el('github-edit-webhook-secret').value;\n const connectorSecret = el('github-edit-logto-secret').value;\n const webhookSecretStored = currentStatus.secrets.some((secret) => secret.key === 'github.webhookSecret' && secret.configured);\n if ((appChanged || clientChanged) && !clientSecret) throw new Error('Enter the new GitHub client secret');\n if (appChanged && !privateKey) throw new Error('Enter the new GitHub private key as base64');\n if (webhookEnabled && ((appChanged && !webhookSecret) || (webhookEnabling && !webhookSecret && !webhookSecretStored))) throw new Error('Enter the new GitHub webhook secret');\n if (connectorReused && (appChanged || clientChanged) && !connectorSecret) throw new Error('Enter the new Logto connector client secret');\n const secrets = {};\n if (clientSecret) secrets['github.clientSecret'] = clientSecret;\n if (privateKey) secrets['github.privateKeyB64'] = privateKey;\n if (webhookSecret) secrets['github.webhookSecret'] = webhookSecret;\n if (connectorSecret) secrets['logto.githubConnectorClientSecret'] = connectorSecret;\n const nextLogto = connectorReused && values.logto\n ? { ...values.logto, githubConnector: { ...values.logto.githubConnector, appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, webhookEnabled },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\n );\n if (connectorSecret) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function saveSlackConfiguration() {\n if (!currentStatus || !currentStatus.values.slack) throw new Error('Slack is not configured');\n const values = currentStatus.values;\n const previous = values.slack;\n const appId = requiredInput('slack-edit-app-id', 'the Slack App ID');\n const clientId = requiredInput('slack-edit-client-id', 'the Slack Client ID');\n const changed = appId !== previous.appId || clientId !== previous.clientId;\n const clientSecret = el('slack-edit-client-secret').value;\n const signingSecret = el('slack-edit-signing-secret').value;\n if (changed && (!clientSecret || !signingSecret)) throw new Error('Enter both the new Slack client secret and signing secret');\n const secrets = {};\n if (clientSecret) secrets['slack.clientSecret'] = clientSecret;\n if (signingSecret) secrets['slack.signingSecret'] = signingSecret;\n const nextLogto = values.logto && values.logto.slackConnector\n ? { ...values.logto, slackConnector: { ...values.logto.slackConnector, appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\n );\n if (clientSecret && nextLogto && nextLogto.slackConnector) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function clearProvider(provider) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const label = provider === 'github' ? 'GitHub' : provider === 'slack' ? 'Slack' : provider === 'google' ? 'Google' : provider === 'feishu' ? 'Feishu' : 'Lark';\n if (!window.confirm('Clear the saved ' + label + ' configuration and secrets?')) return;\n const values = currentStatus.values;\n let next = values;\n let secrets = {};\n if (provider === 'github') {\n const connectorReused = githubConnectorUsesDeployment(values);\n const logto = connectorReused && values.logto\n ? {\n ...values.logto,\n githubConnector: null\n }\n : values.logto;\n next = { ...values, github: null, ...(logto ? { logto } : {}) };\n secrets = {\n 'github.clientSecret': null,\n 'github.privateKeyB64': null,\n 'github.webhookSecret': null,\n ...(connectorReused ? { 'logto.githubConnectorClientSecret': null } : {})\n };\n } else if (provider === 'slack') {\n const logto = values.logto\n ? {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'slack') }\n : values.logto.browser,\n slackConnector: null\n }\n : values.logto;\n next = { ...values, slack: null, ...(logto ? { logto } : {}) };\n secrets = { 'slack.clientSecret': null, 'slack.signingSecret': null };\n } else if (provider === 'google') {\n if (!values.logto) return;\n next = {\n ...values,\n logto: {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'google') }\n : values.logto.browser,\n googleConnector: null\n }\n };\n secrets = { 'logto.googleConnectorClientSecret': null };\n } else if (provider === 'feishu' || provider === 'lark') {\n next = { ...values, [provider]: null };\n secrets = { [provider + '.loginAppSecret']: null };\n }\n await replaceConfiguration(next, secrets, label + ' configuration cleared. Its setup controls are available again.');\n }\n\n async function startGithub(prefix) {\n const result = await json(await fetch(api + '/create/github/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n purpose: 'deployment',\n name: el(prefix + '-name').value,\n ownership: ownership(prefix),\n connectLogto: prefix === 'bootstrap-github'\n })\n }));\n const form = document.createElement('form');\n form.method = 'post'; form.action = result.action;\n const manifest = document.createElement('input');\n manifest.type = 'hidden'; manifest.name = 'manifest'; manifest.value = JSON.stringify(result.manifest);\n form.append(manifest); document.body.append(form);\n message('Opening GitHub to review the complete integration App.');\n form.submit();\n }\n\n async function loadBootstrapInfo() {\n bootstrapInfo = await json(await fetch(api + '/bootstrap-info'));\n el('open-logto').href = bootstrapInfo.logtoAdminEndpoint;\n el('logto-settings').href = bootstrapInfo.logtoAdminEndpoint;\n if (!el('logto-app-id').value && bootstrapInfo.logtoManagementAppId) {\n el('logto-app-id').value = bootstrapInfo.logtoManagementAppId;\n }\n renderUriList('bootstrap-google-origins', bootstrapInfo.google.javascriptOrigins);\n renderUriList('bootstrap-google-redirects', bootstrapInfo.google.redirectUris);\n renderUriList('bootstrap-slack-redirects', [bootstrapInfo.slackLoginRedirectUrl]);\n el('logto-management-api-resource').value = bootstrapInfo.logtoManagementResource;\n el('bootstrap-github-submit').disabled = !bootstrapInfo.githubAvailable;\n if (!bootstrapInfo.githubAvailable) {\n el('bootstrap-github-note').textContent = 'GitHub App creation needs valid saved Web, API, and ingress URLs.';\n } else if (!bootstrapInfo.githubWebhookActive) {\n el('bootstrap-github-note').textContent = 'Creates the complete GitHub App now without submitting the localhost webhook URL. Add it after saving reachable HTTPS ingress.';\n } else {\n el('bootstrap-github-note').textContent = 'This creates one complete App for both GitHub sign-in and repository integration.';\n }\n el('bootstrap-slack-option').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-submit').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-note').textContent = bootstrapInfo.slackAvailable\n ? 'Creates one complete Slack App. Sign-in uses a separate openid profile email flow from workspace installation.'\n : 'Slack sign-in needs all public service URLs to use HTTPS. Use Google locally or expose the stack through trusted HTTPS endpoints.';\n if (!bootstrapInfo.slackAvailable && el('bootstrap-provider').value === 'slack') {\n el('bootstrap-provider').value = 'google';\n updateBootstrapProvider();\n }\n return bootstrapInfo;\n }\n\n async function logtoBootstrapFinding() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n for (const id of ['logto.client_credentials', 'logto.roles_read']) {\n const finding = report.findings.find((candidate) => candidate.id === id);\n if (!finding || finding.status !== 'pass') {\n return finding || { status: 'fail', message: 'Logto Management API permissions could not be verified.' };\n }\n }\n return { status: 'pass', message: 'Logto Management API access is ready.' };\n }\n\n async function showBootstrap() {\n el('bootstrap').hidden = false;\n el('show-bootstrap').hidden = true;\n const info = bootstrapInfo || await loadBootstrapInfo();\n if (!info.logtoConfigured) {\n showBootstrapStep('logto');\n return;\n }\n const finding = await logtoBootstrapFinding();\n if (finding && finding.status === 'pass') {\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n return;\n }\n showBootstrapStep('logto');\n message(finding?.message || 'Logto Management API credentials could not be verified.', true);\n }\n\n async function bootstrapLogto() {\n await json(await fetch(api + '/bootstrap/logto', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n managementAppId: el('logto-app-id').value,\n managementAppSecret: el('logto-app-secret').value,\n managementResource: el('logto-management-api-resource').value\n })\n }));\n el('logto-app-secret').value = '';\n await loadBootstrapInfo();\n const finding = await logtoBootstrapFinding();\n if (!finding || finding.status !== 'pass') {\n throw new Error(finding?.message || 'Logto Management API credentials could not be verified.');\n }\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n }\n\n async function bootstrapGoogle() {\n const secret = el('bootstrap-google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ clientId: el('bootstrap-google-id').value, clientSecret: secret })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto(true);\n await load();\n }\n\n async function bootstrapGithub() {\n if (!bootstrapInfo || !bootstrapInfo.githubAvailable) throw new Error('GitHub App creation needs valid saved Web, API, and ingress URLs.');\n await startGithub('bootstrap-github');\n }\n\n async function bootstrapSlack() {\n if (!bootstrapInfo || !bootstrapInfo.slackAvailable) throw new Error('Slack sign-in requires all public service URLs to use HTTPS.');\n await createSlack('bootstrap-slack', true);\n await reconcileLogto();\n await load();\n }\n\n async function createSlack(prefix = 'slack', connectLogto = false) {\n const result = await json(await fetch(api + '/create/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n name: el(prefix + '-name').value,\n configToken: el(prefix + '-token').value,\n connectLogto\n })\n }));\n el(prefix + '-token').value = '';\n if (!connectLogto) await load();\n message('Slack App ' + result.app.id + ' was created with the default integration manifest. Restart AgentConnect to apply it.');\n }\n\n async function checkGithub() {\n const result = await json(await fetch(api + '/check/github', { headers: bearer() }));\n el('github-settings').href = result.settingsUrl;\n el('github-settings').hidden = false;\n const label = result.missing.length ? 'Update required' : \"Can't verify automatically\";\n showDiff('github-drift', result.diff || [], label);\n match('github-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : label);\n message(result.status === 'pass' ? 'GitHub App matches the expected integration manifest.' : result.missing.length ? 'GitHub App settings need an update.' : \"GitHub can't expose callback, setup, or webhook-active settings through its API; check them in GitHub.\", result.status === 'fail');\n }\n\n async function connectGithubLogin() {\n await json(await fetch(api + '/configure/github-login', { method: 'POST', headers: bearer() }));\n await reconcileLogto();\n await load();\n message('The deployment GitHub App is now also used for Logto sign-in. Update its displayed callback URLs.');\n }\n\n async function connectSlackLogin() {\n if (!currentStatus || !currentStatus.values.slack || !currentStatus.values.logto) {\n throw new Error('Save the Slack App and Logto configuration first');\n }\n const values = currentStatus.values;\n const slack = values.slack;\n const logto = values.logto;\n await replaceConfiguration(\n {\n ...values,\n logto: {\n ...logto,\n browser: logto.browser\n ? { ...logto.browser, socialProviders: [...new Set([...logto.browser.socialProviders, 'slack'])] }\n : logto.browser,\n slackConnector: {\n appId: slack.appId,\n clientId: slack.clientId\n }\n }\n },\n undefined,\n 'Slack App is ready to connect to Logto.'\n );\n await reconcileLogto();\n await load();\n message('The deployment Slack App is now also used for Logto sign-in.');\n }\n\n async function checkSlack() {\n const token = requiredInput('slack-token', 'the temporary Slack App configuration token');\n const result = await json(await fetch(api + '/check/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ configToken: token })\n }));\n el('slack-token').value = '';\n showDiff('slack-drift', result.diff || []);\n match('slack-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : 'Update required');\n message(result.status === 'pass' ? 'Slack App matches the default integration manifest.' : 'Slack App settings need an update.', result.status !== 'pass');\n }\n\n async function checkGoogle() {\n const report = await checkLogto();\n const connector = report.findings.find((finding) => finding.id === 'logto.connectors');\n const google = currentStatus && currentStatus.values.logto && currentStatus.values.logto.googleConnector;\n const connectorDiff = connector && connector.diff\n ? connector.diff.filter((item) => item.field.toLowerCase().startsWith('google'))\n : [];\n const passed = Boolean(google && connector && connectorDiff.length === 0);\n showDiff('google-drift', connectorDiff);\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Logto matches' : 'Update required');\n message(\n passed\n ? \"The Logto Google connector matches. Google OAuth redirect settings can't be verified automatically.\"\n : 'The Logto Google connector needs an update.',\n !passed\n );\n }\n\n async function checkRegionalLoginApp(region) {\n const result = await json(await fetch(api + '/check/regional-login-app/' + region, { headers: bearer() }));\n const label = region === 'feishu' ? 'Feishu' : 'Lark';\n showDiff(region + '-drift', result.diff || []);\n match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Matches' : result.status === 'fail' ? 'Update required' : 'Could not check');\n el(region + '-login-status').textContent = result.message;\n el(region + '-login-status').className = result.status === 'pass' ? 'ok' : result.status === 'fail' ? 'warn' : 'muted';\n message(result.message || (label + ' credential check completed.'), result.status === 'fail');\n }\n\n async function saveGoogle() {\n const secret = el('google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n clientId: el('google-id').value,\n ...(secret ? { clientSecret: secret } : {})\n })\n }));\n el('google-secret').value = '';\n await reconcileLogto(Boolean(secret));\n await load();\n message('Google OAuth client and Logto connector are configured. Restart AgentConnect to apply the saved settings.');\n }\n\n function regionalLoginApp(prefix) {\n const appId = el(prefix + '-login-id').value.trim();\n const appSecret = el(prefix + '-login-secret').value;\n return appId ? { appId, ...(appSecret ? { appSecret } : {}) } : null;\n }\n\n async function saveRegionalLoginApp(region) {\n const app = regionalLoginApp(region);\n if (!app) throw new Error('Enter the ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App ID');\n await json(await fetch(api + '/configure/regional-login-app', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, app })\n }));\n el(region + '-login-secret').value = '';\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' tenant App is saved. Restart AgentConnect services to apply it.');\n }\n\n async function createRegionalLoginApp(region) {\n const popup = window.open('about:blank', '_blank');\n try {\n const started = await json(await fetch(api + '/create/regional-login-app/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, name: el(region + '-create-name').value })\n }));\n if (popup) popup.location.replace(started.authorizationUrl);\n else window.open(started.authorizationUrl, '_blank', 'noopener');\n message('Approve the new ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App in the opened page. This page will finish saving it automatically.');\n while (true) {\n const result = await json(await fetch(api + '/create/regional-login-app/' + encodeURIComponent(started.id), { headers: bearer() }));\n if (result.status === 'completed') {\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' App ' + result.appId + ' was created and saved. Restart AgentConnect services to apply it.');\n return;\n }\n if (result.status === 'failed') throw new Error('Regional App creation ' + result.reason + '. Start it again.');\n await new Promise((resolve) => setTimeout(resolve, Math.max(500, Math.min(result.retryAfterMs || 2000, 5000))));\n }\n } catch (error) {\n if (popup) popup.close();\n throw error;\n }\n }\n\n async function reconcileLogto(refreshConnectorSecrets) {\n return json(await fetch(api + '/reconcile/logto', {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ refreshConnectorSecrets: Boolean(refreshConnectorSecrets) })\n }));\n }\n\n async function refreshDeploymentConfig() {\n const status = await json(await fetch(api + '/deployment-config', { headers: bearer() }));\n el('access').hidden = true;\n el('setup').hidden = false;\n el('editor').hidden = false;\n currentRevision = status.revision;\n renderApps(status);\n return status;\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n await refreshDeploymentConfig();\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, connectors, and social-only sign-in match.'\n : failures.map((finding) => finding.message).join(' ');\n el('logto-status').className = failures.length === 0 ? 'ok' : 'warn';\n match('logto-match', failures.length === 0 ? 'pass' : 'warn', failures.length === 0 ? 'Matches' : 'Update required');\n showDiff(\n 'logto-drift',\n failures.flatMap((finding) => finding.diff && finding.diff.length\n ? finding.diff\n : [{ field: finding.id.replace(/^logto./, '').replaceAll('_', ' '), current: finding.message, expected: 'Matches expected configuration' }])\n );\n el('logto-settings').hidden = failures.length === 0;\n return report;\n }\n\n function githubNotice(value) {\n if (value === 'deployment-created') return 'GitHub integration App created. Its private key and webhook secret are stored.';\n if (value === 'deployment-login-created') return 'GitHub integration App created and connected to Logto sign-in.';\n if (value === 'cancelled') return 'GitHub App creation was cancelled.';\n if (value === 'expired') return 'GitHub App creation expired. Start it again.';\n if (value === 'invalid-callback') return 'GitHub returned an invalid App creation callback.';\n if (value === 'conversion-failed') return 'GitHub may have created the App, but did not return complete credentials. Delete the orphaned App and retry.';\n if (value === 'save-failed') return 'GitHub created the App, but its credentials could not be saved. Delete the orphaned App and retry.';\n return '';\n }\n\n async function load() {\n let publicAuth = await authConfig();\n const currentUrl = new URL(location.href);\n const githubResult = currentUrl.searchParams.get('github');\n const notice = githubNotice(githubResult);\n if (githubResult === 'deployment-login-created' && publicAuth.mode === 'none') {\n await reconcileLogto();\n publicAuth = await authConfig();\n }\n if (githubResult) history.replaceState({}, '', '/');\n const hasToken = Boolean(sessionStorage.getItem(tokenKey));\n el('access').hidden = false;\n el('setup').hidden = true;\n el('login').hidden = true;\n el('open-logto').hidden = true;\n el('show-bootstrap').hidden = true;\n el('editor').hidden = true;\n el('bootstrap').hidden = true;\n if (publicAuth.logtoAdminEndpoint) {\n el('open-logto').href = publicAuth.logtoAdminEndpoint;\n el('logto-settings').href = publicAuth.logtoAdminEndpoint;\n }\n if (publicAuth.mode === 'none') {\n sessionStorage.removeItem(tokenKey);\n await loadBootstrapInfo();\n el('open-logto').hidden = false;\n el('show-bootstrap').hidden = false;\n message(notice || 'Logto sign-in is not configured. Set up the initial Logto administrator first.');\n return;\n }\n if (publicAuth.mode === 'unavailable') {\n el('open-logto').hidden = false;\n message(publicAuth.message || 'Logto sign-in is unavailable. Open Logto Console to review its settings.', true);\n return;\n }\n if (!hasToken) {\n el('login').hidden = false;\n message(notice || 'Sign in with Logto to continue.');\n return;\n }\n if (publicAuth.claimAvailable) {\n try {\n await json(await fetch(api + '/bootstrap/claim', { method: 'POST', headers: bearer() }));\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('This first user is now an ADMIN. Sign in again to refresh the role claim.');\n return;\n } catch (error) {\n if (error.status !== 401 && error.status !== 403) throw error;\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with Logto to continue.', true);\n return;\n }\n }\n try {\n await loadBootstrapInfo();\n await refreshDeploymentConfig();\n message(notice);\n checkLogto().catch((error) => {\n el('logto-status').textContent = error.message;\n el('logto-status').className = 'warn';\n el('logto-settings').hidden = false;\n });\n } catch (error) {\n if (error.status === 401 || error.status === 403) {\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with a Logto ADMIN account.', true);\n }\n else throw error;\n }\n }\n\n async function saveSecretReplacement(key, value) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n if (!value) throw new Error('Enter the replacement secret');\n const refreshLogto =\n (key === 'logto.githubConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.githubConnector) ||\n (key === 'logto.googleConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.googleConnector) ||\n (key === 'slack.clientSecret' && currentStatus.values.logto && currentStatus.values.logto.slackConnector);\n await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values: currentStatus.values, secrets: { [key]: value } })\n }));\n try {\n if (refreshLogto) await reconcileLogto(true);\n } finally {\n await load();\n }\n if (refreshLogto) {\n message('Secret replaced and applied to the Logto connector. Restart AgentConnect to apply deployment changes.');\n } else {\n message('Secret replaced. Restart AgentConnect to apply it.');\n }\n }\n\n function editSecret(button) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const row = button.closest('.secret-line');\n const display = el(button.dataset.secretDisplay);\n if (!row || !display || row.querySelector('.secret-editor')) return;\n const editor = document.createElement('span'); editor.className = 'secret-editor';\n const input = document.createElement('input');\n input.type = 'password'; input.autocomplete = 'new-password'; input.placeholder = 'Enter replacement secret';\n const save = document.createElement('button'); save.textContent = 'Save';\n const cancel = document.createElement('button'); cancel.textContent = 'Cancel';\n const close = () => { editor.remove(); display.hidden = false; button.hidden = false; };\n save.onclick = async () => {\n save.disabled = true;\n try { await saveSecretReplacement(button.dataset.secretKey, input.value); }\n catch (error) { save.disabled = false; message(error.message, true); }\n };\n cancel.onclick = close;\n input.onkeydown = (event) => { if (event.key === 'Enter') save.click(); if (event.key === 'Escape') close(); };\n display.hidden = true; button.hidden = true;\n editor.append(input, save, cancel); row.append(editor); input.focus();\n }\n\n async function saveOptions() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = {\n ...currentStatus.values,\n features: {\n presetAgentsEnabled: el('preset-agents-enabled').checked\n }\n };\n const saved = await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values })\n }));\n message('Saved deployment options. Restart AgentConnect to apply them.');\n await load();\n }\n\n el('login').onclick = () => signIn().catch((error) => message(error.message, true));\n el('show-bootstrap').onclick = () => showBootstrap().catch((error) => message(error.message, true));\n el('bootstrap-provider').onchange = updateBootstrapProvider;\n el('bootstrap-github-owner').onchange = () => updateOwner('bootstrap-github');\n el('github-owner').onchange = () => updateOwner('github');\n el('bootstrap-logto-submit').onclick = () => bootstrapLogto().catch((error) => message(error.message, true));\n el('bootstrap-back').onclick = () => showBootstrapStep('logto');\n el('bootstrap-google-submit').onclick = () => bootstrapGoogle().catch((error) => message(error.message, true));\n el('bootstrap-github-submit').onclick = () => bootstrapGithub().catch((error) => message(error.message, true));\n el('bootstrap-slack-submit').onclick = () => bootstrapSlack().catch((error) => message(error.message, true));\n el('create-github').onclick = () => startGithub('github').catch((error) => message(error.message, true));\n el('save-logto-configuration').onclick = () => saveLogtoConfiguration().catch((error) => message(error.message, true));\n el('cancel-logto-configuration').onclick = cancelConfigurationEdit;\n el('save-github-configuration').onclick = () => saveGithubConfiguration().catch((error) => message(error.message, true));\n el('cancel-github-configuration').onclick = cancelConfigurationEdit;\n el('clear-github').onclick = () => clearProvider('github').catch((error) => message(error.message, true));\n el('connect-github-login').onclick = () => connectGithubLogin().catch((error) => message(error.message, true));\n el('check-github').onclick = () => checkGithub().catch((error) => message(error.message, true));\n el('create-slack').onclick = () => createSlack('slack').catch((error) => message(error.message, true));\n el('connect-slack-login').onclick = () => connectSlackLogin().catch((error) => message(error.message, true));\n el('save-slack-configuration').onclick = () => saveSlackConfiguration().catch((error) => message(error.message, true));\n el('cancel-slack-configuration').onclick = cancelConfigurationEdit;\n el('clear-slack').onclick = () => clearProvider('slack').catch((error) => message(error.message, true));\n el('check-slack').onclick = () => checkSlack().catch((error) => message(error.message, true));\n el('save-google').onclick = () => saveGoogle().catch((error) => message(error.message, true));\n el('cancel-google-configuration').onclick = cancelConfigurationEdit;\n el('clear-google').onclick = () => clearProvider('google').catch((error) => message(error.message, true));\n el('check-google').onclick = () => checkGoogle().catch((error) => message(error.message, true));\n el('create-feishu-login-app').onclick = () => createRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('save-feishu-login-app').onclick = () => saveRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('cancel-feishu-configuration').onclick = cancelConfigurationEdit;\n el('clear-feishu').onclick = () => clearProvider('feishu').catch((error) => message(error.message, true));\n el('check-feishu-login-app').onclick = () => checkRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('create-lark-login-app').onclick = () => createRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('save-lark-login-app').onclick = () => saveRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('cancel-lark-configuration').onclick = cancelConfigurationEdit;\n el('clear-lark').onclick = () => clearProvider('lark').catch((error) => message(error.message, true));\n el('check-lark-login-app').onclick = () => checkRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('check-logto').onclick = () => checkLogto().catch((error) => message(error.message, true));\n el('reconcile-logto').onclick = () => reconcileLogto().then(load).catch((error) => message(error.message, true));\n el('logout').onclick = () => { sessionStorage.removeItem(tokenKey); load().catch((error) => message(error.message, true)); };\n el('save-options').onclick = () => saveOptions().catch((error) => message(error.message, true));\n for (const button of document.querySelectorAll('.edit-secret')) button.onclick = () => editSecret(button);\n for (const button of document.querySelectorAll('.edit-configuration')) button.onclick = () => beginConfigurationEdit(button.dataset.provider);\n updateBootstrapProvider();\n finishSignIn().then(load).catch((error) => message(error.message, true));\n <\/script>\n</body>\n</html>"], ["<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>AgentConnect deployment settings</title>\n <style>\n :root { color-scheme: light dark; font: 15px/1.5 system-ui, sans-serif; }\n body { max-width: 1280px; margin: 48px auto; padding: 0 20px 60px; }\n h1 { margin-bottom: 4px; } h2 { margin-top: 32px; } h3 { margin: 0 0 8px; }\n .muted { color: #777; } .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }\n button, .button { padding: 8px 13px; cursor: pointer; }\n .button { border: 1px solid #8888; border-radius: 4px; color: inherit; text-decoration: none; display: inline-block; }\n input, select { width: min(520px, 100%); padding: 7px; box-sizing: border-box; }\n input[type=\"checkbox\"] { width: auto; padding: 0; }\n .field { display: grid; gap: 4px; margin: 10px 0; }\n .provider-stack { display: grid; gap: 16px; }\n .panel { border: 1px solid #8886; border-radius: 8px; padding: 16px; }\n .section-head, .provider-head { display: flex; justify-content: space-between; gap: 16px; align-items: start; }\n .section-head h2, .provider-head h3 { margin: 0; }\n .provider-head p { margin: 4px 0 0; }\n .badge { flex: none; border: 1px solid #8886; border-radius: 999px; padding: 3px 9px; font-size: 13px; }\n .badge.pass { color: #198754; border-color: #19875466; background: #19875412; }\n .badge.warn { color: #a66b00; border-color: #d99b1366; background: #d99b1315; }\n .badge.fail { color: #c33; border-color: #c333; background: #c3331111; }\n .credentials { display: grid; grid-template-columns: minmax(160px, 220px) minmax(0, 1fr); gap: 7px 16px; margin: 16px 0; }\n .credentials dt { color: #777; }\n .credentials dd { margin: 0; overflow-wrap: anywhere; }\n .credentials code { user-select: all; }\n .redacted { font-family: ui-monospace, monospace; letter-spacing: .08em; }\n .secret-line, .value-line { display: flex; gap: 10px; align-items: center; min-height: 30px; }\n .edit-secret { padding: 2px 8px; font-size: 13px; }\n .secret-editor { display: flex; gap: 8px; flex-wrap: wrap; width: 100%; }\n .secret-editor input { width: min(420px, 100%); }\n .edit-configuration { padding: 2px 8px; font-size: 13px; }\n .startup-owned { color: #777; font-size: 13px; }\n .danger { color: #c33; border-color: #c336; }\n .subsection { margin-top: 16px; padding-top: 14px; border-top: 1px solid #8883; }\n textarea { width: min(720px, 100%); min-height: 100px; padding: 7px; box-sizing: border-box; }\n .uris { margin: 8px 0; padding-left: 20px; } .uris code { user-select: all; }\n .notice { border-left: 4px solid #d99b13; padding: 8px 12px; background: #d99b1315; }\n .diff-title { display: block; margin-bottom: 8px; }\n .config-diff { display: grid; gap: 1px; border: 1px solid #8884; border-radius: 6px; overflow: hidden; background: #8884; }\n .diff-row { display: grid; grid-template-columns: minmax(140px, .8fr) minmax(0, 1.2fr) minmax(0, 1.2fr); background: Canvas; }\n .diff-row > * { min-width: 0; padding: 7px 9px; overflow-wrap: anywhere; white-space: pre-wrap; }\n .diff-head { font-size: 12px; font-weight: 600; color: #777; }\n .diff-field { font-weight: 600; }\n .diff-value { font-family: ui-monospace, monospace; font-size: 13px; }\n pre { padding: 12px; border-radius: 6px; background: #8881; overflow-x: auto; user-select: all; }\n #message { white-space: pre-wrap; padding: 10px 0; min-height: 1.5em; }\n .error { color: #c33; } .ok { color: #198754; } .warn { color: #a66b00; }\n code { overflow-wrap: anywhere; }\n .setup-layout { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 32px; align-items: start; }\n .setup-nav { position: sticky; top: 24px; display: grid; gap: 3px; padding: 10px; border: 1px solid #8886; border-radius: 8px; background: Canvas; }\n .setup-nav a { padding: 7px 9px; border-radius: 5px; color: inherit; text-decoration: none; white-space: nowrap; }\n .setup-nav a:hover { background: #8882; }\n .setup-section { scroll-margin-top: 24px; }\n details.environment { margin: 0 0 24px; } details.environment summary { cursor: pointer; font-weight: 600; }\n @media (max-width: 760px) {\n body { margin-top: 24px; }\n .setup-layout { grid-template-columns: 1fr; gap: 18px; }\n .setup-nav { position: static; display: flex; overflow-x: auto; }\n .credentials { grid-template-columns: 1fr; gap: 2px; }\n .credentials dd { margin-bottom: 8px; }\n .diff-row { grid-template-columns: 1fr; }\n .diff-head { display: none; }\n .diff-value::before { display: block; margin-bottom: 2px; color: #777; font: 11px/1.4 system-ui, sans-serif; }\n .diff-current::before { content: 'Current'; }\n .diff-expected::before { content: 'Expected'; }\n }\n [hidden] { display: none !important; }\n </style>\n</head>\n<body>\n <section id=\"access\">\n <h1>AgentConnect Setup</h1>\n <p id=\"access-message\" class=\"muted\" aria-live=\"polite\">Checking Logto sign-in…</p>\n <div class=\"row\">\n <button id=\"login\" hidden>Sign in with Logto</button>\n <a id=\"open-logto\" class=\"button\" href=\"http://localhost:3002\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n <button id=\"show-bootstrap\" hidden>Continue setup</button>\n </div>\n\n <section id=\"bootstrap\" hidden>\n <h2>Set up sign-in</h2>\n <p id=\"bootstrap-progress\" class=\"muted\">Step 1 of 2</p>\n <div id=\"bootstrap-logto-step\" class=\"panel\">\n <h3>Connect Logto</h3>\n <p class=\"muted\">Enter the one-time Logto Management API credential. It is sealed in the deployment database and verified before continuing.</p>\n <label class=\"field\">Logto M2M App ID<input id=\"logto-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Logto M2M App Secret<input id=\"logto-app-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-management-api-resource\" type=\"url\" autocomplete=\"off\"></label>\n <p class=\"muted\">Logto Cloud uses <code>https://&lt;tenant-id&gt;.logto.app/api</code>. Keep <code>https://default.logto.app/api</code> for Logto OSS.</p>\n <button id=\"bootstrap-logto-submit\">Save Logto and continue</button>\n </div>\n <div id=\"bootstrap-provider-step\" hidden>\n <h3>Choose a sign-in provider</h3>\n <p class=\"muted\">Logto Management API access is ready. Configure one provider to enable sign-in.</p>\n <label class=\"field\">Sign-in provider\n <select id=\"bootstrap-provider\"><option value=\"google\">Google (works on localhost)</option><option value=\"github\">GitHub integration App</option><option id=\"bootstrap-slack-option\" value=\"slack\">Slack integration App</option></select>\n </label>\n <div id=\"bootstrap-google\" class=\"panel\">\n <h3>Google OAuth client</h3>\n <p class=\"muted\">Create a Web application client in Google Auth Platform, then paste its credentials here.</p>\n <p>Authorized JavaScript origin:</p><ul id=\"bootstrap-google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"bootstrap-google-redirects\" class=\"uris\"></ul>\n <a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a>\n <label class=\"field\">Client ID<input id=\"bootstrap-google-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client Secret<input id=\"bootstrap-google-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-google-submit\">Save Google OAuth and configure Logto</button>\n </div>\n <div id=\"bootstrap-github\" class=\"panel\" hidden>\n <h3>GitHub integration App</h3>\n <p id=\"bootstrap-github-note\" class=\"muted\">This creates one complete App for both GitHub sign-in and repository integration.</p>\n <label class=\"field\">Owner\n <select id=\"bootstrap-github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"bootstrap-github-org-field\" class=\"field\" hidden>Organization login<input id=\"bootstrap-github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"bootstrap-github-name\" value=\"AgentConnect\"></label>\n <button id=\"bootstrap-github-submit\">Create GitHub App and configure Logto</button>\n </div>\n <div id=\"bootstrap-slack\" class=\"panel\" hidden>\n <h3>Slack integration App</h3>\n <p id=\"bootstrap-slack-note\" class=\"muted\">Creates one complete Slack App for workspace installation and a separate Sign in with Slack OIDC flow.</p>\n <p>Logto redirect URI:</p><ul id=\"bootstrap-slack-redirects\" class=\"uris\"></ul>\n <label class=\"field\">App name<input id=\"bootstrap-slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"bootstrap-slack-token\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-slack-submit\">Create Slack App and configure Logto</button>\n </div>\n <button id=\"bootstrap-back\">Back to Logto credentials</button>\n </div>\n </section>\n </section>\n\n <main id=\"setup\" hidden>\n <div class=\"setup-layout\">\n <nav class=\"setup-nav\" aria-label=\"Deployment settings\">\n <a href=\"#startup-section\">Startup</a>\n <a href=\"#logto-section\">Logto</a>\n <a href=\"#github-section\">GitHub</a>\n <a href=\"#slack-section\">Slack</a>\n <a href=\"#google-section\">Google</a>\n <a href=\"#feishu-section\">Feishu</a>\n <a href=\"#lark-section\">Lark</a>\n <a href=\"#options-section\">Options</a>\n </nav>\n <div class=\"setup-content\">\n <h1>AgentConnect deployment settings</h1>\n <p class=\"muted\">Saved settings take effect after the stack is restarted.</p>\n <div class=\"row\">\n <button id=\"logout\">Log out</button>\n </div>\n <div id=\"message\" aria-live=\"polite\"></div>\n\n <section id=\"editor\" hidden>\n <details id=\"startup-section\" class=\"environment setup-section\" open>\n <summary>Startup environment</summary>\n <p class=\"notice\">Public service URLs come from <code>.env</code>. Provider callbacks below are derived from these values.</p>\n <pre id=\"startup-environment\"></pre>\n </details>\n\n <section id=\"logto-section\" class=\"setup-section\" aria-labelledby=\"logto-heading\">\n <div class=\"section-head\">\n <div><h2 id=\"logto-heading\">Logto</h2><p class=\"muted\">Authentication and administrator access.</p></div>\n <span id=\"logto-match\" class=\"badge\">Not checked</span>\n </div>\n <div class=\"panel\">\n <dl class=\"credentials\">\n <dt>Management endpoint</dt><dd class=\"value-line\"><code id=\"logto-management-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>Management App ID</dt><dd class=\"value-line\"><code id=\"logto-management-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management API resource</dt><dd class=\"value-line\"><code id=\"logto-management-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management App secret</dt><dd class=\"secret-line\"><span id=\"logto-management-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.managementAppSecret\" data-secret-display=\"logto-management-secret-display\">Edit</button></dd>\n <dt>Sign-in endpoint</dt><dd class=\"value-line\"><code id=\"logto-browser-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>SPA App ID</dt><dd class=\"value-line\"><code id=\"logto-browser-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Browser API resource</dt><dd class=\"value-line\"><code id=\"logto-browser-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n </dl>\n <div id=\"logto-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Logto configuration</h3>\n <label class=\"field\">Management App ID<input id=\"logto-edit-management-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-edit-management-resource\" autocomplete=\"off\"></label>\n <label class=\"field\">New Management App secret<input id=\"logto-edit-management-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when management identity changes\"></label>\n <label class=\"field\">SPA App ID<input id=\"logto-edit-browser-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Browser API resource<input id=\"logto-edit-browser-resource\" autocomplete=\"off\" placeholder=\"Optional\"></label>\n <div class=\"row\"><button id=\"save-logto-configuration\">Save configuration</button><button id=\"cancel-logto-configuration\">Cancel</button></div>\n </div>\n <p id=\"logto-status\" class=\"muted\">Checking redirects and sign-in settings…</p>\n <div id=\"logto-drift\" class=\"notice\" hidden></div>\n <div class=\"row\">\n <button id=\"check-logto\">Check match</button>\n <button id=\"reconcile-logto\">Apply expected settings</button>\n <a id=\"logto-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n </div>\n </div>\n </section>\n\n <h2>Providers</h2>\n <div class=\"provider-stack\">\n <section id=\"github-section\" class=\"panel setup-section\" aria-labelledby=\"github-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"github-heading\">GitHub</h3><p class=\"muted\">Repository integration and optional Logto sign-in.</p></div>\n <span id=\"github-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"github-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>App slug</dt><dd class=\"value-line\"><code id=\"github-app-slug\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"github-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Webhook delivery</dt><dd class=\"value-line\"><code id=\"github-webhook-enabled\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"github-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.clientSecret\" data-secret-display=\"github-client-secret-display\">Edit</button></dd>\n <dt>Private key</dt><dd class=\"secret-line\"><span id=\"github-private-key-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.privateKeyB64\" data-secret-display=\"github-private-key-display\">Edit</button></dd>\n <dt>Webhook secret</dt><dd class=\"secret-line\"><span id=\"github-webhook-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.webhookSecret\" data-secret-display=\"github-webhook-secret-display\">Edit</button></dd>\n <dt>Logto connector secret</dt><dd class=\"secret-line\"><span id=\"github-logto-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.githubConnectorClientSecret\" data-secret-display=\"github-logto-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"github-status\" class=\"muted\"></p>\n <div id=\"github-drift\" class=\"notice\" hidden></div>\n <div id=\"github-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit GitHub App identity</h3>\n <label class=\"field\">App ID<input id=\"github-edit-app-id\" inputmode=\"numeric\" autocomplete=\"off\"></label>\n <label class=\"field\">App slug<input id=\"github-edit-slug\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"github-edit-client-id\" autocomplete=\"off\"></label>\n <label><input id=\"github-edit-webhook-enabled\" type=\"checkbox\"> Enable Relay webhook delivery</label>\n <label class=\"field\">New client secret<input id=\"github-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when App or Client ID changes\"></label>\n <label class=\"field\">New private key (base64)<textarea id=\"github-edit-private-key\" autocomplete=\"off\" placeholder=\"Required when App ID changes\"></textarea></label>\n <label class=\"field\">New webhook secret<input id=\"github-edit-webhook-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when an active webhook App ID changes\"></label>\n <label id=\"github-edit-logto-secret-field\" class=\"field\" hidden>New Logto connector client secret<input id=\"github-edit-logto-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when this App is also used for sign-in\"></label>\n <div class=\"row\"><button id=\"save-github-configuration\">Save configuration</button><button id=\"cancel-github-configuration\">Cancel</button></div>\n </div>\n <div id=\"github-create-controls\" class=\"subsection\">\n <label class=\"field\">Owner\n <select id=\"github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"github-org-field\" class=\"field\" hidden>Organization login<input id=\"github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"github-name\" value=\"AgentConnect\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-github\">Create GitHub App</button>\n <button id=\"connect-github-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-github\" hidden>Check match</button>\n <button id=\"clear-github\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"github-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open GitHub settings</a>\n </div>\n </section>\n\n <section id=\"slack-section\" class=\"panel setup-section\" aria-labelledby=\"slack-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"slack-heading\">Slack</h3><p class=\"muted\">One App for workspace integration and Logto sign-in.</p></div>\n <span id=\"slack-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"slack-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"slack-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"slack-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.clientSecret\" data-secret-display=\"slack-client-secret-display\">Edit</button></dd>\n <dt>Signing secret</dt><dd class=\"secret-line\"><span id=\"slack-signing-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.signingSecret\" data-secret-display=\"slack-signing-secret-display\">Edit</button></dd>\n <dt>Logto sign-in</dt><dd id=\"slack-logto-status\">Not configured</dd>\n </dl>\n <p id=\"slack-status\" class=\"muted\"></p>\n <div id=\"slack-drift\" class=\"notice\" hidden></div>\n <div id=\"slack-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Slack App identity</h3>\n <label class=\"field\">App ID<input id=\"slack-edit-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"slack-edit-client-id\" autocomplete=\"off\"></label>\n <label class=\"field\">New client secret<input id=\"slack-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <label class=\"field\">New signing secret<input id=\"slack-edit-signing-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <div class=\"row\"><button id=\"save-slack-configuration\">Save configuration</button><button id=\"cancel-slack-configuration\">Cancel</button></div>\n </div>\n <div class=\"subsection\">\n <label id=\"slack-name-field\" class=\"field\">App name<input id=\"slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"slack-token\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Used only for create or check\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-slack\">Create Slack App</button>\n <button id=\"connect-slack-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-slack\" hidden>Check match</button>\n <button id=\"clear-slack\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"slack-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Slack settings</a>\n </div>\n </section>\n\n <section id=\"google-section\" class=\"panel setup-section\" aria-labelledby=\"google-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"google-heading\">Google</h3><p class=\"muted\">OAuth client used by the Logto Google connector.</p></div>\n <span id=\"google-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"google-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"google\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"google-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.googleConnectorClientSecret\" data-secret-display=\"google-client-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"google-status\" class=\"muted\"></p>\n <div id=\"google-drift\" class=\"notice\" hidden></div>\n <p>Authorized JavaScript origin:</p><ul id=\"google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"google-redirects\" class=\"uris\"></ul>\n <p class=\"muted\">Google does not expose OAuth client redirect settings through an API. Copy these required values into Google Auth Platform; Setup can verify only the Logto connector.</p>\n <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a></div>\n <div id=\"google-config-controls\" class=\"subsection\">\n <label class=\"field\">Client ID<input id=\"google-id\" autocomplete=\"off\"></label>\n <label id=\"google-initial-secret-field\" class=\"field\">Client secret<input id=\"google-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when Client ID changes\"></label>\n </div>\n <div class=\"row\"><button id=\"save-google\">Save Google client</button><button id=\"cancel-google-configuration\" hidden>Cancel</button><button id=\"check-google\" hidden>Check match</button><button id=\"clear-google\" class=\"danger\" hidden>Clear configuration</button></div>\n </section>\n\n <section id=\"feishu-section\" class=\"panel setup-section\" aria-labelledby=\"feishu-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"feishu-heading\">Feishu</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Feishu, OAuth callbacks, and published App setup.</p></div>\n <span id=\"feishu-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"feishu-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"feishu\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"feishu-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"feishu.loginAppSecret\" data-secret-display=\"feishu-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"feishu-login-status\" class=\"muted\"></p>\n <div id=\"feishu-drift\" class=\"notice\" hidden></div>\n <div id=\"feishu-config-controls\" class=\"subsection\">\n <label id=\"feishu-create-name-field\" class=\"field\">New App name<input id=\"feishu-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"feishu-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"feishu-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-feishu-login-app\">Create Feishu App</button>\n <button id=\"save-feishu-login-app\">Save existing App</button>\n <button id=\"cancel-feishu-configuration\" hidden>Cancel</button>\n <button id=\"check-feishu-login-app\" hidden>Check setup</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"feishu-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Feishu settings</a>\n </div>\n </section>\n\n <section id=\"lark-section\" class=\"panel setup-section\" aria-labelledby=\"lark-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"lark-heading\">Lark</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Lark, OAuth callbacks, and published App setup.</p></div>\n <span id=\"lark-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"lark-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"lark\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"lark-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"lark.loginAppSecret\" data-secret-display=\"lark-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"lark-login-status\" class=\"muted\"></p>\n <div id=\"lark-drift\" class=\"notice\" hidden></div>\n <div id=\"lark-config-controls\" class=\"subsection\">\n <label id=\"lark-create-name-field\" class=\"field\">New App name<input id=\"lark-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"lark-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"lark-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-lark-login-app\">Create Lark App</button>\n <button id=\"save-lark-login-app\">Save existing App</button>\n <button id=\"cancel-lark-configuration\" hidden>Cancel</button>\n <button id=\"check-lark-login-app\" hidden>Check setup</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"lark-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Lark settings</a>\n </div>\n </section>\n </div>\n\n <section id=\"options-section\" class=\"setup-section\">\n <h2>Deployment options</h2>\n <div class=\"panel\">\n <label class=\"field\"><span><input id=\"preset-agents-enabled\" type=\"checkbox\"> Enable preset Agents</span></label>\n <div class=\"row\"><button id=\"save-options\">Save options</button></div>\n </div>\n </section>\n </section>\n </div>\n </div>\n </main>\n\n <script>\n const api = '/api/v1';\n const tokenKey = 'agentconnect.setup.token';\n const verifierKey = 'agentconnect.setup.pkce';\n const stateKey = 'agentconnect.setup.state';\n let currentRevision = 0;\n let currentStatus = null;\n let bootstrapInfo = null;\n const el = (id) => document.getElementById(id);\n const message = (text, error = false) => {\n const target = el('setup').hidden ? el('access-message') : el('message');\n target.textContent = text;\n target.className = error ? 'error' : 'ok';\n };\n const bearer = () => {\n const token = sessionStorage.getItem(tokenKey);\n return token ? { authorization: 'Bearer ' + token } : {};\n };\n const json = async (response) => {\n const body = await response.json().catch(() => ({}));\n if (!response.ok) throw Object.assign(new Error(body.message || ('HTTP ' + response.status)), { status: response.status, code: body.code });\n return body;\n };\n const base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n const random = () => base64url(crypto.getRandomValues(new Uint8Array(32)));\n const configured = (byKey, key) => Boolean(byKey.get(key) && byKey.get(key).configured);\n\n function showIdentityEditors(provider, show) {\n for (const button of document.querySelectorAll('.edit-configuration[data-provider=\"' + provider + '\"]')) {\n button.hidden = !show;\n }\n }\n\n function text(id, value) {\n el(id).textContent = value === null || value === undefined || value === '' ? 'Not configured' : String(value);\n }\n\n function secretText(id, byKey, key, editable) {\n const stored = configured(byKey, key);\n const display = el(id);\n const button = document.querySelector('[data-secret-display=\"' + id + '\"]');\n const row = display.closest('.secret-line');\n const editor = row && row.querySelector('.secret-editor');\n if (editor) editor.remove();\n display.hidden = false;\n display.textContent = stored ? '***' : 'Not configured';\n display.className = stored ? 'redacted' : 'muted';\n button.hidden = !editable;\n button.textContent = stored ? 'Edit' : 'Set';\n }\n\n function match(id, state, label) {\n const target = el(id);\n target.textContent = label;\n target.className = 'badge' + (state ? ' ' + state : '');\n }\n\n async function authConfig() { return json(await fetch(api + '/auth-config')); }\n\n async function signIn() {\n const config = await authConfig();\n if (config.mode !== 'oidc') throw new Error('Save an OIDC configuration first');\n const verifier = random();\n const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));\n const state = random();\n sessionStorage.setItem(verifierKey, verifier);\n sessionStorage.setItem(stateKey, state);\n const url = new URL(config.authorizationEndpoint);\n url.searchParams.set('client_id', config.appId);\n url.searchParams.set('redirect_uri', config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', 'openid profile email roles');\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n if (config.resource) url.searchParams.set('resource', config.resource);\n location.assign(url);\n }\n\n async function finishSignIn() {\n const url = new URL(location.href);\n const code = url.searchParams.get('code');\n if (!code) return;\n const state = url.searchParams.get('state');\n if (!state || state !== sessionStorage.getItem(stateKey)) throw new Error('OIDC state mismatch');\n const verifier = sessionStorage.getItem(verifierKey);\n if (!verifier) throw new Error('PKCE verifier is missing; start sign-in again');\n const config = await authConfig();\n const body = new URLSearchParams({\n grant_type: 'authorization_code', code, client_id: config.appId,\n redirect_uri: config.redirectUri, code_verifier: verifier\n });\n if (config.resource) body.set('resource', config.resource);\n const tokens = await json(await fetch(config.tokenEndpoint, {\n method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body\n }));\n if (typeof tokens.id_token !== 'string') throw new Error('Logto did not return an ID token');\n sessionStorage.setItem(tokenKey, tokens.id_token);\n sessionStorage.removeItem(verifierKey);\n sessionStorage.removeItem(stateKey);\n history.replaceState({}, '', '/');\n }\n\n function renderUriList(id, values) {\n el(id).replaceChildren(...(values || []).map((value) => {\n const item = document.createElement('li');\n const code = document.createElement('code'); code.textContent = value;\n item.append(code); return item;\n }));\n }\n\n function updateBootstrapProvider() {\n const provider = el('bootstrap-provider').value;\n el('bootstrap-google').hidden = provider !== 'google';\n el('bootstrap-github').hidden = provider !== 'github';\n el('bootstrap-slack').hidden = provider !== 'slack';\n }\n\n function showBootstrapStep(step) {\n const logto = step === 'logto';\n el('bootstrap-logto-step').hidden = !logto;\n el('bootstrap-provider-step').hidden = logto;\n el('bootstrap-progress').textContent = logto ? 'Step 1 of 2' : 'Step 2 of 2';\n }\n\n function updateOwner(prefix) {\n el(prefix + '-org-field').hidden = el(prefix + '-owner').value !== 'organization';\n }\n\n function ownership(prefix) {\n if (el(prefix + '-owner').value === 'personal') return { owner: 'personal', organization: null };\n const organization = el(prefix + '-org').value.trim();\n if (!organization) throw new Error('Enter the GitHub organization login');\n return { owner: 'organization', organization };\n }\n\n function formatDiffValue(value) {\n if (value === null || value === undefined || value === '') return 'Not configured';\n if (Array.isArray(value)) return value.length ? value.map(formatDiffValue).join('\\n') : '[]';\n if (typeof value === 'object') {\n return Object.entries(value).map(([key, item]) => key + ': ' + (Array.isArray(item) ? item.join(', ') : formatDiffValue(item))).join('\\n');\n }\n return String(value);\n }\n\n function showDiff(id, rows, label = 'Update required') {\n const box = el(id);\n box.hidden = rows.length === 0;\n box.replaceChildren();\n if (rows.length === 0) return;\n const title = document.createElement('strong');\n title.className = 'diff-title';\n title.textContent = label;\n const grid = document.createElement('div');\n grid.className = 'config-diff';\n const head = document.createElement('div');\n head.className = 'diff-row diff-head';\n for (const text of ['Field', 'Current', 'Expected']) {\n const cell = document.createElement('span'); cell.textContent = text; head.append(cell);\n }\n grid.append(head);\n for (const item of rows) {\n const row = document.createElement('div'); row.className = 'diff-row';\n const field = document.createElement('span'); field.className = 'diff-field'; field.textContent = item.field;\n const current = document.createElement('span'); current.className = 'diff-value diff-current'; current.textContent = formatDiffValue(item.current);\n const expected = document.createElement('span'); expected.className = 'diff-value diff-expected'; expected.textContent = formatDiffValue(item.expected);\n row.append(field, current, expected); grid.append(row);\n }\n box.append(title, grid);\n }\n\n function renderStartupEnvironment() {\n const services = bootstrapInfo.services;\n const environment = [\n ['AGENTCONNECT_PUBLIC_CP_URL', services.controlPlane],\n ['AGENTCONNECT_PUBLIC_RELAY_URL', services.relay],\n ['AGENTCONNECT_PUBLIC_WEB_URL', services.web],\n ['LOGTO_ENDPOINT', bootstrapInfo.logtoEndpoint],\n ['LOGTO_MGMT_ENDPOINT', bootstrapInfo.logtoManagementEndpoint]\n ];\n el('startup-environment').textContent = environment\n .filter(([, value]) => value)\n .map(([key, value]) => key + '=' + value)\n .join('\\n');\n }\n\n function renderApps(status) {\n currentStatus = status;\n const byKey = new Map((status.secrets || []).map((item) => [item.key, item]));\n const values = status.values;\n const expected = status.providerExpectations || { github: null, slack: null, google: { origins: [], redirects: [] } };\n renderStartupEnvironment();\n\n const logto = values.logto;\n text('logto-management-endpoint', logto && bootstrapInfo.logtoManagementEndpoint);\n text('logto-management-id', logto && logto.managementAppId);\n text('logto-management-resource', logto && logto.managementResource);\n secretText('logto-management-secret-display', byKey, 'logto.managementAppSecret', Boolean(logto));\n text('logto-browser-endpoint', logto && logto.browser && bootstrapInfo.logtoEndpoint);\n text('logto-browser-id', values.auth.mode === 'oidc' ? values.auth.browserClient.appId : null);\n text('logto-browser-resource', logto && logto.browser && logto.browser.apiResource);\n el('logto-edit-controls').hidden = true;\n showIdentityEditors('logto', Boolean(logto && logto.browser && values.auth.mode === 'oidc'));\n match(\n 'logto-match',\n logto && configured(byKey, 'logto.managementAppSecret') ? '' : 'warn',\n logto && configured(byKey, 'logto.managementAppSecret') ? 'Ready to check' : 'Not configured'\n );\n\n el('preset-agents-enabled').checked = values.features.presetAgentsEnabled;\n\n const github = values.github;\n const webhookStored = byKey.get('github.webhookSecret') && byKey.get('github.webhookSecret').configured;\n const webhookInactive = github && github.webhookEnabled === false;\n text('github-app-id', github && github.appId);\n text('github-app-slug', github && github.slug);\n text('github-client-id', github && github.clientId);\n text('github-webhook-enabled', github && (github.webhookEnabled === false ? 'Disabled' : 'Enabled'));\n el('github-edit-controls').hidden = true;\n showIdentityEditors('github', Boolean(github));\n secretText('github-client-secret-display', byKey, 'github.clientSecret', Boolean(github));\n secretText('github-private-key-display', byKey, 'github.privateKeyB64', Boolean(github));\n secretText('github-webhook-secret-display', byKey, 'github.webhookSecret', Boolean(github));\n secretText('github-logto-secret-display', byKey, 'logto.githubConnectorClientSecret', Boolean(values.logto && values.logto.githubConnector));\n el('github-status').textContent = github\n ? github.slug + ' is configured. Webhook secret: ' + (webhookStored ? 'stored' : webhookInactive ? 'not required yet' : 'missing') + '.' +\n (webhookInactive ? ' Relay webhook delivery is disabled.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github\n ? expected.github\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'Valid Web, API, and ingress URLs' }]\n : [];\n match('github-match', !github ? '' : githubDrift.length ? 'warn' : '', !github ? 'Not configured' : githubDrift.length ? 'Expected URLs changed' : 'Ready to check');\n el('github-create-controls').hidden = Boolean(github);\n el('create-github').hidden = Boolean(github);\n el('clear-github').hidden = !github;\n el('connect-github-login').hidden = !github || !values.logto || Boolean(values.logto.githubConnector);\n el('check-github').hidden = !github;\n if (github) showDiff('github-drift', githubDrift, 'Expected URLs changed since App creation');\n else el('github-drift').hidden = true;\n\n const slack = values.slack;\n text('slack-app-id', slack && slack.appId);\n text('slack-client-id', slack && slack.clientId);\n el('slack-edit-controls').hidden = true;\n showIdentityEditors('slack', Boolean(slack));\n secretText('slack-client-secret-display', byKey, 'slack.clientSecret', Boolean(slack));\n secretText('slack-signing-secret-display', byKey, 'slack.signingSecret', Boolean(slack));\n el('slack-logto-status').textContent = values.logto && values.logto.slackConnector\n ? 'Enabled; reuses the Slack client secret above.'\n : 'Not enabled.';\n el('slack-status').textContent = slack ? slack.appId + ' is configured.' : 'Creates the default AgentConnect integration manifest.';\n const slackDrift = slack\n ? expected.slack\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'HTTPS Web, API, and ingress URLs' }]\n : [];\n match('slack-match', !slack ? '' : slackDrift.length ? 'warn' : '', !slack ? 'Not configured' : slackDrift.length ? 'Update required' : 'Ready to check');\n el('slack-name-field').hidden = Boolean(slack);\n el('create-slack').hidden = Boolean(slack);\n el('connect-slack-login').hidden = !slack || !values.logto || Boolean(values.logto.slackConnector) || !bootstrapInfo?.slackAvailable;\n el('clear-slack').hidden = !slack;\n el('check-slack').hidden = !slack;\n el('slack-settings').hidden = !slack;\n if (slack) {\n el('slack-settings').href = 'https://api.slack.com/apps/' + encodeURIComponent(slack.appId);\n showDiff('slack-drift', slackDrift);\n } else el('slack-drift').hidden = true;\n\n const google = values.logto && values.logto.googleConnector;\n const googleSecret = configured(byKey, 'logto.googleConnectorClientSecret');\n const expectedGoogle = expected.google;\n renderUriList('google-origins', expectedGoogle.origins);\n renderUriList('google-redirects', expectedGoogle.redirects);\n text('google-client-id', google && google.clientId);\n secretText('google-client-secret-display', byKey, 'logto.googleConnectorClientSecret', Boolean(google));\n showIdentityEditors('google', Boolean(google));\n el('google-id').value = google ? google.clientId : '';\n el('google-status').textContent = google ? 'Google OAuth client is configured.' : 'Create a Web application OAuth client manually, then save it here.';\n showDiff('google-drift', []);\n match('google-match', google && googleSecret ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : \"Can't verify automatically\");\n el('google-initial-secret-field').hidden = googleSecret;\n el('save-google').textContent = google ? 'Confirm callback settings' : 'Save Google client';\n el('google-config-controls').hidden = Boolean(google);\n el('save-google').hidden = Boolean(google);\n el('cancel-google-configuration').hidden = true;\n el('check-google').hidden = !google || !googleSecret;\n el('clear-google').hidden = !google;\n\n const feishuSecret = byKey.get('feishu.loginAppSecret');\n const larkSecret = byKey.get('lark.loginAppSecret');\n el('feishu-login-id').value = values.feishu ? values.feishu.loginAppId : '';\n el('lark-login-id').value = values.lark ? values.lark.loginAppId : '';\n text('feishu-app-id', values.feishu && values.feishu.loginAppId);\n text('lark-app-id', values.lark && values.lark.loginAppId);\n showIdentityEditors('feishu', Boolean(values.feishu));\n showIdentityEditors('lark', Boolean(values.lark));\n secretText('feishu-app-secret-display', byKey, 'feishu.loginAppSecret', Boolean(values.feishu));\n secretText('lark-app-secret-display', byKey, 'lark.loginAppSecret', Boolean(values.lark));\n el('feishu-login-status').textContent = values.feishu && feishuSecret && feishuSecret.configured\n ? values.feishu.loginAppId + ' is configured.'\n : 'No Feishu tenant App is configured.';\n el('lark-login-status').textContent = values.lark && larkSecret && larkSecret.configured\n ? values.lark.loginAppId + ' is configured.'\n : 'No Lark tenant App is configured.';\n match('feishu-match', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? '' : '', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? 'Ready to check' : 'Not configured');\n match('lark-match', values.lark && configured(byKey, 'lark.loginAppSecret') ? '' : '', values.lark && configured(byKey, 'lark.loginAppSecret') ? 'Ready to check' : 'Not configured');\n showDiff('feishu-drift', []);\n showDiff('lark-drift', []);\n el('feishu-config-controls').hidden = Boolean(values.feishu);\n el('lark-config-controls').hidden = Boolean(values.lark);\n el('feishu-create-name-field').hidden = Boolean(values.feishu);\n el('lark-create-name-field').hidden = Boolean(values.lark);\n el('create-feishu-login-app').hidden = Boolean(values.feishu);\n el('create-lark-login-app').hidden = Boolean(values.lark);\n el('save-feishu-login-app').hidden = Boolean(values.feishu);\n el('save-lark-login-app').hidden = Boolean(values.lark);\n el('cancel-feishu-configuration').hidden = true;\n el('cancel-lark-configuration').hidden = true;\n el('clear-feishu').hidden = !values.feishu;\n el('clear-lark').hidden = !values.lark;\n el('check-feishu-login-app').hidden = !values.feishu || !configured(byKey, 'feishu.loginAppSecret');\n el('check-lark-login-app').hidden = !values.lark || !configured(byKey, 'lark.loginAppSecret');\n el('feishu-settings').hidden = !values.feishu;\n el('lark-settings').hidden = !values.lark;\n if (values.feishu) el('feishu-settings').href = 'https://open.feishu.cn/app/' + encodeURIComponent(values.feishu.loginAppId);\n if (values.lark) el('lark-settings').href = 'https://open.larksuite.com/app/' + encodeURIComponent(values.lark.loginAppId);\n }\n\n function requiredInput(id, label) {\n const value = el(id).value.trim();\n if (!value) throw new Error('Enter ' + label);\n return value;\n }\n\n function githubConnectorUsesDeployment(values) {\n const github = values.github;\n const connector = values.logto && values.logto.githubConnector;\n return Boolean(\n github && connector &&\n connector.appId === github.appId &&\n connector.slug === github.slug &&\n connector.clientId === github.clientId\n );\n }\n\n function beginConfigurationEdit(provider) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const values = currentStatus.values;\n if (provider === 'logto') {\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') return;\n el('logto-edit-management-id').value = logto.managementAppId;\n el('logto-edit-management-resource').value = logto.managementResource;\n el('logto-edit-management-secret').value = '';\n el('logto-edit-browser-id').value = values.auth.browserClient.appId;\n el('logto-edit-browser-resource').value = logto.browser.apiResource || '';\n el('logto-edit-controls').hidden = false;\n el('logto-edit-management-id').focus();\n } else if (provider === 'github') {\n const github = values.github;\n if (!github) return;\n el('github-edit-app-id').value = String(github.appId);\n el('github-edit-slug').value = github.slug;\n el('github-edit-client-id').value = github.clientId || '';\n el('github-edit-webhook-enabled').checked = github.webhookEnabled !== false;\n for (const id of ['github-edit-client-secret', 'github-edit-private-key', 'github-edit-webhook-secret', 'github-edit-logto-secret']) el(id).value = '';\n el('github-edit-logto-secret-field').hidden = !githubConnectorUsesDeployment(values);\n el('github-edit-controls').hidden = false;\n el('clear-github').hidden = true;\n el('github-edit-app-id').focus();\n } else if (provider === 'slack') {\n const slack = values.slack;\n if (!slack) return;\n el('slack-edit-app-id').value = slack.appId;\n el('slack-edit-client-id').value = slack.clientId;\n el('slack-edit-client-secret').value = '';\n el('slack-edit-signing-secret').value = '';\n el('slack-edit-controls').hidden = false;\n el('clear-slack').hidden = true;\n el('slack-edit-app-id').focus();\n } else if (provider === 'google') {\n const google = values.logto && values.logto.googleConnector;\n if (!google) return;\n el('google-id').value = google.clientId;\n el('google-secret').value = '';\n el('google-config-controls').hidden = false;\n el('google-initial-secret-field').hidden = false;\n el('save-google').hidden = false;\n el('save-google').textContent = 'Save Google client';\n el('cancel-google-configuration').hidden = false;\n el('clear-google').hidden = true;\n el('google-id').focus();\n } else if (provider === 'feishu' || provider === 'lark') {\n el(provider + '-config-controls').hidden = false;\n el(provider + '-create-name-field').hidden = true;\n el('create-' + provider + '-login-app').hidden = true;\n el('save-' + provider + '-login-app').hidden = false;\n el('cancel-' + provider + '-configuration').hidden = false;\n el('clear-' + provider).hidden = true;\n el(provider + '-login-secret').value = '';\n el(provider + '-login-id').focus();\n }\n showIdentityEditors(provider, false);\n }\n\n function cancelConfigurationEdit() {\n if (currentStatus) renderApps(currentStatus);\n }\n\n async function replaceConfiguration(values, secrets, successMessage) {\n const response = await json(await fetch(api + '/deployment-config', {\n method: 'PUT',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values, ...(secrets ? { secrets } : {}) })\n }));\n await load();\n message(successMessage || 'Configuration saved. Restart AgentConnect to apply it.');\n return response;\n }\n\n async function saveLogtoConfiguration() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = currentStatus.values;\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') throw new Error('Logto is not configured');\n const managementAppId = requiredInput('logto-edit-management-id', 'the Management App ID');\n const managementResource = requiredInput('logto-edit-management-resource', 'the Management API resource');\n const browserAppId = requiredInput('logto-edit-browser-id', 'the SPA App ID');\n const browserResource = el('logto-edit-browser-resource').value.trim() || null;\n const managementIdentityChanged = managementAppId !== logto.managementAppId;\n const managementSecret = el('logto-edit-management-secret').value;\n if (managementIdentityChanged && !managementSecret) throw new Error('Enter the new Management App secret');\n const browser = { ...logto.browser, apiResource: browserResource };\n const auth = {\n ...values.auth,\n audience: browserResource || browserAppId,\n browserClient: { appId: browserAppId, apiResource: browserResource }\n };\n await replaceConfiguration(\n {\n ...values,\n auth,\n logto: { ...logto, managementAppId, managementResource, browser }\n },\n managementSecret ? { 'logto.managementAppSecret': managementSecret } : undefined,\n 'Logto configuration saved. Sign in again if the SPA identity changed, then restart AgentConnect.'\n );\n }\n\n async function saveGithubConfiguration() {\n if (!currentStatus || !currentStatus.values.github) throw new Error('GitHub is not configured');\n const values = currentStatus.values;\n const previous = values.github;\n const appId = Number(requiredInput('github-edit-app-id', 'the GitHub App ID'));\n if (!Number.isSafeInteger(appId) || appId <= 0) throw new Error('GitHub App ID must be a positive integer');\n const slug = requiredInput('github-edit-slug', 'the GitHub App slug');\n const clientId = requiredInput('github-edit-client-id', 'the GitHub Client ID');\n const appChanged = appId !== previous.appId;\n const clientChanged = clientId !== previous.clientId;\n const webhookEnabled = el('github-edit-webhook-enabled').checked;\n const webhookEnabling = webhookEnabled && previous.webhookEnabled === false;\n const connectorReused = githubConnectorUsesDeployment(values);\n const clientSecret = el('github-edit-client-secret').value;\n const privateKey = el('github-edit-private-key').value.trim();\n const webhookSecret = el('github-edit-webhook-secret').value;\n const connectorSecret = el('github-edit-logto-secret').value;\n const webhookSecretStored = currentStatus.secrets.some((secret) => secret.key === 'github.webhookSecret' && secret.configured);\n if ((appChanged || clientChanged) && !clientSecret) throw new Error('Enter the new GitHub client secret');\n if (appChanged && !privateKey) throw new Error('Enter the new GitHub private key as base64');\n if (webhookEnabled && ((appChanged && !webhookSecret) || (webhookEnabling && !webhookSecret && !webhookSecretStored))) throw new Error('Enter the new GitHub webhook secret');\n if (connectorReused && (appChanged || clientChanged) && !connectorSecret) throw new Error('Enter the new Logto connector client secret');\n const secrets = {};\n if (clientSecret) secrets['github.clientSecret'] = clientSecret;\n if (privateKey) secrets['github.privateKeyB64'] = privateKey;\n if (webhookSecret) secrets['github.webhookSecret'] = webhookSecret;\n if (connectorSecret) secrets['logto.githubConnectorClientSecret'] = connectorSecret;\n const nextLogto = connectorReused && values.logto\n ? { ...values.logto, githubConnector: { ...values.logto.githubConnector, appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, webhookEnabled },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\n );\n if (connectorSecret) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function saveSlackConfiguration() {\n if (!currentStatus || !currentStatus.values.slack) throw new Error('Slack is not configured');\n const values = currentStatus.values;\n const previous = values.slack;\n const appId = requiredInput('slack-edit-app-id', 'the Slack App ID');\n const clientId = requiredInput('slack-edit-client-id', 'the Slack Client ID');\n const changed = appId !== previous.appId || clientId !== previous.clientId;\n const clientSecret = el('slack-edit-client-secret').value;\n const signingSecret = el('slack-edit-signing-secret').value;\n if (changed && (!clientSecret || !signingSecret)) throw new Error('Enter both the new Slack client secret and signing secret');\n const secrets = {};\n if (clientSecret) secrets['slack.clientSecret'] = clientSecret;\n if (signingSecret) secrets['slack.signingSecret'] = signingSecret;\n const nextLogto = values.logto && values.logto.slackConnector\n ? { ...values.logto, slackConnector: { ...values.logto.slackConnector, appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\n );\n if (clientSecret && nextLogto && nextLogto.slackConnector) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function clearProvider(provider) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const label = provider === 'github' ? 'GitHub' : provider === 'slack' ? 'Slack' : provider === 'google' ? 'Google' : provider === 'feishu' ? 'Feishu' : 'Lark';\n if (!window.confirm('Clear the saved ' + label + ' configuration and secrets?')) return;\n const values = currentStatus.values;\n let next = values;\n let secrets = {};\n if (provider === 'github') {\n const connectorReused = githubConnectorUsesDeployment(values);\n const logto = connectorReused && values.logto\n ? {\n ...values.logto,\n githubConnector: null\n }\n : values.logto;\n next = { ...values, github: null, ...(logto ? { logto } : {}) };\n secrets = {\n 'github.clientSecret': null,\n 'github.privateKeyB64': null,\n 'github.webhookSecret': null,\n ...(connectorReused ? { 'logto.githubConnectorClientSecret': null } : {})\n };\n } else if (provider === 'slack') {\n const logto = values.logto\n ? {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'slack') }\n : values.logto.browser,\n slackConnector: null\n }\n : values.logto;\n next = { ...values, slack: null, ...(logto ? { logto } : {}) };\n secrets = { 'slack.clientSecret': null, 'slack.signingSecret': null };\n } else if (provider === 'google') {\n if (!values.logto) return;\n next = {\n ...values,\n logto: {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'google') }\n : values.logto.browser,\n googleConnector: null\n }\n };\n secrets = { 'logto.googleConnectorClientSecret': null };\n } else if (provider === 'feishu' || provider === 'lark') {\n next = { ...values, [provider]: null };\n secrets = { [provider + '.loginAppSecret']: null };\n }\n await replaceConfiguration(next, secrets, label + ' configuration cleared. Its setup controls are available again.');\n }\n\n async function startGithub(prefix) {\n const result = await json(await fetch(api + '/create/github/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n purpose: 'deployment',\n name: el(prefix + '-name').value,\n ownership: ownership(prefix),\n connectLogto: prefix === 'bootstrap-github'\n })\n }));\n const form = document.createElement('form');\n form.method = 'post'; form.action = result.action;\n const manifest = document.createElement('input');\n manifest.type = 'hidden'; manifest.name = 'manifest'; manifest.value = JSON.stringify(result.manifest);\n form.append(manifest); document.body.append(form);\n message('Opening GitHub to review the complete integration App.');\n form.submit();\n }\n\n async function loadBootstrapInfo() {\n bootstrapInfo = await json(await fetch(api + '/bootstrap-info'));\n el('open-logto').href = bootstrapInfo.logtoAdminEndpoint;\n el('logto-settings').href = bootstrapInfo.logtoAdminEndpoint;\n if (!el('logto-app-id').value && bootstrapInfo.logtoManagementAppId) {\n el('logto-app-id').value = bootstrapInfo.logtoManagementAppId;\n }\n renderUriList('bootstrap-google-origins', bootstrapInfo.google.javascriptOrigins);\n renderUriList('bootstrap-google-redirects', bootstrapInfo.google.redirectUris);\n renderUriList('bootstrap-slack-redirects', [bootstrapInfo.slackLoginRedirectUrl]);\n el('logto-management-api-resource').value = bootstrapInfo.logtoManagementResource;\n el('bootstrap-github-submit').disabled = !bootstrapInfo.githubAvailable;\n if (!bootstrapInfo.githubAvailable) {\n el('bootstrap-github-note').textContent = 'GitHub App creation needs valid saved Web, API, and ingress URLs.';\n } else if (!bootstrapInfo.githubWebhookActive) {\n el('bootstrap-github-note').textContent = 'Creates the complete GitHub App now without submitting the localhost webhook URL. Add it after saving reachable HTTPS ingress.';\n } else {\n el('bootstrap-github-note').textContent = 'This creates one complete App for both GitHub sign-in and repository integration.';\n }\n el('bootstrap-slack-option').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-submit').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-note').textContent = bootstrapInfo.slackAvailable\n ? 'Creates one complete Slack App. Sign-in uses a separate openid profile email flow from workspace installation.'\n : 'Slack sign-in needs all public service URLs to use HTTPS. Use Google locally or expose the stack through trusted HTTPS endpoints.';\n if (!bootstrapInfo.slackAvailable && el('bootstrap-provider').value === 'slack') {\n el('bootstrap-provider').value = 'google';\n updateBootstrapProvider();\n }\n return bootstrapInfo;\n }\n\n async function logtoBootstrapFinding() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n for (const id of ['logto.client_credentials', 'logto.roles_read']) {\n const finding = report.findings.find((candidate) => candidate.id === id);\n if (!finding || finding.status !== 'pass') {\n return finding || { status: 'fail', message: 'Logto Management API permissions could not be verified.' };\n }\n }\n return { status: 'pass', message: 'Logto Management API access is ready.' };\n }\n\n async function showBootstrap() {\n el('bootstrap').hidden = false;\n el('show-bootstrap').hidden = true;\n const info = bootstrapInfo || await loadBootstrapInfo();\n if (!info.logtoConfigured) {\n showBootstrapStep('logto');\n return;\n }\n const finding = await logtoBootstrapFinding();\n if (finding && finding.status === 'pass') {\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n return;\n }\n showBootstrapStep('logto');\n message(finding?.message || 'Logto Management API credentials could not be verified.', true);\n }\n\n async function bootstrapLogto() {\n await json(await fetch(api + '/bootstrap/logto', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n managementAppId: el('logto-app-id').value,\n managementAppSecret: el('logto-app-secret').value,\n managementResource: el('logto-management-api-resource').value\n })\n }));\n el('logto-app-secret').value = '';\n await loadBootstrapInfo();\n const finding = await logtoBootstrapFinding();\n if (!finding || finding.status !== 'pass') {\n throw new Error(finding?.message || 'Logto Management API credentials could not be verified.');\n }\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n }\n\n async function bootstrapGoogle() {\n const secret = el('bootstrap-google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ clientId: el('bootstrap-google-id').value, clientSecret: secret })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto(true);\n await load();\n }\n\n async function bootstrapGithub() {\n if (!bootstrapInfo || !bootstrapInfo.githubAvailable) throw new Error('GitHub App creation needs valid saved Web, API, and ingress URLs.');\n await startGithub('bootstrap-github');\n }\n\n async function bootstrapSlack() {\n if (!bootstrapInfo || !bootstrapInfo.slackAvailable) throw new Error('Slack sign-in requires all public service URLs to use HTTPS.');\n await createSlack('bootstrap-slack', true);\n await reconcileLogto();\n await load();\n }\n\n async function createSlack(prefix = 'slack', connectLogto = false) {\n const result = await json(await fetch(api + '/create/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n name: el(prefix + '-name').value,\n configToken: el(prefix + '-token').value,\n connectLogto\n })\n }));\n el(prefix + '-token').value = '';\n if (!connectLogto) await load();\n message('Slack App ' + result.app.id + ' was created with the default integration manifest. Restart AgentConnect to apply it.');\n }\n\n async function checkGithub() {\n const result = await json(await fetch(api + '/check/github', { headers: bearer() }));\n el('github-settings').href = result.settingsUrl;\n el('github-settings').hidden = false;\n const label = result.missing.length ? 'Update required' : \"Can't verify automatically\";\n showDiff('github-drift', result.diff || [], label);\n match('github-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : label);\n message(result.status === 'pass' ? 'GitHub App matches the expected integration manifest.' : result.missing.length ? 'GitHub App settings need an update.' : \"GitHub can't expose callback, setup, or webhook-active settings through its API; check them in GitHub.\", result.status === 'fail');\n }\n\n async function connectGithubLogin() {\n await json(await fetch(api + '/configure/github-login', { method: 'POST', headers: bearer() }));\n await reconcileLogto();\n await load();\n message('The deployment GitHub App is now also used for Logto sign-in. Update its displayed callback URLs.');\n }\n\n async function connectSlackLogin() {\n if (!currentStatus || !currentStatus.values.slack || !currentStatus.values.logto) {\n throw new Error('Save the Slack App and Logto configuration first');\n }\n const values = currentStatus.values;\n const slack = values.slack;\n const logto = values.logto;\n await replaceConfiguration(\n {\n ...values,\n logto: {\n ...logto,\n browser: logto.browser\n ? { ...logto.browser, socialProviders: [...new Set([...logto.browser.socialProviders, 'slack'])] }\n : logto.browser,\n slackConnector: {\n appId: slack.appId,\n clientId: slack.clientId\n }\n }\n },\n undefined,\n 'Slack App is ready to connect to Logto.'\n );\n await reconcileLogto();\n await load();\n message('The deployment Slack App is now also used for Logto sign-in.');\n }\n\n async function checkSlack() {\n const token = requiredInput('slack-token', 'the temporary Slack App configuration token');\n const result = await json(await fetch(api + '/check/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ configToken: token })\n }));\n el('slack-token').value = '';\n showDiff('slack-drift', result.diff || []);\n match('slack-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : 'Update required');\n message(result.status === 'pass' ? 'Slack App matches the default integration manifest.' : 'Slack App settings need an update.', result.status !== 'pass');\n }\n\n async function checkGoogle() {\n const report = await checkLogto();\n const connector = report.findings.find((finding) => finding.id === 'logto.connectors');\n const google = currentStatus && currentStatus.values.logto && currentStatus.values.logto.googleConnector;\n const connectorDiff = connector && connector.diff\n ? connector.diff.filter((item) => item.field.toLowerCase().startsWith('google'))\n : [];\n const passed = Boolean(google && connector && connectorDiff.length === 0);\n showDiff('google-drift', connectorDiff);\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Logto matches' : 'Update required');\n message(\n passed\n ? \"The Logto Google connector matches. Google OAuth redirect settings can't be verified automatically.\"\n : 'The Logto Google connector needs an update.',\n !passed\n );\n }\n\n async function checkRegionalLoginApp(region) {\n const result = await json(await fetch(api + '/check/regional-login-app/' + region, { headers: bearer() }));\n const label = region === 'feishu' ? 'Feishu' : 'Lark';\n showDiff(region + '-drift', result.diff || []);\n match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Matches' : result.status === 'fail' ? 'Update required' : 'Could not check');\n el(region + '-login-status').textContent = result.message;\n el(region + '-login-status').className = result.status === 'pass' ? 'ok' : result.status === 'fail' ? 'warn' : 'muted';\n message(result.message || (label + ' credential check completed.'), result.status === 'fail');\n }\n\n async function saveGoogle() {\n const secret = el('google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n clientId: el('google-id').value,\n ...(secret ? { clientSecret: secret } : {})\n })\n }));\n el('google-secret').value = '';\n await reconcileLogto(Boolean(secret));\n await load();\n message('Google OAuth client and Logto connector are configured. Restart AgentConnect to apply the saved settings.');\n }\n\n function regionalLoginApp(prefix) {\n const appId = el(prefix + '-login-id').value.trim();\n const appSecret = el(prefix + '-login-secret').value;\n return appId ? { appId, ...(appSecret ? { appSecret } : {}) } : null;\n }\n\n async function saveRegionalLoginApp(region) {\n const app = regionalLoginApp(region);\n if (!app) throw new Error('Enter the ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App ID');\n await json(await fetch(api + '/configure/regional-login-app', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, app })\n }));\n el(region + '-login-secret').value = '';\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' tenant App is saved. Restart AgentConnect services to apply it.');\n }\n\n async function createRegionalLoginApp(region) {\n const popup = window.open('about:blank', '_blank');\n try {\n const started = await json(await fetch(api + '/create/regional-login-app/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, name: el(region + '-create-name').value })\n }));\n if (popup) popup.location.replace(started.authorizationUrl);\n else window.open(started.authorizationUrl, '_blank', 'noopener');\n message('Approve the new ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App in the opened page. This page will finish saving it automatically.');\n while (true) {\n const result = await json(await fetch(api + '/create/regional-login-app/' + encodeURIComponent(started.id), { headers: bearer() }));\n if (result.status === 'completed') {\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' App ' + result.appId + ' was created and saved. Restart AgentConnect services to apply it.');\n return;\n }\n if (result.status === 'failed') throw new Error('Regional App creation ' + result.reason + '. Start it again.');\n await new Promise((resolve) => setTimeout(resolve, Math.max(500, Math.min(result.retryAfterMs || 2000, 5000))));\n }\n } catch (error) {\n if (popup) popup.close();\n throw error;\n }\n }\n\n async function reconcileLogto(refreshConnectorSecrets) {\n return json(await fetch(api + '/reconcile/logto', {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ refreshConnectorSecrets: Boolean(refreshConnectorSecrets) })\n }));\n }\n\n async function refreshDeploymentConfig() {\n const status = await json(await fetch(api + '/deployment-config', { headers: bearer() }));\n el('access').hidden = true;\n el('setup').hidden = false;\n el('editor').hidden = false;\n currentRevision = status.revision;\n renderApps(status);\n return status;\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n await refreshDeploymentConfig();\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, connectors, and social-only sign-in match.'\n : failures.map((finding) => finding.message).join(' ');\n el('logto-status').className = failures.length === 0 ? 'ok' : 'warn';\n match('logto-match', failures.length === 0 ? 'pass' : 'warn', failures.length === 0 ? 'Matches' : 'Update required');\n showDiff(\n 'logto-drift',\n failures.flatMap((finding) => finding.diff && finding.diff.length\n ? finding.diff\n : [{ field: finding.id.replace(/^logto\\./, '').replaceAll('_', ' '), current: finding.message, expected: 'Matches expected configuration' }])\n );\n el('logto-settings').hidden = failures.length === 0;\n return report;\n }\n\n function githubNotice(value) {\n if (value === 'deployment-created') return 'GitHub integration App created. Its private key and webhook secret are stored.';\n if (value === 'deployment-login-created') return 'GitHub integration App created and connected to Logto sign-in.';\n if (value === 'cancelled') return 'GitHub App creation was cancelled.';\n if (value === 'expired') return 'GitHub App creation expired. Start it again.';\n if (value === 'invalid-callback') return 'GitHub returned an invalid App creation callback.';\n if (value === 'conversion-failed') return 'GitHub may have created the App, but did not return complete credentials. Delete the orphaned App and retry.';\n if (value === 'save-failed') return 'GitHub created the App, but its credentials could not be saved. Delete the orphaned App and retry.';\n return '';\n }\n\n async function load() {\n let publicAuth = await authConfig();\n const currentUrl = new URL(location.href);\n const githubResult = currentUrl.searchParams.get('github');\n const notice = githubNotice(githubResult);\n if (githubResult === 'deployment-login-created' && publicAuth.mode === 'none') {\n await reconcileLogto();\n publicAuth = await authConfig();\n }\n if (githubResult) history.replaceState({}, '', '/');\n const hasToken = Boolean(sessionStorage.getItem(tokenKey));\n el('access').hidden = false;\n el('setup').hidden = true;\n el('login').hidden = true;\n el('open-logto').hidden = true;\n el('show-bootstrap').hidden = true;\n el('editor').hidden = true;\n el('bootstrap').hidden = true;\n if (publicAuth.logtoAdminEndpoint) {\n el('open-logto').href = publicAuth.logtoAdminEndpoint;\n el('logto-settings').href = publicAuth.logtoAdminEndpoint;\n }\n if (publicAuth.mode === 'none') {\n sessionStorage.removeItem(tokenKey);\n await loadBootstrapInfo();\n el('open-logto').hidden = false;\n el('show-bootstrap').hidden = false;\n message(notice || 'Logto sign-in is not configured. Set up the initial Logto administrator first.');\n return;\n }\n if (publicAuth.mode === 'unavailable') {\n el('open-logto').hidden = false;\n message(publicAuth.message || 'Logto sign-in is unavailable. Open Logto Console to review its settings.', true);\n return;\n }\n if (!hasToken) {\n el('login').hidden = false;\n message(notice || 'Sign in with Logto to continue.');\n return;\n }\n if (publicAuth.claimAvailable) {\n try {\n await json(await fetch(api + '/bootstrap/claim', { method: 'POST', headers: bearer() }));\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('This first user is now an ADMIN. Sign in again to refresh the role claim.');\n return;\n } catch (error) {\n if (error.status !== 401 && error.status !== 403) throw error;\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with Logto to continue.', true);\n return;\n }\n }\n try {\n await loadBootstrapInfo();\n await refreshDeploymentConfig();\n message(notice);\n checkLogto().catch((error) => {\n el('logto-status').textContent = error.message;\n el('logto-status').className = 'warn';\n el('logto-settings').hidden = false;\n });\n } catch (error) {\n if (error.status === 401 || error.status === 403) {\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with a Logto ADMIN account.', true);\n }\n else throw error;\n }\n }\n\n async function saveSecretReplacement(key, value) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n if (!value) throw new Error('Enter the replacement secret');\n const refreshLogto =\n (key === 'logto.githubConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.githubConnector) ||\n (key === 'logto.googleConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.googleConnector) ||\n (key === 'slack.clientSecret' && currentStatus.values.logto && currentStatus.values.logto.slackConnector);\n await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values: currentStatus.values, secrets: { [key]: value } })\n }));\n try {\n if (refreshLogto) await reconcileLogto(true);\n } finally {\n await load();\n }\n if (refreshLogto) {\n message('Secret replaced and applied to the Logto connector. Restart AgentConnect to apply deployment changes.');\n } else {\n message('Secret replaced. Restart AgentConnect to apply it.');\n }\n }\n\n function editSecret(button) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const row = button.closest('.secret-line');\n const display = el(button.dataset.secretDisplay);\n if (!row || !display || row.querySelector('.secret-editor')) return;\n const editor = document.createElement('span'); editor.className = 'secret-editor';\n const input = document.createElement('input');\n input.type = 'password'; input.autocomplete = 'new-password'; input.placeholder = 'Enter replacement secret';\n const save = document.createElement('button'); save.textContent = 'Save';\n const cancel = document.createElement('button'); cancel.textContent = 'Cancel';\n const close = () => { editor.remove(); display.hidden = false; button.hidden = false; };\n save.onclick = async () => {\n save.disabled = true;\n try { await saveSecretReplacement(button.dataset.secretKey, input.value); }\n catch (error) { save.disabled = false; message(error.message, true); }\n };\n cancel.onclick = close;\n input.onkeydown = (event) => { if (event.key === 'Enter') save.click(); if (event.key === 'Escape') close(); };\n display.hidden = true; button.hidden = true;\n editor.append(input, save, cancel); row.append(editor); input.focus();\n }\n\n async function saveOptions() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = {\n ...currentStatus.values,\n features: {\n presetAgentsEnabled: el('preset-agents-enabled').checked\n }\n };\n const saved = await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values })\n }));\n message('Saved deployment options. Restart AgentConnect to apply them.');\n await load();\n }\n\n el('login').onclick = () => signIn().catch((error) => message(error.message, true));\n el('show-bootstrap').onclick = () => showBootstrap().catch((error) => message(error.message, true));\n el('bootstrap-provider').onchange = updateBootstrapProvider;\n el('bootstrap-github-owner').onchange = () => updateOwner('bootstrap-github');\n el('github-owner').onchange = () => updateOwner('github');\n el('bootstrap-logto-submit').onclick = () => bootstrapLogto().catch((error) => message(error.message, true));\n el('bootstrap-back').onclick = () => showBootstrapStep('logto');\n el('bootstrap-google-submit').onclick = () => bootstrapGoogle().catch((error) => message(error.message, true));\n el('bootstrap-github-submit').onclick = () => bootstrapGithub().catch((error) => message(error.message, true));\n el('bootstrap-slack-submit').onclick = () => bootstrapSlack().catch((error) => message(error.message, true));\n el('create-github').onclick = () => startGithub('github').catch((error) => message(error.message, true));\n el('save-logto-configuration').onclick = () => saveLogtoConfiguration().catch((error) => message(error.message, true));\n el('cancel-logto-configuration').onclick = cancelConfigurationEdit;\n el('save-github-configuration').onclick = () => saveGithubConfiguration().catch((error) => message(error.message, true));\n el('cancel-github-configuration').onclick = cancelConfigurationEdit;\n el('clear-github').onclick = () => clearProvider('github').catch((error) => message(error.message, true));\n el('connect-github-login').onclick = () => connectGithubLogin().catch((error) => message(error.message, true));\n el('check-github').onclick = () => checkGithub().catch((error) => message(error.message, true));\n el('create-slack').onclick = () => createSlack('slack').catch((error) => message(error.message, true));\n el('connect-slack-login').onclick = () => connectSlackLogin().catch((error) => message(error.message, true));\n el('save-slack-configuration').onclick = () => saveSlackConfiguration().catch((error) => message(error.message, true));\n el('cancel-slack-configuration').onclick = cancelConfigurationEdit;\n el('clear-slack').onclick = () => clearProvider('slack').catch((error) => message(error.message, true));\n el('check-slack').onclick = () => checkSlack().catch((error) => message(error.message, true));\n el('save-google').onclick = () => saveGoogle().catch((error) => message(error.message, true));\n el('cancel-google-configuration').onclick = cancelConfigurationEdit;\n el('clear-google').onclick = () => clearProvider('google').catch((error) => message(error.message, true));\n el('check-google').onclick = () => checkGoogle().catch((error) => message(error.message, true));\n el('create-feishu-login-app').onclick = () => createRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('save-feishu-login-app').onclick = () => saveRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('cancel-feishu-configuration').onclick = cancelConfigurationEdit;\n el('clear-feishu').onclick = () => clearProvider('feishu').catch((error) => message(error.message, true));\n el('check-feishu-login-app').onclick = () => checkRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('create-lark-login-app').onclick = () => createRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('save-lark-login-app').onclick = () => saveRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('cancel-lark-configuration').onclick = cancelConfigurationEdit;\n el('clear-lark').onclick = () => clearProvider('lark').catch((error) => message(error.message, true));\n el('check-lark-login-app').onclick = () => checkRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('check-logto').onclick = () => checkLogto().catch((error) => message(error.message, true));\n el('reconcile-logto').onclick = () => reconcileLogto().then(load).catch((error) => message(error.message, true));\n el('logout').onclick = () => { sessionStorage.removeItem(tokenKey); load().catch((error) => message(error.message, true)); };\n el('save-options').onclick = () => saveOptions().catch((error) => message(error.message, true));\n for (const button of document.querySelectorAll('.edit-secret')) button.onclick = () => editSecret(button);\n for (const button of document.querySelectorAll('.edit-configuration')) button.onclick = () => beginConfigurationEdit(button.dataset.provider);\n updateBootstrapProvider();\n finishSignIn().then(load).catch((error) => message(error.message, true));\n <\/script>\n</body>\n</html>"])));
62206
61093
  //#endregion
62207
- //#region src/admin/logto-management.ts
61094
+ //#region src/server/logto-management.ts
62208
61095
  /** Minimal Logto Management API client for setup reconciliation and ADMIN claim. */
62209
61096
  const MANAGED_APP_TAG = "agentconnectSetup";
62210
61097
  const MANAGED_APP_TAG_VALUE = {
@@ -62758,7 +61645,7 @@ var LogtoAdminClaimClient = class {
62758
61645
  }
62759
61646
  };
62760
61647
  //#endregion
62761
- //#region src/admin/server.ts
61648
+ //#region src/server/index.ts
62762
61649
  const PutDeploymentConfigBody = strictObject({
62763
61650
  expectedRevision: number$1().int().nonnegative(),
62764
61651
  values: DeploymentConfigValuesV1Schema,
@@ -62915,7 +61802,7 @@ function slackProviderAppConfig(services) {
62915
61802
  requireProviderAppEndpoints(config);
62916
61803
  return config;
62917
61804
  }
62918
- function logtoSetup(record, tenantAdminUrl, services, endpoint) {
61805
+ function logtoSetup(record, setupUrl, services, endpoint) {
62919
61806
  const { auth, logto } = record.values;
62920
61807
  if (!logto) return null;
62921
61808
  const browser = logto.browser ?? (auth.mode === "oidc" ? {
@@ -62930,7 +61817,7 @@ function logtoSetup(record, tenantAdminUrl, services, endpoint) {
62930
61817
  const slackSecret = record.secrets["slack.clientSecret"];
62931
61818
  const applicationId = auth.mode === "oidc" ? auth.browserClient.appId : void 0;
62932
61819
  const webOrigin = new URL(services.web).origin;
62933
- const adminOrigin = new URL(tenantAdminUrl).origin;
61820
+ const setupOrigin = new URL(setupUrl).origin;
62934
61821
  return {
62935
61822
  endpoint: resolvedEndpoint,
62936
61823
  apiResource: browser.apiResource,
@@ -62938,7 +61825,7 @@ function logtoSetup(record, tenantAdminUrl, services, endpoint) {
62938
61825
  desired: {
62939
61826
  ...applicationId ? { applicationId } : {},
62940
61827
  applicationName: browser.applicationName,
62941
- redirectUris: [appendPath(webOrigin, "/auth/callback"), appendPath(adminOrigin, "/auth/callback")],
61828
+ redirectUris: [appendPath(webOrigin, "/auth/callback"), appendPath(setupOrigin, "/auth/callback")],
62942
61829
  postLogoutRedirectUris: [appendPath(webOrigin, "/login")],
62943
61830
  socialProviders: [...browser.socialProviders],
62944
61831
  ...logto.githubConnector && githubSecret ? { github: {
@@ -62958,7 +61845,7 @@ function logtoSetup(record, tenantAdminUrl, services, endpoint) {
62958
61845
  };
62959
61846
  }
62960
61847
  /** Build the thin UI/API over the shared typed deployment-config service. */
62961
- function buildTenantAdminServer(deps, options = {}) {
61848
+ function buildSetupServer(deps, options = {}) {
62962
61849
  const app = (0, import_fastify.default)({
62963
61850
  logger: false,
62964
61851
  ...options
@@ -62967,7 +61854,7 @@ function buildTenantAdminServer(deps, options = {}) {
62967
61854
  const auditFeishuAppSetup = deps.auditFeishuAppSetup ?? createFeishuAppSetupAuditor(fetchImpl);
62968
61855
  const slackConfigApi = deps.slackConfigApi ?? createSlackConfigApi({ fetch: fetchImpl });
62969
61856
  const requireLocal = localOnly(deps.publicUrl, deps.allowContainerLoopbackProxy);
62970
- const authenticator = new TenantAdminAuthenticator({ get: async () => oidcOf(await deps.store.getAdmin(), localAuthBootstrap.issuer, logtoManagementEndpoint) }, deps.verifyOidcToken);
61857
+ const authenticator = new SetupAuthenticator({ get: async () => oidcOf(await deps.store.getAdmin(), localAuthBootstrap.issuer, logtoManagementEndpoint) }, deps.verifyOidcToken);
62971
61858
  const discovery = /* @__PURE__ */ new Map();
62972
61859
  let mutationTail = Promise.resolve();
62973
61860
  const defaultEnvironment = loadDeploymentEnvironment({});
@@ -63084,9 +61971,9 @@ function buildTenantAdminServer(deps, options = {}) {
63084
61971
  const claimedFor = current ? deploymentAdminClaimKey(current.values, localAuthBootstrap.issuer) : null;
63085
61972
  if (!current || current.values.auth.mode === "none" || !claimedFor || current.adminClaimedFor !== claimedFor) return requireLocal(request, reply);
63086
61973
  try {
63087
- request.tenantAdminPrincipal = await authenticator.authenticate(request.headers.authorization);
61974
+ request.setupPrincipal = await authenticator.authenticate(request.headers.authorization);
63088
61975
  } catch (error) {
63089
- if (!(error instanceof TenantAdminAuthError)) throw error;
61976
+ if (!(error instanceof SetupAuthError)) throw error;
63090
61977
  authFailure(reply, error);
63091
61978
  }
63092
61979
  };
@@ -63099,12 +61986,12 @@ function buildTenantAdminServer(deps, options = {}) {
63099
61986
  if (error instanceof DeploymentConfigSecretRefreshRequiredError) return problem(reply, 400, error.message, error.code);
63100
61987
  if (error instanceof DeploymentConfigConflictError) return problem(reply, 409, error.message, error.code);
63101
61988
  if (error instanceof LogtoManagementError) return problem(reply, error.code === "LOGTO_UNAVAILABLE" ? 502 : error.code === "SOCIAL_CONNECTOR_UNSUPPORTED" ? 400 : 409, error.message, error.code);
63102
- request.log.error({ err: error }, "tenant-admin request failed");
61989
+ request.log.error({ err: error }, "setup-server request failed");
63103
61990
  return problem(reply, 500, "internal server error");
63104
61991
  });
63105
61992
  app.get("/livez", async () => ({ status: "ok" }));
63106
- app.get("/", async (_request, reply) => reply.type("text/html; charset=utf-8").send(TENANT_ADMIN_HTML));
63107
- app.get("/auth/callback", async (_request, reply) => reply.type("text/html; charset=utf-8").send(TENANT_ADMIN_HTML));
61993
+ app.get("/", async (_request, reply) => reply.type("text/html; charset=utf-8").send(SETUP_HTML));
61994
+ app.get("/auth/callback", async (_request, reply) => reply.type("text/html; charset=utf-8").send(SETUP_HTML));
63108
61995
  app.get("/api/v1/auth-config", async (_request, reply) => {
63109
61996
  const current = await deps.store.getAdmin();
63110
61997
  const auth = current?.values.auth;
@@ -63828,7 +62715,7 @@ function buildTenantAdminServer(deps, options = {}) {
63828
62715
  } : {
63829
62716
  id: "logto.admin_role",
63830
62717
  status: "fail",
63831
- message: adminRole.exists ? "A role named ADMIN exists, but it is not a non-default User role." : "The exact global User role ADMIN does not exist; the first Tenant Admin sign-in will create it.",
62718
+ message: adminRole.exists ? "A role named ADMIN exists, but it is not a non-default User role." : "The exact global User role ADMIN does not exist; the first administrator sign-in through Setup will create it.",
63832
62719
  diff: [{
63833
62720
  field: "ADMIN role",
63834
62721
  current: adminRole.exists ? {
@@ -63917,7 +62804,7 @@ function buildTenantAdminServer(deps, options = {}) {
63917
62804
  try {
63918
62805
  principal = await authenticator.authenticate(request.headers.authorization, false);
63919
62806
  } catch (error) {
63920
- if (!(error instanceof TenantAdminAuthError)) throw error;
62807
+ if (!(error instanceof SetupAuthError)) throw error;
63921
62808
  return authFailure(reply, error);
63922
62809
  }
63923
62810
  const logto = runtime.values.logto;
@@ -63940,14 +62827,14 @@ function buildTenantAdminServer(deps, options = {}) {
63940
62827
  }));
63941
62828
  return app;
63942
62829
  }
63943
- /** Run the Tenant Admin HTTP server. */
63944
- async function serveTenantAdmin(env = process.env) {
63945
- const config = loadTenantAdminProcessConfig(env);
62830
+ /** Run the Setup Server. */
62831
+ async function serveSetupServer(env = process.env) {
62832
+ const config = loadSetupServerProcessConfig(env);
63946
62833
  const host = config.HOST;
63947
62834
  const port = config.PORT;
63948
- const publicUrl = env.TENANT_ADMIN_URL ?? `http://localhost:${port}`;
62835
+ const publicUrl = env.SETUP_SERVER_URL ?? `http://localhost:${port}`;
63949
62836
  const deploymentEnvironment = loadDeploymentEnvironment(env);
63950
- if (!isLoopbackHostname(host) && !config.TENANT_ADMIN_ALLOW_CONTAINER_PROXY) throw new Error("Tenant Admin may bind outside loopback only behind an isolated local port forward");
62837
+ if (!isLoopbackHostname(host) && !config.SETUP_SERVER_ALLOW_CONTAINER_PROXY) throw new Error("Setup Server may bind outside loopback only behind an isolated local port forward");
63951
62838
  const handle = openDeploymentConfigStore({
63952
62839
  databaseUrl: config.DATABASE_URL,
63953
62840
  SECRET_CIPHER: config.SECRET_CIPHER,
@@ -63960,10 +62847,10 @@ async function serveTenantAdmin(env = process.env) {
63960
62847
  ...config.VAULT_TOKEN ? { VAULT_TOKEN: config.VAULT_TOKEN } : {},
63961
62848
  ...config.VAULT_JWT_ROLE ? { VAULT_JWT_ROLE: config.VAULT_JWT_ROLE } : {}
63962
62849
  });
63963
- const app = buildTenantAdminServer({
62850
+ const app = buildSetupServer({
63964
62851
  store: handle.store,
63965
62852
  publicUrl,
63966
- allowContainerLoopbackProxy: config.TENANT_ADMIN_ALLOW_CONTAINER_PROXY,
62853
+ allowContainerLoopbackProxy: config.SETUP_SERVER_ALLOW_CONTAINER_PROXY,
63967
62854
  localAuthBootstrap: {
63968
62855
  issuer: deploymentEnvironment.issuer,
63969
62856
  managementEndpoint: deploymentEnvironment.managementEndpoint,
@@ -63976,7 +62863,7 @@ async function serveTenantAdmin(env = process.env) {
63976
62863
  host,
63977
62864
  port
63978
62865
  });
63979
- app.log.info(`Tenant Admin listening at ${publicUrl}`);
62866
+ app.log.info(`Setup Server listening at ${publicUrl}`);
63980
62867
  await new Promise((resolve) => {
63981
62868
  const stop = () => resolve();
63982
62869
  process.once("SIGINT", stop);
@@ -63995,9 +62882,9 @@ function loadDefaultEnvironment() {
63995
62882
  if (path) loadEnvFile(path);
63996
62883
  }
63997
62884
  loadDefaultEnvironment();
63998
- serveTenantAdmin().catch((error) => {
62885
+ serveSetupServer().catch((error) => {
63999
62886
  const message = error instanceof Error ? error.message : String(error);
64000
- console.error(`tenant-admin: ${message}`);
62887
+ console.error(`setup-server: ${message}`);
64001
62888
  process.exitCode = 1;
64002
62889
  });
64003
62890
  //#endregion