@agentconnect.md/setup 1.34.0-rc.4 → 1.34.0-rc.7

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
@@ -57490,16 +57490,19 @@ const LogtoBrowserSchema = strictObject({
57490
57490
  socialProviders: array(string().trim().min(1)).default([])
57491
57491
  });
57492
57492
  const LogtoGithubConnectorSchema = strictObject({
57493
+ connectorId: string().trim().min(1).default("agentconnect-github"),
57493
57494
  appId: number$1().int().positive(),
57494
57495
  slug: string().trim().min(1),
57495
57496
  clientId: string().trim().min(1)
57496
57497
  });
57497
57498
  const LogtoGoogleConnectorSchema = strictObject({
57499
+ connectorId: string().trim().min(1).default("agentconnect-google"),
57498
57500
  clientId: string().trim().min(1),
57499
57501
  /** Provider-console callbacks last confirmed by the operator. */
57500
57502
  configuredRedirectUris: array(SecureHttpUrlSchema).min(1)
57501
57503
  });
57502
57504
  const LogtoSlackConnectorSchema = strictObject({
57505
+ connectorId: string().trim().min(1).default("agentconnect-slack"),
57503
57506
  appId: string().trim().min(1),
57504
57507
  clientId: string().trim().min(1)
57505
57508
  });
@@ -58210,6 +58213,10 @@ function loadDeploymentEnvironment(environment = process.env) {
58210
58213
  };
58211
58214
  }
58212
58215
  //#endregion
58216
+ //#region src/logto-connectors.ts
58217
+ const LOGTO_GITHUB_CONNECTOR_ID = "agentconnect-github";
58218
+ const LOGTO_SLACK_CONNECTOR_ID = "agentconnect-slack";
58219
+ //#endregion
58213
58220
  //#region src/deployment-config-client.ts
58214
58221
  /** Typed deployment mutations used by the Tenant Admin provider workflows. */
58215
58222
  const DeploymentConfigPutSchema = strictObject({
@@ -58236,6 +58243,7 @@ function githubDeploymentPut(current, credentials, options = {}) {
58236
58243
  socialProviders: [.../* @__PURE__ */ new Set([...connectLogto.browser.socialProviders, "github"])]
58237
58244
  } : connectLogto.browser,
58238
58245
  githubConnector: {
58246
+ connectorId: LOGTO_GITHUB_CONNECTOR_ID,
58239
58247
  appId,
58240
58248
  slug: credentials.slug,
58241
58249
  clientId: credentials.clientId
@@ -58288,6 +58296,7 @@ function logtoGithubConnectorPut(current, credentials) {
58288
58296
  socialProviders: [.../* @__PURE__ */ new Set([...current.values.logto.browser.socialProviders, "github"])]
58289
58297
  } : current.values.logto.browser,
58290
58298
  githubConnector: {
58299
+ connectorId: current.values.logto.githubConnector?.connectorId ?? "agentconnect-github",
58291
58300
  appId,
58292
58301
  slug: credentials.slug,
58293
58302
  clientId: credentials.clientId
@@ -58314,6 +58323,7 @@ function slackDeploymentPut(current, credentials, configuredUrls, connectLogto =
58314
58323
  socialProviders: [.../* @__PURE__ */ new Set([...logto.browser.socialProviders, "slack"])]
58315
58324
  } : logto.browser,
58316
58325
  slackConnector: {
58326
+ connectorId: logto.slackConnector?.connectorId ?? "agentconnect-slack",
58317
58327
  appId: credentials.appId,
58318
58328
  clientId: credentials.clientId
58319
58329
  }
@@ -58338,6 +58348,7 @@ function logtoGoogleConnectorPut(current, credentials) {
58338
58348
  socialProviders: [.../* @__PURE__ */ new Set([...current.values.logto.browser.socialProviders, "google"])]
58339
58349
  } : current.values.logto.browser,
58340
58350
  googleConnector: {
58351
+ connectorId: credentials.connectorId ?? current.values.logto.googleConnector?.connectorId ?? "agentconnect-google",
58341
58352
  clientId: credentials.clientId,
58342
58353
  configuredRedirectUris: credentials.configuredRedirectUris
58343
58354
  }
@@ -60364,9 +60375,12 @@ const GITHUB_APP_EVENTS = [
60364
60375
  "issues",
60365
60376
  "issue_comment",
60366
60377
  "pull_request",
60378
+ "pull_request_review",
60367
60379
  "pull_request_review_comment",
60368
60380
  "check_run",
60369
- "check_suite"
60381
+ "check_suite",
60382
+ "release",
60383
+ "repository"
60370
60384
  ];
60371
60385
  const GITHUB_ORG = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
60372
60386
  function appendPath$2(base, path) {
@@ -60431,6 +60445,9 @@ function sameRecord(left, right) {
60431
60445
  const expectedKeys = Object.keys(right).sort();
60432
60446
  return JSON.stringify(actualKeys) === JSON.stringify(expectedKeys) && expectedKeys.every((key) => actual[key] === right[key]);
60433
60447
  }
60448
+ function githubApiPermissions(manifestPermissions) {
60449
+ return Object.fromEntries(Object.entries(manifestPermissions).map(([key, value]) => [key === "email_addresses" ? "emails" : key, value]));
60450
+ }
60434
60451
  function sameStringSet(left, right) {
60435
60452
  if (!Array.isArray(left) || !left.every((value) => typeof value === "string")) return false;
60436
60453
  return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
@@ -60448,13 +60465,32 @@ async function auditGithubApp(identity, privateKeyBase64, expectedManifest, fetc
60448
60465
  auth: jwt,
60449
60466
  fetchImpl
60450
60467
  }) : Promise.resolve(void 0)]);
60451
- const missing = [];
60452
- if (app.id !== identity.appId || app.slug !== identity.slug || identity.clientId !== null && app.client_id !== identity.clientId) missing.push("identity");
60453
- if (app.external_url !== expectedManifest.url) missing.push("external_url");
60454
- if (!sameRecord(app.permissions, GITHUB_APP_PERMISSIONS)) missing.push("permissions");
60468
+ const diff = [];
60469
+ const addDiff = (id, field, current, expected) => {
60470
+ diff.push({
60471
+ id,
60472
+ field,
60473
+ current,
60474
+ expected
60475
+ });
60476
+ };
60477
+ const currentIdentity = {
60478
+ appId: app.id,
60479
+ slug: app.slug,
60480
+ clientId: app.client_id ?? null
60481
+ };
60482
+ const expectedIdentity = {
60483
+ appId: identity.appId,
60484
+ slug: identity.slug,
60485
+ clientId: identity.clientId
60486
+ };
60487
+ if (app.id !== identity.appId || app.slug !== identity.slug || identity.clientId !== null && app.client_id !== identity.clientId) addDiff("identity", "App identity", currentIdentity, expectedIdentity);
60488
+ if (app.external_url !== expectedManifest.url) addDiff("external_url", "Homepage URL", app.external_url ?? null, expectedManifest.url ?? null);
60489
+ const expectedPermissions = githubApiPermissions(GITHUB_APP_PERMISSIONS);
60490
+ if (!sameRecord(app.permissions, expectedPermissions)) addDiff("permissions", "Repository permissions", asRecord$3(app.permissions), expectedPermissions);
60455
60491
  const expectedEvents = Array.isArray(expectedManifest.default_events) ? expectedManifest.default_events.filter((event) => typeof event === "string") : [];
60456
- if (!sameStringSet(app.events, expectedEvents)) missing.push("events");
60457
- if (typeof expectedHook.url === "string" && hook?.url !== expectedHook.url) missing.push("webhook_url");
60492
+ if (!sameStringSet(app.events, expectedEvents)) addDiff("events", "Webhook events", Array.isArray(app.events) ? app.events : [], expectedEvents);
60493
+ if (typeof expectedHook.url === "string" && hook?.url !== expectedHook.url) addDiff("webhook_url", "Webhook URL", hook?.url ?? null, expectedHook.url);
60458
60494
  const owner = asRecord$3(app.owner);
60459
60495
  const ownerLogin = typeof owner.login === "string" ? owner.login : null;
60460
60496
  const settingsUrl = owner.type === "Organization" && ownerLogin ? `https://github.com/organizations/${encodeURIComponent(ownerLogin)}/settings/apps/${encodeURIComponent(identity.slug)}` : `https://github.com/settings/apps/${encodeURIComponent(identity.slug)}`;
@@ -60465,7 +60501,8 @@ async function auditGithubApp(identity, privateKeyBase64, expectedManifest, fetc
60465
60501
  owner: ownerLogin,
60466
60502
  settingsUrl
60467
60503
  },
60468
- missing
60504
+ missing: [...new Set(diff.map((item) => item.id))],
60505
+ diff
60469
60506
  };
60470
60507
  }
60471
60508
  function requiredString(record, field) {
@@ -60613,9 +60650,12 @@ function asRecord$1(value) {
60613
60650
  function asStrings(value) {
60614
60651
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
60615
60652
  }
60616
- /** Returns stable field names only; it never includes provider response values. */
60617
- function auditSlackManifest(actual, expected) {
60618
- const missing = [];
60653
+ function sameStrings$2(left, right) {
60654
+ return JSON.stringify([...asStrings(left)].sort()) === JSON.stringify([...asStrings(right)].sort());
60655
+ }
60656
+ /** Returns only public App settings; secrets never appear in this diff. */
60657
+ function diffSlackManifest(actual, expected) {
60658
+ const diff = [];
60619
60659
  const actualOauth = asRecord$1(actual.oauth_config);
60620
60660
  const expectedOauth = asRecord$1(expected.oauth_config);
60621
60661
  const actualScopes = asRecord$1(actualOauth.scopes);
@@ -60626,19 +60666,58 @@ function auditSlackManifest(actual, expected) {
60626
60666
  const expectedEvents = asRecord$1(expectedSettings.event_subscriptions);
60627
60667
  const actualInteractivity = asRecord$1(actualSettings.interactivity);
60628
60668
  const expectedInteractivity = asRecord$1(expectedSettings.interactivity);
60629
- for (const scope of asStrings(asRecord$1(expectedScopes).bot)) if (!asStrings(asRecord$1(actualScopes).bot).includes(scope)) missing.push(`scope:${scope}`);
60630
- for (const event of asStrings(expectedEvents.bot_events)) if (!asStrings(actualEvents.bot_events).includes(event)) missing.push(`event:${event}`);
60631
- for (const [field, expectedValue] of [
60632
- ["oauth_config.redirect_urls", expectedOauth.redirect_urls],
60633
- ["settings.event_subscriptions.request_url", expectedEvents.request_url],
60634
- ["settings.interactivity.request_url", expectedInteractivity.request_url],
60635
- ["settings.interactivity.message_menu_options_url", expectedInteractivity.message_menu_options_url],
60636
- ["settings.socket_mode_enabled", expectedSettings.socket_mode_enabled]
60669
+ for (const scope of asStrings(asRecord$1(expectedScopes).bot)) if (!asStrings(asRecord$1(actualScopes).bot).includes(scope)) diff.push({
60670
+ id: `scope:${scope}`,
60671
+ field: `Bot scope: ${scope}`,
60672
+ current: "Missing",
60673
+ expected: "Required"
60674
+ });
60675
+ for (const event of asStrings(expectedEvents.bot_events)) if (!asStrings(actualEvents.bot_events).includes(event)) diff.push({
60676
+ id: `event:${event}`,
60677
+ field: `Bot event: ${event}`,
60678
+ current: "Missing",
60679
+ expected: "Required"
60680
+ });
60681
+ for (const [id, field, expectedValue] of [
60682
+ [
60683
+ "oauth_config.redirect_urls",
60684
+ "OAuth redirect URLs",
60685
+ expectedOauth.redirect_urls
60686
+ ],
60687
+ [
60688
+ "settings.event_subscriptions.request_url",
60689
+ "Events request URL",
60690
+ expectedEvents.request_url
60691
+ ],
60692
+ [
60693
+ "settings.interactivity.request_url",
60694
+ "Interactivity request URL",
60695
+ expectedInteractivity.request_url
60696
+ ],
60697
+ [
60698
+ "settings.interactivity.message_menu_options_url",
60699
+ "Message menu options URL",
60700
+ expectedInteractivity.message_menu_options_url
60701
+ ],
60702
+ [
60703
+ "settings.socket_mode_enabled",
60704
+ "Socket Mode enabled",
60705
+ expectedSettings.socket_mode_enabled
60706
+ ]
60637
60707
  ]) {
60638
- const actualValue = field === "oauth_config.redirect_urls" ? actualOauth.redirect_urls : field === "settings.event_subscriptions.request_url" ? actualEvents.request_url : field === "settings.interactivity.request_url" ? actualInteractivity.request_url : field === "settings.interactivity.message_menu_options_url" ? actualInteractivity.message_menu_options_url : actualSettings.socket_mode_enabled;
60639
- if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) missing.push(field);
60708
+ const actualValue = id === "oauth_config.redirect_urls" ? actualOauth.redirect_urls : id === "settings.event_subscriptions.request_url" ? actualEvents.request_url : id === "settings.interactivity.request_url" ? actualInteractivity.request_url : id === "settings.interactivity.message_menu_options_url" ? actualInteractivity.message_menu_options_url : actualSettings.socket_mode_enabled;
60709
+ if (!(id === "oauth_config.redirect_urls" ? sameStrings$2(actualValue, expectedValue) : JSON.stringify(actualValue) === JSON.stringify(expectedValue))) diff.push({
60710
+ id,
60711
+ field,
60712
+ current: actualValue ?? null,
60713
+ expected: expectedValue ?? null
60714
+ });
60640
60715
  }
60641
- return missing;
60716
+ return diff;
60717
+ }
60718
+ /** Returns stable field names only; it never includes provider response values. */
60719
+ function auditSlackManifest(actual, expected) {
60720
+ return diffSlackManifest(actual, expected).map((item) => item.id);
60642
60721
  }
60643
60722
  //#endregion
60644
60723
  //#region ../../node_modules/.pnpm/jose@6.2.4/node_modules/jose/dist/webapi/lib/buffer_utils.js
@@ -61947,18 +62026,18 @@ function _taggedTemplateLiteral(e, t) {
61947
62026
  //#region src/admin/html.ts
61948
62027
  var _templateObject;
61949
62028
  /** One deliberately small, dependency-free deployment administration page. */
61950
- 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 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 }\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://admin.agentconnect.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 <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 Auth Platform</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 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>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 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=\"confirm-github\" hidden>I updated callback/setup URLs</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 <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google Auth Platform</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\">Tenant App used to admit Feishu Bot Apps.</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-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 credentials</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\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\">Tenant App used to admit Lark Bot Apps.</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-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 credentials</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\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 same = (a, b) => JSON.stringify([...(a || [])].sort()) === JSON.stringify([...(b || [])].sort());\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 fieldsChanged(actual, expected) {\n if (!actual) return Object.keys(expected);\n return Object.keys(expected).filter((key) => Array.isArray(expected[key]) ? !same(actual[key], expected[key]) : actual[key] !== expected[key]);\n }\n\n function showDrift(id, fields, expected) {\n const box = el(id);\n box.hidden = fields.length === 0;\n box.textContent = fields.length === 0 ? '' : 'Update required: ' + fields.join(', ') + '. Expected values: ' + JSON.stringify(expected);\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.configuredUrls && github.configuredUrls.webhookActive === 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 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 ? ' Webhook delivery is not registered until HTTPS ingress is configured.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github ? (expected.github ? fieldsChanged(github.configuredUrls, expected.github) : ['startup public URLs']) : [];\n match('github-match', !github ? '' : githubDrift.length ? 'warn' : '', !github ? 'Not configured' : githubDrift.length ? 'Update required' : '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) showDrift('github-drift', githubDrift, expected.github || {});\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 ? (expected.slack ? fieldsChanged(slack.configuredUrls, expected.slack) : ['startup public URLs']) : [];\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 showDrift('slack-drift', slackDrift, expected.slack || {});\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 const googleDrift = google && !same(google.configuredRedirectUris, expectedGoogle.redirects) ? ['authorized redirect URIs'] : [];\n showDrift('google-drift', googleDrift, expectedGoogle);\n match('google-match', !google || !googleSecret ? '' : googleDrift.length ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : googleDrift.length ? 'Update required' : 'Ready to check');\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 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 }\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 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 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 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 (appChanged && previous.configuredUrls?.webhookActive !== false && !webhookSecret) 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: { appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, ...(appChanged ? { configuredUrls: undefined } : {}) },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\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: { appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId, ...(changed ? { configuredUrls: undefined } : {}) },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\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('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 HTTPS Logto, Web, Control Plane, and Relay URLs. Use Google locally or expose the stack through a trusted HTTPS endpoint.';\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 })\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 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: el('bootstrap-google-secret').value })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto();\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 saved HTTPS Logto, Web, Control Plane, and Relay URLs.');\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 el('confirm-github').hidden = !result.missing.some((field) => field === 'callback_urls' || field === 'setup_url' || field === 'webhook_active');\n showDrift('github-drift', result.missing, result.expected);\n match('github-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : 'Update required');\n message(result.status === 'pass' ? 'GitHub App matches the expected integration manifest.' : 'GitHub App settings need an update.', result.status !== 'pass');\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: { appId: slack.appId, clientId: slack.clientId }\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 confirmGithub() {\n await json(await fetch(api + '/confirm/github-urls', { method: 'POST', headers: bearer() }));\n el('confirm-github').hidden = true;\n await load();\n await checkGithub();\n }\n\n async function checkSlack() {\n const token = el('slack-token').value;\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 if (result.status === 'pass') await load();\n showDrift('slack-drift', result.missing, result.expected);\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 expected = currentStatus ? currentStatus.providerExpectations.google : { redirects: [] };\n const callbacksMatch = google && same(google.configuredRedirectUris, expected.redirects);\n const passed = connector && connector.status === 'pass' && callbacksMatch;\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Matches' : 'Update required');\n message(passed ? 'Google callbacks and the Logto connector match.' : 'Google or its Logto connector needs an update.', !passed);\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 match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Credentials match' : result.status === 'fail' ? 'Invalid credentials' : 'Could not check');\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({ clientId: el('google-id').value, ...(secret ? { clientSecret: secret } : {}) })\n }));\n el('google-secret').value = '';\n await reconcileLogto();\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() {\n return json(await fetch(api + '/reconcile/logto', { method: 'POST', headers: bearer() }));\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, CORS, 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 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 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 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 saved = 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 await load();\n message('Secret replaced. Restart AgentConnect to apply it.');\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('confirm-github').onclick = () => confirmGithub().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 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 }\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://admin.agentconnect.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 <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 Auth Platform</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 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>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 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=\"confirm-github\" hidden>I updated callback/setup URLs</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 <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google Auth Platform</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\">Tenant App used to admit Feishu Bot Apps.</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-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 credentials</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\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\">Tenant App used to admit Lark Bot Apps.</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-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 credentials</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\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 same = (a, b) => JSON.stringify([...(a || [])].sort()) === JSON.stringify([...(b || [])].sort());\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 fieldsChanged(actual, expected) {\n if (!actual) return Object.keys(expected);\n return Object.keys(expected).filter((key) => Array.isArray(expected[key]) ? !same(actual[key], expected[key]) : actual[key] !== expected[key]);\n }\n\n function showDrift(id, fields, expected) {\n const box = el(id);\n box.hidden = fields.length === 0;\n box.textContent = fields.length === 0 ? '' : 'Update required: ' + fields.join(', ') + '. Expected values: ' + JSON.stringify(expected);\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.configuredUrls && github.configuredUrls.webhookActive === 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 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 ? ' Webhook delivery is not registered until HTTPS ingress is configured.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github ? (expected.github ? fieldsChanged(github.configuredUrls, expected.github) : ['startup public URLs']) : [];\n match('github-match', !github ? '' : githubDrift.length ? 'warn' : '', !github ? 'Not configured' : githubDrift.length ? 'Update required' : '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) showDrift('github-drift', githubDrift, expected.github || {});\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 ? (expected.slack ? fieldsChanged(slack.configuredUrls, expected.slack) : ['startup public URLs']) : [];\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 showDrift('slack-drift', slackDrift, expected.slack || {});\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 const googleDrift = google && !same(google.configuredRedirectUris, expectedGoogle.redirects) ? ['authorized redirect URIs'] : [];\n showDrift('google-drift', googleDrift, expectedGoogle);\n match('google-match', !google || !googleSecret ? '' : googleDrift.length ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : googleDrift.length ? 'Update required' : 'Ready to check');\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 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 }\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 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 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 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 (appChanged && previous.configuredUrls?.webhookActive !== false && !webhookSecret) 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: { appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, ...(appChanged ? { configuredUrls: undefined } : {}) },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\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: { appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId, ...(changed ? { configuredUrls: undefined } : {}) },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\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('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 HTTPS Logto, Web, Control Plane, and Relay URLs. Use Google locally or expose the stack through a trusted HTTPS endpoint.';\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 })\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 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: el('bootstrap-google-secret').value })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto();\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 saved HTTPS Logto, Web, Control Plane, and Relay URLs.');\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 el('confirm-github').hidden = !result.missing.some((field) => field === 'callback_urls' || field === 'setup_url' || field === 'webhook_active');\n showDrift('github-drift', result.missing, result.expected);\n match('github-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : 'Update required');\n message(result.status === 'pass' ? 'GitHub App matches the expected integration manifest.' : 'GitHub App settings need an update.', result.status !== 'pass');\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: { appId: slack.appId, clientId: slack.clientId }\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 confirmGithub() {\n await json(await fetch(api + '/confirm/github-urls', { method: 'POST', headers: bearer() }));\n el('confirm-github').hidden = true;\n await load();\n await checkGithub();\n }\n\n async function checkSlack() {\n const token = el('slack-token').value;\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 if (result.status === 'pass') await load();\n showDrift('slack-drift', result.missing, result.expected);\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 expected = currentStatus ? currentStatus.providerExpectations.google : { redirects: [] };\n const callbacksMatch = google && same(google.configuredRedirectUris, expected.redirects);\n const passed = connector && connector.status === 'pass' && callbacksMatch;\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Matches' : 'Update required');\n message(passed ? 'Google callbacks and the Logto connector match.' : 'Google or its Logto connector needs an update.', !passed);\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 match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Credentials match' : result.status === 'fail' ? 'Invalid credentials' : 'Could not check');\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({ clientId: el('google-id').value, ...(secret ? { clientSecret: secret } : {}) })\n }));\n el('google-secret').value = '';\n await reconcileLogto();\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() {\n return json(await fetch(api + '/reconcile/logto', { method: 'POST', headers: bearer() }));\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, CORS, 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 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 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 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 saved = 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 await load();\n message('Secret replaced. Restart AgentConnect to apply it.');\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('confirm-github').onclick = () => confirmGithub().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>"])));
62029
+ 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://admin.agentconnect.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 <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 Auth Platform</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>Logto connector ID</dt><dd class=\"value-line\"><code id=\"github-logto-connector-id\">Not enabled</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 id=\"github-edit-connector-id-field\" class=\"field\" hidden>Logto connector ID<input id=\"github-edit-connector-id\" autocomplete=\"off\"></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=\"confirm-github\" hidden>I updated callback/setup URLs</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>Logto connector ID</dt><dd class=\"value-line\"><code id=\"slack-logto-connector-id\">Not enabled</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 id=\"slack-edit-connector-id-field\" class=\"field\" hidden>Logto connector ID<input id=\"slack-edit-connector-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>Logto connector ID</dt><dd class=\"value-line\"><code id=\"google-connector-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 <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google Auth Platform</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 class=\"field\">Logto connector ID<input id=\"google-connector-id-input\" 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\">Tenant App used to admit Feishu Bot Apps.</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-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 credentials</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\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\">Tenant App used to admit Lark Bot Apps.</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-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 credentials</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\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 same = (a, b) => JSON.stringify([...(a || [])].sort()) === JSON.stringify([...(b || [])].sort());\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 valuesMatch(current, expected) {\n if (Array.isArray(expected)) return same(current, expected);\n return JSON.stringify(current) === JSON.stringify(expected);\n }\n\n function objectDiff(current, expected, labels = {}) {\n if (!expected) return [];\n return Object.keys(expected)\n .filter((key) => !current || !valuesMatch(current[key], expected[key]))\n .map((key) => ({\n field: labels[key] || key,\n current: current ? current[key] : 'Not verified',\n expected: expected[key]\n }));\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.configuredUrls && github.configuredUrls.webhookActive === 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-logto-connector-id', values.logto && values.logto.githubConnector && values.logto.githubConnector.connectorId);\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 ? ' Webhook delivery is not registered until HTTPS ingress is configured.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github\n ? expected.github\n ? objectDiff(github.configuredUrls, expected.github, {\n externalUrl: 'Homepage URL', setupUrl: 'Setup URL', webhookUrl: 'Webhook URL',\n webhookActive: 'Webhook active', callbackUrls: 'Callback URLs'\n })\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'Valid Web, API, and ingress URLs' }]\n : [];\n const githubVerified = Boolean(github && github.configuredUrls);\n match('github-match', !github ? '' : !githubVerified || githubDrift.length ? 'warn' : '', !github ? 'Not configured' : !githubVerified ? 'Not verified' : githubDrift.length ? 'Update required' : '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, githubVerified ? 'Update required' : 'Not verified');\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 text('slack-logto-connector-id', values.logto && values.logto.slackConnector && values.logto.slackConnector.connectorId);\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 ? objectDiff(slack.configuredUrls, expected.slack, {\n oauthRedirectUrl: 'OAuth redirect URL', eventsUrl: 'Events request URL',\n interactionsUrl: 'Interactivity request URL', loginRedirectUrl: 'Logto redirect URL'\n })\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'HTTPS Web, API, and ingress URLs' }]\n : [];\n const slackVerified = Boolean(slack && slack.configuredUrls);\n match('slack-match', !slack ? '' : !slackVerified || slackDrift.length ? 'warn' : '', !slack ? 'Not configured' : !slackVerified ? 'Not verified' : 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, slackVerified ? 'Update required' : 'Not verified');\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 text('google-connector-id', google && google.connectorId);\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-connector-id-input').value = google ? google.connectorId : bootstrapInfo.googleConnectorId;\n el('google-status').textContent = google ? 'Google OAuth client is configured.' : 'Create a Web application OAuth client manually, then save it here.';\n const googleDrift = google && !same(google.configuredRedirectUris, expectedGoogle.redirects)\n ? [{ field: 'Authorized redirect URIs', current: google.configuredRedirectUris, expected: expectedGoogle.redirects }]\n : [];\n showDiff('google-drift', googleDrift);\n match('google-match', !google || !googleSecret ? '' : googleDrift.length ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : googleDrift.length ? 'Update required' : 'Ready to check');\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 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 }\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 const githubConnector = values.logto && values.logto.githubConnector;\n el('github-edit-connector-id').value = githubConnector ? githubConnector.connectorId : '';\n el('github-edit-connector-id-field').hidden = !githubConnector;\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 const slackConnector = values.logto && values.logto.slackConnector;\n el('slack-edit-connector-id').value = slackConnector ? slackConnector.connectorId : '';\n el('slack-edit-connector-id-field').hidden = !slackConnector;\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-connector-id-input').value = google.connectorId;\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 connectorReused = githubConnectorUsesDeployment(values);\n const connectorId = connectorReused ? requiredInput('github-edit-connector-id', 'the Logto connector ID') : null;\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 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 (appChanged && previous.configuredUrls?.webhookActive !== false && !webhookSecret) 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, connectorId, appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, ...(appChanged ? { configuredUrls: undefined } : {}) },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\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 connectorId = values.logto && values.logto.slackConnector\n ? requiredInput('slack-edit-connector-id', 'the Logto connector ID')\n : null;\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, connectorId, appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId, ...(changed ? { configuredUrls: undefined } : {}) },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\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('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 HTTPS Logto, Web, Control Plane, and Relay URLs. Use Google locally or expose the stack through a trusted HTTPS endpoint.';\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 })\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 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: el('bootstrap-google-secret').value })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto();\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 saved HTTPS Logto, Web, Control Plane, and Relay URLs.');\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 confirmationFields = result.unverified || [];\n el('confirm-github').hidden = ![...result.missing, ...confirmationFields].some((field) => field === 'callback_urls' || field === 'setup_url' || field === 'webhook_active');\n const label = result.missing.length ? 'Update required' : 'Not verified';\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.' : 'Confirm the callback, setup, and webhook-active settings 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 connectorId: bootstrapInfo.slackConnectorId,\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 confirmGithub() {\n await json(await fetch(api + '/confirm/github-urls', { method: 'POST', headers: bearer() }));\n el('confirm-github').hidden = true;\n await load();\n await checkGithub();\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 if (result.status === 'pass') await load();\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 expected = currentStatus ? currentStatus.providerExpectations.google : { redirects: [] };\n const callbacksMatch = google && same(google.configuredRedirectUris, expected.redirects);\n const passed = connector && connector.status === 'pass' && callbacksMatch;\n const connectorDiff = connector && connector.diff\n ? connector.diff.filter((item) => item.field.toLowerCase().startsWith('google'))\n : [];\n showDiff(\n 'google-drift',\n [\n ...connectorDiff,\n ...(callbacksMatch || !google\n ? []\n : [{ field: 'Authorized redirect URIs', current: google.configuredRedirectUris, expected: expected.redirects }])\n ]\n );\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Matches' : 'Update required');\n message(passed ? 'Google callbacks and the Logto connector match.' : 'Google or its Logto connector needs an update.', !passed);\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 match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Credentials match' : result.status === 'fail' ? 'Invalid credentials' : 'Could not check');\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 connectorId: requiredInput('google-connector-id-input', 'the Logto connector ID'),\n clientId: el('google-id').value,\n ...(secret ? { clientSecret: secret } : {})\n })\n }));\n el('google-secret').value = '';\n await reconcileLogto();\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() {\n return json(await fetch(api + '/reconcile/logto', { method: 'POST', headers: bearer() }));\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, CORS, 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 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 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 saved = 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 await load();\n message('Secret replaced. Restart AgentConnect to apply it.');\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('confirm-github').onclick = () => confirmGithub().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://admin.agentconnect.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 <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 Auth Platform</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>Logto connector ID</dt><dd class=\"value-line\"><code id=\"github-logto-connector-id\">Not enabled</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 id=\"github-edit-connector-id-field\" class=\"field\" hidden>Logto connector ID<input id=\"github-edit-connector-id\" autocomplete=\"off\"></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=\"confirm-github\" hidden>I updated callback/setup URLs</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>Logto connector ID</dt><dd class=\"value-line\"><code id=\"slack-logto-connector-id\">Not enabled</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 id=\"slack-edit-connector-id-field\" class=\"field\" hidden>Logto connector ID<input id=\"slack-edit-connector-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>Logto connector ID</dt><dd class=\"value-line\"><code id=\"google-connector-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 <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google Auth Platform</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 class=\"field\">Logto connector ID<input id=\"google-connector-id-input\" 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\">Tenant App used to admit Feishu Bot Apps.</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-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 credentials</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\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\">Tenant App used to admit Lark Bot Apps.</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-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 credentials</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\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 same = (a, b) => JSON.stringify([...(a || [])].sort()) === JSON.stringify([...(b || [])].sort());\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 valuesMatch(current, expected) {\n if (Array.isArray(expected)) return same(current, expected);\n return JSON.stringify(current) === JSON.stringify(expected);\n }\n\n function objectDiff(current, expected, labels = {}) {\n if (!expected) return [];\n return Object.keys(expected)\n .filter((key) => !current || !valuesMatch(current[key], expected[key]))\n .map((key) => ({\n field: labels[key] || key,\n current: current ? current[key] : 'Not verified',\n expected: expected[key]\n }));\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.configuredUrls && github.configuredUrls.webhookActive === 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-logto-connector-id', values.logto && values.logto.githubConnector && values.logto.githubConnector.connectorId);\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 ? ' Webhook delivery is not registered until HTTPS ingress is configured.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github\n ? expected.github\n ? objectDiff(github.configuredUrls, expected.github, {\n externalUrl: 'Homepage URL', setupUrl: 'Setup URL', webhookUrl: 'Webhook URL',\n webhookActive: 'Webhook active', callbackUrls: 'Callback URLs'\n })\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'Valid Web, API, and ingress URLs' }]\n : [];\n const githubVerified = Boolean(github && github.configuredUrls);\n match('github-match', !github ? '' : !githubVerified || githubDrift.length ? 'warn' : '', !github ? 'Not configured' : !githubVerified ? 'Not verified' : githubDrift.length ? 'Update required' : '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, githubVerified ? 'Update required' : 'Not verified');\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 text('slack-logto-connector-id', values.logto && values.logto.slackConnector && values.logto.slackConnector.connectorId);\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 ? objectDiff(slack.configuredUrls, expected.slack, {\n oauthRedirectUrl: 'OAuth redirect URL', eventsUrl: 'Events request URL',\n interactionsUrl: 'Interactivity request URL', loginRedirectUrl: 'Logto redirect URL'\n })\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'HTTPS Web, API, and ingress URLs' }]\n : [];\n const slackVerified = Boolean(slack && slack.configuredUrls);\n match('slack-match', !slack ? '' : !slackVerified || slackDrift.length ? 'warn' : '', !slack ? 'Not configured' : !slackVerified ? 'Not verified' : 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, slackVerified ? 'Update required' : 'Not verified');\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 text('google-connector-id', google && google.connectorId);\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-connector-id-input').value = google ? google.connectorId : bootstrapInfo.googleConnectorId;\n el('google-status').textContent = google ? 'Google OAuth client is configured.' : 'Create a Web application OAuth client manually, then save it here.';\n const googleDrift = google && !same(google.configuredRedirectUris, expectedGoogle.redirects)\n ? [{ field: 'Authorized redirect URIs', current: google.configuredRedirectUris, expected: expectedGoogle.redirects }]\n : [];\n showDiff('google-drift', googleDrift);\n match('google-match', !google || !googleSecret ? '' : googleDrift.length ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : googleDrift.length ? 'Update required' : 'Ready to check');\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 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 }\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 const githubConnector = values.logto && values.logto.githubConnector;\n el('github-edit-connector-id').value = githubConnector ? githubConnector.connectorId : '';\n el('github-edit-connector-id-field').hidden = !githubConnector;\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 const slackConnector = values.logto && values.logto.slackConnector;\n el('slack-edit-connector-id').value = slackConnector ? slackConnector.connectorId : '';\n el('slack-edit-connector-id-field').hidden = !slackConnector;\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-connector-id-input').value = google.connectorId;\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 connectorReused = githubConnectorUsesDeployment(values);\n const connectorId = connectorReused ? requiredInput('github-edit-connector-id', 'the Logto connector ID') : null;\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 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 (appChanged && previous.configuredUrls?.webhookActive !== false && !webhookSecret) 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, connectorId, appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, ...(appChanged ? { configuredUrls: undefined } : {}) },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\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 connectorId = values.logto && values.logto.slackConnector\n ? requiredInput('slack-edit-connector-id', 'the Logto connector ID')\n : null;\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, connectorId, appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId, ...(changed ? { configuredUrls: undefined } : {}) },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\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('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 HTTPS Logto, Web, Control Plane, and Relay URLs. Use Google locally or expose the stack through a trusted HTTPS endpoint.';\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 })\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 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: el('bootstrap-google-secret').value })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto();\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 saved HTTPS Logto, Web, Control Plane, and Relay URLs.');\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 confirmationFields = result.unverified || [];\n el('confirm-github').hidden = ![...result.missing, ...confirmationFields].some((field) => field === 'callback_urls' || field === 'setup_url' || field === 'webhook_active');\n const label = result.missing.length ? 'Update required' : 'Not verified';\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.' : 'Confirm the callback, setup, and webhook-active settings 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 connectorId: bootstrapInfo.slackConnectorId,\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 confirmGithub() {\n await json(await fetch(api + '/confirm/github-urls', { method: 'POST', headers: bearer() }));\n el('confirm-github').hidden = true;\n await load();\n await checkGithub();\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 if (result.status === 'pass') await load();\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 expected = currentStatus ? currentStatus.providerExpectations.google : { redirects: [] };\n const callbacksMatch = google && same(google.configuredRedirectUris, expected.redirects);\n const passed = connector && connector.status === 'pass' && callbacksMatch;\n const connectorDiff = connector && connector.diff\n ? connector.diff.filter((item) => item.field.toLowerCase().startsWith('google'))\n : [];\n showDiff(\n 'google-drift',\n [\n ...connectorDiff,\n ...(callbacksMatch || !google\n ? []\n : [{ field: 'Authorized redirect URIs', current: google.configuredRedirectUris, expected: expected.redirects }])\n ]\n );\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Matches' : 'Update required');\n message(passed ? 'Google callbacks and the Logto connector match.' : 'Google or its Logto connector needs an update.', !passed);\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 match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Credentials match' : result.status === 'fail' ? 'Invalid credentials' : 'Could not check');\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 connectorId: requiredInput('google-connector-id-input', 'the Logto connector ID'),\n clientId: el('google-id').value,\n ...(secret ? { clientSecret: secret } : {})\n })\n }));\n el('google-secret').value = '';\n await reconcileLogto();\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() {\n return json(await fetch(api + '/reconcile/logto', { method: 'POST', headers: bearer() }));\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, CORS, 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 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 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 saved = 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 await load();\n message('Secret replaced. Restart AgentConnect to apply it.');\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('confirm-github').onclick = () => confirmGithub().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>"])));
61951
62030
  //#endregion
61952
62031
  //#region src/admin/logto-management.ts
61953
62032
  /** Minimal Logto Management API client for setup reconciliation and ADMIN claim. */
61954
- const LOGTO_GITHUB_CONNECTOR_ID = "agentconnect-github";
61955
- const LOGTO_GOOGLE_CONNECTOR_ID = "agentconnect-google";
61956
- const LOGTO_SLACK_CONNECTOR_ID = "agentconnect-slack";
61957
62033
  const MANAGED_APP_TAG = "agentconnectSetup";
61958
62034
  const MANAGED_APP_TAG_VALUE = {
61959
62035
  version: 1,
61960
62036
  resource: "browser"
61961
62037
  };
62038
+ function isManagedConnectorTarget(target) {
62039
+ return target === "github" || target === "google" || target === "slack";
62040
+ }
61962
62041
  var LogtoManagementError = class extends Error {
61963
62042
  code;
61964
62043
  status;
@@ -61978,6 +62057,13 @@ function stringArray(value) {
61978
62057
  function sameStrings$1(left, right) {
61979
62058
  return JSON.stringify(stringArray(left)) === JSON.stringify(right);
61980
62059
  }
62060
+ function addDiff(diff, field, current, expected, matches = JSON.stringify(current) === JSON.stringify(expected)) {
62061
+ if (!matches) diff.push({
62062
+ field,
62063
+ current: current ?? null,
62064
+ expected: expected ?? null
62065
+ });
62066
+ }
61981
62067
  function hasManagedTag(application) {
61982
62068
  const tag = asRecord(application.customData[MANAGED_APP_TAG]);
61983
62069
  return tag.version === MANAGED_APP_TAG_VALUE.version && tag.resource === MANAGED_APP_TAG_VALUE.resource;
@@ -62009,29 +62095,32 @@ function parseConnector(value) {
62009
62095
  function desiredConnector(target, desired) {
62010
62096
  if (target === "github") {
62011
62097
  if (!desired.github) throw new LogtoManagementError("GITHUB_CONNECTOR_CREDENTIALS_REQUIRED", "the Logto GitHub connector needs the deployment GitHub App client id and secret");
62098
+ const { connectorId: id, ...config } = desired.github;
62012
62099
  return {
62013
- id: LOGTO_GITHUB_CONNECTOR_ID,
62100
+ id,
62014
62101
  connectorId: "github-universal",
62015
- config: desired.github
62102
+ config
62016
62103
  };
62017
62104
  }
62018
62105
  if (target === "google") {
62019
62106
  if (!desired.google) throw new LogtoManagementError("GOOGLE_CONNECTOR_CREDENTIALS_REQUIRED", "the Logto Google connector needs a Google OAuth client id and secret");
62107
+ const { connectorId: id, ...config } = desired.google;
62020
62108
  return {
62021
- id: LOGTO_GOOGLE_CONNECTOR_ID,
62109
+ id,
62022
62110
  connectorId: "google-universal",
62023
- config: desired.google
62111
+ config
62024
62112
  };
62025
62113
  }
62026
62114
  if (target === "slack") {
62027
62115
  if (!desired.slack) throw new LogtoManagementError("SLACK_CONNECTOR_CREDENTIALS_REQUIRED", "the Logto Slack connector needs the deployment Slack App client id and secret");
62116
+ const { connectorId: id, ...config } = desired.slack;
62028
62117
  return {
62029
- id: LOGTO_SLACK_CONNECTOR_ID,
62118
+ id,
62030
62119
  connectorId: "slack-universal",
62031
- config: desired.slack
62120
+ config
62032
62121
  };
62033
62122
  }
62034
- throw new LogtoManagementError("SOCIAL_CONNECTOR_UNSUPPORTED", `automatic creation is not supported for the missing Logto social connector ${target}`);
62123
+ throw new Error(`unsupported managed Logto connector target: ${target}`);
62035
62124
  }
62036
62125
  function connectorMatches(connector, desired) {
62037
62126
  return connector.connectorId === desired.connectorId && JSON.stringify(connector.config) === JSON.stringify(desired.config);
@@ -62081,24 +62170,77 @@ var LogtoAdminClaimClient = class {
62081
62170
  const application = await this.findApplication(desired.applicationId);
62082
62171
  const connectors = await this.listConnectors();
62083
62172
  const signInExperience = await this.getSignInExperience();
62173
+ const applicationDiff = [];
62174
+ if (!application) applicationDiff.push({
62175
+ field: "SPA application",
62176
+ current: "Missing",
62177
+ expected: desired.applicationId ?? desired.applicationName
62178
+ });
62179
+ else {
62180
+ addDiff(applicationDiff, "Redirect URIs", stringArray(application.oidcClientMetadata.redirectUris), desired.redirectUris, sameStrings$1(application.oidcClientMetadata.redirectUris, desired.redirectUris));
62181
+ addDiff(applicationDiff, "Post sign-out redirect URIs", stringArray(application.oidcClientMetadata.postLogoutRedirectUris), desired.postLogoutRedirectUris, sameStrings$1(application.oidcClientMetadata.postLogoutRedirectUris, desired.postLogoutRedirectUris));
62182
+ addDiff(applicationDiff, "CORS allowed origins", stringArray(application.customClientMetadata.corsAllowedOrigins), desired.corsAllowedOrigins, sameStrings$1(application.customClientMetadata.corsAllowedOrigins, desired.corsAllowedOrigins));
62183
+ addDiff(applicationDiff, "AgentConnect managed application", hasManagedTag(application), true);
62184
+ }
62185
+ const signInExperienceDiff = [];
62186
+ addDiff(signInExperienceDiff, "Sign-in methods", signInExperience.signIn.methods ?? null, []);
62187
+ addDiff(signInExperienceDiff, "Sign-up identifiers", signInExperience.signUp.identifiers ?? null, []);
62188
+ addDiff(signInExperienceDiff, "Secondary identifiers", signInExperience.signUp.secondaryIdentifiers ?? [], []);
62189
+ addDiff(signInExperienceDiff, "Password sign-up", signInExperience.signUp.password ?? null, false);
62190
+ addDiff(signInExperienceDiff, "Sign-up verification", signInExperience.signUp.verify ?? null, false);
62191
+ addDiff(signInExperienceDiff, "Skip required identifiers", signInExperience.socialSignIn.skipRequiredIdentifiers ?? null, true);
62192
+ addDiff(signInExperienceDiff, "Social providers", signInExperience.socialSignInConnectorTargets, desired.socialProviders, sameStrings$1(signInExperience.socialSignInConnectorTargets, desired.socialProviders));
62193
+ addDiff(signInExperienceDiff, "Sign-in mode", signInExperience.signInMode ?? null, "SignInAndRegister");
62084
62194
  return {
62085
62195
  application: {
62086
62196
  id: application?.id ?? null,
62087
62197
  exists: application !== void 0,
62088
- matches: application ? this.applicationMatches(application, desired) : false
62198
+ matches: application ? this.applicationMatches(application, desired) : false,
62199
+ diff: applicationDiff
62089
62200
  },
62090
62201
  connectors: desired.socialProviders.map((target) => {
62091
62202
  const matches = connectors.filter((connector) => connector.target === target);
62092
62203
  if (matches.length > 1) throw new LogtoManagementError("SOCIAL_CONNECTOR_AMBIGUOUS", `Logto has more than one social connector for target ${target}`);
62093
62204
  const connector = matches[0];
62205
+ if (!isManagedConnectorTarget(target)) return {
62206
+ target,
62207
+ id: connector?.id ?? null,
62208
+ exists: connector !== void 0,
62209
+ matches: connector !== void 0,
62210
+ diff: connector ? [] : [{
62211
+ field: `${target} connector`,
62212
+ current: "Missing",
62213
+ expected: "Configured"
62214
+ }]
62215
+ };
62216
+ const expected = desiredConnector(target, desired);
62217
+ const diff = [];
62218
+ if (!connector) diff.push({
62219
+ field: `${target} connector`,
62220
+ current: "Missing",
62221
+ expected: expected.id
62222
+ });
62223
+ else {
62224
+ addDiff(diff, `${target} connector ID`, connector.id, expected.id);
62225
+ addDiff(diff, `${target} connector type`, connector.connectorId, expected.connectorId);
62226
+ addDiff(diff, `${target} client ID`, connector.config.clientId ?? null, expected.config.clientId ?? null);
62227
+ if (target === "slack") addDiff(diff, "Slack OIDC scope", connector.config.scope ?? null, expected.config.scope ?? null);
62228
+ if (!connectorMatches(connector, expected) && diff.length === 0) diff.push({
62229
+ field: `${target} OAuth client settings`,
62230
+ current: "*** (different)",
62231
+ expected: "***"
62232
+ });
62233
+ }
62094
62234
  return {
62095
62235
  target,
62096
62236
  id: connector?.id ?? null,
62097
62237
  exists: connector !== void 0,
62098
- matches: connector ? connectorMatches(connector, desiredConnector(target, desired)) : false
62238
+ matches: connector ? connector.id === expected.id && connectorMatches(connector, expected) : false,
62239
+ diff
62099
62240
  };
62100
62241
  }),
62101
- signInExperienceMatches: this.signInExperienceMatches(signInExperience, desired.socialProviders)
62242
+ signInExperienceMatches: this.signInExperienceMatches(signInExperience, desired.socialProviders),
62243
+ signInExperienceDiff
62102
62244
  };
62103
62245
  }
62104
62246
  async reconcileSetup(desired) {
@@ -62233,6 +62375,16 @@ var LogtoAdminClaimClient = class {
62233
62375
  for (const target of desired.socialProviders) {
62234
62376
  const matches = existing.filter((connector) => connector.target === target);
62235
62377
  if (matches.length > 1) throw new LogtoManagementError("SOCIAL_CONNECTOR_AMBIGUOUS", `Logto has more than one social connector for target ${target}`);
62378
+ if (!isManagedConnectorTarget(target)) {
62379
+ if (!matches[0]) throw new LogtoManagementError("SOCIAL_CONNECTOR_UNSUPPORTED", `automatic creation is not supported for the missing Logto social connector ${target}`);
62380
+ result.push({
62381
+ target,
62382
+ id: matches[0].id,
62383
+ created: false,
62384
+ changed: false
62385
+ });
62386
+ continue;
62387
+ }
62236
62388
  const expected = desiredConnector(target, desired);
62237
62389
  if (matches[0] && connectorMatches(matches[0], expected)) {
62238
62390
  result.push({
@@ -62438,6 +62590,7 @@ const CreateSlackBody = strictObject({
62438
62590
  connectLogto: boolean().optional().default(false)
62439
62591
  });
62440
62592
  const ConfigureGoogleBody = strictObject({
62593
+ connectorId: string().trim().min(1).max(500).optional(),
62441
62594
  clientId: string().trim().min(1).max(500),
62442
62595
  clientSecret: string().min(1).max(1e4).optional()
62443
62596
  });
@@ -62538,8 +62691,8 @@ function logtoConfig(record, endpoint) {
62538
62691
  function appendPath(origin, path) {
62539
62692
  return new URL(path, `${origin.replace(/\/+$/, "")}/`).toString();
62540
62693
  }
62541
- function googleRedirectUris(endpoint) {
62542
- return [appendPath(endpoint, `/callback/${LOGTO_GOOGLE_CONNECTOR_ID}`), appendPath(endpoint, `/account/callback/social/${LOGTO_GOOGLE_CONNECTOR_ID}`)];
62694
+ function googleRedirectUris(endpoint, connectorId) {
62695
+ return [appendPath(endpoint, `/callback/${connectorId}`), appendPath(endpoint, `/account/callback/social/${connectorId}`)];
62543
62696
  }
62544
62697
  function sameStrings(left, right) {
62545
62698
  return JSON.stringify([...left ?? []].sort()) === JSON.stringify([...right].sort());
@@ -62580,14 +62733,17 @@ function logtoSetup(record, tenantAdminUrl, services, endpoint) {
62580
62733
  corsAllowedOrigins: [.../* @__PURE__ */ new Set([webOrigin, adminOrigin])],
62581
62734
  socialProviders: [...browser.socialProviders],
62582
62735
  ...logto.githubConnector && githubSecret ? { github: {
62736
+ connectorId: logto.githubConnector.connectorId,
62583
62737
  clientId: logto.githubConnector.clientId,
62584
62738
  clientSecret: githubSecret
62585
62739
  } } : {},
62586
62740
  ...logto.googleConnector && googleSecret ? { google: {
62741
+ connectorId: logto.googleConnector.connectorId,
62587
62742
  clientId: logto.googleConnector.clientId,
62588
62743
  clientSecret: googleSecret
62589
62744
  } } : {},
62590
62745
  ...logto.slackConnector && slackSecret ? { slack: {
62746
+ connectorId: logto.slackConnector.connectorId,
62591
62747
  clientId: logto.slackConnector.clientId,
62592
62748
  clientSecret: slackSecret,
62593
62749
  scope: "openid profile email"
@@ -62595,6 +62751,31 @@ function logtoSetup(record, tenantAdminUrl, services, endpoint) {
62595
62751
  }
62596
62752
  };
62597
62753
  }
62754
+ function withResolvedConnectorIds(values, connectors) {
62755
+ if (!values.logto) return values;
62756
+ const connectorIds = new Map(connectors.map((connector) => [connector.target, connector.id]));
62757
+ const githubId = connectorIds.get("github");
62758
+ const googleId = connectorIds.get("google");
62759
+ const slackId = connectorIds.get("slack");
62760
+ return {
62761
+ ...values,
62762
+ logto: {
62763
+ ...values.logto,
62764
+ ...values.logto.githubConnector && githubId ? { githubConnector: {
62765
+ ...values.logto.githubConnector,
62766
+ connectorId: githubId
62767
+ } } : {},
62768
+ ...values.logto.googleConnector && googleId ? { googleConnector: {
62769
+ ...values.logto.googleConnector,
62770
+ connectorId: googleId
62771
+ } } : {},
62772
+ ...values.logto.slackConnector && slackId ? { slackConnector: {
62773
+ ...values.logto.slackConnector,
62774
+ connectorId: slackId
62775
+ } } : {}
62776
+ }
62777
+ };
62778
+ }
62598
62779
  /** Build the thin UI/API over the shared typed deployment-config service. */
62599
62780
  function buildTenantAdminServer(deps, options = {}) {
62600
62781
  const app = (0, import_fastify.default)({
@@ -62632,14 +62813,14 @@ function buildTenantAdminServer(deps, options = {}) {
62632
62813
  return buildGithubAppManifest(providerAppConfig(localAuthBootstrap.services), "AgentConnect", new URL("/api/v1/create/github/callback", deps.publicUrl).toString(), connectLogto && browser ? {
62633
62814
  webUrl: localAuthBootstrap.services.web,
62634
62815
  logtoEndpoint,
62635
- connectorId: LOGTO_GITHUB_CONNECTOR_ID
62816
+ connectorId: values.logto.githubConnector.connectorId
62636
62817
  } : void 0);
62637
62818
  };
62638
62819
  const expectedSlackManifest = (values) => {
62639
62820
  const browser = values.logto?.browser;
62640
62821
  return buildSlackDeploymentManifest(slackProviderAppConfig(localAuthBootstrap.services), "AgentConnect", values.logto?.slackConnector && browser ? {
62641
62822
  logtoEndpoint,
62642
- connectorId: LOGTO_SLACK_CONNECTOR_ID
62823
+ connectorId: values.logto.slackConnector.connectorId
62643
62824
  } : void 0);
62644
62825
  };
62645
62826
  const providerExpectations = (values) => {
@@ -62657,7 +62838,7 @@ function buildTenantAdminServer(deps, options = {}) {
62657
62838
  slack,
62658
62839
  google: values.logto?.browser ? {
62659
62840
  origins: [logtoEndpoint],
62660
- redirects: googleRedirectUris(logtoEndpoint)
62841
+ redirects: googleRedirectUris(logtoEndpoint, values.logto.googleConnector?.connectorId ?? "agentconnect-google")
62661
62842
  } : {
62662
62843
  origins: [],
62663
62844
  redirects: []
@@ -62751,8 +62932,10 @@ function buildTenantAdminServer(deps, options = {}) {
62751
62932
  }
62752
62933
  });
62753
62934
  app.get("/api/v1/bootstrap-info", { preHandler: requireLocal }, async () => {
62754
- const redirectUris = googleRedirectUris(logtoEndpoint);
62755
62935
  const current = await deps.store.getAdmin();
62936
+ const googleConnectorId = current?.values.logto?.googleConnector?.connectorId ?? "agentconnect-google";
62937
+ const slackConnectorId = current?.values.logto?.slackConnector?.connectorId ?? "agentconnect-slack";
62938
+ const redirectUris = googleRedirectUris(logtoEndpoint, googleConnectorId);
62756
62939
  const logtoConfigured = Boolean(current?.values.logto?.managementAppId && current.secrets.some((secret) => secret.key === "logto.managementAppSecret" && secret.configured));
62757
62940
  let githubAvailable = true;
62758
62941
  let githubWebhookActive = false;
@@ -62776,6 +62959,7 @@ function buildTenantAdminServer(deps, options = {}) {
62776
62959
  logtoAdminEndpoint,
62777
62960
  logtoConfigured,
62778
62961
  logtoManagementAppId: current?.values.logto?.managementAppId ?? null,
62962
+ googleConnectorId,
62779
62963
  google: {
62780
62964
  javascriptOrigins: [logtoEndpoint],
62781
62965
  redirectUris
@@ -62783,7 +62967,8 @@ function buildTenantAdminServer(deps, options = {}) {
62783
62967
  githubAvailable,
62784
62968
  githubWebhookActive,
62785
62969
  slackAvailable,
62786
- slackLoginRedirectUrl: appendPath(slackLoginEndpoint, `/callback/${LOGTO_SLACK_CONNECTOR_ID}`)
62970
+ slackConnectorId,
62971
+ slackLoginRedirectUrl: appendPath(slackLoginEndpoint, `/callback/${slackConnectorId}`)
62787
62972
  };
62788
62973
  });
62789
62974
  app.get("/api/v1/deployment-config", { preHandler: requireConfigurationAccess }, async () => statusWithExpectations(await deps.store.getAdmin()));
@@ -62921,8 +63106,10 @@ function buildTenantAdminServer(deps, options = {}) {
62921
63106
  return serializeMutation(async () => {
62922
63107
  const current = await deps.store.getAdmin();
62923
63108
  if (!current?.values.logto?.browser) return problem(reply, 409, "save Logto browser configuration first");
62924
- const redirectUris = googleRedirectUris(logtoEndpoint);
63109
+ const connectorId = parsed.data.connectorId ?? current.values.logto.googleConnector?.connectorId ?? "agentconnect-google";
63110
+ const redirectUris = googleRedirectUris(logtoEndpoint, connectorId);
62925
63111
  const put = logtoGoogleConnectorPut(current, {
63112
+ ...parsed.data.connectorId ? { connectorId: parsed.data.connectorId } : {},
62926
63113
  clientId: parsed.data.clientId,
62927
63114
  ...parsed.data.clientSecret ? { clientSecret: parsed.data.clientSecret } : {},
62928
63115
  configuredRedirectUris: redirectUris
@@ -63140,16 +63327,62 @@ function buildTenantAdminServer(deps, options = {}) {
63140
63327
  const expectedUrls = githubConfiguredUrls(providerAppConfig(localAuthBootstrap.services), manifest);
63141
63328
  const confirmed = github.configuredUrls;
63142
63329
  const missing = [...audited.missing];
63143
- if (confirmed?.setupUrl !== expectedUrls.setupUrl) missing.push("setup_url");
63144
- if (!sameStrings(confirmed?.callbackUrls, expectedUrls.callbackUrls)) missing.push("callback_urls");
63145
- if (confirmed?.webhookActive !== expectedUrls.webhookActive) missing.push("webhook_active");
63330
+ const unverified = [];
63331
+ const diff = audited.diff.map(({ field, current, expected }) => ({
63332
+ field,
63333
+ current,
63334
+ expected
63335
+ }));
63336
+ if (!confirmed) {
63337
+ unverified.push("setup_url", "callback_urls", "webhook_active");
63338
+ diff.push({
63339
+ field: "Setup URL",
63340
+ current: "Not verified",
63341
+ expected: expectedUrls.setupUrl
63342
+ }, {
63343
+ field: "Callback URLs",
63344
+ current: "Not verified",
63345
+ expected: expectedUrls.callbackUrls
63346
+ }, {
63347
+ field: "Webhook active",
63348
+ current: "Not verified",
63349
+ expected: expectedUrls.webhookActive
63350
+ });
63351
+ } else {
63352
+ if (confirmed.setupUrl !== expectedUrls.setupUrl) {
63353
+ missing.push("setup_url");
63354
+ diff.push({
63355
+ field: "Setup URL",
63356
+ current: confirmed.setupUrl,
63357
+ expected: expectedUrls.setupUrl
63358
+ });
63359
+ }
63360
+ if (!sameStrings(confirmed.callbackUrls, expectedUrls.callbackUrls)) {
63361
+ missing.push("callback_urls");
63362
+ diff.push({
63363
+ field: "Callback URLs",
63364
+ current: confirmed.callbackUrls,
63365
+ expected: expectedUrls.callbackUrls
63366
+ });
63367
+ }
63368
+ if (confirmed.webhookActive !== expectedUrls.webhookActive) {
63369
+ missing.push("webhook_active");
63370
+ diff.push({
63371
+ field: "Webhook active",
63372
+ current: confirmed.webhookActive,
63373
+ expected: expectedUrls.webhookActive
63374
+ });
63375
+ }
63376
+ }
63146
63377
  return {
63147
63378
  provider: "github",
63148
- status: missing.length === 0 ? "pass" : "fail",
63379
+ status: missing.length > 0 ? "fail" : unverified.length > 0 ? "unknown" : "pass",
63149
63380
  missing: [...new Set(missing)],
63381
+ unverified,
63382
+ diff,
63150
63383
  expected: expectedUrls,
63151
63384
  settingsUrl: audited.app.settingsUrl,
63152
- note: "GitHub exposes permissions, events, and webhook URL to this check. Callback, setup, and webhook-active settings require operator confirmation after editing the App."
63385
+ note: "GitHub exposes permissions, events, external URL, and webhook URL to this check. Callback, setup, and webhook-active settings require operator confirmation."
63153
63386
  };
63154
63387
  });
63155
63388
  app.post("/api/v1/confirm/github-urls", { preHandler: requireConfigurationAccess }, async (_request, reply) => serializeMutation(async () => {
@@ -63198,8 +63431,13 @@ function buildTenantAdminServer(deps, options = {}) {
63198
63431
  const exported = await slackConfigApi.exportApp(parsed.data.configToken, slack.appId);
63199
63432
  if (!exported.ok) return problem(reply, 502, `Slack App settings could not be checked: ${exported.error}`);
63200
63433
  const actual = exported.manifest;
63434
+ const manifestDiff = diffSlackManifest(actual, manifest);
63201
63435
  const missing = auditSlackManifest(actual, manifest);
63202
63436
  const expected = slackConfiguredUrls(manifest);
63437
+ let observed = null;
63438
+ try {
63439
+ observed = slackConfiguredUrls(actual);
63440
+ } catch {}
63203
63441
  let revision = current.revision;
63204
63442
  if (missing.length === 0 && JSON.stringify(slack.configuredUrls) !== JSON.stringify(expected)) revision = (await deps.store.replace({
63205
63443
  expectedRevision: current.revision,
@@ -63215,6 +63453,12 @@ function buildTenantAdminServer(deps, options = {}) {
63215
63453
  provider: "slack",
63216
63454
  status: missing.length === 0 ? "pass" : "fail",
63217
63455
  missing,
63456
+ diff: manifestDiff.map(({ field, current, expected }) => ({
63457
+ field,
63458
+ current,
63459
+ expected
63460
+ })),
63461
+ actual: observed,
63218
63462
  expected,
63219
63463
  settingsUrl: `https://api.slack.com/apps/${encodeURIComponent(slack.appId)}`,
63220
63464
  revision,
@@ -63267,10 +63511,10 @@ function buildTenantAdminServer(deps, options = {}) {
63267
63511
  },
63268
63512
  socialProviders: setup.socialProviders
63269
63513
  };
63270
- const nextValues = {
63514
+ const nextValues = withResolvedConnectorIds({
63271
63515
  ...runtime.values,
63272
63516
  auth
63273
- };
63517
+ }, reconciled.connectors);
63274
63518
  const configChanged = JSON.stringify(runtime.values) !== JSON.stringify(nextValues);
63275
63519
  const saved = configChanged ? await deps.store.replace({
63276
63520
  expectedRevision: runtime.revision,
@@ -63302,7 +63546,12 @@ function buildTenantAdminServer(deps, options = {}) {
63302
63546
  findings.push({
63303
63547
  id: "logto.configuration",
63304
63548
  status: "fail",
63305
- message: "Deployment configuration has not been saved."
63549
+ message: "Deployment configuration has not been saved.",
63550
+ diff: [{
63551
+ field: "Logto configuration",
63552
+ current: "Missing",
63553
+ expected: "Configured"
63554
+ }]
63306
63555
  });
63307
63556
  return report();
63308
63557
  }
@@ -63310,7 +63559,12 @@ function buildTenantAdminServer(deps, options = {}) {
63310
63559
  findings.push({
63311
63560
  id: "logto.configuration",
63312
63561
  status: "fail",
63313
- message: "Logto Management API configuration is missing."
63562
+ message: "Logto Management API configuration is missing.",
63563
+ diff: [{
63564
+ field: "Management API configuration",
63565
+ current: "Missing",
63566
+ expected: "Configured"
63567
+ }]
63314
63568
  });
63315
63569
  return report();
63316
63570
  }
@@ -63318,7 +63572,12 @@ function buildTenantAdminServer(deps, options = {}) {
63318
63572
  findings.push({
63319
63573
  id: "logto.configuration",
63320
63574
  status: "fail",
63321
- message: "Logto Management API application secret is missing."
63575
+ message: "Logto Management API application secret is missing.",
63576
+ diff: [{
63577
+ field: "Management App secret",
63578
+ current: "Not configured",
63579
+ expected: "***"
63580
+ }]
63322
63581
  });
63323
63582
  return report();
63324
63583
  }
@@ -63343,7 +63602,12 @@ function buildTenantAdminServer(deps, options = {}) {
63343
63602
  findings.push({
63344
63603
  id: "logto.client_credentials",
63345
63604
  status,
63346
- message: status === "fail" ? `Logto rejected the Management API client_credentials grant (HTTP ${upstreamStatus}).` : "The Management API client_credentials grant could not be verified because Logto or the network is unavailable."
63605
+ message: status === "fail" ? `Logto rejected the Management API client_credentials grant (HTTP ${upstreamStatus}).` : "The Management API client_credentials grant could not be verified because Logto or the network is unavailable.",
63606
+ diff: [{
63607
+ field: "Management API credentials",
63608
+ current: status === "fail" ? `Rejected (HTTP ${upstreamStatus})` : "Could not check",
63609
+ expected: "Accepted"
63610
+ }]
63347
63611
  });
63348
63612
  return report();
63349
63613
  }
@@ -63361,7 +63625,12 @@ function buildTenantAdminServer(deps, options = {}) {
63361
63625
  findings.push({
63362
63626
  id: "logto.roles_read",
63363
63627
  status,
63364
- message: status === "fail" ? `The Management API application cannot read Logto roles (HTTP ${upstreamStatus}).` : "The Logto roles permission could not be verified because Logto or the network is unavailable."
63628
+ message: status === "fail" ? `The Management API application cannot read Logto roles (HTTP ${upstreamStatus}).` : "The Logto roles permission could not be verified because Logto or the network is unavailable.",
63629
+ diff: [{
63630
+ field: "Management API roles access",
63631
+ current: status === "fail" ? `Denied (HTTP ${upstreamStatus})` : "Could not check",
63632
+ expected: "Allowed"
63633
+ }]
63365
63634
  });
63366
63635
  return report();
63367
63636
  }
@@ -63372,13 +63641,29 @@ function buildTenantAdminServer(deps, options = {}) {
63372
63641
  } : {
63373
63642
  id: "logto.admin_role",
63374
63643
  status: "fail",
63375
- 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."
63644
+ 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.",
63645
+ diff: [{
63646
+ field: "ADMIN role",
63647
+ current: adminRole.exists ? {
63648
+ type: adminRole.type,
63649
+ default: adminRole.isDefault
63650
+ } : "Missing",
63651
+ expected: {
63652
+ type: "User",
63653
+ default: false
63654
+ }
63655
+ }]
63376
63656
  });
63377
63657
  if (!setup) {
63378
63658
  findings.push({
63379
63659
  id: "logto.setup_configuration",
63380
63660
  status: "fail",
63381
- message: "Logto browser desired state is missing."
63661
+ message: "Logto browser desired state is missing.",
63662
+ diff: [{
63663
+ field: "Browser application configuration",
63664
+ current: "Missing",
63665
+ expected: "Configured"
63666
+ }]
63382
63667
  });
63383
63668
  return report();
63384
63669
  }
@@ -63390,7 +63675,12 @@ function buildTenantAdminServer(deps, options = {}) {
63390
63675
  findings.push({
63391
63676
  id: "logto.setup_configuration",
63392
63677
  status,
63393
- message: status === "fail" ? "Logto browser resources are invalid or ambiguous." : "Logto browser resources could not be checked because Logto or the network is unavailable."
63678
+ message: status === "fail" ? "Logto browser resources are invalid or ambiguous." : "Logto browser resources could not be checked because Logto or the network is unavailable.",
63679
+ diff: [{
63680
+ field: "Browser resources",
63681
+ current: status === "fail" ? "Invalid or ambiguous" : "Could not check",
63682
+ expected: "Configured once"
63683
+ }]
63394
63684
  });
63395
63685
  return report();
63396
63686
  }
@@ -63401,7 +63691,8 @@ function buildTenantAdminServer(deps, options = {}) {
63401
63691
  } : {
63402
63692
  id: "logto.application",
63403
63693
  status: "fail",
63404
- message: setupInspection.application.exists ? "The selected Logto SPA does not match the expected redirects and CORS origins." : "The AgentConnect Logto SPA does not exist."
63694
+ message: setupInspection.application.exists ? "The selected Logto SPA does not match the expected redirects and CORS origins." : "The AgentConnect Logto SPA does not exist.",
63695
+ diff: setupInspection.application.diff
63405
63696
  });
63406
63697
  const invalidConnectors = setupInspection.connectors.filter((connector) => !connector.exists || !connector.matches).map((connector) => connector.target);
63407
63698
  findings.push(invalidConnectors.length === 0 ? {
@@ -63411,7 +63702,8 @@ function buildTenantAdminServer(deps, options = {}) {
63411
63702
  } : {
63412
63703
  id: "logto.connectors",
63413
63704
  status: "fail",
63414
- message: `Missing or mismatched Logto social connectors: ${invalidConnectors.join(", ")}.`
63705
+ message: `Missing or mismatched Logto social connectors: ${invalidConnectors.join(", ")}.`,
63706
+ diff: setupInspection.connectors.filter((connector) => !connector.exists || !connector.matches).flatMap((connector) => connector.diff)
63415
63707
  });
63416
63708
  findings.push(setupInspection.signInExperienceMatches ? {
63417
63709
  id: "logto.sign_in_experience",
@@ -63420,7 +63712,8 @@ function buildTenantAdminServer(deps, options = {}) {
63420
63712
  } : {
63421
63713
  id: "logto.sign_in_experience",
63422
63714
  status: "fail",
63423
- message: "Logto sign-in methods do not match the deployment configuration."
63715
+ message: "Logto sign-in methods do not match the deployment configuration.",
63716
+ diff: setupInspection.signInExperienceDiff
63424
63717
  });
63425
63718
  return report();
63426
63719
  });