@agentconnect.md/setup 1.36.1-rc.1 → 1.37.0-rc.3
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/README.md +10 -10
- package/dist/index.js +45 -45
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -37445,7 +37445,7 @@ function slackInteractionsRequestUrl(relayUrl) {
|
|
|
37445
37445
|
function normalizeSlackRelayUrl(relayUrl) {
|
|
37446
37446
|
return relayUrl.replace(/^ws(s?):\/\//i, "http$1://").replace(/\/+$/, "");
|
|
37447
37447
|
}
|
|
37448
|
-
/** One canonical Slack manifest shared by browser, Control Plane, and
|
|
37448
|
+
/** One canonical Slack manifest shared by browser, Control Plane, and Setup Server. */
|
|
37449
37449
|
function buildSlackAppManifest(name, options = {}) {
|
|
37450
37450
|
const appName = name.trim() || "agentconnect";
|
|
37451
37451
|
const displayName = options.displayName?.trim() || appName;
|
|
@@ -58307,7 +58307,7 @@ function makeSecretCipher(config) {
|
|
|
58307
58307
|
/**
|
|
58308
58308
|
* Safe, side-effect-free deployment-config subpath for operator tooling.
|
|
58309
58309
|
*
|
|
58310
|
-
* Importing the package root starts the Control Plane process, so
|
|
58310
|
+
* Importing the package root starts the Control Plane process, so Setup Server
|
|
58311
58311
|
* uses this narrow composition facade instead. The typed service remains the
|
|
58312
58312
|
* only place that validates the document, seals secrets, computes fingerprints,
|
|
58313
58313
|
* and applies an atomic replacement.
|
|
@@ -58391,7 +58391,7 @@ const LOGTO_GOOGLE_CONNECTOR_ID = "agentconnect-google";
|
|
|
58391
58391
|
const LOGTO_SLACK_CONNECTOR_ID = "agentconnect-slack";
|
|
58392
58392
|
//#endregion
|
|
58393
58393
|
//#region src/deployment-config-client.ts
|
|
58394
|
-
/** Typed deployment mutations used by the
|
|
58394
|
+
/** Typed deployment mutations used by the Setup Server provider workflows. */
|
|
58395
58395
|
const DeploymentConfigPutSchema = strictObject({
|
|
58396
58396
|
values: DeploymentConfigValuesV1Schema,
|
|
58397
58397
|
secrets: DeploymentSecretPatchSchema.optional()
|
|
@@ -62046,14 +62046,14 @@ function createRemoteJWKSet(url, options) {
|
|
|
62046
62046
|
return remoteJWKSet;
|
|
62047
62047
|
}
|
|
62048
62048
|
const ADMIN_ROLE = "ADMIN";
|
|
62049
|
-
var
|
|
62049
|
+
var SetupAuthError = class extends Error {
|
|
62050
62050
|
statusCode;
|
|
62051
62051
|
code;
|
|
62052
62052
|
constructor(statusCode, code, message) {
|
|
62053
62053
|
super(message);
|
|
62054
62054
|
this.statusCode = statusCode;
|
|
62055
62055
|
this.code = code;
|
|
62056
|
-
this.name = "
|
|
62056
|
+
this.name = "SetupAuthError";
|
|
62057
62057
|
}
|
|
62058
62058
|
};
|
|
62059
62059
|
function urlAtOrigin(value, origin) {
|
|
@@ -62098,7 +62098,7 @@ function extractAdminRoles(claim) {
|
|
|
62098
62098
|
if (typeof claim === "string") return claim.split(/[\s,]+/).filter(Boolean);
|
|
62099
62099
|
return [];
|
|
62100
62100
|
}
|
|
62101
|
-
var
|
|
62101
|
+
var SetupAuthenticator = class {
|
|
62102
62102
|
config;
|
|
62103
62103
|
verifyToken;
|
|
62104
62104
|
constructor(config, verifyToken = createOidcTokenVerifier()) {
|
|
@@ -62106,19 +62106,19 @@ var TenantAdminAuthenticator = class {
|
|
|
62106
62106
|
this.verifyToken = verifyToken;
|
|
62107
62107
|
}
|
|
62108
62108
|
/** Verify an OIDC identity. Only the one-time local bootstrap flow may call
|
|
62109
|
-
* this with `requireAdminRole=false`. Ordinary
|
|
62109
|
+
* this with `requireAdminRole=false`. Ordinary Setup routes always require it. */
|
|
62110
62110
|
async authenticate(authorization, requireAdminRole = true) {
|
|
62111
62111
|
const oidc = await this.config.get();
|
|
62112
|
-
if (!oidc) throw new
|
|
62113
|
-
if (!authorization?.startsWith("Bearer ")) throw new
|
|
62112
|
+
if (!oidc) throw new SetupAuthError(503, "ADMIN_OIDC_NOT_CONFIGURED", "Setup Server OIDC is not configured");
|
|
62113
|
+
if (!authorization?.startsWith("Bearer ")) throw new SetupAuthError(401, "INVALID_TOKEN", "missing bearer token");
|
|
62114
62114
|
let payload;
|
|
62115
62115
|
try {
|
|
62116
62116
|
payload = await this.verifyToken(authorization.slice(7), oidc);
|
|
62117
62117
|
} catch {
|
|
62118
|
-
throw new
|
|
62118
|
+
throw new SetupAuthError(401, "INVALID_TOKEN", "invalid bearer token");
|
|
62119
62119
|
}
|
|
62120
|
-
if (typeof payload.sub !== "string" || payload.sub.length === 0) throw new
|
|
62121
|
-
if (requireAdminRole && !extractAdminRoles(payload["roles"]).includes("ADMIN")) throw new
|
|
62120
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0) throw new SetupAuthError(401, "INVALID_TOKEN", "token is missing its subject");
|
|
62121
|
+
if (requireAdminRole && !extractAdminRoles(payload["roles"]).includes("ADMIN")) throw new SetupAuthError(403, "ADMIN_ROLE_REQUIRED", "ADMIN role required");
|
|
62122
62122
|
return {
|
|
62123
62123
|
subject: payload.sub,
|
|
62124
62124
|
...typeof payload.email === "string" ? { email: payload.email } : {}
|
|
@@ -62126,9 +62126,9 @@ var TenantAdminAuthenticator = class {
|
|
|
62126
62126
|
}
|
|
62127
62127
|
};
|
|
62128
62128
|
//#endregion
|
|
62129
|
-
//#region src/
|
|
62129
|
+
//#region src/server/config.ts
|
|
62130
62130
|
/**
|
|
62131
|
-
* Process bootstrap configuration for the
|
|
62131
|
+
* Process bootstrap configuration for the Setup Server.
|
|
62132
62132
|
*
|
|
62133
62133
|
* Product/provider settings intentionally do not live here: they are loaded
|
|
62134
62134
|
* from the deployment config singleton after initialization, or held only in
|
|
@@ -62156,7 +62156,7 @@ const SecureOriginSchema = HttpOriginSchema.superRefine((value, ctx) => {
|
|
|
62156
62156
|
message: "must use HTTPS unless it is loopback"
|
|
62157
62157
|
});
|
|
62158
62158
|
});
|
|
62159
|
-
const
|
|
62159
|
+
const SetupServerProcessConfigSchema = object({
|
|
62160
62160
|
NODE_ENV: _enum([
|
|
62161
62161
|
"development",
|
|
62162
62162
|
"test",
|
|
@@ -62164,8 +62164,8 @@ const TenantAdminProcessConfigSchema = object({
|
|
|
62164
62164
|
]).default("development"),
|
|
62165
62165
|
HOST: string().default("127.0.0.1"),
|
|
62166
62166
|
PORT: number().int().min(1).max(65535).default(8091),
|
|
62167
|
-
|
|
62168
|
-
|
|
62167
|
+
SETUP_SERVER_URL: SecureOriginSchema.default("http://localhost:8091"),
|
|
62168
|
+
SETUP_SERVER_ALLOW_CONTAINER_PROXY: stringbool().default(false),
|
|
62169
62169
|
LOGTO_ADMIN_ENDPOINT: SecureOriginSchema.default("http://localhost:3002"),
|
|
62170
62170
|
DATABASE_URL: string().url().default("postgresql://agentconnect:agentconnect-local-only@127.0.0.1:5432/agentconnect?schema=public"),
|
|
62171
62171
|
SECRET_CIPHER: _enum(["none", "vault-transit"]).default("none"),
|
|
@@ -62190,8 +62190,8 @@ const TenantAdminProcessConfigSchema = object({
|
|
|
62190
62190
|
message: "SECRET_CIPHER=vault-transit requires exactly one of VAULT_TOKEN or VAULT_JWT_ROLE"
|
|
62191
62191
|
});
|
|
62192
62192
|
});
|
|
62193
|
-
function
|
|
62194
|
-
return
|
|
62193
|
+
function loadSetupServerProcessConfig(env = process.env) {
|
|
62194
|
+
return SetupServerProcessConfigSchema.parse(env);
|
|
62195
62195
|
}
|
|
62196
62196
|
//#endregion
|
|
62197
62197
|
//#region \0@oxc-project+runtime@0.138.0/helpers/esm/taggedTemplateLiteral.js
|
|
@@ -62199,12 +62199,12 @@ function _taggedTemplateLiteral(e, t) {
|
|
|
62199
62199
|
return t || (t = e.slice(0)), Object.freeze(Object.defineProperties(e, { raw: { value: Object.freeze(t) } }));
|
|
62200
62200
|
}
|
|
62201
62201
|
//#endregion
|
|
62202
|
-
//#region src/
|
|
62202
|
+
//#region src/server/html.ts
|
|
62203
62203
|
var _templateObject;
|
|
62204
62204
|
/** One deliberately small, dependency-free deployment administration page. */
|
|
62205
|
-
const TENANT_ADMIN_HTML = String.raw(_templateObject || (_templateObject = _taggedTemplateLiteral(["<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>AgentConnect deployment settings</title>\n <style>\n :root { color-scheme: light dark; font: 15px/1.5 system-ui, sans-serif; }\n body { max-width: 1280px; margin: 48px auto; padding: 0 20px 60px; }\n h1 { margin-bottom: 4px; } h2 { margin-top: 32px; } h3 { margin: 0 0 8px; }\n .muted { color: #777; } .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }\n button, .button { padding: 8px 13px; cursor: pointer; }\n .button { border: 1px solid #8888; border-radius: 4px; color: inherit; text-decoration: none; display: inline-block; }\n input, select { width: min(520px, 100%); padding: 7px; box-sizing: border-box; }\n input[type=\"checkbox\"] { width: auto; padding: 0; }\n .field { display: grid; gap: 4px; margin: 10px 0; }\n .provider-stack { display: grid; gap: 16px; }\n .panel { border: 1px solid #8886; border-radius: 8px; padding: 16px; }\n .section-head, .provider-head { display: flex; justify-content: space-between; gap: 16px; align-items: start; }\n .section-head h2, .provider-head h3 { margin: 0; }\n .provider-head p { margin: 4px 0 0; }\n .badge { flex: none; border: 1px solid #8886; border-radius: 999px; padding: 3px 9px; font-size: 13px; }\n .badge.pass { color: #198754; border-color: #19875466; background: #19875412; }\n .badge.warn { color: #a66b00; border-color: #d99b1366; background: #d99b1315; }\n .badge.fail { color: #c33; border-color: #c333; background: #c3331111; }\n .credentials { display: grid; grid-template-columns: minmax(160px, 220px) minmax(0, 1fr); gap: 7px 16px; margin: 16px 0; }\n .credentials dt { color: #777; }\n .credentials dd { margin: 0; overflow-wrap: anywhere; }\n .credentials code { user-select: all; }\n .redacted { font-family: ui-monospace, monospace; letter-spacing: .08em; }\n .secret-line, .value-line { display: flex; gap: 10px; align-items: center; min-height: 30px; }\n .edit-secret { padding: 2px 8px; font-size: 13px; }\n .secret-editor { display: flex; gap: 8px; flex-wrap: wrap; width: 100%; }\n .secret-editor input { width: min(420px, 100%); }\n .edit-configuration { padding: 2px 8px; font-size: 13px; }\n .startup-owned { color: #777; font-size: 13px; }\n .danger { color: #c33; border-color: #c336; }\n .subsection { margin-top: 16px; padding-top: 14px; border-top: 1px solid #8883; }\n textarea { width: min(720px, 100%); min-height: 100px; padding: 7px; box-sizing: border-box; }\n .uris { margin: 8px 0; padding-left: 20px; } .uris code { user-select: all; }\n .notice { border-left: 4px solid #d99b13; padding: 8px 12px; background: #d99b1315; }\n .diff-title { display: block; margin-bottom: 8px; }\n .config-diff { display: grid; gap: 1px; border: 1px solid #8884; border-radius: 6px; overflow: hidden; background: #8884; }\n .diff-row { display: grid; grid-template-columns: minmax(140px, .8fr) minmax(0, 1.2fr) minmax(0, 1.2fr); background: Canvas; }\n .diff-row > * { min-width: 0; padding: 7px 9px; overflow-wrap: anywhere; white-space: pre-wrap; }\n .diff-head { font-size: 12px; font-weight: 600; color: #777; }\n .diff-field { font-weight: 600; }\n .diff-value { font-family: ui-monospace, monospace; font-size: 13px; }\n pre { padding: 12px; border-radius: 6px; background: #8881; overflow-x: auto; user-select: all; }\n #message { white-space: pre-wrap; padding: 10px 0; min-height: 1.5em; }\n .error { color: #c33; } .ok { color: #198754; } .warn { color: #a66b00; }\n code { overflow-wrap: anywhere; }\n .admin-layout { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 32px; align-items: start; }\n .admin-nav { position: sticky; top: 24px; display: grid; gap: 3px; padding: 10px; border: 1px solid #8886; border-radius: 8px; background: Canvas; }\n .admin-nav a { padding: 7px 9px; border-radius: 5px; color: inherit; text-decoration: none; white-space: nowrap; }\n .admin-nav a:hover { background: #8882; }\n .admin-section { scroll-margin-top: 24px; }\n details.environment { margin: 0 0 24px; } details.environment summary { cursor: pointer; font-weight: 600; }\n @media (max-width: 760px) {\n body { margin-top: 24px; }\n .admin-layout { grid-template-columns: 1fr; gap: 18px; }\n .admin-nav { position: static; display: flex; overflow-x: auto; }\n .credentials { grid-template-columns: 1fr; gap: 2px; }\n .credentials dd { margin-bottom: 8px; }\n .diff-row { grid-template-columns: 1fr; }\n .diff-head { display: none; }\n .diff-value::before { display: block; margin-bottom: 2px; color: #777; font: 11px/1.4 system-ui, sans-serif; }\n .diff-current::before { content: 'Current'; }\n .diff-expected::before { content: 'Expected'; }\n }\n [hidden] { display: none !important; }\n </style>\n</head>\n<body>\n <section id=\"access\">\n <h1>AgentConnect Tenant Admin</h1>\n <p id=\"access-message\" class=\"muted\" aria-live=\"polite\">Checking Logto sign-in…</p>\n <div class=\"row\">\n <button id=\"login\" hidden>Sign in with Logto</button>\n <a id=\"open-logto\" class=\"button\" href=\"http://localhost:3002\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n <button id=\"show-bootstrap\" hidden>Continue setup</button>\n </div>\n\n <section id=\"bootstrap\" hidden>\n <h2>Set up sign-in</h2>\n <p id=\"bootstrap-progress\" class=\"muted\">Step 1 of 2</p>\n <div id=\"bootstrap-logto-step\" class=\"panel\">\n <h3>Connect Logto</h3>\n <p class=\"muted\">Enter the one-time Logto Management API credential. It is sealed in the deployment database and verified before continuing.</p>\n <label class=\"field\">Logto M2M App ID<input id=\"logto-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Logto M2M App Secret<input id=\"logto-app-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-management-api-resource\" type=\"url\" autocomplete=\"off\"></label>\n <p class=\"muted\">Logto Cloud uses <code>https://<tenant-id>.logto.app/api</code>. Keep <code>https://default.logto.app/api</code> for Logto OSS.</p>\n <button id=\"bootstrap-logto-submit\">Save Logto and continue</button>\n </div>\n <div id=\"bootstrap-provider-step\" hidden>\n <h3>Choose a sign-in provider</h3>\n <p class=\"muted\">Logto Management API access is ready. Configure one provider to enable sign-in.</p>\n <label class=\"field\">Sign-in provider\n <select id=\"bootstrap-provider\"><option value=\"google\">Google (works on localhost)</option><option value=\"github\">GitHub integration App</option><option id=\"bootstrap-slack-option\" value=\"slack\">Slack integration App</option></select>\n </label>\n <div id=\"bootstrap-google\" class=\"panel\">\n <h3>Google OAuth client</h3>\n <p class=\"muted\">Create a Web application client in Google Auth Platform, then paste its credentials here.</p>\n <p>Authorized JavaScript origin:</p><ul id=\"bootstrap-google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"bootstrap-google-redirects\" class=\"uris\"></ul>\n <a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a>\n <label class=\"field\">Client ID<input id=\"bootstrap-google-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client Secret<input id=\"bootstrap-google-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-google-submit\">Save Google OAuth and configure Logto</button>\n </div>\n <div id=\"bootstrap-github\" class=\"panel\" hidden>\n <h3>GitHub integration App</h3>\n <p id=\"bootstrap-github-note\" class=\"muted\">This creates one complete App for both GitHub sign-in and repository integration.</p>\n <label class=\"field\">Owner\n <select id=\"bootstrap-github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"bootstrap-github-org-field\" class=\"field\" hidden>Organization login<input id=\"bootstrap-github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"bootstrap-github-name\" value=\"AgentConnect\"></label>\n <button id=\"bootstrap-github-submit\">Create GitHub App and configure Logto</button>\n </div>\n <div id=\"bootstrap-slack\" class=\"panel\" hidden>\n <h3>Slack integration App</h3>\n <p id=\"bootstrap-slack-note\" class=\"muted\">Creates one complete Slack App for workspace installation and a separate Sign in with Slack OIDC flow.</p>\n <p>Logto redirect URI:</p><ul id=\"bootstrap-slack-redirects\" class=\"uris\"></ul>\n <label class=\"field\">App name<input id=\"bootstrap-slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"bootstrap-slack-token\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-slack-submit\">Create Slack App and configure Logto</button>\n </div>\n <button id=\"bootstrap-back\">Back to Logto credentials</button>\n </div>\n </section>\n </section>\n\n <main id=\"admin\" hidden>\n <div class=\"admin-layout\">\n <nav class=\"admin-nav\" aria-label=\"Deployment settings\">\n <a href=\"#startup-section\">Startup</a>\n <a href=\"#logto-section\">Logto</a>\n <a href=\"#github-section\">GitHub</a>\n <a href=\"#slack-section\">Slack</a>\n <a href=\"#google-section\">Google</a>\n <a href=\"#feishu-section\">Feishu</a>\n <a href=\"#lark-section\">Lark</a>\n <a href=\"#options-section\">Options</a>\n </nav>\n <div class=\"admin-content\">\n <h1>AgentConnect deployment settings</h1>\n <p class=\"muted\">Saved settings take effect after the stack is restarted.</p>\n <div class=\"row\">\n <button id=\"logout\">Log out</button>\n </div>\n <div id=\"message\" aria-live=\"polite\"></div>\n\n <section id=\"editor\" hidden>\n <details id=\"startup-section\" class=\"environment admin-section\" open>\n <summary>Startup environment</summary>\n <p class=\"notice\">Public service URLs come from <code>.env</code>. Provider callbacks below are derived from these values.</p>\n <pre id=\"startup-environment\"></pre>\n </details>\n\n <section id=\"logto-section\" class=\"admin-section\" aria-labelledby=\"logto-heading\">\n <div class=\"section-head\">\n <div><h2 id=\"logto-heading\">Logto</h2><p class=\"muted\">Authentication and Tenant Admin access.</p></div>\n <span id=\"logto-match\" class=\"badge\">Not checked</span>\n </div>\n <div class=\"panel\">\n <dl class=\"credentials\">\n <dt>Management endpoint</dt><dd class=\"value-line\"><code id=\"logto-management-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>Management App ID</dt><dd class=\"value-line\"><code id=\"logto-management-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management API resource</dt><dd class=\"value-line\"><code id=\"logto-management-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management App secret</dt><dd class=\"secret-line\"><span id=\"logto-management-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.managementAppSecret\" data-secret-display=\"logto-management-secret-display\">Edit</button></dd>\n <dt>Sign-in endpoint</dt><dd class=\"value-line\"><code id=\"logto-browser-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>SPA App ID</dt><dd class=\"value-line\"><code id=\"logto-browser-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Browser API resource</dt><dd class=\"value-line\"><code id=\"logto-browser-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n </dl>\n <div id=\"logto-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Logto configuration</h3>\n <label class=\"field\">Management App ID<input id=\"logto-edit-management-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-edit-management-resource\" autocomplete=\"off\"></label>\n <label class=\"field\">New Management App secret<input id=\"logto-edit-management-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when management identity changes\"></label>\n <label class=\"field\">SPA App ID<input id=\"logto-edit-browser-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Browser API resource<input id=\"logto-edit-browser-resource\" autocomplete=\"off\" placeholder=\"Optional\"></label>\n <div class=\"row\"><button id=\"save-logto-configuration\">Save configuration</button><button id=\"cancel-logto-configuration\">Cancel</button></div>\n </div>\n <p id=\"logto-status\" class=\"muted\">Checking redirects and sign-in settings…</p>\n <div id=\"logto-drift\" class=\"notice\" hidden></div>\n <div class=\"row\">\n <button id=\"check-logto\">Check match</button>\n <button id=\"reconcile-logto\">Apply expected settings</button>\n <a id=\"logto-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n </div>\n </div>\n </section>\n\n <h2>Providers</h2>\n <div class=\"provider-stack\">\n <section id=\"github-section\" class=\"panel admin-section\" aria-labelledby=\"github-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"github-heading\">GitHub</h3><p class=\"muted\">Repository integration and optional Logto sign-in.</p></div>\n <span id=\"github-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"github-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>App slug</dt><dd class=\"value-line\"><code id=\"github-app-slug\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"github-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Webhook delivery</dt><dd class=\"value-line\"><code id=\"github-webhook-enabled\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"github-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.clientSecret\" data-secret-display=\"github-client-secret-display\">Edit</button></dd>\n <dt>Private key</dt><dd class=\"secret-line\"><span id=\"github-private-key-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.privateKeyB64\" data-secret-display=\"github-private-key-display\">Edit</button></dd>\n <dt>Webhook secret</dt><dd class=\"secret-line\"><span id=\"github-webhook-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.webhookSecret\" data-secret-display=\"github-webhook-secret-display\">Edit</button></dd>\n <dt>Logto connector secret</dt><dd class=\"secret-line\"><span id=\"github-logto-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.githubConnectorClientSecret\" data-secret-display=\"github-logto-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"github-status\" class=\"muted\"></p>\n <div id=\"github-drift\" class=\"notice\" hidden></div>\n <div id=\"github-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit GitHub App identity</h3>\n <label class=\"field\">App ID<input id=\"github-edit-app-id\" inputmode=\"numeric\" autocomplete=\"off\"></label>\n <label class=\"field\">App slug<input id=\"github-edit-slug\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"github-edit-client-id\" autocomplete=\"off\"></label>\n <label><input id=\"github-edit-webhook-enabled\" type=\"checkbox\"> Enable Relay webhook delivery</label>\n <label class=\"field\">New client secret<input id=\"github-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when App or Client ID changes\"></label>\n <label class=\"field\">New private key (base64)<textarea id=\"github-edit-private-key\" autocomplete=\"off\" placeholder=\"Required when App ID changes\"></textarea></label>\n <label class=\"field\">New webhook secret<input id=\"github-edit-webhook-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when an active webhook App ID changes\"></label>\n <label id=\"github-edit-logto-secret-field\" class=\"field\" hidden>New Logto connector client secret<input id=\"github-edit-logto-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when this App is also used for sign-in\"></label>\n <div class=\"row\"><button id=\"save-github-configuration\">Save configuration</button><button id=\"cancel-github-configuration\">Cancel</button></div>\n </div>\n <div id=\"github-create-controls\" class=\"subsection\">\n <label class=\"field\">Owner\n <select id=\"github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"github-org-field\" class=\"field\" hidden>Organization login<input id=\"github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"github-name\" value=\"AgentConnect\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-github\">Create GitHub App</button>\n <button id=\"connect-github-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-github\" hidden>Check match</button>\n <button id=\"clear-github\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"github-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open GitHub settings</a>\n </div>\n </section>\n\n <section id=\"slack-section\" class=\"panel admin-section\" aria-labelledby=\"slack-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"slack-heading\">Slack</h3><p class=\"muted\">One App for workspace integration and Logto sign-in.</p></div>\n <span id=\"slack-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"slack-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"slack-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"slack-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.clientSecret\" data-secret-display=\"slack-client-secret-display\">Edit</button></dd>\n <dt>Signing secret</dt><dd class=\"secret-line\"><span id=\"slack-signing-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.signingSecret\" data-secret-display=\"slack-signing-secret-display\">Edit</button></dd>\n <dt>Logto sign-in</dt><dd id=\"slack-logto-status\">Not configured</dd>\n </dl>\n <p id=\"slack-status\" class=\"muted\"></p>\n <div id=\"slack-drift\" class=\"notice\" hidden></div>\n <div id=\"slack-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Slack App identity</h3>\n <label class=\"field\">App ID<input id=\"slack-edit-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"slack-edit-client-id\" autocomplete=\"off\"></label>\n <label class=\"field\">New client secret<input id=\"slack-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <label class=\"field\">New signing secret<input id=\"slack-edit-signing-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <div class=\"row\"><button id=\"save-slack-configuration\">Save configuration</button><button id=\"cancel-slack-configuration\">Cancel</button></div>\n </div>\n <div class=\"subsection\">\n <label id=\"slack-name-field\" class=\"field\">App name<input id=\"slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"slack-token\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Used only for create or check\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-slack\">Create Slack App</button>\n <button id=\"connect-slack-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-slack\" hidden>Check match</button>\n <button id=\"clear-slack\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"slack-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Slack settings</a>\n </div>\n </section>\n\n <section id=\"google-section\" class=\"panel admin-section\" aria-labelledby=\"google-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"google-heading\">Google</h3><p class=\"muted\">OAuth client used by the Logto Google connector.</p></div>\n <span id=\"google-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"google-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"google\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"google-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.googleConnectorClientSecret\" data-secret-display=\"google-client-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"google-status\" class=\"muted\"></p>\n <div id=\"google-drift\" class=\"notice\" hidden></div>\n <p>Authorized JavaScript origin:</p><ul id=\"google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"google-redirects\" class=\"uris\"></ul>\n <p class=\"muted\">Google does not expose OAuth client redirect settings through an API. Copy these required values into Google Auth Platform; Tenant Admin can verify only the Logto connector.</p>\n <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a></div>\n <div id=\"google-config-controls\" class=\"subsection\">\n <label class=\"field\">Client ID<input id=\"google-id\" autocomplete=\"off\"></label>\n <label id=\"google-initial-secret-field\" class=\"field\">Client secret<input id=\"google-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when Client ID changes\"></label>\n </div>\n <div class=\"row\"><button id=\"save-google\">Save Google client</button><button id=\"cancel-google-configuration\" hidden>Cancel</button><button id=\"check-google\" hidden>Check match</button><button id=\"clear-google\" class=\"danger\" hidden>Clear configuration</button></div>\n </section>\n\n <section id=\"feishu-section\" class=\"panel admin-section\" aria-labelledby=\"feishu-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"feishu-heading\">Feishu</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Feishu, OAuth callbacks, and published App setup.</p></div>\n <span id=\"feishu-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"feishu-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"feishu\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"feishu-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"feishu.loginAppSecret\" data-secret-display=\"feishu-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"feishu-login-status\" class=\"muted\"></p>\n <div id=\"feishu-drift\" class=\"notice\" hidden></div>\n <div id=\"feishu-config-controls\" class=\"subsection\">\n <label id=\"feishu-create-name-field\" class=\"field\">New App name<input id=\"feishu-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"feishu-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"feishu-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-feishu-login-app\">Create Feishu App</button>\n <button id=\"save-feishu-login-app\">Save existing App</button>\n <button id=\"cancel-feishu-configuration\" hidden>Cancel</button>\n <button id=\"check-feishu-login-app\" hidden>Check setup</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"feishu-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Feishu settings</a>\n </div>\n </section>\n\n <section id=\"lark-section\" class=\"panel admin-section\" aria-labelledby=\"lark-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"lark-heading\">Lark</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Lark, OAuth callbacks, and published App setup.</p></div>\n <span id=\"lark-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"lark-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"lark\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"lark-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"lark.loginAppSecret\" data-secret-display=\"lark-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"lark-login-status\" class=\"muted\"></p>\n <div id=\"lark-drift\" class=\"notice\" hidden></div>\n <div id=\"lark-config-controls\" class=\"subsection\">\n <label id=\"lark-create-name-field\" class=\"field\">New App name<input id=\"lark-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"lark-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"lark-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-lark-login-app\">Create Lark App</button>\n <button id=\"save-lark-login-app\">Save existing App</button>\n <button id=\"cancel-lark-configuration\" hidden>Cancel</button>\n <button id=\"check-lark-login-app\" hidden>Check setup</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"lark-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Lark settings</a>\n </div>\n </section>\n </div>\n\n <section id=\"options-section\" class=\"admin-section\">\n <h2>Deployment options</h2>\n <div class=\"panel\">\n <label class=\"field\"><span><input id=\"preset-agents-enabled\" type=\"checkbox\"> Enable preset Agents</span></label>\n <div class=\"row\"><button id=\"save-options\">Save options</button></div>\n </div>\n </section>\n </section>\n </div>\n </div>\n </main>\n\n <script>\n const api = '/api/v1';\n const tokenKey = 'agentconnect.tenant-admin.token';\n const verifierKey = 'agentconnect.tenant-admin.pkce';\n const stateKey = 'agentconnect.tenant-admin.state';\n let currentRevision = 0;\n let currentStatus = null;\n let bootstrapInfo = null;\n const el = (id) => document.getElementById(id);\n const message = (text, error = false) => {\n const target = el('admin').hidden ? el('access-message') : el('message');\n target.textContent = text;\n target.className = error ? 'error' : 'ok';\n };\n const bearer = () => {\n const token = sessionStorage.getItem(tokenKey);\n return token ? { authorization: 'Bearer ' + token } : {};\n };\n const json = async (response) => {\n const body = await response.json().catch(() => ({}));\n if (!response.ok) throw Object.assign(new Error(body.message || ('HTTP ' + response.status)), { status: response.status, code: body.code });\n return body;\n };\n const base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/+/g, '-').replace(///g, '_').replace(/=+$/, '');\n const random = () => base64url(crypto.getRandomValues(new Uint8Array(32)));\n const configured = (byKey, key) => Boolean(byKey.get(key) && byKey.get(key).configured);\n\n function showIdentityEditors(provider, show) {\n for (const button of document.querySelectorAll('.edit-configuration[data-provider=\"' + provider + '\"]')) {\n button.hidden = !show;\n }\n }\n\n function text(id, value) {\n el(id).textContent = value === null || value === undefined || value === '' ? 'Not configured' : String(value);\n }\n\n function secretText(id, byKey, key, editable) {\n const stored = configured(byKey, key);\n const display = el(id);\n const button = document.querySelector('[data-secret-display=\"' + id + '\"]');\n const row = display.closest('.secret-line');\n const editor = row && row.querySelector('.secret-editor');\n if (editor) editor.remove();\n display.hidden = false;\n display.textContent = stored ? '***' : 'Not configured';\n display.className = stored ? 'redacted' : 'muted';\n button.hidden = !editable;\n button.textContent = stored ? 'Edit' : 'Set';\n }\n\n function match(id, state, label) {\n const target = el(id);\n target.textContent = label;\n target.className = 'badge' + (state ? ' ' + state : '');\n }\n\n async function authConfig() { return json(await fetch(api + '/auth-config')); }\n\n async function signIn() {\n const config = await authConfig();\n if (config.mode !== 'oidc') throw new Error('Save an OIDC configuration first');\n const verifier = random();\n const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));\n const state = random();\n sessionStorage.setItem(verifierKey, verifier);\n sessionStorage.setItem(stateKey, state);\n const url = new URL(config.authorizationEndpoint);\n url.searchParams.set('client_id', config.appId);\n url.searchParams.set('redirect_uri', config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', 'openid profile email roles');\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n if (config.resource) url.searchParams.set('resource', config.resource);\n location.assign(url);\n }\n\n async function finishSignIn() {\n const url = new URL(location.href);\n const code = url.searchParams.get('code');\n if (!code) return;\n const state = url.searchParams.get('state');\n if (!state || state !== sessionStorage.getItem(stateKey)) throw new Error('OIDC state mismatch');\n const verifier = sessionStorage.getItem(verifierKey);\n if (!verifier) throw new Error('PKCE verifier is missing; start sign-in again');\n const config = await authConfig();\n const body = new URLSearchParams({\n grant_type: 'authorization_code', code, client_id: config.appId,\n redirect_uri: config.redirectUri, code_verifier: verifier\n });\n if (config.resource) body.set('resource', config.resource);\n const tokens = await json(await fetch(config.tokenEndpoint, {\n method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body\n }));\n if (typeof tokens.id_token !== 'string') throw new Error('Logto did not return an ID token');\n sessionStorage.setItem(tokenKey, tokens.id_token);\n sessionStorage.removeItem(verifierKey);\n sessionStorage.removeItem(stateKey);\n history.replaceState({}, '', '/');\n }\n\n function renderUriList(id, values) {\n el(id).replaceChildren(...(values || []).map((value) => {\n const item = document.createElement('li');\n const code = document.createElement('code'); code.textContent = value;\n item.append(code); return item;\n }));\n }\n\n function updateBootstrapProvider() {\n const provider = el('bootstrap-provider').value;\n el('bootstrap-google').hidden = provider !== 'google';\n el('bootstrap-github').hidden = provider !== 'github';\n el('bootstrap-slack').hidden = provider !== 'slack';\n }\n\n function showBootstrapStep(step) {\n const logto = step === 'logto';\n el('bootstrap-logto-step').hidden = !logto;\n el('bootstrap-provider-step').hidden = logto;\n el('bootstrap-progress').textContent = logto ? 'Step 1 of 2' : 'Step 2 of 2';\n }\n\n function updateOwner(prefix) {\n el(prefix + '-org-field').hidden = el(prefix + '-owner').value !== 'organization';\n }\n\n function ownership(prefix) {\n if (el(prefix + '-owner').value === 'personal') return { owner: 'personal', organization: null };\n const organization = el(prefix + '-org').value.trim();\n if (!organization) throw new Error('Enter the GitHub organization login');\n return { owner: 'organization', organization };\n }\n\n function formatDiffValue(value) {\n if (value === null || value === undefined || value === '') return 'Not configured';\n if (Array.isArray(value)) return value.length ? value.map(formatDiffValue).join('\n') : '[]';\n if (typeof value === 'object') {\n return Object.entries(value).map(([key, item]) => key + ': ' + (Array.isArray(item) ? item.join(', ') : formatDiffValue(item))).join('\n');\n }\n return String(value);\n }\n\n function showDiff(id, rows, label = 'Update required') {\n const box = el(id);\n box.hidden = rows.length === 0;\n box.replaceChildren();\n if (rows.length === 0) return;\n const title = document.createElement('strong');\n title.className = 'diff-title';\n title.textContent = label;\n const grid = document.createElement('div');\n grid.className = 'config-diff';\n const head = document.createElement('div');\n head.className = 'diff-row diff-head';\n for (const text of ['Field', 'Current', 'Expected']) {\n const cell = document.createElement('span'); cell.textContent = text; head.append(cell);\n }\n grid.append(head);\n for (const item of rows) {\n const row = document.createElement('div'); row.className = 'diff-row';\n const field = document.createElement('span'); field.className = 'diff-field'; field.textContent = item.field;\n const current = document.createElement('span'); current.className = 'diff-value diff-current'; current.textContent = formatDiffValue(item.current);\n const expected = document.createElement('span'); expected.className = 'diff-value diff-expected'; expected.textContent = formatDiffValue(item.expected);\n row.append(field, current, expected); grid.append(row);\n }\n box.append(title, grid);\n }\n\n function renderStartupEnvironment() {\n const services = bootstrapInfo.services;\n const environment = [\n ['AGENTCONNECT_PUBLIC_CP_URL', services.controlPlane],\n ['AGENTCONNECT_PUBLIC_RELAY_URL', services.relay],\n ['AGENTCONNECT_PUBLIC_WEB_URL', services.web],\n ['LOGTO_ENDPOINT', bootstrapInfo.logtoEndpoint],\n ['LOGTO_MGMT_ENDPOINT', bootstrapInfo.logtoManagementEndpoint]\n ];\n el('startup-environment').textContent = environment\n .filter(([, value]) => value)\n .map(([key, value]) => key + '=' + value)\n .join('\n');\n }\n\n function renderApps(status) {\n currentStatus = status;\n const byKey = new Map((status.secrets || []).map((item) => [item.key, item]));\n const values = status.values;\n const expected = status.providerExpectations || { github: null, slack: null, google: { origins: [], redirects: [] } };\n renderStartupEnvironment();\n\n const logto = values.logto;\n text('logto-management-endpoint', logto && bootstrapInfo.logtoManagementEndpoint);\n text('logto-management-id', logto && logto.managementAppId);\n text('logto-management-resource', logto && logto.managementResource);\n secretText('logto-management-secret-display', byKey, 'logto.managementAppSecret', Boolean(logto));\n text('logto-browser-endpoint', logto && logto.browser && bootstrapInfo.logtoEndpoint);\n text('logto-browser-id', values.auth.mode === 'oidc' ? values.auth.browserClient.appId : null);\n text('logto-browser-resource', logto && logto.browser && logto.browser.apiResource);\n el('logto-edit-controls').hidden = true;\n showIdentityEditors('logto', Boolean(logto && logto.browser && values.auth.mode === 'oidc'));\n match(\n 'logto-match',\n logto && configured(byKey, 'logto.managementAppSecret') ? '' : 'warn',\n logto && configured(byKey, 'logto.managementAppSecret') ? 'Ready to check' : 'Not configured'\n );\n\n el('preset-agents-enabled').checked = values.features.presetAgentsEnabled;\n\n const github = values.github;\n const webhookStored = byKey.get('github.webhookSecret') && byKey.get('github.webhookSecret').configured;\n const webhookInactive = github && github.webhookEnabled === false;\n text('github-app-id', github && github.appId);\n text('github-app-slug', github && github.slug);\n text('github-client-id', github && github.clientId);\n text('github-webhook-enabled', github && (github.webhookEnabled === false ? 'Disabled' : 'Enabled'));\n el('github-edit-controls').hidden = true;\n showIdentityEditors('github', Boolean(github));\n secretText('github-client-secret-display', byKey, 'github.clientSecret', Boolean(github));\n secretText('github-private-key-display', byKey, 'github.privateKeyB64', Boolean(github));\n secretText('github-webhook-secret-display', byKey, 'github.webhookSecret', Boolean(github));\n secretText('github-logto-secret-display', byKey, 'logto.githubConnectorClientSecret', Boolean(values.logto && values.logto.githubConnector));\n el('github-status').textContent = github\n ? github.slug + ' is configured. Webhook secret: ' + (webhookStored ? 'stored' : webhookInactive ? 'not required yet' : 'missing') + '.' +\n (webhookInactive ? ' Relay webhook delivery is disabled.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github\n ? expected.github\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'Valid Web, API, and ingress URLs' }]\n : [];\n match('github-match', !github ? '' : githubDrift.length ? 'warn' : '', !github ? 'Not configured' : githubDrift.length ? 'Expected URLs changed' : 'Ready to check');\n el('github-create-controls').hidden = Boolean(github);\n el('create-github').hidden = Boolean(github);\n el('clear-github').hidden = !github;\n el('connect-github-login').hidden = !github || !values.logto || Boolean(values.logto.githubConnector);\n el('check-github').hidden = !github;\n if (github) showDiff('github-drift', githubDrift, 'Expected URLs changed since App creation');\n else el('github-drift').hidden = true;\n\n const slack = values.slack;\n text('slack-app-id', slack && slack.appId);\n text('slack-client-id', slack && slack.clientId);\n el('slack-edit-controls').hidden = true;\n showIdentityEditors('slack', Boolean(slack));\n secretText('slack-client-secret-display', byKey, 'slack.clientSecret', Boolean(slack));\n secretText('slack-signing-secret-display', byKey, 'slack.signingSecret', Boolean(slack));\n el('slack-logto-status').textContent = values.logto && values.logto.slackConnector\n ? 'Enabled; reuses the Slack client secret above.'\n : 'Not enabled.';\n el('slack-status').textContent = slack ? slack.appId + ' is configured.' : 'Creates the default AgentConnect integration manifest.';\n const slackDrift = slack\n ? expected.slack\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'HTTPS Web, API, and ingress URLs' }]\n : [];\n match('slack-match', !slack ? '' : slackDrift.length ? 'warn' : '', !slack ? 'Not configured' : slackDrift.length ? 'Update required' : 'Ready to check');\n el('slack-name-field').hidden = Boolean(slack);\n el('create-slack').hidden = Boolean(slack);\n el('connect-slack-login').hidden = !slack || !values.logto || Boolean(values.logto.slackConnector) || !bootstrapInfo?.slackAvailable;\n el('clear-slack').hidden = !slack;\n el('check-slack').hidden = !slack;\n el('slack-settings').hidden = !slack;\n if (slack) {\n el('slack-settings').href = 'https://api.slack.com/apps/' + encodeURIComponent(slack.appId);\n showDiff('slack-drift', slackDrift);\n } else el('slack-drift').hidden = true;\n\n const google = values.logto && values.logto.googleConnector;\n const googleSecret = configured(byKey, 'logto.googleConnectorClientSecret');\n const expectedGoogle = expected.google;\n renderUriList('google-origins', expectedGoogle.origins);\n renderUriList('google-redirects', expectedGoogle.redirects);\n text('google-client-id', google && google.clientId);\n secretText('google-client-secret-display', byKey, 'logto.googleConnectorClientSecret', Boolean(google));\n showIdentityEditors('google', Boolean(google));\n el('google-id').value = google ? google.clientId : '';\n el('google-status').textContent = google ? 'Google OAuth client is configured.' : 'Create a Web application OAuth client manually, then save it here.';\n showDiff('google-drift', []);\n match('google-match', google && googleSecret ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : \"Can't verify automatically\");\n el('google-initial-secret-field').hidden = googleSecret;\n el('save-google').textContent = google ? 'Confirm callback settings' : 'Save Google client';\n el('google-config-controls').hidden = Boolean(google);\n el('save-google').hidden = Boolean(google);\n el('cancel-google-configuration').hidden = true;\n el('check-google').hidden = !google || !googleSecret;\n el('clear-google').hidden = !google;\n\n const feishuSecret = byKey.get('feishu.loginAppSecret');\n const larkSecret = byKey.get('lark.loginAppSecret');\n el('feishu-login-id').value = values.feishu ? values.feishu.loginAppId : '';\n el('lark-login-id').value = values.lark ? values.lark.loginAppId : '';\n text('feishu-app-id', values.feishu && values.feishu.loginAppId);\n text('lark-app-id', values.lark && values.lark.loginAppId);\n showIdentityEditors('feishu', Boolean(values.feishu));\n showIdentityEditors('lark', Boolean(values.lark));\n secretText('feishu-app-secret-display', byKey, 'feishu.loginAppSecret', Boolean(values.feishu));\n secretText('lark-app-secret-display', byKey, 'lark.loginAppSecret', Boolean(values.lark));\n el('feishu-login-status').textContent = values.feishu && feishuSecret && feishuSecret.configured\n ? values.feishu.loginAppId + ' is configured.'\n : 'No Feishu tenant App is configured.';\n el('lark-login-status').textContent = values.lark && larkSecret && larkSecret.configured\n ? values.lark.loginAppId + ' is configured.'\n : 'No Lark tenant App is configured.';\n match('feishu-match', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? '' : '', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? 'Ready to check' : 'Not configured');\n match('lark-match', values.lark && configured(byKey, 'lark.loginAppSecret') ? '' : '', values.lark && configured(byKey, 'lark.loginAppSecret') ? 'Ready to check' : 'Not configured');\n showDiff('feishu-drift', []);\n showDiff('lark-drift', []);\n el('feishu-config-controls').hidden = Boolean(values.feishu);\n el('lark-config-controls').hidden = Boolean(values.lark);\n el('feishu-create-name-field').hidden = Boolean(values.feishu);\n el('lark-create-name-field').hidden = Boolean(values.lark);\n el('create-feishu-login-app').hidden = Boolean(values.feishu);\n el('create-lark-login-app').hidden = Boolean(values.lark);\n el('save-feishu-login-app').hidden = Boolean(values.feishu);\n el('save-lark-login-app').hidden = Boolean(values.lark);\n el('cancel-feishu-configuration').hidden = true;\n el('cancel-lark-configuration').hidden = true;\n el('clear-feishu').hidden = !values.feishu;\n el('clear-lark').hidden = !values.lark;\n el('check-feishu-login-app').hidden = !values.feishu || !configured(byKey, 'feishu.loginAppSecret');\n el('check-lark-login-app').hidden = !values.lark || !configured(byKey, 'lark.loginAppSecret');\n el('feishu-settings').hidden = !values.feishu;\n el('lark-settings').hidden = !values.lark;\n if (values.feishu) el('feishu-settings').href = 'https://open.feishu.cn/app/' + encodeURIComponent(values.feishu.loginAppId);\n if (values.lark) el('lark-settings').href = 'https://open.larksuite.com/app/' + encodeURIComponent(values.lark.loginAppId);\n }\n\n function requiredInput(id, label) {\n const value = el(id).value.trim();\n if (!value) throw new Error('Enter ' + label);\n return value;\n }\n\n function githubConnectorUsesDeployment(values) {\n const github = values.github;\n const connector = values.logto && values.logto.githubConnector;\n return Boolean(\n github && connector &&\n connector.appId === github.appId &&\n connector.slug === github.slug &&\n connector.clientId === github.clientId\n );\n }\n\n function beginConfigurationEdit(provider) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const values = currentStatus.values;\n if (provider === 'logto') {\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') return;\n el('logto-edit-management-id').value = logto.managementAppId;\n el('logto-edit-management-resource').value = logto.managementResource;\n el('logto-edit-management-secret').value = '';\n el('logto-edit-browser-id').value = values.auth.browserClient.appId;\n el('logto-edit-browser-resource').value = logto.browser.apiResource || '';\n el('logto-edit-controls').hidden = false;\n el('logto-edit-management-id').focus();\n } else if (provider === 'github') {\n const github = values.github;\n if (!github) return;\n el('github-edit-app-id').value = String(github.appId);\n el('github-edit-slug').value = github.slug;\n el('github-edit-client-id').value = github.clientId || '';\n el('github-edit-webhook-enabled').checked = github.webhookEnabled !== false;\n for (const id of ['github-edit-client-secret', 'github-edit-private-key', 'github-edit-webhook-secret', 'github-edit-logto-secret']) el(id).value = '';\n el('github-edit-logto-secret-field').hidden = !githubConnectorUsesDeployment(values);\n el('github-edit-controls').hidden = false;\n el('clear-github').hidden = true;\n el('github-edit-app-id').focus();\n } else if (provider === 'slack') {\n const slack = values.slack;\n if (!slack) return;\n el('slack-edit-app-id').value = slack.appId;\n el('slack-edit-client-id').value = slack.clientId;\n el('slack-edit-client-secret').value = '';\n el('slack-edit-signing-secret').value = '';\n el('slack-edit-controls').hidden = false;\n el('clear-slack').hidden = true;\n el('slack-edit-app-id').focus();\n } else if (provider === 'google') {\n const google = values.logto && values.logto.googleConnector;\n if (!google) return;\n el('google-id').value = google.clientId;\n el('google-secret').value = '';\n el('google-config-controls').hidden = false;\n el('google-initial-secret-field').hidden = false;\n el('save-google').hidden = false;\n el('save-google').textContent = 'Save Google client';\n el('cancel-google-configuration').hidden = false;\n el('clear-google').hidden = true;\n el('google-id').focus();\n } else if (provider === 'feishu' || provider === 'lark') {\n el(provider + '-config-controls').hidden = false;\n el(provider + '-create-name-field').hidden = true;\n el('create-' + provider + '-login-app').hidden = true;\n el('save-' + provider + '-login-app').hidden = false;\n el('cancel-' + provider + '-configuration').hidden = false;\n el('clear-' + provider).hidden = true;\n el(provider + '-login-secret').value = '';\n el(provider + '-login-id').focus();\n }\n showIdentityEditors(provider, false);\n }\n\n function cancelConfigurationEdit() {\n if (currentStatus) renderApps(currentStatus);\n }\n\n async function replaceConfiguration(values, secrets, successMessage) {\n const response = await json(await fetch(api + '/deployment-config', {\n method: 'PUT',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values, ...(secrets ? { secrets } : {}) })\n }));\n await load();\n message(successMessage || 'Configuration saved. Restart AgentConnect to apply it.');\n return response;\n }\n\n async function saveLogtoConfiguration() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = currentStatus.values;\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') throw new Error('Logto is not configured');\n const managementAppId = requiredInput('logto-edit-management-id', 'the Management App ID');\n const managementResource = requiredInput('logto-edit-management-resource', 'the Management API resource');\n const browserAppId = requiredInput('logto-edit-browser-id', 'the SPA App ID');\n const browserResource = el('logto-edit-browser-resource').value.trim() || null;\n const managementIdentityChanged = managementAppId !== logto.managementAppId;\n const managementSecret = el('logto-edit-management-secret').value;\n if (managementIdentityChanged && !managementSecret) throw new Error('Enter the new Management App secret');\n const browser = { ...logto.browser, apiResource: browserResource };\n const auth = {\n ...values.auth,\n audience: browserResource || browserAppId,\n browserClient: { appId: browserAppId, apiResource: browserResource }\n };\n await replaceConfiguration(\n {\n ...values,\n auth,\n logto: { ...logto, managementAppId, managementResource, browser }\n },\n managementSecret ? { 'logto.managementAppSecret': managementSecret } : undefined,\n 'Logto configuration saved. Sign in again if the SPA identity changed, then restart AgentConnect.'\n );\n }\n\n async function saveGithubConfiguration() {\n if (!currentStatus || !currentStatus.values.github) throw new Error('GitHub is not configured');\n const values = currentStatus.values;\n const previous = values.github;\n const appId = Number(requiredInput('github-edit-app-id', 'the GitHub App ID'));\n if (!Number.isSafeInteger(appId) || appId <= 0) throw new Error('GitHub App ID must be a positive integer');\n const slug = requiredInput('github-edit-slug', 'the GitHub App slug');\n const clientId = requiredInput('github-edit-client-id', 'the GitHub Client ID');\n const appChanged = appId !== previous.appId;\n const clientChanged = clientId !== previous.clientId;\n const webhookEnabled = el('github-edit-webhook-enabled').checked;\n const webhookEnabling = webhookEnabled && previous.webhookEnabled === false;\n const connectorReused = githubConnectorUsesDeployment(values);\n const clientSecret = el('github-edit-client-secret').value;\n const privateKey = el('github-edit-private-key').value.trim();\n const webhookSecret = el('github-edit-webhook-secret').value;\n const connectorSecret = el('github-edit-logto-secret').value;\n const webhookSecretStored = currentStatus.secrets.some((secret) => secret.key === 'github.webhookSecret' && secret.configured);\n if ((appChanged || clientChanged) && !clientSecret) throw new Error('Enter the new GitHub client secret');\n if (appChanged && !privateKey) throw new Error('Enter the new GitHub private key as base64');\n if (webhookEnabled && ((appChanged && !webhookSecret) || (webhookEnabling && !webhookSecret && !webhookSecretStored))) throw new Error('Enter the new GitHub webhook secret');\n if (connectorReused && (appChanged || clientChanged) && !connectorSecret) throw new Error('Enter the new Logto connector client secret');\n const secrets = {};\n if (clientSecret) secrets['github.clientSecret'] = clientSecret;\n if (privateKey) secrets['github.privateKeyB64'] = privateKey;\n if (webhookSecret) secrets['github.webhookSecret'] = webhookSecret;\n if (connectorSecret) secrets['logto.githubConnectorClientSecret'] = connectorSecret;\n const nextLogto = connectorReused && values.logto\n ? { ...values.logto, githubConnector: { ...values.logto.githubConnector, appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, webhookEnabled },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\n );\n if (connectorSecret) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function saveSlackConfiguration() {\n if (!currentStatus || !currentStatus.values.slack) throw new Error('Slack is not configured');\n const values = currentStatus.values;\n const previous = values.slack;\n const appId = requiredInput('slack-edit-app-id', 'the Slack App ID');\n const clientId = requiredInput('slack-edit-client-id', 'the Slack Client ID');\n const changed = appId !== previous.appId || clientId !== previous.clientId;\n const clientSecret = el('slack-edit-client-secret').value;\n const signingSecret = el('slack-edit-signing-secret').value;\n if (changed && (!clientSecret || !signingSecret)) throw new Error('Enter both the new Slack client secret and signing secret');\n const secrets = {};\n if (clientSecret) secrets['slack.clientSecret'] = clientSecret;\n if (signingSecret) secrets['slack.signingSecret'] = signingSecret;\n const nextLogto = values.logto && values.logto.slackConnector\n ? { ...values.logto, slackConnector: { ...values.logto.slackConnector, appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\n );\n if (clientSecret && nextLogto && nextLogto.slackConnector) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function clearProvider(provider) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const label = provider === 'github' ? 'GitHub' : provider === 'slack' ? 'Slack' : provider === 'google' ? 'Google' : provider === 'feishu' ? 'Feishu' : 'Lark';\n if (!window.confirm('Clear the saved ' + label + ' configuration and secrets?')) return;\n const values = currentStatus.values;\n let next = values;\n let secrets = {};\n if (provider === 'github') {\n const connectorReused = githubConnectorUsesDeployment(values);\n const logto = connectorReused && values.logto\n ? {\n ...values.logto,\n githubConnector: null\n }\n : values.logto;\n next = { ...values, github: null, ...(logto ? { logto } : {}) };\n secrets = {\n 'github.clientSecret': null,\n 'github.privateKeyB64': null,\n 'github.webhookSecret': null,\n ...(connectorReused ? { 'logto.githubConnectorClientSecret': null } : {})\n };\n } else if (provider === 'slack') {\n const logto = values.logto\n ? {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'slack') }\n : values.logto.browser,\n slackConnector: null\n }\n : values.logto;\n next = { ...values, slack: null, ...(logto ? { logto } : {}) };\n secrets = { 'slack.clientSecret': null, 'slack.signingSecret': null };\n } else if (provider === 'google') {\n if (!values.logto) return;\n next = {\n ...values,\n logto: {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'google') }\n : values.logto.browser,\n googleConnector: null\n }\n };\n secrets = { 'logto.googleConnectorClientSecret': null };\n } else if (provider === 'feishu' || provider === 'lark') {\n next = { ...values, [provider]: null };\n secrets = { [provider + '.loginAppSecret']: null };\n }\n await replaceConfiguration(next, secrets, label + ' configuration cleared. Its setup controls are available again.');\n }\n\n async function startGithub(prefix) {\n const result = await json(await fetch(api + '/create/github/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n purpose: 'deployment',\n name: el(prefix + '-name').value,\n ownership: ownership(prefix),\n connectLogto: prefix === 'bootstrap-github'\n })\n }));\n const form = document.createElement('form');\n form.method = 'post'; form.action = result.action;\n const manifest = document.createElement('input');\n manifest.type = 'hidden'; manifest.name = 'manifest'; manifest.value = JSON.stringify(result.manifest);\n form.append(manifest); document.body.append(form);\n message('Opening GitHub to review the complete integration App.');\n form.submit();\n }\n\n async function loadBootstrapInfo() {\n bootstrapInfo = await json(await fetch(api + '/bootstrap-info'));\n el('open-logto').href = bootstrapInfo.logtoAdminEndpoint;\n el('logto-settings').href = bootstrapInfo.logtoAdminEndpoint;\n if (!el('logto-app-id').value && bootstrapInfo.logtoManagementAppId) {\n el('logto-app-id').value = bootstrapInfo.logtoManagementAppId;\n }\n renderUriList('bootstrap-google-origins', bootstrapInfo.google.javascriptOrigins);\n renderUriList('bootstrap-google-redirects', bootstrapInfo.google.redirectUris);\n renderUriList('bootstrap-slack-redirects', [bootstrapInfo.slackLoginRedirectUrl]);\n el('logto-management-api-resource').value = bootstrapInfo.logtoManagementResource;\n el('bootstrap-github-submit').disabled = !bootstrapInfo.githubAvailable;\n if (!bootstrapInfo.githubAvailable) {\n el('bootstrap-github-note').textContent = 'GitHub App creation needs valid saved Web, API, and ingress URLs.';\n } else if (!bootstrapInfo.githubWebhookActive) {\n el('bootstrap-github-note').textContent = 'Creates the complete GitHub App now without submitting the localhost webhook URL. Add it after saving reachable HTTPS ingress.';\n } else {\n el('bootstrap-github-note').textContent = 'This creates one complete App for both GitHub sign-in and repository integration.';\n }\n el('bootstrap-slack-option').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-submit').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-note').textContent = bootstrapInfo.slackAvailable\n ? 'Creates one complete Slack App. Sign-in uses a separate openid profile email flow from workspace installation.'\n : 'Slack sign-in needs all public service URLs to use HTTPS. Use Google locally or expose the stack through trusted HTTPS endpoints.';\n if (!bootstrapInfo.slackAvailable && el('bootstrap-provider').value === 'slack') {\n el('bootstrap-provider').value = 'google';\n updateBootstrapProvider();\n }\n return bootstrapInfo;\n }\n\n async function logtoBootstrapFinding() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n for (const id of ['logto.client_credentials', 'logto.roles_read']) {\n const finding = report.findings.find((candidate) => candidate.id === id);\n if (!finding || finding.status !== 'pass') {\n return finding || { status: 'fail', message: 'Logto Management API permissions could not be verified.' };\n }\n }\n return { status: 'pass', message: 'Logto Management API access is ready.' };\n }\n\n async function showBootstrap() {\n el('bootstrap').hidden = false;\n el('show-bootstrap').hidden = true;\n const info = bootstrapInfo || await loadBootstrapInfo();\n if (!info.logtoConfigured) {\n showBootstrapStep('logto');\n return;\n }\n const finding = await logtoBootstrapFinding();\n if (finding && finding.status === 'pass') {\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n return;\n }\n showBootstrapStep('logto');\n message(finding?.message || 'Logto Management API credentials could not be verified.', true);\n }\n\n async function bootstrapLogto() {\n await json(await fetch(api + '/bootstrap/logto', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n managementAppId: el('logto-app-id').value,\n managementAppSecret: el('logto-app-secret').value,\n managementResource: el('logto-management-api-resource').value\n })\n }));\n el('logto-app-secret').value = '';\n await loadBootstrapInfo();\n const finding = await logtoBootstrapFinding();\n if (!finding || finding.status !== 'pass') {\n throw new Error(finding?.message || 'Logto Management API credentials could not be verified.');\n }\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n }\n\n async function bootstrapGoogle() {\n const secret = el('bootstrap-google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ clientId: el('bootstrap-google-id').value, clientSecret: secret })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto(true);\n await load();\n }\n\n async function bootstrapGithub() {\n if (!bootstrapInfo || !bootstrapInfo.githubAvailable) throw new Error('GitHub App creation needs valid saved Web, API, and ingress URLs.');\n await startGithub('bootstrap-github');\n }\n\n async function bootstrapSlack() {\n if (!bootstrapInfo || !bootstrapInfo.slackAvailable) throw new Error('Slack sign-in requires all public service URLs to use HTTPS.');\n await createSlack('bootstrap-slack', true);\n await reconcileLogto();\n await load();\n }\n\n async function createSlack(prefix = 'slack', connectLogto = false) {\n const result = await json(await fetch(api + '/create/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n name: el(prefix + '-name').value,\n configToken: el(prefix + '-token').value,\n connectLogto\n })\n }));\n el(prefix + '-token').value = '';\n if (!connectLogto) await load();\n message('Slack App ' + result.app.id + ' was created with the default integration manifest. Restart AgentConnect to apply it.');\n }\n\n async function checkGithub() {\n const result = await json(await fetch(api + '/check/github', { headers: bearer() }));\n el('github-settings').href = result.settingsUrl;\n el('github-settings').hidden = false;\n const label = result.missing.length ? 'Update required' : \"Can't verify automatically\";\n showDiff('github-drift', result.diff || [], label);\n match('github-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : label);\n message(result.status === 'pass' ? 'GitHub App matches the expected integration manifest.' : result.missing.length ? 'GitHub App settings need an update.' : \"GitHub can't expose callback, setup, or webhook-active settings through its API; check them in GitHub.\", result.status === 'fail');\n }\n\n async function connectGithubLogin() {\n await json(await fetch(api + '/configure/github-login', { method: 'POST', headers: bearer() }));\n await reconcileLogto();\n await load();\n message('The deployment GitHub App is now also used for Logto sign-in. Update its displayed callback URLs.');\n }\n\n async function connectSlackLogin() {\n if (!currentStatus || !currentStatus.values.slack || !currentStatus.values.logto) {\n throw new Error('Save the Slack App and Logto configuration first');\n }\n const values = currentStatus.values;\n const slack = values.slack;\n const logto = values.logto;\n await replaceConfiguration(\n {\n ...values,\n logto: {\n ...logto,\n browser: logto.browser\n ? { ...logto.browser, socialProviders: [...new Set([...logto.browser.socialProviders, 'slack'])] }\n : logto.browser,\n slackConnector: {\n appId: slack.appId,\n clientId: slack.clientId\n }\n }\n },\n undefined,\n 'Slack App is ready to connect to Logto.'\n );\n await reconcileLogto();\n await load();\n message('The deployment Slack App is now also used for Logto sign-in.');\n }\n\n async function checkSlack() {\n const token = requiredInput('slack-token', 'the temporary Slack App configuration token');\n const result = await json(await fetch(api + '/check/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ configToken: token })\n }));\n el('slack-token').value = '';\n showDiff('slack-drift', result.diff || []);\n match('slack-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : 'Update required');\n message(result.status === 'pass' ? 'Slack App matches the default integration manifest.' : 'Slack App settings need an update.', result.status !== 'pass');\n }\n\n async function checkGoogle() {\n const report = await checkLogto();\n const connector = report.findings.find((finding) => finding.id === 'logto.connectors');\n const google = currentStatus && currentStatus.values.logto && currentStatus.values.logto.googleConnector;\n const connectorDiff = connector && connector.diff\n ? connector.diff.filter((item) => item.field.toLowerCase().startsWith('google'))\n : [];\n const passed = Boolean(google && connector && connectorDiff.length === 0);\n showDiff('google-drift', connectorDiff);\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Logto matches' : 'Update required');\n message(\n passed\n ? \"The Logto Google connector matches. Google OAuth redirect settings can't be verified automatically.\"\n : 'The Logto Google connector needs an update.',\n !passed\n );\n }\n\n async function checkRegionalLoginApp(region) {\n const result = await json(await fetch(api + '/check/regional-login-app/' + region, { headers: bearer() }));\n const label = region === 'feishu' ? 'Feishu' : 'Lark';\n showDiff(region + '-drift', result.diff || []);\n match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Matches' : result.status === 'fail' ? 'Update required' : 'Could not check');\n el(region + '-login-status').textContent = result.message;\n el(region + '-login-status').className = result.status === 'pass' ? 'ok' : result.status === 'fail' ? 'warn' : 'muted';\n message(result.message || (label + ' credential check completed.'), result.status === 'fail');\n }\n\n async function saveGoogle() {\n const secret = el('google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n clientId: el('google-id').value,\n ...(secret ? { clientSecret: secret } : {})\n })\n }));\n el('google-secret').value = '';\n await reconcileLogto(Boolean(secret));\n await load();\n message('Google OAuth client and Logto connector are configured. Restart AgentConnect to apply the saved settings.');\n }\n\n function regionalLoginApp(prefix) {\n const appId = el(prefix + '-login-id').value.trim();\n const appSecret = el(prefix + '-login-secret').value;\n return appId ? { appId, ...(appSecret ? { appSecret } : {}) } : null;\n }\n\n async function saveRegionalLoginApp(region) {\n const app = regionalLoginApp(region);\n if (!app) throw new Error('Enter the ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App ID');\n await json(await fetch(api + '/configure/regional-login-app', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, app })\n }));\n el(region + '-login-secret').value = '';\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' tenant App is saved. Restart AgentConnect services to apply it.');\n }\n\n async function createRegionalLoginApp(region) {\n const popup = window.open('about:blank', '_blank');\n try {\n const started = await json(await fetch(api + '/create/regional-login-app/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, name: el(region + '-create-name').value })\n }));\n if (popup) popup.location.replace(started.authorizationUrl);\n else window.open(started.authorizationUrl, '_blank', 'noopener');\n message('Approve the new ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App in the opened page. This page will finish saving it automatically.');\n while (true) {\n const result = await json(await fetch(api + '/create/regional-login-app/' + encodeURIComponent(started.id), { headers: bearer() }));\n if (result.status === 'completed') {\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' App ' + result.appId + ' was created and saved. Restart AgentConnect services to apply it.');\n return;\n }\n if (result.status === 'failed') throw new Error('Regional App creation ' + result.reason + '. Start it again.');\n await new Promise((resolve) => setTimeout(resolve, Math.max(500, Math.min(result.retryAfterMs || 2000, 5000))));\n }\n } catch (error) {\n if (popup) popup.close();\n throw error;\n }\n }\n\n async function reconcileLogto(refreshConnectorSecrets) {\n return json(await fetch(api + '/reconcile/logto', {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ refreshConnectorSecrets: Boolean(refreshConnectorSecrets) })\n }));\n }\n\n async function refreshDeploymentConfig() {\n const status = await json(await fetch(api + '/deployment-config', { headers: bearer() }));\n el('access').hidden = true;\n el('admin').hidden = false;\n el('editor').hidden = false;\n currentRevision = status.revision;\n renderApps(status);\n return status;\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n await refreshDeploymentConfig();\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, connectors, and social-only sign-in match.'\n : failures.map((finding) => finding.message).join(' ');\n el('logto-status').className = failures.length === 0 ? 'ok' : 'warn';\n match('logto-match', failures.length === 0 ? 'pass' : 'warn', failures.length === 0 ? 'Matches' : 'Update required');\n showDiff(\n 'logto-drift',\n failures.flatMap((finding) => finding.diff && finding.diff.length\n ? finding.diff\n : [{ field: finding.id.replace(/^logto./, '').replaceAll('_', ' '), current: finding.message, expected: 'Matches expected configuration' }])\n );\n el('logto-settings').hidden = failures.length === 0;\n return report;\n }\n\n function githubNotice(value) {\n if (value === 'deployment-created') return 'GitHub integration App created. Its private key and webhook secret are stored.';\n if (value === 'deployment-login-created') return 'GitHub integration App created and connected to Logto sign-in.';\n if (value === 'cancelled') return 'GitHub App creation was cancelled.';\n if (value === 'expired') return 'GitHub App creation expired. Start it again.';\n if (value === 'invalid-callback') return 'GitHub returned an invalid App creation callback.';\n if (value === 'conversion-failed') return 'GitHub may have created the App, but did not return complete credentials. Delete the orphaned App and retry.';\n if (value === 'save-failed') return 'GitHub created the App, but its credentials could not be saved. Delete the orphaned App and retry.';\n return '';\n }\n\n async function load() {\n let publicAuth = await authConfig();\n const currentUrl = new URL(location.href);\n const githubResult = currentUrl.searchParams.get('github');\n const notice = githubNotice(githubResult);\n if (githubResult === 'deployment-login-created' && publicAuth.mode === 'none') {\n await reconcileLogto();\n publicAuth = await authConfig();\n }\n if (githubResult) history.replaceState({}, '', '/');\n const hasToken = Boolean(sessionStorage.getItem(tokenKey));\n el('access').hidden = false;\n el('admin').hidden = true;\n el('login').hidden = true;\n el('open-logto').hidden = true;\n el('show-bootstrap').hidden = true;\n el('editor').hidden = true;\n el('bootstrap').hidden = true;\n if (publicAuth.logtoAdminEndpoint) {\n el('open-logto').href = publicAuth.logtoAdminEndpoint;\n el('logto-settings').href = publicAuth.logtoAdminEndpoint;\n }\n if (publicAuth.mode === 'none') {\n sessionStorage.removeItem(tokenKey);\n await loadBootstrapInfo();\n el('open-logto').hidden = false;\n el('show-bootstrap').hidden = false;\n message(notice || 'Logto sign-in is not configured. Set up the initial Logto administrator first.');\n return;\n }\n if (publicAuth.mode === 'unavailable') {\n el('open-logto').hidden = false;\n message(publicAuth.message || 'Logto sign-in is unavailable. Open Logto Console to review its settings.', true);\n return;\n }\n if (!hasToken) {\n el('login').hidden = false;\n message(notice || 'Sign in with Logto to continue.');\n return;\n }\n if (publicAuth.claimAvailable) {\n try {\n await json(await fetch(api + '/bootstrap/claim', { method: 'POST', headers: bearer() }));\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('This first user is now an ADMIN. Sign in again to refresh the role claim.');\n return;\n } catch (error) {\n if (error.status !== 401 && error.status !== 403) throw error;\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with Logto to continue.', true);\n return;\n }\n }\n try {\n await loadBootstrapInfo();\n await refreshDeploymentConfig();\n message(notice);\n checkLogto().catch((error) => {\n el('logto-status').textContent = error.message;\n el('logto-status').className = 'warn';\n el('logto-settings').hidden = false;\n });\n } catch (error) {\n if (error.status === 401 || error.status === 403) {\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with a Logto ADMIN account.', true);\n }\n else throw error;\n }\n }\n\n async function saveSecretReplacement(key, value) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n if (!value) throw new Error('Enter the replacement secret');\n const refreshLogto =\n (key === 'logto.githubConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.githubConnector) ||\n (key === 'logto.googleConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.googleConnector) ||\n (key === 'slack.clientSecret' && currentStatus.values.logto && currentStatus.values.logto.slackConnector);\n await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values: currentStatus.values, secrets: { [key]: value } })\n }));\n try {\n if (refreshLogto) await reconcileLogto(true);\n } finally {\n await load();\n }\n if (refreshLogto) {\n message('Secret replaced and applied to the Logto connector. Restart AgentConnect to apply deployment changes.');\n } else {\n message('Secret replaced. Restart AgentConnect to apply it.');\n }\n }\n\n function editSecret(button) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const row = button.closest('.secret-line');\n const display = el(button.dataset.secretDisplay);\n if (!row || !display || row.querySelector('.secret-editor')) return;\n const editor = document.createElement('span'); editor.className = 'secret-editor';\n const input = document.createElement('input');\n input.type = 'password'; input.autocomplete = 'new-password'; input.placeholder = 'Enter replacement secret';\n const save = document.createElement('button'); save.textContent = 'Save';\n const cancel = document.createElement('button'); cancel.textContent = 'Cancel';\n const close = () => { editor.remove(); display.hidden = false; button.hidden = false; };\n save.onclick = async () => {\n save.disabled = true;\n try { await saveSecretReplacement(button.dataset.secretKey, input.value); }\n catch (error) { save.disabled = false; message(error.message, true); }\n };\n cancel.onclick = close;\n input.onkeydown = (event) => { if (event.key === 'Enter') save.click(); if (event.key === 'Escape') close(); };\n display.hidden = true; button.hidden = true;\n editor.append(input, save, cancel); row.append(editor); input.focus();\n }\n\n async function saveOptions() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = {\n ...currentStatus.values,\n features: {\n presetAgentsEnabled: el('preset-agents-enabled').checked\n }\n };\n const saved = await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values })\n }));\n message('Saved deployment options. Restart AgentConnect to apply them.');\n await load();\n }\n\n el('login').onclick = () => signIn().catch((error) => message(error.message, true));\n el('show-bootstrap').onclick = () => showBootstrap().catch((error) => message(error.message, true));\n el('bootstrap-provider').onchange = updateBootstrapProvider;\n el('bootstrap-github-owner').onchange = () => updateOwner('bootstrap-github');\n el('github-owner').onchange = () => updateOwner('github');\n el('bootstrap-logto-submit').onclick = () => bootstrapLogto().catch((error) => message(error.message, true));\n el('bootstrap-back').onclick = () => showBootstrapStep('logto');\n el('bootstrap-google-submit').onclick = () => bootstrapGoogle().catch((error) => message(error.message, true));\n el('bootstrap-github-submit').onclick = () => bootstrapGithub().catch((error) => message(error.message, true));\n el('bootstrap-slack-submit').onclick = () => bootstrapSlack().catch((error) => message(error.message, true));\n el('create-github').onclick = () => startGithub('github').catch((error) => message(error.message, true));\n el('save-logto-configuration').onclick = () => saveLogtoConfiguration().catch((error) => message(error.message, true));\n el('cancel-logto-configuration').onclick = cancelConfigurationEdit;\n el('save-github-configuration').onclick = () => saveGithubConfiguration().catch((error) => message(error.message, true));\n el('cancel-github-configuration').onclick = cancelConfigurationEdit;\n el('clear-github').onclick = () => clearProvider('github').catch((error) => message(error.message, true));\n el('connect-github-login').onclick = () => connectGithubLogin().catch((error) => message(error.message, true));\n el('check-github').onclick = () => checkGithub().catch((error) => message(error.message, true));\n el('create-slack').onclick = () => createSlack('slack').catch((error) => message(error.message, true));\n el('connect-slack-login').onclick = () => connectSlackLogin().catch((error) => message(error.message, true));\n el('save-slack-configuration').onclick = () => saveSlackConfiguration().catch((error) => message(error.message, true));\n el('cancel-slack-configuration').onclick = cancelConfigurationEdit;\n el('clear-slack').onclick = () => clearProvider('slack').catch((error) => message(error.message, true));\n el('check-slack').onclick = () => checkSlack().catch((error) => message(error.message, true));\n el('save-google').onclick = () => saveGoogle().catch((error) => message(error.message, true));\n el('cancel-google-configuration').onclick = cancelConfigurationEdit;\n el('clear-google').onclick = () => clearProvider('google').catch((error) => message(error.message, true));\n el('check-google').onclick = () => checkGoogle().catch((error) => message(error.message, true));\n el('create-feishu-login-app').onclick = () => createRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('save-feishu-login-app').onclick = () => saveRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('cancel-feishu-configuration').onclick = cancelConfigurationEdit;\n el('clear-feishu').onclick = () => clearProvider('feishu').catch((error) => message(error.message, true));\n el('check-feishu-login-app').onclick = () => checkRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('create-lark-login-app').onclick = () => createRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('save-lark-login-app').onclick = () => saveRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('cancel-lark-configuration').onclick = cancelConfigurationEdit;\n el('clear-lark').onclick = () => clearProvider('lark').catch((error) => message(error.message, true));\n el('check-lark-login-app').onclick = () => checkRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('check-logto').onclick = () => checkLogto().catch((error) => message(error.message, true));\n el('reconcile-logto').onclick = () => reconcileLogto().then(load).catch((error) => message(error.message, true));\n el('logout').onclick = () => { sessionStorage.removeItem(tokenKey); load().catch((error) => message(error.message, true)); };\n el('save-options').onclick = () => saveOptions().catch((error) => message(error.message, true));\n for (const button of document.querySelectorAll('.edit-secret')) button.onclick = () => editSecret(button);\n for (const button of document.querySelectorAll('.edit-configuration')) button.onclick = () => beginConfigurationEdit(button.dataset.provider);\n updateBootstrapProvider();\n finishSignIn().then(load).catch((error) => message(error.message, true));\n <\/script>\n</body>\n</html>"], ["<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>AgentConnect deployment settings</title>\n <style>\n :root { color-scheme: light dark; font: 15px/1.5 system-ui, sans-serif; }\n body { max-width: 1280px; margin: 48px auto; padding: 0 20px 60px; }\n h1 { margin-bottom: 4px; } h2 { margin-top: 32px; } h3 { margin: 0 0 8px; }\n .muted { color: #777; } .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }\n button, .button { padding: 8px 13px; cursor: pointer; }\n .button { border: 1px solid #8888; border-radius: 4px; color: inherit; text-decoration: none; display: inline-block; }\n input, select { width: min(520px, 100%); padding: 7px; box-sizing: border-box; }\n input[type=\"checkbox\"] { width: auto; padding: 0; }\n .field { display: grid; gap: 4px; margin: 10px 0; }\n .provider-stack { display: grid; gap: 16px; }\n .panel { border: 1px solid #8886; border-radius: 8px; padding: 16px; }\n .section-head, .provider-head { display: flex; justify-content: space-between; gap: 16px; align-items: start; }\n .section-head h2, .provider-head h3 { margin: 0; }\n .provider-head p { margin: 4px 0 0; }\n .badge { flex: none; border: 1px solid #8886; border-radius: 999px; padding: 3px 9px; font-size: 13px; }\n .badge.pass { color: #198754; border-color: #19875466; background: #19875412; }\n .badge.warn { color: #a66b00; border-color: #d99b1366; background: #d99b1315; }\n .badge.fail { color: #c33; border-color: #c333; background: #c3331111; }\n .credentials { display: grid; grid-template-columns: minmax(160px, 220px) minmax(0, 1fr); gap: 7px 16px; margin: 16px 0; }\n .credentials dt { color: #777; }\n .credentials dd { margin: 0; overflow-wrap: anywhere; }\n .credentials code { user-select: all; }\n .redacted { font-family: ui-monospace, monospace; letter-spacing: .08em; }\n .secret-line, .value-line { display: flex; gap: 10px; align-items: center; min-height: 30px; }\n .edit-secret { padding: 2px 8px; font-size: 13px; }\n .secret-editor { display: flex; gap: 8px; flex-wrap: wrap; width: 100%; }\n .secret-editor input { width: min(420px, 100%); }\n .edit-configuration { padding: 2px 8px; font-size: 13px; }\n .startup-owned { color: #777; font-size: 13px; }\n .danger { color: #c33; border-color: #c336; }\n .subsection { margin-top: 16px; padding-top: 14px; border-top: 1px solid #8883; }\n textarea { width: min(720px, 100%); min-height: 100px; padding: 7px; box-sizing: border-box; }\n .uris { margin: 8px 0; padding-left: 20px; } .uris code { user-select: all; }\n .notice { border-left: 4px solid #d99b13; padding: 8px 12px; background: #d99b1315; }\n .diff-title { display: block; margin-bottom: 8px; }\n .config-diff { display: grid; gap: 1px; border: 1px solid #8884; border-radius: 6px; overflow: hidden; background: #8884; }\n .diff-row { display: grid; grid-template-columns: minmax(140px, .8fr) minmax(0, 1.2fr) minmax(0, 1.2fr); background: Canvas; }\n .diff-row > * { min-width: 0; padding: 7px 9px; overflow-wrap: anywhere; white-space: pre-wrap; }\n .diff-head { font-size: 12px; font-weight: 600; color: #777; }\n .diff-field { font-weight: 600; }\n .diff-value { font-family: ui-monospace, monospace; font-size: 13px; }\n pre { padding: 12px; border-radius: 6px; background: #8881; overflow-x: auto; user-select: all; }\n #message { white-space: pre-wrap; padding: 10px 0; min-height: 1.5em; }\n .error { color: #c33; } .ok { color: #198754; } .warn { color: #a66b00; }\n code { overflow-wrap: anywhere; }\n .admin-layout { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 32px; align-items: start; }\n .admin-nav { position: sticky; top: 24px; display: grid; gap: 3px; padding: 10px; border: 1px solid #8886; border-radius: 8px; background: Canvas; }\n .admin-nav a { padding: 7px 9px; border-radius: 5px; color: inherit; text-decoration: none; white-space: nowrap; }\n .admin-nav a:hover { background: #8882; }\n .admin-section { scroll-margin-top: 24px; }\n details.environment { margin: 0 0 24px; } details.environment summary { cursor: pointer; font-weight: 600; }\n @media (max-width: 760px) {\n body { margin-top: 24px; }\n .admin-layout { grid-template-columns: 1fr; gap: 18px; }\n .admin-nav { position: static; display: flex; overflow-x: auto; }\n .credentials { grid-template-columns: 1fr; gap: 2px; }\n .credentials dd { margin-bottom: 8px; }\n .diff-row { grid-template-columns: 1fr; }\n .diff-head { display: none; }\n .diff-value::before { display: block; margin-bottom: 2px; color: #777; font: 11px/1.4 system-ui, sans-serif; }\n .diff-current::before { content: 'Current'; }\n .diff-expected::before { content: 'Expected'; }\n }\n [hidden] { display: none !important; }\n </style>\n</head>\n<body>\n <section id=\"access\">\n <h1>AgentConnect Tenant Admin</h1>\n <p id=\"access-message\" class=\"muted\" aria-live=\"polite\">Checking Logto sign-in…</p>\n <div class=\"row\">\n <button id=\"login\" hidden>Sign in with Logto</button>\n <a id=\"open-logto\" class=\"button\" href=\"http://localhost:3002\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n <button id=\"show-bootstrap\" hidden>Continue setup</button>\n </div>\n\n <section id=\"bootstrap\" hidden>\n <h2>Set up sign-in</h2>\n <p id=\"bootstrap-progress\" class=\"muted\">Step 1 of 2</p>\n <div id=\"bootstrap-logto-step\" class=\"panel\">\n <h3>Connect Logto</h3>\n <p class=\"muted\">Enter the one-time Logto Management API credential. It is sealed in the deployment database and verified before continuing.</p>\n <label class=\"field\">Logto M2M App ID<input id=\"logto-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Logto M2M App Secret<input id=\"logto-app-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-management-api-resource\" type=\"url\" autocomplete=\"off\"></label>\n <p class=\"muted\">Logto Cloud uses <code>https://<tenant-id>.logto.app/api</code>. Keep <code>https://default.logto.app/api</code> for Logto OSS.</p>\n <button id=\"bootstrap-logto-submit\">Save Logto and continue</button>\n </div>\n <div id=\"bootstrap-provider-step\" hidden>\n <h3>Choose a sign-in provider</h3>\n <p class=\"muted\">Logto Management API access is ready. Configure one provider to enable sign-in.</p>\n <label class=\"field\">Sign-in provider\n <select id=\"bootstrap-provider\"><option value=\"google\">Google (works on localhost)</option><option value=\"github\">GitHub integration App</option><option id=\"bootstrap-slack-option\" value=\"slack\">Slack integration App</option></select>\n </label>\n <div id=\"bootstrap-google\" class=\"panel\">\n <h3>Google OAuth client</h3>\n <p class=\"muted\">Create a Web application client in Google Auth Platform, then paste its credentials here.</p>\n <p>Authorized JavaScript origin:</p><ul id=\"bootstrap-google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"bootstrap-google-redirects\" class=\"uris\"></ul>\n <a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a>\n <label class=\"field\">Client ID<input id=\"bootstrap-google-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client Secret<input id=\"bootstrap-google-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-google-submit\">Save Google OAuth and configure Logto</button>\n </div>\n <div id=\"bootstrap-github\" class=\"panel\" hidden>\n <h3>GitHub integration App</h3>\n <p id=\"bootstrap-github-note\" class=\"muted\">This creates one complete App for both GitHub sign-in and repository integration.</p>\n <label class=\"field\">Owner\n <select id=\"bootstrap-github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"bootstrap-github-org-field\" class=\"field\" hidden>Organization login<input id=\"bootstrap-github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"bootstrap-github-name\" value=\"AgentConnect\"></label>\n <button id=\"bootstrap-github-submit\">Create GitHub App and configure Logto</button>\n </div>\n <div id=\"bootstrap-slack\" class=\"panel\" hidden>\n <h3>Slack integration App</h3>\n <p id=\"bootstrap-slack-note\" class=\"muted\">Creates one complete Slack App for workspace installation and a separate Sign in with Slack OIDC flow.</p>\n <p>Logto redirect URI:</p><ul id=\"bootstrap-slack-redirects\" class=\"uris\"></ul>\n <label class=\"field\">App name<input id=\"bootstrap-slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"bootstrap-slack-token\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-slack-submit\">Create Slack App and configure Logto</button>\n </div>\n <button id=\"bootstrap-back\">Back to Logto credentials</button>\n </div>\n </section>\n </section>\n\n <main id=\"admin\" hidden>\n <div class=\"admin-layout\">\n <nav class=\"admin-nav\" aria-label=\"Deployment settings\">\n <a href=\"#startup-section\">Startup</a>\n <a href=\"#logto-section\">Logto</a>\n <a href=\"#github-section\">GitHub</a>\n <a href=\"#slack-section\">Slack</a>\n <a href=\"#google-section\">Google</a>\n <a href=\"#feishu-section\">Feishu</a>\n <a href=\"#lark-section\">Lark</a>\n <a href=\"#options-section\">Options</a>\n </nav>\n <div class=\"admin-content\">\n <h1>AgentConnect deployment settings</h1>\n <p class=\"muted\">Saved settings take effect after the stack is restarted.</p>\n <div class=\"row\">\n <button id=\"logout\">Log out</button>\n </div>\n <div id=\"message\" aria-live=\"polite\"></div>\n\n <section id=\"editor\" hidden>\n <details id=\"startup-section\" class=\"environment admin-section\" open>\n <summary>Startup environment</summary>\n <p class=\"notice\">Public service URLs come from <code>.env</code>. Provider callbacks below are derived from these values.</p>\n <pre id=\"startup-environment\"></pre>\n </details>\n\n <section id=\"logto-section\" class=\"admin-section\" aria-labelledby=\"logto-heading\">\n <div class=\"section-head\">\n <div><h2 id=\"logto-heading\">Logto</h2><p class=\"muted\">Authentication and Tenant Admin access.</p></div>\n <span id=\"logto-match\" class=\"badge\">Not checked</span>\n </div>\n <div class=\"panel\">\n <dl class=\"credentials\">\n <dt>Management endpoint</dt><dd class=\"value-line\"><code id=\"logto-management-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>Management App ID</dt><dd class=\"value-line\"><code id=\"logto-management-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management API resource</dt><dd class=\"value-line\"><code id=\"logto-management-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management App secret</dt><dd class=\"secret-line\"><span id=\"logto-management-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.managementAppSecret\" data-secret-display=\"logto-management-secret-display\">Edit</button></dd>\n <dt>Sign-in endpoint</dt><dd class=\"value-line\"><code id=\"logto-browser-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>SPA App ID</dt><dd class=\"value-line\"><code id=\"logto-browser-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Browser API resource</dt><dd class=\"value-line\"><code id=\"logto-browser-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n </dl>\n <div id=\"logto-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Logto configuration</h3>\n <label class=\"field\">Management App ID<input id=\"logto-edit-management-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-edit-management-resource\" autocomplete=\"off\"></label>\n <label class=\"field\">New Management App secret<input id=\"logto-edit-management-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when management identity changes\"></label>\n <label class=\"field\">SPA App ID<input id=\"logto-edit-browser-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Browser API resource<input id=\"logto-edit-browser-resource\" autocomplete=\"off\" placeholder=\"Optional\"></label>\n <div class=\"row\"><button id=\"save-logto-configuration\">Save configuration</button><button id=\"cancel-logto-configuration\">Cancel</button></div>\n </div>\n <p id=\"logto-status\" class=\"muted\">Checking redirects and sign-in settings…</p>\n <div id=\"logto-drift\" class=\"notice\" hidden></div>\n <div class=\"row\">\n <button id=\"check-logto\">Check match</button>\n <button id=\"reconcile-logto\">Apply expected settings</button>\n <a id=\"logto-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n </div>\n </div>\n </section>\n\n <h2>Providers</h2>\n <div class=\"provider-stack\">\n <section id=\"github-section\" class=\"panel admin-section\" aria-labelledby=\"github-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"github-heading\">GitHub</h3><p class=\"muted\">Repository integration and optional Logto sign-in.</p></div>\n <span id=\"github-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"github-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>App slug</dt><dd class=\"value-line\"><code id=\"github-app-slug\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"github-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Webhook delivery</dt><dd class=\"value-line\"><code id=\"github-webhook-enabled\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"github-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.clientSecret\" data-secret-display=\"github-client-secret-display\">Edit</button></dd>\n <dt>Private key</dt><dd class=\"secret-line\"><span id=\"github-private-key-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.privateKeyB64\" data-secret-display=\"github-private-key-display\">Edit</button></dd>\n <dt>Webhook secret</dt><dd class=\"secret-line\"><span id=\"github-webhook-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.webhookSecret\" data-secret-display=\"github-webhook-secret-display\">Edit</button></dd>\n <dt>Logto connector secret</dt><dd class=\"secret-line\"><span id=\"github-logto-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.githubConnectorClientSecret\" data-secret-display=\"github-logto-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"github-status\" class=\"muted\"></p>\n <div id=\"github-drift\" class=\"notice\" hidden></div>\n <div id=\"github-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit GitHub App identity</h3>\n <label class=\"field\">App ID<input id=\"github-edit-app-id\" inputmode=\"numeric\" autocomplete=\"off\"></label>\n <label class=\"field\">App slug<input id=\"github-edit-slug\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"github-edit-client-id\" autocomplete=\"off\"></label>\n <label><input id=\"github-edit-webhook-enabled\" type=\"checkbox\"> Enable Relay webhook delivery</label>\n <label class=\"field\">New client secret<input id=\"github-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when App or Client ID changes\"></label>\n <label class=\"field\">New private key (base64)<textarea id=\"github-edit-private-key\" autocomplete=\"off\" placeholder=\"Required when App ID changes\"></textarea></label>\n <label class=\"field\">New webhook secret<input id=\"github-edit-webhook-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when an active webhook App ID changes\"></label>\n <label id=\"github-edit-logto-secret-field\" class=\"field\" hidden>New Logto connector client secret<input id=\"github-edit-logto-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when this App is also used for sign-in\"></label>\n <div class=\"row\"><button id=\"save-github-configuration\">Save configuration</button><button id=\"cancel-github-configuration\">Cancel</button></div>\n </div>\n <div id=\"github-create-controls\" class=\"subsection\">\n <label class=\"field\">Owner\n <select id=\"github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"github-org-field\" class=\"field\" hidden>Organization login<input id=\"github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"github-name\" value=\"AgentConnect\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-github\">Create GitHub App</button>\n <button id=\"connect-github-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-github\" hidden>Check match</button>\n <button id=\"clear-github\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"github-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open GitHub settings</a>\n </div>\n </section>\n\n <section id=\"slack-section\" class=\"panel admin-section\" aria-labelledby=\"slack-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"slack-heading\">Slack</h3><p class=\"muted\">One App for workspace integration and Logto sign-in.</p></div>\n <span id=\"slack-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"slack-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"slack-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"slack-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.clientSecret\" data-secret-display=\"slack-client-secret-display\">Edit</button></dd>\n <dt>Signing secret</dt><dd class=\"secret-line\"><span id=\"slack-signing-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.signingSecret\" data-secret-display=\"slack-signing-secret-display\">Edit</button></dd>\n <dt>Logto sign-in</dt><dd id=\"slack-logto-status\">Not configured</dd>\n </dl>\n <p id=\"slack-status\" class=\"muted\"></p>\n <div id=\"slack-drift\" class=\"notice\" hidden></div>\n <div id=\"slack-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Slack App identity</h3>\n <label class=\"field\">App ID<input id=\"slack-edit-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"slack-edit-client-id\" autocomplete=\"off\"></label>\n <label class=\"field\">New client secret<input id=\"slack-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <label class=\"field\">New signing secret<input id=\"slack-edit-signing-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <div class=\"row\"><button id=\"save-slack-configuration\">Save configuration</button><button id=\"cancel-slack-configuration\">Cancel</button></div>\n </div>\n <div class=\"subsection\">\n <label id=\"slack-name-field\" class=\"field\">App name<input id=\"slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"slack-token\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Used only for create or check\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-slack\">Create Slack App</button>\n <button id=\"connect-slack-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-slack\" hidden>Check match</button>\n <button id=\"clear-slack\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"slack-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Slack settings</a>\n </div>\n </section>\n\n <section id=\"google-section\" class=\"panel admin-section\" aria-labelledby=\"google-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"google-heading\">Google</h3><p class=\"muted\">OAuth client used by the Logto Google connector.</p></div>\n <span id=\"google-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"google-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"google\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"google-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.googleConnectorClientSecret\" data-secret-display=\"google-client-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"google-status\" class=\"muted\"></p>\n <div id=\"google-drift\" class=\"notice\" hidden></div>\n <p>Authorized JavaScript origin:</p><ul id=\"google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"google-redirects\" class=\"uris\"></ul>\n <p class=\"muted\">Google does not expose OAuth client redirect settings through an API. Copy these required values into Google Auth Platform; Tenant Admin can verify only the Logto connector.</p>\n <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a></div>\n <div id=\"google-config-controls\" class=\"subsection\">\n <label class=\"field\">Client ID<input id=\"google-id\" autocomplete=\"off\"></label>\n <label id=\"google-initial-secret-field\" class=\"field\">Client secret<input id=\"google-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when Client ID changes\"></label>\n </div>\n <div class=\"row\"><button id=\"save-google\">Save Google client</button><button id=\"cancel-google-configuration\" hidden>Cancel</button><button id=\"check-google\" hidden>Check match</button><button id=\"clear-google\" class=\"danger\" hidden>Clear configuration</button></div>\n </section>\n\n <section id=\"feishu-section\" class=\"panel admin-section\" aria-labelledby=\"feishu-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"feishu-heading\">Feishu</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Feishu, OAuth callbacks, and published App setup.</p></div>\n <span id=\"feishu-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"feishu-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"feishu\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"feishu-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"feishu.loginAppSecret\" data-secret-display=\"feishu-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"feishu-login-status\" class=\"muted\"></p>\n <div id=\"feishu-drift\" class=\"notice\" hidden></div>\n <div id=\"feishu-config-controls\" class=\"subsection\">\n <label id=\"feishu-create-name-field\" class=\"field\">New App name<input id=\"feishu-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"feishu-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"feishu-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-feishu-login-app\">Create Feishu App</button>\n <button id=\"save-feishu-login-app\">Save existing App</button>\n <button id=\"cancel-feishu-configuration\" hidden>Cancel</button>\n <button id=\"check-feishu-login-app\" hidden>Check setup</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"feishu-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Feishu settings</a>\n </div>\n </section>\n\n <section id=\"lark-section\" class=\"panel admin-section\" aria-labelledby=\"lark-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"lark-heading\">Lark</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Lark, OAuth callbacks, and published App setup.</p></div>\n <span id=\"lark-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"lark-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"lark\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"lark-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"lark.loginAppSecret\" data-secret-display=\"lark-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"lark-login-status\" class=\"muted\"></p>\n <div id=\"lark-drift\" class=\"notice\" hidden></div>\n <div id=\"lark-config-controls\" class=\"subsection\">\n <label id=\"lark-create-name-field\" class=\"field\">New App name<input id=\"lark-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"lark-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"lark-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-lark-login-app\">Create Lark App</button>\n <button id=\"save-lark-login-app\">Save existing App</button>\n <button id=\"cancel-lark-configuration\" hidden>Cancel</button>\n <button id=\"check-lark-login-app\" hidden>Check setup</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"lark-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Lark settings</a>\n </div>\n </section>\n </div>\n\n <section id=\"options-section\" class=\"admin-section\">\n <h2>Deployment options</h2>\n <div class=\"panel\">\n <label class=\"field\"><span><input id=\"preset-agents-enabled\" type=\"checkbox\"> Enable preset Agents</span></label>\n <div class=\"row\"><button id=\"save-options\">Save options</button></div>\n </div>\n </section>\n </section>\n </div>\n </div>\n </main>\n\n <script>\n const api = '/api/v1';\n const tokenKey = 'agentconnect.tenant-admin.token';\n const verifierKey = 'agentconnect.tenant-admin.pkce';\n const stateKey = 'agentconnect.tenant-admin.state';\n let currentRevision = 0;\n let currentStatus = null;\n let bootstrapInfo = null;\n const el = (id) => document.getElementById(id);\n const message = (text, error = false) => {\n const target = el('admin').hidden ? el('access-message') : el('message');\n target.textContent = text;\n target.className = error ? 'error' : 'ok';\n };\n const bearer = () => {\n const token = sessionStorage.getItem(tokenKey);\n return token ? { authorization: 'Bearer ' + token } : {};\n };\n const json = async (response) => {\n const body = await response.json().catch(() => ({}));\n if (!response.ok) throw Object.assign(new Error(body.message || ('HTTP ' + response.status)), { status: response.status, code: body.code });\n return body;\n };\n const base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n const random = () => base64url(crypto.getRandomValues(new Uint8Array(32)));\n const configured = (byKey, key) => Boolean(byKey.get(key) && byKey.get(key).configured);\n\n function showIdentityEditors(provider, show) {\n for (const button of document.querySelectorAll('.edit-configuration[data-provider=\"' + provider + '\"]')) {\n button.hidden = !show;\n }\n }\n\n function text(id, value) {\n el(id).textContent = value === null || value === undefined || value === '' ? 'Not configured' : String(value);\n }\n\n function secretText(id, byKey, key, editable) {\n const stored = configured(byKey, key);\n const display = el(id);\n const button = document.querySelector('[data-secret-display=\"' + id + '\"]');\n const row = display.closest('.secret-line');\n const editor = row && row.querySelector('.secret-editor');\n if (editor) editor.remove();\n display.hidden = false;\n display.textContent = stored ? '***' : 'Not configured';\n display.className = stored ? 'redacted' : 'muted';\n button.hidden = !editable;\n button.textContent = stored ? 'Edit' : 'Set';\n }\n\n function match(id, state, label) {\n const target = el(id);\n target.textContent = label;\n target.className = 'badge' + (state ? ' ' + state : '');\n }\n\n async function authConfig() { return json(await fetch(api + '/auth-config')); }\n\n async function signIn() {\n const config = await authConfig();\n if (config.mode !== 'oidc') throw new Error('Save an OIDC configuration first');\n const verifier = random();\n const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));\n const state = random();\n sessionStorage.setItem(verifierKey, verifier);\n sessionStorage.setItem(stateKey, state);\n const url = new URL(config.authorizationEndpoint);\n url.searchParams.set('client_id', config.appId);\n url.searchParams.set('redirect_uri', config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', 'openid profile email roles');\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n if (config.resource) url.searchParams.set('resource', config.resource);\n location.assign(url);\n }\n\n async function finishSignIn() {\n const url = new URL(location.href);\n const code = url.searchParams.get('code');\n if (!code) return;\n const state = url.searchParams.get('state');\n if (!state || state !== sessionStorage.getItem(stateKey)) throw new Error('OIDC state mismatch');\n const verifier = sessionStorage.getItem(verifierKey);\n if (!verifier) throw new Error('PKCE verifier is missing; start sign-in again');\n const config = await authConfig();\n const body = new URLSearchParams({\n grant_type: 'authorization_code', code, client_id: config.appId,\n redirect_uri: config.redirectUri, code_verifier: verifier\n });\n if (config.resource) body.set('resource', config.resource);\n const tokens = await json(await fetch(config.tokenEndpoint, {\n method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body\n }));\n if (typeof tokens.id_token !== 'string') throw new Error('Logto did not return an ID token');\n sessionStorage.setItem(tokenKey, tokens.id_token);\n sessionStorage.removeItem(verifierKey);\n sessionStorage.removeItem(stateKey);\n history.replaceState({}, '', '/');\n }\n\n function renderUriList(id, values) {\n el(id).replaceChildren(...(values || []).map((value) => {\n const item = document.createElement('li');\n const code = document.createElement('code'); code.textContent = value;\n item.append(code); return item;\n }));\n }\n\n function updateBootstrapProvider() {\n const provider = el('bootstrap-provider').value;\n el('bootstrap-google').hidden = provider !== 'google';\n el('bootstrap-github').hidden = provider !== 'github';\n el('bootstrap-slack').hidden = provider !== 'slack';\n }\n\n function showBootstrapStep(step) {\n const logto = step === 'logto';\n el('bootstrap-logto-step').hidden = !logto;\n el('bootstrap-provider-step').hidden = logto;\n el('bootstrap-progress').textContent = logto ? 'Step 1 of 2' : 'Step 2 of 2';\n }\n\n function updateOwner(prefix) {\n el(prefix + '-org-field').hidden = el(prefix + '-owner').value !== 'organization';\n }\n\n function ownership(prefix) {\n if (el(prefix + '-owner').value === 'personal') return { owner: 'personal', organization: null };\n const organization = el(prefix + '-org').value.trim();\n if (!organization) throw new Error('Enter the GitHub organization login');\n return { owner: 'organization', organization };\n }\n\n function formatDiffValue(value) {\n if (value === null || value === undefined || value === '') return 'Not configured';\n if (Array.isArray(value)) return value.length ? value.map(formatDiffValue).join('\\n') : '[]';\n if (typeof value === 'object') {\n return Object.entries(value).map(([key, item]) => key + ': ' + (Array.isArray(item) ? item.join(', ') : formatDiffValue(item))).join('\\n');\n }\n return String(value);\n }\n\n function showDiff(id, rows, label = 'Update required') {\n const box = el(id);\n box.hidden = rows.length === 0;\n box.replaceChildren();\n if (rows.length === 0) return;\n const title = document.createElement('strong');\n title.className = 'diff-title';\n title.textContent = label;\n const grid = document.createElement('div');\n grid.className = 'config-diff';\n const head = document.createElement('div');\n head.className = 'diff-row diff-head';\n for (const text of ['Field', 'Current', 'Expected']) {\n const cell = document.createElement('span'); cell.textContent = text; head.append(cell);\n }\n grid.append(head);\n for (const item of rows) {\n const row = document.createElement('div'); row.className = 'diff-row';\n const field = document.createElement('span'); field.className = 'diff-field'; field.textContent = item.field;\n const current = document.createElement('span'); current.className = 'diff-value diff-current'; current.textContent = formatDiffValue(item.current);\n const expected = document.createElement('span'); expected.className = 'diff-value diff-expected'; expected.textContent = formatDiffValue(item.expected);\n row.append(field, current, expected); grid.append(row);\n }\n box.append(title, grid);\n }\n\n function renderStartupEnvironment() {\n const services = bootstrapInfo.services;\n const environment = [\n ['AGENTCONNECT_PUBLIC_CP_URL', services.controlPlane],\n ['AGENTCONNECT_PUBLIC_RELAY_URL', services.relay],\n ['AGENTCONNECT_PUBLIC_WEB_URL', services.web],\n ['LOGTO_ENDPOINT', bootstrapInfo.logtoEndpoint],\n ['LOGTO_MGMT_ENDPOINT', bootstrapInfo.logtoManagementEndpoint]\n ];\n el('startup-environment').textContent = environment\n .filter(([, value]) => value)\n .map(([key, value]) => key + '=' + value)\n .join('\\n');\n }\n\n function renderApps(status) {\n currentStatus = status;\n const byKey = new Map((status.secrets || []).map((item) => [item.key, item]));\n const values = status.values;\n const expected = status.providerExpectations || { github: null, slack: null, google: { origins: [], redirects: [] } };\n renderStartupEnvironment();\n\n const logto = values.logto;\n text('logto-management-endpoint', logto && bootstrapInfo.logtoManagementEndpoint);\n text('logto-management-id', logto && logto.managementAppId);\n text('logto-management-resource', logto && logto.managementResource);\n secretText('logto-management-secret-display', byKey, 'logto.managementAppSecret', Boolean(logto));\n text('logto-browser-endpoint', logto && logto.browser && bootstrapInfo.logtoEndpoint);\n text('logto-browser-id', values.auth.mode === 'oidc' ? values.auth.browserClient.appId : null);\n text('logto-browser-resource', logto && logto.browser && logto.browser.apiResource);\n el('logto-edit-controls').hidden = true;\n showIdentityEditors('logto', Boolean(logto && logto.browser && values.auth.mode === 'oidc'));\n match(\n 'logto-match',\n logto && configured(byKey, 'logto.managementAppSecret') ? '' : 'warn',\n logto && configured(byKey, 'logto.managementAppSecret') ? 'Ready to check' : 'Not configured'\n );\n\n el('preset-agents-enabled').checked = values.features.presetAgentsEnabled;\n\n const github = values.github;\n const webhookStored = byKey.get('github.webhookSecret') && byKey.get('github.webhookSecret').configured;\n const webhookInactive = github && github.webhookEnabled === false;\n text('github-app-id', github && github.appId);\n text('github-app-slug', github && github.slug);\n text('github-client-id', github && github.clientId);\n text('github-webhook-enabled', github && (github.webhookEnabled === false ? 'Disabled' : 'Enabled'));\n el('github-edit-controls').hidden = true;\n showIdentityEditors('github', Boolean(github));\n secretText('github-client-secret-display', byKey, 'github.clientSecret', Boolean(github));\n secretText('github-private-key-display', byKey, 'github.privateKeyB64', Boolean(github));\n secretText('github-webhook-secret-display', byKey, 'github.webhookSecret', Boolean(github));\n secretText('github-logto-secret-display', byKey, 'logto.githubConnectorClientSecret', Boolean(values.logto && values.logto.githubConnector));\n el('github-status').textContent = github\n ? github.slug + ' is configured. Webhook secret: ' + (webhookStored ? 'stored' : webhookInactive ? 'not required yet' : 'missing') + '.' +\n (webhookInactive ? ' Relay webhook delivery is disabled.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github\n ? expected.github\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'Valid Web, API, and ingress URLs' }]\n : [];\n match('github-match', !github ? '' : githubDrift.length ? 'warn' : '', !github ? 'Not configured' : githubDrift.length ? 'Expected URLs changed' : 'Ready to check');\n el('github-create-controls').hidden = Boolean(github);\n el('create-github').hidden = Boolean(github);\n el('clear-github').hidden = !github;\n el('connect-github-login').hidden = !github || !values.logto || Boolean(values.logto.githubConnector);\n el('check-github').hidden = !github;\n if (github) showDiff('github-drift', githubDrift, 'Expected URLs changed since App creation');\n else el('github-drift').hidden = true;\n\n const slack = values.slack;\n text('slack-app-id', slack && slack.appId);\n text('slack-client-id', slack && slack.clientId);\n el('slack-edit-controls').hidden = true;\n showIdentityEditors('slack', Boolean(slack));\n secretText('slack-client-secret-display', byKey, 'slack.clientSecret', Boolean(slack));\n secretText('slack-signing-secret-display', byKey, 'slack.signingSecret', Boolean(slack));\n el('slack-logto-status').textContent = values.logto && values.logto.slackConnector\n ? 'Enabled; reuses the Slack client secret above.'\n : 'Not enabled.';\n el('slack-status').textContent = slack ? slack.appId + ' is configured.' : 'Creates the default AgentConnect integration manifest.';\n const slackDrift = slack\n ? expected.slack\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'HTTPS Web, API, and ingress URLs' }]\n : [];\n match('slack-match', !slack ? '' : slackDrift.length ? 'warn' : '', !slack ? 'Not configured' : slackDrift.length ? 'Update required' : 'Ready to check');\n el('slack-name-field').hidden = Boolean(slack);\n el('create-slack').hidden = Boolean(slack);\n el('connect-slack-login').hidden = !slack || !values.logto || Boolean(values.logto.slackConnector) || !bootstrapInfo?.slackAvailable;\n el('clear-slack').hidden = !slack;\n el('check-slack').hidden = !slack;\n el('slack-settings').hidden = !slack;\n if (slack) {\n el('slack-settings').href = 'https://api.slack.com/apps/' + encodeURIComponent(slack.appId);\n showDiff('slack-drift', slackDrift);\n } else el('slack-drift').hidden = true;\n\n const google = values.logto && values.logto.googleConnector;\n const googleSecret = configured(byKey, 'logto.googleConnectorClientSecret');\n const expectedGoogle = expected.google;\n renderUriList('google-origins', expectedGoogle.origins);\n renderUriList('google-redirects', expectedGoogle.redirects);\n text('google-client-id', google && google.clientId);\n secretText('google-client-secret-display', byKey, 'logto.googleConnectorClientSecret', Boolean(google));\n showIdentityEditors('google', Boolean(google));\n el('google-id').value = google ? google.clientId : '';\n el('google-status').textContent = google ? 'Google OAuth client is configured.' : 'Create a Web application OAuth client manually, then save it here.';\n showDiff('google-drift', []);\n match('google-match', google && googleSecret ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : \"Can't verify automatically\");\n el('google-initial-secret-field').hidden = googleSecret;\n el('save-google').textContent = google ? 'Confirm callback settings' : 'Save Google client';\n el('google-config-controls').hidden = Boolean(google);\n el('save-google').hidden = Boolean(google);\n el('cancel-google-configuration').hidden = true;\n el('check-google').hidden = !google || !googleSecret;\n el('clear-google').hidden = !google;\n\n const feishuSecret = byKey.get('feishu.loginAppSecret');\n const larkSecret = byKey.get('lark.loginAppSecret');\n el('feishu-login-id').value = values.feishu ? values.feishu.loginAppId : '';\n el('lark-login-id').value = values.lark ? values.lark.loginAppId : '';\n text('feishu-app-id', values.feishu && values.feishu.loginAppId);\n text('lark-app-id', values.lark && values.lark.loginAppId);\n showIdentityEditors('feishu', Boolean(values.feishu));\n showIdentityEditors('lark', Boolean(values.lark));\n secretText('feishu-app-secret-display', byKey, 'feishu.loginAppSecret', Boolean(values.feishu));\n secretText('lark-app-secret-display', byKey, 'lark.loginAppSecret', Boolean(values.lark));\n el('feishu-login-status').textContent = values.feishu && feishuSecret && feishuSecret.configured\n ? values.feishu.loginAppId + ' is configured.'\n : 'No Feishu tenant App is configured.';\n el('lark-login-status').textContent = values.lark && larkSecret && larkSecret.configured\n ? values.lark.loginAppId + ' is configured.'\n : 'No Lark tenant App is configured.';\n match('feishu-match', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? '' : '', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? 'Ready to check' : 'Not configured');\n match('lark-match', values.lark && configured(byKey, 'lark.loginAppSecret') ? '' : '', values.lark && configured(byKey, 'lark.loginAppSecret') ? 'Ready to check' : 'Not configured');\n showDiff('feishu-drift', []);\n showDiff('lark-drift', []);\n el('feishu-config-controls').hidden = Boolean(values.feishu);\n el('lark-config-controls').hidden = Boolean(values.lark);\n el('feishu-create-name-field').hidden = Boolean(values.feishu);\n el('lark-create-name-field').hidden = Boolean(values.lark);\n el('create-feishu-login-app').hidden = Boolean(values.feishu);\n el('create-lark-login-app').hidden = Boolean(values.lark);\n el('save-feishu-login-app').hidden = Boolean(values.feishu);\n el('save-lark-login-app').hidden = Boolean(values.lark);\n el('cancel-feishu-configuration').hidden = true;\n el('cancel-lark-configuration').hidden = true;\n el('clear-feishu').hidden = !values.feishu;\n el('clear-lark').hidden = !values.lark;\n el('check-feishu-login-app').hidden = !values.feishu || !configured(byKey, 'feishu.loginAppSecret');\n el('check-lark-login-app').hidden = !values.lark || !configured(byKey, 'lark.loginAppSecret');\n el('feishu-settings').hidden = !values.feishu;\n el('lark-settings').hidden = !values.lark;\n if (values.feishu) el('feishu-settings').href = 'https://open.feishu.cn/app/' + encodeURIComponent(values.feishu.loginAppId);\n if (values.lark) el('lark-settings').href = 'https://open.larksuite.com/app/' + encodeURIComponent(values.lark.loginAppId);\n }\n\n function requiredInput(id, label) {\n const value = el(id).value.trim();\n if (!value) throw new Error('Enter ' + label);\n return value;\n }\n\n function githubConnectorUsesDeployment(values) {\n const github = values.github;\n const connector = values.logto && values.logto.githubConnector;\n return Boolean(\n github && connector &&\n connector.appId === github.appId &&\n connector.slug === github.slug &&\n connector.clientId === github.clientId\n );\n }\n\n function beginConfigurationEdit(provider) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const values = currentStatus.values;\n if (provider === 'logto') {\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') return;\n el('logto-edit-management-id').value = logto.managementAppId;\n el('logto-edit-management-resource').value = logto.managementResource;\n el('logto-edit-management-secret').value = '';\n el('logto-edit-browser-id').value = values.auth.browserClient.appId;\n el('logto-edit-browser-resource').value = logto.browser.apiResource || '';\n el('logto-edit-controls').hidden = false;\n el('logto-edit-management-id').focus();\n } else if (provider === 'github') {\n const github = values.github;\n if (!github) return;\n el('github-edit-app-id').value = String(github.appId);\n el('github-edit-slug').value = github.slug;\n el('github-edit-client-id').value = github.clientId || '';\n el('github-edit-webhook-enabled').checked = github.webhookEnabled !== false;\n for (const id of ['github-edit-client-secret', 'github-edit-private-key', 'github-edit-webhook-secret', 'github-edit-logto-secret']) el(id).value = '';\n el('github-edit-logto-secret-field').hidden = !githubConnectorUsesDeployment(values);\n el('github-edit-controls').hidden = false;\n el('clear-github').hidden = true;\n el('github-edit-app-id').focus();\n } else if (provider === 'slack') {\n const slack = values.slack;\n if (!slack) return;\n el('slack-edit-app-id').value = slack.appId;\n el('slack-edit-client-id').value = slack.clientId;\n el('slack-edit-client-secret').value = '';\n el('slack-edit-signing-secret').value = '';\n el('slack-edit-controls').hidden = false;\n el('clear-slack').hidden = true;\n el('slack-edit-app-id').focus();\n } else if (provider === 'google') {\n const google = values.logto && values.logto.googleConnector;\n if (!google) return;\n el('google-id').value = google.clientId;\n el('google-secret').value = '';\n el('google-config-controls').hidden = false;\n el('google-initial-secret-field').hidden = false;\n el('save-google').hidden = false;\n el('save-google').textContent = 'Save Google client';\n el('cancel-google-configuration').hidden = false;\n el('clear-google').hidden = true;\n el('google-id').focus();\n } else if (provider === 'feishu' || provider === 'lark') {\n el(provider + '-config-controls').hidden = false;\n el(provider + '-create-name-field').hidden = true;\n el('create-' + provider + '-login-app').hidden = true;\n el('save-' + provider + '-login-app').hidden = false;\n el('cancel-' + provider + '-configuration').hidden = false;\n el('clear-' + provider).hidden = true;\n el(provider + '-login-secret').value = '';\n el(provider + '-login-id').focus();\n }\n showIdentityEditors(provider, false);\n }\n\n function cancelConfigurationEdit() {\n if (currentStatus) renderApps(currentStatus);\n }\n\n async function replaceConfiguration(values, secrets, successMessage) {\n const response = await json(await fetch(api + '/deployment-config', {\n method: 'PUT',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values, ...(secrets ? { secrets } : {}) })\n }));\n await load();\n message(successMessage || 'Configuration saved. Restart AgentConnect to apply it.');\n return response;\n }\n\n async function saveLogtoConfiguration() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = currentStatus.values;\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') throw new Error('Logto is not configured');\n const managementAppId = requiredInput('logto-edit-management-id', 'the Management App ID');\n const managementResource = requiredInput('logto-edit-management-resource', 'the Management API resource');\n const browserAppId = requiredInput('logto-edit-browser-id', 'the SPA App ID');\n const browserResource = el('logto-edit-browser-resource').value.trim() || null;\n const managementIdentityChanged = managementAppId !== logto.managementAppId;\n const managementSecret = el('logto-edit-management-secret').value;\n if (managementIdentityChanged && !managementSecret) throw new Error('Enter the new Management App secret');\n const browser = { ...logto.browser, apiResource: browserResource };\n const auth = {\n ...values.auth,\n audience: browserResource || browserAppId,\n browserClient: { appId: browserAppId, apiResource: browserResource }\n };\n await replaceConfiguration(\n {\n ...values,\n auth,\n logto: { ...logto, managementAppId, managementResource, browser }\n },\n managementSecret ? { 'logto.managementAppSecret': managementSecret } : undefined,\n 'Logto configuration saved. Sign in again if the SPA identity changed, then restart AgentConnect.'\n );\n }\n\n async function saveGithubConfiguration() {\n if (!currentStatus || !currentStatus.values.github) throw new Error('GitHub is not configured');\n const values = currentStatus.values;\n const previous = values.github;\n const appId = Number(requiredInput('github-edit-app-id', 'the GitHub App ID'));\n if (!Number.isSafeInteger(appId) || appId <= 0) throw new Error('GitHub App ID must be a positive integer');\n const slug = requiredInput('github-edit-slug', 'the GitHub App slug');\n const clientId = requiredInput('github-edit-client-id', 'the GitHub Client ID');\n const appChanged = appId !== previous.appId;\n const clientChanged = clientId !== previous.clientId;\n const webhookEnabled = el('github-edit-webhook-enabled').checked;\n const webhookEnabling = webhookEnabled && previous.webhookEnabled === false;\n const connectorReused = githubConnectorUsesDeployment(values);\n const clientSecret = el('github-edit-client-secret').value;\n const privateKey = el('github-edit-private-key').value.trim();\n const webhookSecret = el('github-edit-webhook-secret').value;\n const connectorSecret = el('github-edit-logto-secret').value;\n const webhookSecretStored = currentStatus.secrets.some((secret) => secret.key === 'github.webhookSecret' && secret.configured);\n if ((appChanged || clientChanged) && !clientSecret) throw new Error('Enter the new GitHub client secret');\n if (appChanged && !privateKey) throw new Error('Enter the new GitHub private key as base64');\n if (webhookEnabled && ((appChanged && !webhookSecret) || (webhookEnabling && !webhookSecret && !webhookSecretStored))) throw new Error('Enter the new GitHub webhook secret');\n if (connectorReused && (appChanged || clientChanged) && !connectorSecret) throw new Error('Enter the new Logto connector client secret');\n const secrets = {};\n if (clientSecret) secrets['github.clientSecret'] = clientSecret;\n if (privateKey) secrets['github.privateKeyB64'] = privateKey;\n if (webhookSecret) secrets['github.webhookSecret'] = webhookSecret;\n if (connectorSecret) secrets['logto.githubConnectorClientSecret'] = connectorSecret;\n const nextLogto = connectorReused && values.logto\n ? { ...values.logto, githubConnector: { ...values.logto.githubConnector, appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, webhookEnabled },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\n );\n if (connectorSecret) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function saveSlackConfiguration() {\n if (!currentStatus || !currentStatus.values.slack) throw new Error('Slack is not configured');\n const values = currentStatus.values;\n const previous = values.slack;\n const appId = requiredInput('slack-edit-app-id', 'the Slack App ID');\n const clientId = requiredInput('slack-edit-client-id', 'the Slack Client ID');\n const changed = appId !== previous.appId || clientId !== previous.clientId;\n const clientSecret = el('slack-edit-client-secret').value;\n const signingSecret = el('slack-edit-signing-secret').value;\n if (changed && (!clientSecret || !signingSecret)) throw new Error('Enter both the new Slack client secret and signing secret');\n const secrets = {};\n if (clientSecret) secrets['slack.clientSecret'] = clientSecret;\n if (signingSecret) secrets['slack.signingSecret'] = signingSecret;\n const nextLogto = values.logto && values.logto.slackConnector\n ? { ...values.logto, slackConnector: { ...values.logto.slackConnector, appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\n );\n if (clientSecret && nextLogto && nextLogto.slackConnector) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function clearProvider(provider) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const label = provider === 'github' ? 'GitHub' : provider === 'slack' ? 'Slack' : provider === 'google' ? 'Google' : provider === 'feishu' ? 'Feishu' : 'Lark';\n if (!window.confirm('Clear the saved ' + label + ' configuration and secrets?')) return;\n const values = currentStatus.values;\n let next = values;\n let secrets = {};\n if (provider === 'github') {\n const connectorReused = githubConnectorUsesDeployment(values);\n const logto = connectorReused && values.logto\n ? {\n ...values.logto,\n githubConnector: null\n }\n : values.logto;\n next = { ...values, github: null, ...(logto ? { logto } : {}) };\n secrets = {\n 'github.clientSecret': null,\n 'github.privateKeyB64': null,\n 'github.webhookSecret': null,\n ...(connectorReused ? { 'logto.githubConnectorClientSecret': null } : {})\n };\n } else if (provider === 'slack') {\n const logto = values.logto\n ? {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'slack') }\n : values.logto.browser,\n slackConnector: null\n }\n : values.logto;\n next = { ...values, slack: null, ...(logto ? { logto } : {}) };\n secrets = { 'slack.clientSecret': null, 'slack.signingSecret': null };\n } else if (provider === 'google') {\n if (!values.logto) return;\n next = {\n ...values,\n logto: {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'google') }\n : values.logto.browser,\n googleConnector: null\n }\n };\n secrets = { 'logto.googleConnectorClientSecret': null };\n } else if (provider === 'feishu' || provider === 'lark') {\n next = { ...values, [provider]: null };\n secrets = { [provider + '.loginAppSecret']: null };\n }\n await replaceConfiguration(next, secrets, label + ' configuration cleared. Its setup controls are available again.');\n }\n\n async function startGithub(prefix) {\n const result = await json(await fetch(api + '/create/github/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n purpose: 'deployment',\n name: el(prefix + '-name').value,\n ownership: ownership(prefix),\n connectLogto: prefix === 'bootstrap-github'\n })\n }));\n const form = document.createElement('form');\n form.method = 'post'; form.action = result.action;\n const manifest = document.createElement('input');\n manifest.type = 'hidden'; manifest.name = 'manifest'; manifest.value = JSON.stringify(result.manifest);\n form.append(manifest); document.body.append(form);\n message('Opening GitHub to review the complete integration App.');\n form.submit();\n }\n\n async function loadBootstrapInfo() {\n bootstrapInfo = await json(await fetch(api + '/bootstrap-info'));\n el('open-logto').href = bootstrapInfo.logtoAdminEndpoint;\n el('logto-settings').href = bootstrapInfo.logtoAdminEndpoint;\n if (!el('logto-app-id').value && bootstrapInfo.logtoManagementAppId) {\n el('logto-app-id').value = bootstrapInfo.logtoManagementAppId;\n }\n renderUriList('bootstrap-google-origins', bootstrapInfo.google.javascriptOrigins);\n renderUriList('bootstrap-google-redirects', bootstrapInfo.google.redirectUris);\n renderUriList('bootstrap-slack-redirects', [bootstrapInfo.slackLoginRedirectUrl]);\n el('logto-management-api-resource').value = bootstrapInfo.logtoManagementResource;\n el('bootstrap-github-submit').disabled = !bootstrapInfo.githubAvailable;\n if (!bootstrapInfo.githubAvailable) {\n el('bootstrap-github-note').textContent = 'GitHub App creation needs valid saved Web, API, and ingress URLs.';\n } else if (!bootstrapInfo.githubWebhookActive) {\n el('bootstrap-github-note').textContent = 'Creates the complete GitHub App now without submitting the localhost webhook URL. Add it after saving reachable HTTPS ingress.';\n } else {\n el('bootstrap-github-note').textContent = 'This creates one complete App for both GitHub sign-in and repository integration.';\n }\n el('bootstrap-slack-option').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-submit').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-note').textContent = bootstrapInfo.slackAvailable\n ? 'Creates one complete Slack App. Sign-in uses a separate openid profile email flow from workspace installation.'\n : 'Slack sign-in needs all public service URLs to use HTTPS. Use Google locally or expose the stack through trusted HTTPS endpoints.';\n if (!bootstrapInfo.slackAvailable && el('bootstrap-provider').value === 'slack') {\n el('bootstrap-provider').value = 'google';\n updateBootstrapProvider();\n }\n return bootstrapInfo;\n }\n\n async function logtoBootstrapFinding() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n for (const id of ['logto.client_credentials', 'logto.roles_read']) {\n const finding = report.findings.find((candidate) => candidate.id === id);\n if (!finding || finding.status !== 'pass') {\n return finding || { status: 'fail', message: 'Logto Management API permissions could not be verified.' };\n }\n }\n return { status: 'pass', message: 'Logto Management API access is ready.' };\n }\n\n async function showBootstrap() {\n el('bootstrap').hidden = false;\n el('show-bootstrap').hidden = true;\n const info = bootstrapInfo || await loadBootstrapInfo();\n if (!info.logtoConfigured) {\n showBootstrapStep('logto');\n return;\n }\n const finding = await logtoBootstrapFinding();\n if (finding && finding.status === 'pass') {\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n return;\n }\n showBootstrapStep('logto');\n message(finding?.message || 'Logto Management API credentials could not be verified.', true);\n }\n\n async function bootstrapLogto() {\n await json(await fetch(api + '/bootstrap/logto', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n managementAppId: el('logto-app-id').value,\n managementAppSecret: el('logto-app-secret').value,\n managementResource: el('logto-management-api-resource').value\n })\n }));\n el('logto-app-secret').value = '';\n await loadBootstrapInfo();\n const finding = await logtoBootstrapFinding();\n if (!finding || finding.status !== 'pass') {\n throw new Error(finding?.message || 'Logto Management API credentials could not be verified.');\n }\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n }\n\n async function bootstrapGoogle() {\n const secret = el('bootstrap-google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ clientId: el('bootstrap-google-id').value, clientSecret: secret })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto(true);\n await load();\n }\n\n async function bootstrapGithub() {\n if (!bootstrapInfo || !bootstrapInfo.githubAvailable) throw new Error('GitHub App creation needs valid saved Web, API, and ingress URLs.');\n await startGithub('bootstrap-github');\n }\n\n async function bootstrapSlack() {\n if (!bootstrapInfo || !bootstrapInfo.slackAvailable) throw new Error('Slack sign-in requires all public service URLs to use HTTPS.');\n await createSlack('bootstrap-slack', true);\n await reconcileLogto();\n await load();\n }\n\n async function createSlack(prefix = 'slack', connectLogto = false) {\n const result = await json(await fetch(api + '/create/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n name: el(prefix + '-name').value,\n configToken: el(prefix + '-token').value,\n connectLogto\n })\n }));\n el(prefix + '-token').value = '';\n if (!connectLogto) await load();\n message('Slack App ' + result.app.id + ' was created with the default integration manifest. Restart AgentConnect to apply it.');\n }\n\n async function checkGithub() {\n const result = await json(await fetch(api + '/check/github', { headers: bearer() }));\n el('github-settings').href = result.settingsUrl;\n el('github-settings').hidden = false;\n const label = result.missing.length ? 'Update required' : \"Can't verify automatically\";\n showDiff('github-drift', result.diff || [], label);\n match('github-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : label);\n message(result.status === 'pass' ? 'GitHub App matches the expected integration manifest.' : result.missing.length ? 'GitHub App settings need an update.' : \"GitHub can't expose callback, setup, or webhook-active settings through its API; check them in GitHub.\", result.status === 'fail');\n }\n\n async function connectGithubLogin() {\n await json(await fetch(api + '/configure/github-login', { method: 'POST', headers: bearer() }));\n await reconcileLogto();\n await load();\n message('The deployment GitHub App is now also used for Logto sign-in. Update its displayed callback URLs.');\n }\n\n async function connectSlackLogin() {\n if (!currentStatus || !currentStatus.values.slack || !currentStatus.values.logto) {\n throw new Error('Save the Slack App and Logto configuration first');\n }\n const values = currentStatus.values;\n const slack = values.slack;\n const logto = values.logto;\n await replaceConfiguration(\n {\n ...values,\n logto: {\n ...logto,\n browser: logto.browser\n ? { ...logto.browser, socialProviders: [...new Set([...logto.browser.socialProviders, 'slack'])] }\n : logto.browser,\n slackConnector: {\n appId: slack.appId,\n clientId: slack.clientId\n }\n }\n },\n undefined,\n 'Slack App is ready to connect to Logto.'\n );\n await reconcileLogto();\n await load();\n message('The deployment Slack App is now also used for Logto sign-in.');\n }\n\n async function checkSlack() {\n const token = requiredInput('slack-token', 'the temporary Slack App configuration token');\n const result = await json(await fetch(api + '/check/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ configToken: token })\n }));\n el('slack-token').value = '';\n showDiff('slack-drift', result.diff || []);\n match('slack-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : 'Update required');\n message(result.status === 'pass' ? 'Slack App matches the default integration manifest.' : 'Slack App settings need an update.', result.status !== 'pass');\n }\n\n async function checkGoogle() {\n const report = await checkLogto();\n const connector = report.findings.find((finding) => finding.id === 'logto.connectors');\n const google = currentStatus && currentStatus.values.logto && currentStatus.values.logto.googleConnector;\n const connectorDiff = connector && connector.diff\n ? connector.diff.filter((item) => item.field.toLowerCase().startsWith('google'))\n : [];\n const passed = Boolean(google && connector && connectorDiff.length === 0);\n showDiff('google-drift', connectorDiff);\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Logto matches' : 'Update required');\n message(\n passed\n ? \"The Logto Google connector matches. Google OAuth redirect settings can't be verified automatically.\"\n : 'The Logto Google connector needs an update.',\n !passed\n );\n }\n\n async function checkRegionalLoginApp(region) {\n const result = await json(await fetch(api + '/check/regional-login-app/' + region, { headers: bearer() }));\n const label = region === 'feishu' ? 'Feishu' : 'Lark';\n showDiff(region + '-drift', result.diff || []);\n match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Matches' : result.status === 'fail' ? 'Update required' : 'Could not check');\n el(region + '-login-status').textContent = result.message;\n el(region + '-login-status').className = result.status === 'pass' ? 'ok' : result.status === 'fail' ? 'warn' : 'muted';\n message(result.message || (label + ' credential check completed.'), result.status === 'fail');\n }\n\n async function saveGoogle() {\n const secret = el('google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n clientId: el('google-id').value,\n ...(secret ? { clientSecret: secret } : {})\n })\n }));\n el('google-secret').value = '';\n await reconcileLogto(Boolean(secret));\n await load();\n message('Google OAuth client and Logto connector are configured. Restart AgentConnect to apply the saved settings.');\n }\n\n function regionalLoginApp(prefix) {\n const appId = el(prefix + '-login-id').value.trim();\n const appSecret = el(prefix + '-login-secret').value;\n return appId ? { appId, ...(appSecret ? { appSecret } : {}) } : null;\n }\n\n async function saveRegionalLoginApp(region) {\n const app = regionalLoginApp(region);\n if (!app) throw new Error('Enter the ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App ID');\n await json(await fetch(api + '/configure/regional-login-app', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, app })\n }));\n el(region + '-login-secret').value = '';\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' tenant App is saved. Restart AgentConnect services to apply it.');\n }\n\n async function createRegionalLoginApp(region) {\n const popup = window.open('about:blank', '_blank');\n try {\n const started = await json(await fetch(api + '/create/regional-login-app/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, name: el(region + '-create-name').value })\n }));\n if (popup) popup.location.replace(started.authorizationUrl);\n else window.open(started.authorizationUrl, '_blank', 'noopener');\n message('Approve the new ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App in the opened page. This page will finish saving it automatically.');\n while (true) {\n const result = await json(await fetch(api + '/create/regional-login-app/' + encodeURIComponent(started.id), { headers: bearer() }));\n if (result.status === 'completed') {\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' App ' + result.appId + ' was created and saved. Restart AgentConnect services to apply it.');\n return;\n }\n if (result.status === 'failed') throw new Error('Regional App creation ' + result.reason + '. Start it again.');\n await new Promise((resolve) => setTimeout(resolve, Math.max(500, Math.min(result.retryAfterMs || 2000, 5000))));\n }\n } catch (error) {\n if (popup) popup.close();\n throw error;\n }\n }\n\n async function reconcileLogto(refreshConnectorSecrets) {\n return json(await fetch(api + '/reconcile/logto', {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ refreshConnectorSecrets: Boolean(refreshConnectorSecrets) })\n }));\n }\n\n async function refreshDeploymentConfig() {\n const status = await json(await fetch(api + '/deployment-config', { headers: bearer() }));\n el('access').hidden = true;\n el('admin').hidden = false;\n el('editor').hidden = false;\n currentRevision = status.revision;\n renderApps(status);\n return status;\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n await refreshDeploymentConfig();\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, connectors, and social-only sign-in match.'\n : failures.map((finding) => finding.message).join(' ');\n el('logto-status').className = failures.length === 0 ? 'ok' : 'warn';\n match('logto-match', failures.length === 0 ? 'pass' : 'warn', failures.length === 0 ? 'Matches' : 'Update required');\n showDiff(\n 'logto-drift',\n failures.flatMap((finding) => finding.diff && finding.diff.length\n ? finding.diff\n : [{ field: finding.id.replace(/^logto\\./, '').replaceAll('_', ' '), current: finding.message, expected: 'Matches expected configuration' }])\n );\n el('logto-settings').hidden = failures.length === 0;\n return report;\n }\n\n function githubNotice(value) {\n if (value === 'deployment-created') return 'GitHub integration App created. Its private key and webhook secret are stored.';\n if (value === 'deployment-login-created') return 'GitHub integration App created and connected to Logto sign-in.';\n if (value === 'cancelled') return 'GitHub App creation was cancelled.';\n if (value === 'expired') return 'GitHub App creation expired. Start it again.';\n if (value === 'invalid-callback') return 'GitHub returned an invalid App creation callback.';\n if (value === 'conversion-failed') return 'GitHub may have created the App, but did not return complete credentials. Delete the orphaned App and retry.';\n if (value === 'save-failed') return 'GitHub created the App, but its credentials could not be saved. Delete the orphaned App and retry.';\n return '';\n }\n\n async function load() {\n let publicAuth = await authConfig();\n const currentUrl = new URL(location.href);\n const githubResult = currentUrl.searchParams.get('github');\n const notice = githubNotice(githubResult);\n if (githubResult === 'deployment-login-created' && publicAuth.mode === 'none') {\n await reconcileLogto();\n publicAuth = await authConfig();\n }\n if (githubResult) history.replaceState({}, '', '/');\n const hasToken = Boolean(sessionStorage.getItem(tokenKey));\n el('access').hidden = false;\n el('admin').hidden = true;\n el('login').hidden = true;\n el('open-logto').hidden = true;\n el('show-bootstrap').hidden = true;\n el('editor').hidden = true;\n el('bootstrap').hidden = true;\n if (publicAuth.logtoAdminEndpoint) {\n el('open-logto').href = publicAuth.logtoAdminEndpoint;\n el('logto-settings').href = publicAuth.logtoAdminEndpoint;\n }\n if (publicAuth.mode === 'none') {\n sessionStorage.removeItem(tokenKey);\n await loadBootstrapInfo();\n el('open-logto').hidden = false;\n el('show-bootstrap').hidden = false;\n message(notice || 'Logto sign-in is not configured. Set up the initial Logto administrator first.');\n return;\n }\n if (publicAuth.mode === 'unavailable') {\n el('open-logto').hidden = false;\n message(publicAuth.message || 'Logto sign-in is unavailable. Open Logto Console to review its settings.', true);\n return;\n }\n if (!hasToken) {\n el('login').hidden = false;\n message(notice || 'Sign in with Logto to continue.');\n return;\n }\n if (publicAuth.claimAvailable) {\n try {\n await json(await fetch(api + '/bootstrap/claim', { method: 'POST', headers: bearer() }));\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('This first user is now an ADMIN. Sign in again to refresh the role claim.');\n return;\n } catch (error) {\n if (error.status !== 401 && error.status !== 403) throw error;\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with Logto to continue.', true);\n return;\n }\n }\n try {\n await loadBootstrapInfo();\n await refreshDeploymentConfig();\n message(notice);\n checkLogto().catch((error) => {\n el('logto-status').textContent = error.message;\n el('logto-status').className = 'warn';\n el('logto-settings').hidden = false;\n });\n } catch (error) {\n if (error.status === 401 || error.status === 403) {\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with a Logto ADMIN account.', true);\n }\n else throw error;\n }\n }\n\n async function saveSecretReplacement(key, value) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n if (!value) throw new Error('Enter the replacement secret');\n const refreshLogto =\n (key === 'logto.githubConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.githubConnector) ||\n (key === 'logto.googleConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.googleConnector) ||\n (key === 'slack.clientSecret' && currentStatus.values.logto && currentStatus.values.logto.slackConnector);\n await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values: currentStatus.values, secrets: { [key]: value } })\n }));\n try {\n if (refreshLogto) await reconcileLogto(true);\n } finally {\n await load();\n }\n if (refreshLogto) {\n message('Secret replaced and applied to the Logto connector. Restart AgentConnect to apply deployment changes.');\n } else {\n message('Secret replaced. Restart AgentConnect to apply it.');\n }\n }\n\n function editSecret(button) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const row = button.closest('.secret-line');\n const display = el(button.dataset.secretDisplay);\n if (!row || !display || row.querySelector('.secret-editor')) return;\n const editor = document.createElement('span'); editor.className = 'secret-editor';\n const input = document.createElement('input');\n input.type = 'password'; input.autocomplete = 'new-password'; input.placeholder = 'Enter replacement secret';\n const save = document.createElement('button'); save.textContent = 'Save';\n const cancel = document.createElement('button'); cancel.textContent = 'Cancel';\n const close = () => { editor.remove(); display.hidden = false; button.hidden = false; };\n save.onclick = async () => {\n save.disabled = true;\n try { await saveSecretReplacement(button.dataset.secretKey, input.value); }\n catch (error) { save.disabled = false; message(error.message, true); }\n };\n cancel.onclick = close;\n input.onkeydown = (event) => { if (event.key === 'Enter') save.click(); if (event.key === 'Escape') close(); };\n display.hidden = true; button.hidden = true;\n editor.append(input, save, cancel); row.append(editor); input.focus();\n }\n\n async function saveOptions() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = {\n ...currentStatus.values,\n features: {\n presetAgentsEnabled: el('preset-agents-enabled').checked\n }\n };\n const saved = await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values })\n }));\n message('Saved deployment options. Restart AgentConnect to apply them.');\n await load();\n }\n\n el('login').onclick = () => signIn().catch((error) => message(error.message, true));\n el('show-bootstrap').onclick = () => showBootstrap().catch((error) => message(error.message, true));\n el('bootstrap-provider').onchange = updateBootstrapProvider;\n el('bootstrap-github-owner').onchange = () => updateOwner('bootstrap-github');\n el('github-owner').onchange = () => updateOwner('github');\n el('bootstrap-logto-submit').onclick = () => bootstrapLogto().catch((error) => message(error.message, true));\n el('bootstrap-back').onclick = () => showBootstrapStep('logto');\n el('bootstrap-google-submit').onclick = () => bootstrapGoogle().catch((error) => message(error.message, true));\n el('bootstrap-github-submit').onclick = () => bootstrapGithub().catch((error) => message(error.message, true));\n el('bootstrap-slack-submit').onclick = () => bootstrapSlack().catch((error) => message(error.message, true));\n el('create-github').onclick = () => startGithub('github').catch((error) => message(error.message, true));\n el('save-logto-configuration').onclick = () => saveLogtoConfiguration().catch((error) => message(error.message, true));\n el('cancel-logto-configuration').onclick = cancelConfigurationEdit;\n el('save-github-configuration').onclick = () => saveGithubConfiguration().catch((error) => message(error.message, true));\n el('cancel-github-configuration').onclick = cancelConfigurationEdit;\n el('clear-github').onclick = () => clearProvider('github').catch((error) => message(error.message, true));\n el('connect-github-login').onclick = () => connectGithubLogin().catch((error) => message(error.message, true));\n el('check-github').onclick = () => checkGithub().catch((error) => message(error.message, true));\n el('create-slack').onclick = () => createSlack('slack').catch((error) => message(error.message, true));\n el('connect-slack-login').onclick = () => connectSlackLogin().catch((error) => message(error.message, true));\n el('save-slack-configuration').onclick = () => saveSlackConfiguration().catch((error) => message(error.message, true));\n el('cancel-slack-configuration').onclick = cancelConfigurationEdit;\n el('clear-slack').onclick = () => clearProvider('slack').catch((error) => message(error.message, true));\n el('check-slack').onclick = () => checkSlack().catch((error) => message(error.message, true));\n el('save-google').onclick = () => saveGoogle().catch((error) => message(error.message, true));\n el('cancel-google-configuration').onclick = cancelConfigurationEdit;\n el('clear-google').onclick = () => clearProvider('google').catch((error) => message(error.message, true));\n el('check-google').onclick = () => checkGoogle().catch((error) => message(error.message, true));\n el('create-feishu-login-app').onclick = () => createRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('save-feishu-login-app').onclick = () => saveRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('cancel-feishu-configuration').onclick = cancelConfigurationEdit;\n el('clear-feishu').onclick = () => clearProvider('feishu').catch((error) => message(error.message, true));\n el('check-feishu-login-app').onclick = () => checkRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('create-lark-login-app').onclick = () => createRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('save-lark-login-app').onclick = () => saveRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('cancel-lark-configuration').onclick = cancelConfigurationEdit;\n el('clear-lark').onclick = () => clearProvider('lark').catch((error) => message(error.message, true));\n el('check-lark-login-app').onclick = () => checkRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('check-logto').onclick = () => checkLogto().catch((error) => message(error.message, true));\n el('reconcile-logto').onclick = () => reconcileLogto().then(load).catch((error) => message(error.message, true));\n el('logout').onclick = () => { sessionStorage.removeItem(tokenKey); load().catch((error) => message(error.message, true)); };\n el('save-options').onclick = () => saveOptions().catch((error) => message(error.message, true));\n for (const button of document.querySelectorAll('.edit-secret')) button.onclick = () => editSecret(button);\n for (const button of document.querySelectorAll('.edit-configuration')) button.onclick = () => beginConfigurationEdit(button.dataset.provider);\n updateBootstrapProvider();\n finishSignIn().then(load).catch((error) => message(error.message, true));\n <\/script>\n</body>\n</html>"])));
|
|
62205
|
+
const SETUP_HTML = String.raw(_templateObject || (_templateObject = _taggedTemplateLiteral(["<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>AgentConnect deployment settings</title>\n <style>\n :root { color-scheme: light dark; font: 15px/1.5 system-ui, sans-serif; }\n body { max-width: 1280px; margin: 48px auto; padding: 0 20px 60px; }\n h1 { margin-bottom: 4px; } h2 { margin-top: 32px; } h3 { margin: 0 0 8px; }\n .muted { color: #777; } .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }\n button, .button { padding: 8px 13px; cursor: pointer; }\n .button { border: 1px solid #8888; border-radius: 4px; color: inherit; text-decoration: none; display: inline-block; }\n input, select { width: min(520px, 100%); padding: 7px; box-sizing: border-box; }\n input[type=\"checkbox\"] { width: auto; padding: 0; }\n .field { display: grid; gap: 4px; margin: 10px 0; }\n .provider-stack { display: grid; gap: 16px; }\n .panel { border: 1px solid #8886; border-radius: 8px; padding: 16px; }\n .section-head, .provider-head { display: flex; justify-content: space-between; gap: 16px; align-items: start; }\n .section-head h2, .provider-head h3 { margin: 0; }\n .provider-head p { margin: 4px 0 0; }\n .badge { flex: none; border: 1px solid #8886; border-radius: 999px; padding: 3px 9px; font-size: 13px; }\n .badge.pass { color: #198754; border-color: #19875466; background: #19875412; }\n .badge.warn { color: #a66b00; border-color: #d99b1366; background: #d99b1315; }\n .badge.fail { color: #c33; border-color: #c333; background: #c3331111; }\n .credentials { display: grid; grid-template-columns: minmax(160px, 220px) minmax(0, 1fr); gap: 7px 16px; margin: 16px 0; }\n .credentials dt { color: #777; }\n .credentials dd { margin: 0; overflow-wrap: anywhere; }\n .credentials code { user-select: all; }\n .redacted { font-family: ui-monospace, monospace; letter-spacing: .08em; }\n .secret-line, .value-line { display: flex; gap: 10px; align-items: center; min-height: 30px; }\n .edit-secret { padding: 2px 8px; font-size: 13px; }\n .secret-editor { display: flex; gap: 8px; flex-wrap: wrap; width: 100%; }\n .secret-editor input { width: min(420px, 100%); }\n .edit-configuration { padding: 2px 8px; font-size: 13px; }\n .startup-owned { color: #777; font-size: 13px; }\n .danger { color: #c33; border-color: #c336; }\n .subsection { margin-top: 16px; padding-top: 14px; border-top: 1px solid #8883; }\n textarea { width: min(720px, 100%); min-height: 100px; padding: 7px; box-sizing: border-box; }\n .uris { margin: 8px 0; padding-left: 20px; } .uris code { user-select: all; }\n .notice { border-left: 4px solid #d99b13; padding: 8px 12px; background: #d99b1315; }\n .diff-title { display: block; margin-bottom: 8px; }\n .config-diff { display: grid; gap: 1px; border: 1px solid #8884; border-radius: 6px; overflow: hidden; background: #8884; }\n .diff-row { display: grid; grid-template-columns: minmax(140px, .8fr) minmax(0, 1.2fr) minmax(0, 1.2fr); background: Canvas; }\n .diff-row > * { min-width: 0; padding: 7px 9px; overflow-wrap: anywhere; white-space: pre-wrap; }\n .diff-head { font-size: 12px; font-weight: 600; color: #777; }\n .diff-field { font-weight: 600; }\n .diff-value { font-family: ui-monospace, monospace; font-size: 13px; }\n pre { padding: 12px; border-radius: 6px; background: #8881; overflow-x: auto; user-select: all; }\n #message { white-space: pre-wrap; padding: 10px 0; min-height: 1.5em; }\n .error { color: #c33; } .ok { color: #198754; } .warn { color: #a66b00; }\n code { overflow-wrap: anywhere; }\n .setup-layout { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 32px; align-items: start; }\n .setup-nav { position: sticky; top: 24px; display: grid; gap: 3px; padding: 10px; border: 1px solid #8886; border-radius: 8px; background: Canvas; }\n .setup-nav a { padding: 7px 9px; border-radius: 5px; color: inherit; text-decoration: none; white-space: nowrap; }\n .setup-nav a:hover { background: #8882; }\n .setup-section { scroll-margin-top: 24px; }\n details.environment { margin: 0 0 24px; } details.environment summary { cursor: pointer; font-weight: 600; }\n @media (max-width: 760px) {\n body { margin-top: 24px; }\n .setup-layout { grid-template-columns: 1fr; gap: 18px; }\n .setup-nav { position: static; display: flex; overflow-x: auto; }\n .credentials { grid-template-columns: 1fr; gap: 2px; }\n .credentials dd { margin-bottom: 8px; }\n .diff-row { grid-template-columns: 1fr; }\n .diff-head { display: none; }\n .diff-value::before { display: block; margin-bottom: 2px; color: #777; font: 11px/1.4 system-ui, sans-serif; }\n .diff-current::before { content: 'Current'; }\n .diff-expected::before { content: 'Expected'; }\n }\n [hidden] { display: none !important; }\n </style>\n</head>\n<body>\n <section id=\"access\">\n <h1>AgentConnect Setup</h1>\n <p id=\"access-message\" class=\"muted\" aria-live=\"polite\">Checking Logto sign-in…</p>\n <div class=\"row\">\n <button id=\"login\" hidden>Sign in with Logto</button>\n <a id=\"open-logto\" class=\"button\" href=\"http://localhost:3002\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n <button id=\"show-bootstrap\" hidden>Continue setup</button>\n </div>\n\n <section id=\"bootstrap\" hidden>\n <h2>Set up sign-in</h2>\n <p id=\"bootstrap-progress\" class=\"muted\">Step 1 of 2</p>\n <div id=\"bootstrap-logto-step\" class=\"panel\">\n <h3>Connect Logto</h3>\n <p class=\"muted\">Enter the one-time Logto Management API credential. It is sealed in the deployment database and verified before continuing.</p>\n <label class=\"field\">Logto M2M App ID<input id=\"logto-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Logto M2M App Secret<input id=\"logto-app-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-management-api-resource\" type=\"url\" autocomplete=\"off\"></label>\n <p class=\"muted\">Logto Cloud uses <code>https://<tenant-id>.logto.app/api</code>. Keep <code>https://default.logto.app/api</code> for Logto OSS.</p>\n <button id=\"bootstrap-logto-submit\">Save Logto and continue</button>\n </div>\n <div id=\"bootstrap-provider-step\" hidden>\n <h3>Choose a sign-in provider</h3>\n <p class=\"muted\">Logto Management API access is ready. Configure one provider to enable sign-in.</p>\n <label class=\"field\">Sign-in provider\n <select id=\"bootstrap-provider\"><option value=\"google\">Google (works on localhost)</option><option value=\"github\">GitHub integration App</option><option id=\"bootstrap-slack-option\" value=\"slack\">Slack integration App</option></select>\n </label>\n <div id=\"bootstrap-google\" class=\"panel\">\n <h3>Google OAuth client</h3>\n <p class=\"muted\">Create a Web application client in Google Auth Platform, then paste its credentials here.</p>\n <p>Authorized JavaScript origin:</p><ul id=\"bootstrap-google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"bootstrap-google-redirects\" class=\"uris\"></ul>\n <a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a>\n <label class=\"field\">Client ID<input id=\"bootstrap-google-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client Secret<input id=\"bootstrap-google-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-google-submit\">Save Google OAuth and configure Logto</button>\n </div>\n <div id=\"bootstrap-github\" class=\"panel\" hidden>\n <h3>GitHub integration App</h3>\n <p id=\"bootstrap-github-note\" class=\"muted\">This creates one complete App for both GitHub sign-in and repository integration.</p>\n <label class=\"field\">Owner\n <select id=\"bootstrap-github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"bootstrap-github-org-field\" class=\"field\" hidden>Organization login<input id=\"bootstrap-github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"bootstrap-github-name\" value=\"AgentConnect\"></label>\n <button id=\"bootstrap-github-submit\">Create GitHub App and configure Logto</button>\n </div>\n <div id=\"bootstrap-slack\" class=\"panel\" hidden>\n <h3>Slack integration App</h3>\n <p id=\"bootstrap-slack-note\" class=\"muted\">Creates one complete Slack App for workspace installation and a separate Sign in with Slack OIDC flow.</p>\n <p>Logto redirect URI:</p><ul id=\"bootstrap-slack-redirects\" class=\"uris\"></ul>\n <label class=\"field\">App name<input id=\"bootstrap-slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"bootstrap-slack-token\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-slack-submit\">Create Slack App and configure Logto</button>\n </div>\n <button id=\"bootstrap-back\">Back to Logto credentials</button>\n </div>\n </section>\n </section>\n\n <main id=\"setup\" hidden>\n <div class=\"setup-layout\">\n <nav class=\"setup-nav\" aria-label=\"Deployment settings\">\n <a href=\"#startup-section\">Startup</a>\n <a href=\"#logto-section\">Logto</a>\n <a href=\"#github-section\">GitHub</a>\n <a href=\"#slack-section\">Slack</a>\n <a href=\"#google-section\">Google</a>\n <a href=\"#feishu-section\">Feishu</a>\n <a href=\"#lark-section\">Lark</a>\n <a href=\"#options-section\">Options</a>\n </nav>\n <div class=\"setup-content\">\n <h1>AgentConnect deployment settings</h1>\n <p class=\"muted\">Saved settings take effect after the stack is restarted.</p>\n <div class=\"row\">\n <button id=\"logout\">Log out</button>\n </div>\n <div id=\"message\" aria-live=\"polite\"></div>\n\n <section id=\"editor\" hidden>\n <details id=\"startup-section\" class=\"environment setup-section\" open>\n <summary>Startup environment</summary>\n <p class=\"notice\">Public service URLs come from <code>.env</code>. Provider callbacks below are derived from these values.</p>\n <pre id=\"startup-environment\"></pre>\n </details>\n\n <section id=\"logto-section\" class=\"setup-section\" aria-labelledby=\"logto-heading\">\n <div class=\"section-head\">\n <div><h2 id=\"logto-heading\">Logto</h2><p class=\"muted\">Authentication and administrator access.</p></div>\n <span id=\"logto-match\" class=\"badge\">Not checked</span>\n </div>\n <div class=\"panel\">\n <dl class=\"credentials\">\n <dt>Management endpoint</dt><dd class=\"value-line\"><code id=\"logto-management-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>Management App ID</dt><dd class=\"value-line\"><code id=\"logto-management-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management API resource</dt><dd class=\"value-line\"><code id=\"logto-management-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management App secret</dt><dd class=\"secret-line\"><span id=\"logto-management-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.managementAppSecret\" data-secret-display=\"logto-management-secret-display\">Edit</button></dd>\n <dt>Sign-in endpoint</dt><dd class=\"value-line\"><code id=\"logto-browser-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>SPA App ID</dt><dd class=\"value-line\"><code id=\"logto-browser-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Browser API resource</dt><dd class=\"value-line\"><code id=\"logto-browser-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n </dl>\n <div id=\"logto-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Logto configuration</h3>\n <label class=\"field\">Management App ID<input id=\"logto-edit-management-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-edit-management-resource\" autocomplete=\"off\"></label>\n <label class=\"field\">New Management App secret<input id=\"logto-edit-management-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when management identity changes\"></label>\n <label class=\"field\">SPA App ID<input id=\"logto-edit-browser-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Browser API resource<input id=\"logto-edit-browser-resource\" autocomplete=\"off\" placeholder=\"Optional\"></label>\n <div class=\"row\"><button id=\"save-logto-configuration\">Save configuration</button><button id=\"cancel-logto-configuration\">Cancel</button></div>\n </div>\n <p id=\"logto-status\" class=\"muted\">Checking redirects and sign-in settings…</p>\n <div id=\"logto-drift\" class=\"notice\" hidden></div>\n <div class=\"row\">\n <button id=\"check-logto\">Check match</button>\n <button id=\"reconcile-logto\">Apply expected settings</button>\n <a id=\"logto-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n </div>\n </div>\n </section>\n\n <h2>Providers</h2>\n <div class=\"provider-stack\">\n <section id=\"github-section\" class=\"panel setup-section\" aria-labelledby=\"github-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"github-heading\">GitHub</h3><p class=\"muted\">Repository integration and optional Logto sign-in.</p></div>\n <span id=\"github-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"github-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>App slug</dt><dd class=\"value-line\"><code id=\"github-app-slug\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"github-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Webhook delivery</dt><dd class=\"value-line\"><code id=\"github-webhook-enabled\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"github-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.clientSecret\" data-secret-display=\"github-client-secret-display\">Edit</button></dd>\n <dt>Private key</dt><dd class=\"secret-line\"><span id=\"github-private-key-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.privateKeyB64\" data-secret-display=\"github-private-key-display\">Edit</button></dd>\n <dt>Webhook secret</dt><dd class=\"secret-line\"><span id=\"github-webhook-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.webhookSecret\" data-secret-display=\"github-webhook-secret-display\">Edit</button></dd>\n <dt>Logto connector secret</dt><dd class=\"secret-line\"><span id=\"github-logto-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.githubConnectorClientSecret\" data-secret-display=\"github-logto-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"github-status\" class=\"muted\"></p>\n <div id=\"github-drift\" class=\"notice\" hidden></div>\n <div id=\"github-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit GitHub App identity</h3>\n <label class=\"field\">App ID<input id=\"github-edit-app-id\" inputmode=\"numeric\" autocomplete=\"off\"></label>\n <label class=\"field\">App slug<input id=\"github-edit-slug\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"github-edit-client-id\" autocomplete=\"off\"></label>\n <label><input id=\"github-edit-webhook-enabled\" type=\"checkbox\"> Enable Relay webhook delivery</label>\n <label class=\"field\">New client secret<input id=\"github-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when App or Client ID changes\"></label>\n <label class=\"field\">New private key (base64)<textarea id=\"github-edit-private-key\" autocomplete=\"off\" placeholder=\"Required when App ID changes\"></textarea></label>\n <label class=\"field\">New webhook secret<input id=\"github-edit-webhook-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when an active webhook App ID changes\"></label>\n <label id=\"github-edit-logto-secret-field\" class=\"field\" hidden>New Logto connector client secret<input id=\"github-edit-logto-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when this App is also used for sign-in\"></label>\n <div class=\"row\"><button id=\"save-github-configuration\">Save configuration</button><button id=\"cancel-github-configuration\">Cancel</button></div>\n </div>\n <div id=\"github-create-controls\" class=\"subsection\">\n <label class=\"field\">Owner\n <select id=\"github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"github-org-field\" class=\"field\" hidden>Organization login<input id=\"github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"github-name\" value=\"AgentConnect\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-github\">Create GitHub App</button>\n <button id=\"connect-github-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-github\" hidden>Check match</button>\n <button id=\"clear-github\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"github-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open GitHub settings</a>\n </div>\n </section>\n\n <section id=\"slack-section\" class=\"panel setup-section\" aria-labelledby=\"slack-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"slack-heading\">Slack</h3><p class=\"muted\">One App for workspace integration and Logto sign-in.</p></div>\n <span id=\"slack-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"slack-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"slack-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"slack-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.clientSecret\" data-secret-display=\"slack-client-secret-display\">Edit</button></dd>\n <dt>Signing secret</dt><dd class=\"secret-line\"><span id=\"slack-signing-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.signingSecret\" data-secret-display=\"slack-signing-secret-display\">Edit</button></dd>\n <dt>Logto sign-in</dt><dd id=\"slack-logto-status\">Not configured</dd>\n </dl>\n <p id=\"slack-status\" class=\"muted\"></p>\n <div id=\"slack-drift\" class=\"notice\" hidden></div>\n <div id=\"slack-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Slack App identity</h3>\n <label class=\"field\">App ID<input id=\"slack-edit-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"slack-edit-client-id\" autocomplete=\"off\"></label>\n <label class=\"field\">New client secret<input id=\"slack-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <label class=\"field\">New signing secret<input id=\"slack-edit-signing-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <div class=\"row\"><button id=\"save-slack-configuration\">Save configuration</button><button id=\"cancel-slack-configuration\">Cancel</button></div>\n </div>\n <div class=\"subsection\">\n <label id=\"slack-name-field\" class=\"field\">App name<input id=\"slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"slack-token\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Used only for create or check\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-slack\">Create Slack App</button>\n <button id=\"connect-slack-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-slack\" hidden>Check match</button>\n <button id=\"clear-slack\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"slack-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Slack settings</a>\n </div>\n </section>\n\n <section id=\"google-section\" class=\"panel setup-section\" aria-labelledby=\"google-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"google-heading\">Google</h3><p class=\"muted\">OAuth client used by the Logto Google connector.</p></div>\n <span id=\"google-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"google-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"google\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"google-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.googleConnectorClientSecret\" data-secret-display=\"google-client-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"google-status\" class=\"muted\"></p>\n <div id=\"google-drift\" class=\"notice\" hidden></div>\n <p>Authorized JavaScript origin:</p><ul id=\"google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"google-redirects\" class=\"uris\"></ul>\n <p class=\"muted\">Google does not expose OAuth client redirect settings through an API. Copy these required values into Google Auth Platform; Setup can verify only the Logto connector.</p>\n <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a></div>\n <div id=\"google-config-controls\" class=\"subsection\">\n <label class=\"field\">Client ID<input id=\"google-id\" autocomplete=\"off\"></label>\n <label id=\"google-initial-secret-field\" class=\"field\">Client secret<input id=\"google-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when Client ID changes\"></label>\n </div>\n <div class=\"row\"><button id=\"save-google\">Save Google client</button><button id=\"cancel-google-configuration\" hidden>Cancel</button><button id=\"check-google\" hidden>Check match</button><button id=\"clear-google\" class=\"danger\" hidden>Clear configuration</button></div>\n </section>\n\n <section id=\"feishu-section\" class=\"panel setup-section\" aria-labelledby=\"feishu-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"feishu-heading\">Feishu</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Feishu, OAuth callbacks, and published App setup.</p></div>\n <span id=\"feishu-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"feishu-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"feishu\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"feishu-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"feishu.loginAppSecret\" data-secret-display=\"feishu-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"feishu-login-status\" class=\"muted\"></p>\n <div id=\"feishu-drift\" class=\"notice\" hidden></div>\n <div id=\"feishu-config-controls\" class=\"subsection\">\n <label id=\"feishu-create-name-field\" class=\"field\">New App name<input id=\"feishu-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"feishu-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"feishu-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-feishu-login-app\">Create Feishu App</button>\n <button id=\"save-feishu-login-app\">Save existing App</button>\n <button id=\"cancel-feishu-configuration\" hidden>Cancel</button>\n <button id=\"check-feishu-login-app\" hidden>Check setup</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"feishu-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Feishu settings</a>\n </div>\n </section>\n\n <section id=\"lark-section\" class=\"panel setup-section\" aria-labelledby=\"lark-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"lark-heading\">Lark</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Lark, OAuth callbacks, and published App setup.</p></div>\n <span id=\"lark-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"lark-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"lark\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"lark-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"lark.loginAppSecret\" data-secret-display=\"lark-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"lark-login-status\" class=\"muted\"></p>\n <div id=\"lark-drift\" class=\"notice\" hidden></div>\n <div id=\"lark-config-controls\" class=\"subsection\">\n <label id=\"lark-create-name-field\" class=\"field\">New App name<input id=\"lark-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"lark-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"lark-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-lark-login-app\">Create Lark App</button>\n <button id=\"save-lark-login-app\">Save existing App</button>\n <button id=\"cancel-lark-configuration\" hidden>Cancel</button>\n <button id=\"check-lark-login-app\" hidden>Check setup</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"lark-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Lark settings</a>\n </div>\n </section>\n </div>\n\n <section id=\"options-section\" class=\"setup-section\">\n <h2>Deployment options</h2>\n <div class=\"panel\">\n <label class=\"field\"><span><input id=\"preset-agents-enabled\" type=\"checkbox\"> Enable preset Agents</span></label>\n <div class=\"row\"><button id=\"save-options\">Save options</button></div>\n </div>\n </section>\n </section>\n </div>\n </div>\n </main>\n\n <script>\n const api = '/api/v1';\n const tokenKey = 'agentconnect.setup.token';\n const verifierKey = 'agentconnect.setup.pkce';\n const stateKey = 'agentconnect.setup.state';\n let currentRevision = 0;\n let currentStatus = null;\n let bootstrapInfo = null;\n const el = (id) => document.getElementById(id);\n const message = (text, error = false) => {\n const target = el('setup').hidden ? el('access-message') : el('message');\n target.textContent = text;\n target.className = error ? 'error' : 'ok';\n };\n const bearer = () => {\n const token = sessionStorage.getItem(tokenKey);\n return token ? { authorization: 'Bearer ' + token } : {};\n };\n const json = async (response) => {\n const body = await response.json().catch(() => ({}));\n if (!response.ok) throw Object.assign(new Error(body.message || ('HTTP ' + response.status)), { status: response.status, code: body.code });\n return body;\n };\n const base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/+/g, '-').replace(///g, '_').replace(/=+$/, '');\n const random = () => base64url(crypto.getRandomValues(new Uint8Array(32)));\n const configured = (byKey, key) => Boolean(byKey.get(key) && byKey.get(key).configured);\n\n function showIdentityEditors(provider, show) {\n for (const button of document.querySelectorAll('.edit-configuration[data-provider=\"' + provider + '\"]')) {\n button.hidden = !show;\n }\n }\n\n function text(id, value) {\n el(id).textContent = value === null || value === undefined || value === '' ? 'Not configured' : String(value);\n }\n\n function secretText(id, byKey, key, editable) {\n const stored = configured(byKey, key);\n const display = el(id);\n const button = document.querySelector('[data-secret-display=\"' + id + '\"]');\n const row = display.closest('.secret-line');\n const editor = row && row.querySelector('.secret-editor');\n if (editor) editor.remove();\n display.hidden = false;\n display.textContent = stored ? '***' : 'Not configured';\n display.className = stored ? 'redacted' : 'muted';\n button.hidden = !editable;\n button.textContent = stored ? 'Edit' : 'Set';\n }\n\n function match(id, state, label) {\n const target = el(id);\n target.textContent = label;\n target.className = 'badge' + (state ? ' ' + state : '');\n }\n\n async function authConfig() { return json(await fetch(api + '/auth-config')); }\n\n async function signIn() {\n const config = await authConfig();\n if (config.mode !== 'oidc') throw new Error('Save an OIDC configuration first');\n const verifier = random();\n const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));\n const state = random();\n sessionStorage.setItem(verifierKey, verifier);\n sessionStorage.setItem(stateKey, state);\n const url = new URL(config.authorizationEndpoint);\n url.searchParams.set('client_id', config.appId);\n url.searchParams.set('redirect_uri', config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', 'openid profile email roles');\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n if (config.resource) url.searchParams.set('resource', config.resource);\n location.assign(url);\n }\n\n async function finishSignIn() {\n const url = new URL(location.href);\n const code = url.searchParams.get('code');\n if (!code) return;\n const state = url.searchParams.get('state');\n if (!state || state !== sessionStorage.getItem(stateKey)) throw new Error('OIDC state mismatch');\n const verifier = sessionStorage.getItem(verifierKey);\n if (!verifier) throw new Error('PKCE verifier is missing; start sign-in again');\n const config = await authConfig();\n const body = new URLSearchParams({\n grant_type: 'authorization_code', code, client_id: config.appId,\n redirect_uri: config.redirectUri, code_verifier: verifier\n });\n if (config.resource) body.set('resource', config.resource);\n const tokens = await json(await fetch(config.tokenEndpoint, {\n method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body\n }));\n if (typeof tokens.id_token !== 'string') throw new Error('Logto did not return an ID token');\n sessionStorage.setItem(tokenKey, tokens.id_token);\n sessionStorage.removeItem(verifierKey);\n sessionStorage.removeItem(stateKey);\n history.replaceState({}, '', '/');\n }\n\n function renderUriList(id, values) {\n el(id).replaceChildren(...(values || []).map((value) => {\n const item = document.createElement('li');\n const code = document.createElement('code'); code.textContent = value;\n item.append(code); return item;\n }));\n }\n\n function updateBootstrapProvider() {\n const provider = el('bootstrap-provider').value;\n el('bootstrap-google').hidden = provider !== 'google';\n el('bootstrap-github').hidden = provider !== 'github';\n el('bootstrap-slack').hidden = provider !== 'slack';\n }\n\n function showBootstrapStep(step) {\n const logto = step === 'logto';\n el('bootstrap-logto-step').hidden = !logto;\n el('bootstrap-provider-step').hidden = logto;\n el('bootstrap-progress').textContent = logto ? 'Step 1 of 2' : 'Step 2 of 2';\n }\n\n function updateOwner(prefix) {\n el(prefix + '-org-field').hidden = el(prefix + '-owner').value !== 'organization';\n }\n\n function ownership(prefix) {\n if (el(prefix + '-owner').value === 'personal') return { owner: 'personal', organization: null };\n const organization = el(prefix + '-org').value.trim();\n if (!organization) throw new Error('Enter the GitHub organization login');\n return { owner: 'organization', organization };\n }\n\n function formatDiffValue(value) {\n if (value === null || value === undefined || value === '') return 'Not configured';\n if (Array.isArray(value)) return value.length ? value.map(formatDiffValue).join('\n') : '[]';\n if (typeof value === 'object') {\n return Object.entries(value).map(([key, item]) => key + ': ' + (Array.isArray(item) ? item.join(', ') : formatDiffValue(item))).join('\n');\n }\n return String(value);\n }\n\n function showDiff(id, rows, label = 'Update required') {\n const box = el(id);\n box.hidden = rows.length === 0;\n box.replaceChildren();\n if (rows.length === 0) return;\n const title = document.createElement('strong');\n title.className = 'diff-title';\n title.textContent = label;\n const grid = document.createElement('div');\n grid.className = 'config-diff';\n const head = document.createElement('div');\n head.className = 'diff-row diff-head';\n for (const text of ['Field', 'Current', 'Expected']) {\n const cell = document.createElement('span'); cell.textContent = text; head.append(cell);\n }\n grid.append(head);\n for (const item of rows) {\n const row = document.createElement('div'); row.className = 'diff-row';\n const field = document.createElement('span'); field.className = 'diff-field'; field.textContent = item.field;\n const current = document.createElement('span'); current.className = 'diff-value diff-current'; current.textContent = formatDiffValue(item.current);\n const expected = document.createElement('span'); expected.className = 'diff-value diff-expected'; expected.textContent = formatDiffValue(item.expected);\n row.append(field, current, expected); grid.append(row);\n }\n box.append(title, grid);\n }\n\n function renderStartupEnvironment() {\n const services = bootstrapInfo.services;\n const environment = [\n ['AGENTCONNECT_PUBLIC_CP_URL', services.controlPlane],\n ['AGENTCONNECT_PUBLIC_RELAY_URL', services.relay],\n ['AGENTCONNECT_PUBLIC_WEB_URL', services.web],\n ['LOGTO_ENDPOINT', bootstrapInfo.logtoEndpoint],\n ['LOGTO_MGMT_ENDPOINT', bootstrapInfo.logtoManagementEndpoint]\n ];\n el('startup-environment').textContent = environment\n .filter(([, value]) => value)\n .map(([key, value]) => key + '=' + value)\n .join('\n');\n }\n\n function renderApps(status) {\n currentStatus = status;\n const byKey = new Map((status.secrets || []).map((item) => [item.key, item]));\n const values = status.values;\n const expected = status.providerExpectations || { github: null, slack: null, google: { origins: [], redirects: [] } };\n renderStartupEnvironment();\n\n const logto = values.logto;\n text('logto-management-endpoint', logto && bootstrapInfo.logtoManagementEndpoint);\n text('logto-management-id', logto && logto.managementAppId);\n text('logto-management-resource', logto && logto.managementResource);\n secretText('logto-management-secret-display', byKey, 'logto.managementAppSecret', Boolean(logto));\n text('logto-browser-endpoint', logto && logto.browser && bootstrapInfo.logtoEndpoint);\n text('logto-browser-id', values.auth.mode === 'oidc' ? values.auth.browserClient.appId : null);\n text('logto-browser-resource', logto && logto.browser && logto.browser.apiResource);\n el('logto-edit-controls').hidden = true;\n showIdentityEditors('logto', Boolean(logto && logto.browser && values.auth.mode === 'oidc'));\n match(\n 'logto-match',\n logto && configured(byKey, 'logto.managementAppSecret') ? '' : 'warn',\n logto && configured(byKey, 'logto.managementAppSecret') ? 'Ready to check' : 'Not configured'\n );\n\n el('preset-agents-enabled').checked = values.features.presetAgentsEnabled;\n\n const github = values.github;\n const webhookStored = byKey.get('github.webhookSecret') && byKey.get('github.webhookSecret').configured;\n const webhookInactive = github && github.webhookEnabled === false;\n text('github-app-id', github && github.appId);\n text('github-app-slug', github && github.slug);\n text('github-client-id', github && github.clientId);\n text('github-webhook-enabled', github && (github.webhookEnabled === false ? 'Disabled' : 'Enabled'));\n el('github-edit-controls').hidden = true;\n showIdentityEditors('github', Boolean(github));\n secretText('github-client-secret-display', byKey, 'github.clientSecret', Boolean(github));\n secretText('github-private-key-display', byKey, 'github.privateKeyB64', Boolean(github));\n secretText('github-webhook-secret-display', byKey, 'github.webhookSecret', Boolean(github));\n secretText('github-logto-secret-display', byKey, 'logto.githubConnectorClientSecret', Boolean(values.logto && values.logto.githubConnector));\n el('github-status').textContent = github\n ? github.slug + ' is configured. Webhook secret: ' + (webhookStored ? 'stored' : webhookInactive ? 'not required yet' : 'missing') + '.' +\n (webhookInactive ? ' Relay webhook delivery is disabled.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github\n ? expected.github\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'Valid Web, API, and ingress URLs' }]\n : [];\n match('github-match', !github ? '' : githubDrift.length ? 'warn' : '', !github ? 'Not configured' : githubDrift.length ? 'Expected URLs changed' : 'Ready to check');\n el('github-create-controls').hidden = Boolean(github);\n el('create-github').hidden = Boolean(github);\n el('clear-github').hidden = !github;\n el('connect-github-login').hidden = !github || !values.logto || Boolean(values.logto.githubConnector);\n el('check-github').hidden = !github;\n if (github) showDiff('github-drift', githubDrift, 'Expected URLs changed since App creation');\n else el('github-drift').hidden = true;\n\n const slack = values.slack;\n text('slack-app-id', slack && slack.appId);\n text('slack-client-id', slack && slack.clientId);\n el('slack-edit-controls').hidden = true;\n showIdentityEditors('slack', Boolean(slack));\n secretText('slack-client-secret-display', byKey, 'slack.clientSecret', Boolean(slack));\n secretText('slack-signing-secret-display', byKey, 'slack.signingSecret', Boolean(slack));\n el('slack-logto-status').textContent = values.logto && values.logto.slackConnector\n ? 'Enabled; reuses the Slack client secret above.'\n : 'Not enabled.';\n el('slack-status').textContent = slack ? slack.appId + ' is configured.' : 'Creates the default AgentConnect integration manifest.';\n const slackDrift = slack\n ? expected.slack\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'HTTPS Web, API, and ingress URLs' }]\n : [];\n match('slack-match', !slack ? '' : slackDrift.length ? 'warn' : '', !slack ? 'Not configured' : slackDrift.length ? 'Update required' : 'Ready to check');\n el('slack-name-field').hidden = Boolean(slack);\n el('create-slack').hidden = Boolean(slack);\n el('connect-slack-login').hidden = !slack || !values.logto || Boolean(values.logto.slackConnector) || !bootstrapInfo?.slackAvailable;\n el('clear-slack').hidden = !slack;\n el('check-slack').hidden = !slack;\n el('slack-settings').hidden = !slack;\n if (slack) {\n el('slack-settings').href = 'https://api.slack.com/apps/' + encodeURIComponent(slack.appId);\n showDiff('slack-drift', slackDrift);\n } else el('slack-drift').hidden = true;\n\n const google = values.logto && values.logto.googleConnector;\n const googleSecret = configured(byKey, 'logto.googleConnectorClientSecret');\n const expectedGoogle = expected.google;\n renderUriList('google-origins', expectedGoogle.origins);\n renderUriList('google-redirects', expectedGoogle.redirects);\n text('google-client-id', google && google.clientId);\n secretText('google-client-secret-display', byKey, 'logto.googleConnectorClientSecret', Boolean(google));\n showIdentityEditors('google', Boolean(google));\n el('google-id').value = google ? google.clientId : '';\n el('google-status').textContent = google ? 'Google OAuth client is configured.' : 'Create a Web application OAuth client manually, then save it here.';\n showDiff('google-drift', []);\n match('google-match', google && googleSecret ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : \"Can't verify automatically\");\n el('google-initial-secret-field').hidden = googleSecret;\n el('save-google').textContent = google ? 'Confirm callback settings' : 'Save Google client';\n el('google-config-controls').hidden = Boolean(google);\n el('save-google').hidden = Boolean(google);\n el('cancel-google-configuration').hidden = true;\n el('check-google').hidden = !google || !googleSecret;\n el('clear-google').hidden = !google;\n\n const feishuSecret = byKey.get('feishu.loginAppSecret');\n const larkSecret = byKey.get('lark.loginAppSecret');\n el('feishu-login-id').value = values.feishu ? values.feishu.loginAppId : '';\n el('lark-login-id').value = values.lark ? values.lark.loginAppId : '';\n text('feishu-app-id', values.feishu && values.feishu.loginAppId);\n text('lark-app-id', values.lark && values.lark.loginAppId);\n showIdentityEditors('feishu', Boolean(values.feishu));\n showIdentityEditors('lark', Boolean(values.lark));\n secretText('feishu-app-secret-display', byKey, 'feishu.loginAppSecret', Boolean(values.feishu));\n secretText('lark-app-secret-display', byKey, 'lark.loginAppSecret', Boolean(values.lark));\n el('feishu-login-status').textContent = values.feishu && feishuSecret && feishuSecret.configured\n ? values.feishu.loginAppId + ' is configured.'\n : 'No Feishu tenant App is configured.';\n el('lark-login-status').textContent = values.lark && larkSecret && larkSecret.configured\n ? values.lark.loginAppId + ' is configured.'\n : 'No Lark tenant App is configured.';\n match('feishu-match', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? '' : '', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? 'Ready to check' : 'Not configured');\n match('lark-match', values.lark && configured(byKey, 'lark.loginAppSecret') ? '' : '', values.lark && configured(byKey, 'lark.loginAppSecret') ? 'Ready to check' : 'Not configured');\n showDiff('feishu-drift', []);\n showDiff('lark-drift', []);\n el('feishu-config-controls').hidden = Boolean(values.feishu);\n el('lark-config-controls').hidden = Boolean(values.lark);\n el('feishu-create-name-field').hidden = Boolean(values.feishu);\n el('lark-create-name-field').hidden = Boolean(values.lark);\n el('create-feishu-login-app').hidden = Boolean(values.feishu);\n el('create-lark-login-app').hidden = Boolean(values.lark);\n el('save-feishu-login-app').hidden = Boolean(values.feishu);\n el('save-lark-login-app').hidden = Boolean(values.lark);\n el('cancel-feishu-configuration').hidden = true;\n el('cancel-lark-configuration').hidden = true;\n el('clear-feishu').hidden = !values.feishu;\n el('clear-lark').hidden = !values.lark;\n el('check-feishu-login-app').hidden = !values.feishu || !configured(byKey, 'feishu.loginAppSecret');\n el('check-lark-login-app').hidden = !values.lark || !configured(byKey, 'lark.loginAppSecret');\n el('feishu-settings').hidden = !values.feishu;\n el('lark-settings').hidden = !values.lark;\n if (values.feishu) el('feishu-settings').href = 'https://open.feishu.cn/app/' + encodeURIComponent(values.feishu.loginAppId);\n if (values.lark) el('lark-settings').href = 'https://open.larksuite.com/app/' + encodeURIComponent(values.lark.loginAppId);\n }\n\n function requiredInput(id, label) {\n const value = el(id).value.trim();\n if (!value) throw new Error('Enter ' + label);\n return value;\n }\n\n function githubConnectorUsesDeployment(values) {\n const github = values.github;\n const connector = values.logto && values.logto.githubConnector;\n return Boolean(\n github && connector &&\n connector.appId === github.appId &&\n connector.slug === github.slug &&\n connector.clientId === github.clientId\n );\n }\n\n function beginConfigurationEdit(provider) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const values = currentStatus.values;\n if (provider === 'logto') {\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') return;\n el('logto-edit-management-id').value = logto.managementAppId;\n el('logto-edit-management-resource').value = logto.managementResource;\n el('logto-edit-management-secret').value = '';\n el('logto-edit-browser-id').value = values.auth.browserClient.appId;\n el('logto-edit-browser-resource').value = logto.browser.apiResource || '';\n el('logto-edit-controls').hidden = false;\n el('logto-edit-management-id').focus();\n } else if (provider === 'github') {\n const github = values.github;\n if (!github) return;\n el('github-edit-app-id').value = String(github.appId);\n el('github-edit-slug').value = github.slug;\n el('github-edit-client-id').value = github.clientId || '';\n el('github-edit-webhook-enabled').checked = github.webhookEnabled !== false;\n for (const id of ['github-edit-client-secret', 'github-edit-private-key', 'github-edit-webhook-secret', 'github-edit-logto-secret']) el(id).value = '';\n el('github-edit-logto-secret-field').hidden = !githubConnectorUsesDeployment(values);\n el('github-edit-controls').hidden = false;\n el('clear-github').hidden = true;\n el('github-edit-app-id').focus();\n } else if (provider === 'slack') {\n const slack = values.slack;\n if (!slack) return;\n el('slack-edit-app-id').value = slack.appId;\n el('slack-edit-client-id').value = slack.clientId;\n el('slack-edit-client-secret').value = '';\n el('slack-edit-signing-secret').value = '';\n el('slack-edit-controls').hidden = false;\n el('clear-slack').hidden = true;\n el('slack-edit-app-id').focus();\n } else if (provider === 'google') {\n const google = values.logto && values.logto.googleConnector;\n if (!google) return;\n el('google-id').value = google.clientId;\n el('google-secret').value = '';\n el('google-config-controls').hidden = false;\n el('google-initial-secret-field').hidden = false;\n el('save-google').hidden = false;\n el('save-google').textContent = 'Save Google client';\n el('cancel-google-configuration').hidden = false;\n el('clear-google').hidden = true;\n el('google-id').focus();\n } else if (provider === 'feishu' || provider === 'lark') {\n el(provider + '-config-controls').hidden = false;\n el(provider + '-create-name-field').hidden = true;\n el('create-' + provider + '-login-app').hidden = true;\n el('save-' + provider + '-login-app').hidden = false;\n el('cancel-' + provider + '-configuration').hidden = false;\n el('clear-' + provider).hidden = true;\n el(provider + '-login-secret').value = '';\n el(provider + '-login-id').focus();\n }\n showIdentityEditors(provider, false);\n }\n\n function cancelConfigurationEdit() {\n if (currentStatus) renderApps(currentStatus);\n }\n\n async function replaceConfiguration(values, secrets, successMessage) {\n const response = await json(await fetch(api + '/deployment-config', {\n method: 'PUT',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values, ...(secrets ? { secrets } : {}) })\n }));\n await load();\n message(successMessage || 'Configuration saved. Restart AgentConnect to apply it.');\n return response;\n }\n\n async function saveLogtoConfiguration() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = currentStatus.values;\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') throw new Error('Logto is not configured');\n const managementAppId = requiredInput('logto-edit-management-id', 'the Management App ID');\n const managementResource = requiredInput('logto-edit-management-resource', 'the Management API resource');\n const browserAppId = requiredInput('logto-edit-browser-id', 'the SPA App ID');\n const browserResource = el('logto-edit-browser-resource').value.trim() || null;\n const managementIdentityChanged = managementAppId !== logto.managementAppId;\n const managementSecret = el('logto-edit-management-secret').value;\n if (managementIdentityChanged && !managementSecret) throw new Error('Enter the new Management App secret');\n const browser = { ...logto.browser, apiResource: browserResource };\n const auth = {\n ...values.auth,\n audience: browserResource || browserAppId,\n browserClient: { appId: browserAppId, apiResource: browserResource }\n };\n await replaceConfiguration(\n {\n ...values,\n auth,\n logto: { ...logto, managementAppId, managementResource, browser }\n },\n managementSecret ? { 'logto.managementAppSecret': managementSecret } : undefined,\n 'Logto configuration saved. Sign in again if the SPA identity changed, then restart AgentConnect.'\n );\n }\n\n async function saveGithubConfiguration() {\n if (!currentStatus || !currentStatus.values.github) throw new Error('GitHub is not configured');\n const values = currentStatus.values;\n const previous = values.github;\n const appId = Number(requiredInput('github-edit-app-id', 'the GitHub App ID'));\n if (!Number.isSafeInteger(appId) || appId <= 0) throw new Error('GitHub App ID must be a positive integer');\n const slug = requiredInput('github-edit-slug', 'the GitHub App slug');\n const clientId = requiredInput('github-edit-client-id', 'the GitHub Client ID');\n const appChanged = appId !== previous.appId;\n const clientChanged = clientId !== previous.clientId;\n const webhookEnabled = el('github-edit-webhook-enabled').checked;\n const webhookEnabling = webhookEnabled && previous.webhookEnabled === false;\n const connectorReused = githubConnectorUsesDeployment(values);\n const clientSecret = el('github-edit-client-secret').value;\n const privateKey = el('github-edit-private-key').value.trim();\n const webhookSecret = el('github-edit-webhook-secret').value;\n const connectorSecret = el('github-edit-logto-secret').value;\n const webhookSecretStored = currentStatus.secrets.some((secret) => secret.key === 'github.webhookSecret' && secret.configured);\n if ((appChanged || clientChanged) && !clientSecret) throw new Error('Enter the new GitHub client secret');\n if (appChanged && !privateKey) throw new Error('Enter the new GitHub private key as base64');\n if (webhookEnabled && ((appChanged && !webhookSecret) || (webhookEnabling && !webhookSecret && !webhookSecretStored))) throw new Error('Enter the new GitHub webhook secret');\n if (connectorReused && (appChanged || clientChanged) && !connectorSecret) throw new Error('Enter the new Logto connector client secret');\n const secrets = {};\n if (clientSecret) secrets['github.clientSecret'] = clientSecret;\n if (privateKey) secrets['github.privateKeyB64'] = privateKey;\n if (webhookSecret) secrets['github.webhookSecret'] = webhookSecret;\n if (connectorSecret) secrets['logto.githubConnectorClientSecret'] = connectorSecret;\n const nextLogto = connectorReused && values.logto\n ? { ...values.logto, githubConnector: { ...values.logto.githubConnector, appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, webhookEnabled },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\n );\n if (connectorSecret) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function saveSlackConfiguration() {\n if (!currentStatus || !currentStatus.values.slack) throw new Error('Slack is not configured');\n const values = currentStatus.values;\n const previous = values.slack;\n const appId = requiredInput('slack-edit-app-id', 'the Slack App ID');\n const clientId = requiredInput('slack-edit-client-id', 'the Slack Client ID');\n const changed = appId !== previous.appId || clientId !== previous.clientId;\n const clientSecret = el('slack-edit-client-secret').value;\n const signingSecret = el('slack-edit-signing-secret').value;\n if (changed && (!clientSecret || !signingSecret)) throw new Error('Enter both the new Slack client secret and signing secret');\n const secrets = {};\n if (clientSecret) secrets['slack.clientSecret'] = clientSecret;\n if (signingSecret) secrets['slack.signingSecret'] = signingSecret;\n const nextLogto = values.logto && values.logto.slackConnector\n ? { ...values.logto, slackConnector: { ...values.logto.slackConnector, appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\n );\n if (clientSecret && nextLogto && nextLogto.slackConnector) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function clearProvider(provider) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const label = provider === 'github' ? 'GitHub' : provider === 'slack' ? 'Slack' : provider === 'google' ? 'Google' : provider === 'feishu' ? 'Feishu' : 'Lark';\n if (!window.confirm('Clear the saved ' + label + ' configuration and secrets?')) return;\n const values = currentStatus.values;\n let next = values;\n let secrets = {};\n if (provider === 'github') {\n const connectorReused = githubConnectorUsesDeployment(values);\n const logto = connectorReused && values.logto\n ? {\n ...values.logto,\n githubConnector: null\n }\n : values.logto;\n next = { ...values, github: null, ...(logto ? { logto } : {}) };\n secrets = {\n 'github.clientSecret': null,\n 'github.privateKeyB64': null,\n 'github.webhookSecret': null,\n ...(connectorReused ? { 'logto.githubConnectorClientSecret': null } : {})\n };\n } else if (provider === 'slack') {\n const logto = values.logto\n ? {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'slack') }\n : values.logto.browser,\n slackConnector: null\n }\n : values.logto;\n next = { ...values, slack: null, ...(logto ? { logto } : {}) };\n secrets = { 'slack.clientSecret': null, 'slack.signingSecret': null };\n } else if (provider === 'google') {\n if (!values.logto) return;\n next = {\n ...values,\n logto: {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'google') }\n : values.logto.browser,\n googleConnector: null\n }\n };\n secrets = { 'logto.googleConnectorClientSecret': null };\n } else if (provider === 'feishu' || provider === 'lark') {\n next = { ...values, [provider]: null };\n secrets = { [provider + '.loginAppSecret']: null };\n }\n await replaceConfiguration(next, secrets, label + ' configuration cleared. Its setup controls are available again.');\n }\n\n async function startGithub(prefix) {\n const result = await json(await fetch(api + '/create/github/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n purpose: 'deployment',\n name: el(prefix + '-name').value,\n ownership: ownership(prefix),\n connectLogto: prefix === 'bootstrap-github'\n })\n }));\n const form = document.createElement('form');\n form.method = 'post'; form.action = result.action;\n const manifest = document.createElement('input');\n manifest.type = 'hidden'; manifest.name = 'manifest'; manifest.value = JSON.stringify(result.manifest);\n form.append(manifest); document.body.append(form);\n message('Opening GitHub to review the complete integration App.');\n form.submit();\n }\n\n async function loadBootstrapInfo() {\n bootstrapInfo = await json(await fetch(api + '/bootstrap-info'));\n el('open-logto').href = bootstrapInfo.logtoAdminEndpoint;\n el('logto-settings').href = bootstrapInfo.logtoAdminEndpoint;\n if (!el('logto-app-id').value && bootstrapInfo.logtoManagementAppId) {\n el('logto-app-id').value = bootstrapInfo.logtoManagementAppId;\n }\n renderUriList('bootstrap-google-origins', bootstrapInfo.google.javascriptOrigins);\n renderUriList('bootstrap-google-redirects', bootstrapInfo.google.redirectUris);\n renderUriList('bootstrap-slack-redirects', [bootstrapInfo.slackLoginRedirectUrl]);\n el('logto-management-api-resource').value = bootstrapInfo.logtoManagementResource;\n el('bootstrap-github-submit').disabled = !bootstrapInfo.githubAvailable;\n if (!bootstrapInfo.githubAvailable) {\n el('bootstrap-github-note').textContent = 'GitHub App creation needs valid saved Web, API, and ingress URLs.';\n } else if (!bootstrapInfo.githubWebhookActive) {\n el('bootstrap-github-note').textContent = 'Creates the complete GitHub App now without submitting the localhost webhook URL. Add it after saving reachable HTTPS ingress.';\n } else {\n el('bootstrap-github-note').textContent = 'This creates one complete App for both GitHub sign-in and repository integration.';\n }\n el('bootstrap-slack-option').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-submit').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-note').textContent = bootstrapInfo.slackAvailable\n ? 'Creates one complete Slack App. Sign-in uses a separate openid profile email flow from workspace installation.'\n : 'Slack sign-in needs all public service URLs to use HTTPS. Use Google locally or expose the stack through trusted HTTPS endpoints.';\n if (!bootstrapInfo.slackAvailable && el('bootstrap-provider').value === 'slack') {\n el('bootstrap-provider').value = 'google';\n updateBootstrapProvider();\n }\n return bootstrapInfo;\n }\n\n async function logtoBootstrapFinding() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n for (const id of ['logto.client_credentials', 'logto.roles_read']) {\n const finding = report.findings.find((candidate) => candidate.id === id);\n if (!finding || finding.status !== 'pass') {\n return finding || { status: 'fail', message: 'Logto Management API permissions could not be verified.' };\n }\n }\n return { status: 'pass', message: 'Logto Management API access is ready.' };\n }\n\n async function showBootstrap() {\n el('bootstrap').hidden = false;\n el('show-bootstrap').hidden = true;\n const info = bootstrapInfo || await loadBootstrapInfo();\n if (!info.logtoConfigured) {\n showBootstrapStep('logto');\n return;\n }\n const finding = await logtoBootstrapFinding();\n if (finding && finding.status === 'pass') {\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n return;\n }\n showBootstrapStep('logto');\n message(finding?.message || 'Logto Management API credentials could not be verified.', true);\n }\n\n async function bootstrapLogto() {\n await json(await fetch(api + '/bootstrap/logto', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n managementAppId: el('logto-app-id').value,\n managementAppSecret: el('logto-app-secret').value,\n managementResource: el('logto-management-api-resource').value\n })\n }));\n el('logto-app-secret').value = '';\n await loadBootstrapInfo();\n const finding = await logtoBootstrapFinding();\n if (!finding || finding.status !== 'pass') {\n throw new Error(finding?.message || 'Logto Management API credentials could not be verified.');\n }\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n }\n\n async function bootstrapGoogle() {\n const secret = el('bootstrap-google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ clientId: el('bootstrap-google-id').value, clientSecret: secret })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto(true);\n await load();\n }\n\n async function bootstrapGithub() {\n if (!bootstrapInfo || !bootstrapInfo.githubAvailable) throw new Error('GitHub App creation needs valid saved Web, API, and ingress URLs.');\n await startGithub('bootstrap-github');\n }\n\n async function bootstrapSlack() {\n if (!bootstrapInfo || !bootstrapInfo.slackAvailable) throw new Error('Slack sign-in requires all public service URLs to use HTTPS.');\n await createSlack('bootstrap-slack', true);\n await reconcileLogto();\n await load();\n }\n\n async function createSlack(prefix = 'slack', connectLogto = false) {\n const result = await json(await fetch(api + '/create/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n name: el(prefix + '-name').value,\n configToken: el(prefix + '-token').value,\n connectLogto\n })\n }));\n el(prefix + '-token').value = '';\n if (!connectLogto) await load();\n message('Slack App ' + result.app.id + ' was created with the default integration manifest. Restart AgentConnect to apply it.');\n }\n\n async function checkGithub() {\n const result = await json(await fetch(api + '/check/github', { headers: bearer() }));\n el('github-settings').href = result.settingsUrl;\n el('github-settings').hidden = false;\n const label = result.missing.length ? 'Update required' : \"Can't verify automatically\";\n showDiff('github-drift', result.diff || [], label);\n match('github-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : label);\n message(result.status === 'pass' ? 'GitHub App matches the expected integration manifest.' : result.missing.length ? 'GitHub App settings need an update.' : \"GitHub can't expose callback, setup, or webhook-active settings through its API; check them in GitHub.\", result.status === 'fail');\n }\n\n async function connectGithubLogin() {\n await json(await fetch(api + '/configure/github-login', { method: 'POST', headers: bearer() }));\n await reconcileLogto();\n await load();\n message('The deployment GitHub App is now also used for Logto sign-in. Update its displayed callback URLs.');\n }\n\n async function connectSlackLogin() {\n if (!currentStatus || !currentStatus.values.slack || !currentStatus.values.logto) {\n throw new Error('Save the Slack App and Logto configuration first');\n }\n const values = currentStatus.values;\n const slack = values.slack;\n const logto = values.logto;\n await replaceConfiguration(\n {\n ...values,\n logto: {\n ...logto,\n browser: logto.browser\n ? { ...logto.browser, socialProviders: [...new Set([...logto.browser.socialProviders, 'slack'])] }\n : logto.browser,\n slackConnector: {\n appId: slack.appId,\n clientId: slack.clientId\n }\n }\n },\n undefined,\n 'Slack App is ready to connect to Logto.'\n );\n await reconcileLogto();\n await load();\n message('The deployment Slack App is now also used for Logto sign-in.');\n }\n\n async function checkSlack() {\n const token = requiredInput('slack-token', 'the temporary Slack App configuration token');\n const result = await json(await fetch(api + '/check/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ configToken: token })\n }));\n el('slack-token').value = '';\n showDiff('slack-drift', result.diff || []);\n match('slack-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : 'Update required');\n message(result.status === 'pass' ? 'Slack App matches the default integration manifest.' : 'Slack App settings need an update.', result.status !== 'pass');\n }\n\n async function checkGoogle() {\n const report = await checkLogto();\n const connector = report.findings.find((finding) => finding.id === 'logto.connectors');\n const google = currentStatus && currentStatus.values.logto && currentStatus.values.logto.googleConnector;\n const connectorDiff = connector && connector.diff\n ? connector.diff.filter((item) => item.field.toLowerCase().startsWith('google'))\n : [];\n const passed = Boolean(google && connector && connectorDiff.length === 0);\n showDiff('google-drift', connectorDiff);\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Logto matches' : 'Update required');\n message(\n passed\n ? \"The Logto Google connector matches. Google OAuth redirect settings can't be verified automatically.\"\n : 'The Logto Google connector needs an update.',\n !passed\n );\n }\n\n async function checkRegionalLoginApp(region) {\n const result = await json(await fetch(api + '/check/regional-login-app/' + region, { headers: bearer() }));\n const label = region === 'feishu' ? 'Feishu' : 'Lark';\n showDiff(region + '-drift', result.diff || []);\n match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Matches' : result.status === 'fail' ? 'Update required' : 'Could not check');\n el(region + '-login-status').textContent = result.message;\n el(region + '-login-status').className = result.status === 'pass' ? 'ok' : result.status === 'fail' ? 'warn' : 'muted';\n message(result.message || (label + ' credential check completed.'), result.status === 'fail');\n }\n\n async function saveGoogle() {\n const secret = el('google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n clientId: el('google-id').value,\n ...(secret ? { clientSecret: secret } : {})\n })\n }));\n el('google-secret').value = '';\n await reconcileLogto(Boolean(secret));\n await load();\n message('Google OAuth client and Logto connector are configured. Restart AgentConnect to apply the saved settings.');\n }\n\n function regionalLoginApp(prefix) {\n const appId = el(prefix + '-login-id').value.trim();\n const appSecret = el(prefix + '-login-secret').value;\n return appId ? { appId, ...(appSecret ? { appSecret } : {}) } : null;\n }\n\n async function saveRegionalLoginApp(region) {\n const app = regionalLoginApp(region);\n if (!app) throw new Error('Enter the ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App ID');\n await json(await fetch(api + '/configure/regional-login-app', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, app })\n }));\n el(region + '-login-secret').value = '';\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' tenant App is saved. Restart AgentConnect services to apply it.');\n }\n\n async function createRegionalLoginApp(region) {\n const popup = window.open('about:blank', '_blank');\n try {\n const started = await json(await fetch(api + '/create/regional-login-app/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, name: el(region + '-create-name').value })\n }));\n if (popup) popup.location.replace(started.authorizationUrl);\n else window.open(started.authorizationUrl, '_blank', 'noopener');\n message('Approve the new ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App in the opened page. This page will finish saving it automatically.');\n while (true) {\n const result = await json(await fetch(api + '/create/regional-login-app/' + encodeURIComponent(started.id), { headers: bearer() }));\n if (result.status === 'completed') {\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' App ' + result.appId + ' was created and saved. Restart AgentConnect services to apply it.');\n return;\n }\n if (result.status === 'failed') throw new Error('Regional App creation ' + result.reason + '. Start it again.');\n await new Promise((resolve) => setTimeout(resolve, Math.max(500, Math.min(result.retryAfterMs || 2000, 5000))));\n }\n } catch (error) {\n if (popup) popup.close();\n throw error;\n }\n }\n\n async function reconcileLogto(refreshConnectorSecrets) {\n return json(await fetch(api + '/reconcile/logto', {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ refreshConnectorSecrets: Boolean(refreshConnectorSecrets) })\n }));\n }\n\n async function refreshDeploymentConfig() {\n const status = await json(await fetch(api + '/deployment-config', { headers: bearer() }));\n el('access').hidden = true;\n el('setup').hidden = false;\n el('editor').hidden = false;\n currentRevision = status.revision;\n renderApps(status);\n return status;\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n await refreshDeploymentConfig();\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, connectors, and social-only sign-in match.'\n : failures.map((finding) => finding.message).join(' ');\n el('logto-status').className = failures.length === 0 ? 'ok' : 'warn';\n match('logto-match', failures.length === 0 ? 'pass' : 'warn', failures.length === 0 ? 'Matches' : 'Update required');\n showDiff(\n 'logto-drift',\n failures.flatMap((finding) => finding.diff && finding.diff.length\n ? finding.diff\n : [{ field: finding.id.replace(/^logto./, '').replaceAll('_', ' '), current: finding.message, expected: 'Matches expected configuration' }])\n );\n el('logto-settings').hidden = failures.length === 0;\n return report;\n }\n\n function githubNotice(value) {\n if (value === 'deployment-created') return 'GitHub integration App created. Its private key and webhook secret are stored.';\n if (value === 'deployment-login-created') return 'GitHub integration App created and connected to Logto sign-in.';\n if (value === 'cancelled') return 'GitHub App creation was cancelled.';\n if (value === 'expired') return 'GitHub App creation expired. Start it again.';\n if (value === 'invalid-callback') return 'GitHub returned an invalid App creation callback.';\n if (value === 'conversion-failed') return 'GitHub may have created the App, but did not return complete credentials. Delete the orphaned App and retry.';\n if (value === 'save-failed') return 'GitHub created the App, but its credentials could not be saved. Delete the orphaned App and retry.';\n return '';\n }\n\n async function load() {\n let publicAuth = await authConfig();\n const currentUrl = new URL(location.href);\n const githubResult = currentUrl.searchParams.get('github');\n const notice = githubNotice(githubResult);\n if (githubResult === 'deployment-login-created' && publicAuth.mode === 'none') {\n await reconcileLogto();\n publicAuth = await authConfig();\n }\n if (githubResult) history.replaceState({}, '', '/');\n const hasToken = Boolean(sessionStorage.getItem(tokenKey));\n el('access').hidden = false;\n el('setup').hidden = true;\n el('login').hidden = true;\n el('open-logto').hidden = true;\n el('show-bootstrap').hidden = true;\n el('editor').hidden = true;\n el('bootstrap').hidden = true;\n if (publicAuth.logtoAdminEndpoint) {\n el('open-logto').href = publicAuth.logtoAdminEndpoint;\n el('logto-settings').href = publicAuth.logtoAdminEndpoint;\n }\n if (publicAuth.mode === 'none') {\n sessionStorage.removeItem(tokenKey);\n await loadBootstrapInfo();\n el('open-logto').hidden = false;\n el('show-bootstrap').hidden = false;\n message(notice || 'Logto sign-in is not configured. Set up the initial Logto administrator first.');\n return;\n }\n if (publicAuth.mode === 'unavailable') {\n el('open-logto').hidden = false;\n message(publicAuth.message || 'Logto sign-in is unavailable. Open Logto Console to review its settings.', true);\n return;\n }\n if (!hasToken) {\n el('login').hidden = false;\n message(notice || 'Sign in with Logto to continue.');\n return;\n }\n if (publicAuth.claimAvailable) {\n try {\n await json(await fetch(api + '/bootstrap/claim', { method: 'POST', headers: bearer() }));\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('This first user is now an ADMIN. Sign in again to refresh the role claim.');\n return;\n } catch (error) {\n if (error.status !== 401 && error.status !== 403) throw error;\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with Logto to continue.', true);\n return;\n }\n }\n try {\n await loadBootstrapInfo();\n await refreshDeploymentConfig();\n message(notice);\n checkLogto().catch((error) => {\n el('logto-status').textContent = error.message;\n el('logto-status').className = 'warn';\n el('logto-settings').hidden = false;\n });\n } catch (error) {\n if (error.status === 401 || error.status === 403) {\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with a Logto ADMIN account.', true);\n }\n else throw error;\n }\n }\n\n async function saveSecretReplacement(key, value) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n if (!value) throw new Error('Enter the replacement secret');\n const refreshLogto =\n (key === 'logto.githubConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.githubConnector) ||\n (key === 'logto.googleConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.googleConnector) ||\n (key === 'slack.clientSecret' && currentStatus.values.logto && currentStatus.values.logto.slackConnector);\n await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values: currentStatus.values, secrets: { [key]: value } })\n }));\n try {\n if (refreshLogto) await reconcileLogto(true);\n } finally {\n await load();\n }\n if (refreshLogto) {\n message('Secret replaced and applied to the Logto connector. Restart AgentConnect to apply deployment changes.');\n } else {\n message('Secret replaced. Restart AgentConnect to apply it.');\n }\n }\n\n function editSecret(button) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const row = button.closest('.secret-line');\n const display = el(button.dataset.secretDisplay);\n if (!row || !display || row.querySelector('.secret-editor')) return;\n const editor = document.createElement('span'); editor.className = 'secret-editor';\n const input = document.createElement('input');\n input.type = 'password'; input.autocomplete = 'new-password'; input.placeholder = 'Enter replacement secret';\n const save = document.createElement('button'); save.textContent = 'Save';\n const cancel = document.createElement('button'); cancel.textContent = 'Cancel';\n const close = () => { editor.remove(); display.hidden = false; button.hidden = false; };\n save.onclick = async () => {\n save.disabled = true;\n try { await saveSecretReplacement(button.dataset.secretKey, input.value); }\n catch (error) { save.disabled = false; message(error.message, true); }\n };\n cancel.onclick = close;\n input.onkeydown = (event) => { if (event.key === 'Enter') save.click(); if (event.key === 'Escape') close(); };\n display.hidden = true; button.hidden = true;\n editor.append(input, save, cancel); row.append(editor); input.focus();\n }\n\n async function saveOptions() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = {\n ...currentStatus.values,\n features: {\n presetAgentsEnabled: el('preset-agents-enabled').checked\n }\n };\n const saved = await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values })\n }));\n message('Saved deployment options. Restart AgentConnect to apply them.');\n await load();\n }\n\n el('login').onclick = () => signIn().catch((error) => message(error.message, true));\n el('show-bootstrap').onclick = () => showBootstrap().catch((error) => message(error.message, true));\n el('bootstrap-provider').onchange = updateBootstrapProvider;\n el('bootstrap-github-owner').onchange = () => updateOwner('bootstrap-github');\n el('github-owner').onchange = () => updateOwner('github');\n el('bootstrap-logto-submit').onclick = () => bootstrapLogto().catch((error) => message(error.message, true));\n el('bootstrap-back').onclick = () => showBootstrapStep('logto');\n el('bootstrap-google-submit').onclick = () => bootstrapGoogle().catch((error) => message(error.message, true));\n el('bootstrap-github-submit').onclick = () => bootstrapGithub().catch((error) => message(error.message, true));\n el('bootstrap-slack-submit').onclick = () => bootstrapSlack().catch((error) => message(error.message, true));\n el('create-github').onclick = () => startGithub('github').catch((error) => message(error.message, true));\n el('save-logto-configuration').onclick = () => saveLogtoConfiguration().catch((error) => message(error.message, true));\n el('cancel-logto-configuration').onclick = cancelConfigurationEdit;\n el('save-github-configuration').onclick = () => saveGithubConfiguration().catch((error) => message(error.message, true));\n el('cancel-github-configuration').onclick = cancelConfigurationEdit;\n el('clear-github').onclick = () => clearProvider('github').catch((error) => message(error.message, true));\n el('connect-github-login').onclick = () => connectGithubLogin().catch((error) => message(error.message, true));\n el('check-github').onclick = () => checkGithub().catch((error) => message(error.message, true));\n el('create-slack').onclick = () => createSlack('slack').catch((error) => message(error.message, true));\n el('connect-slack-login').onclick = () => connectSlackLogin().catch((error) => message(error.message, true));\n el('save-slack-configuration').onclick = () => saveSlackConfiguration().catch((error) => message(error.message, true));\n el('cancel-slack-configuration').onclick = cancelConfigurationEdit;\n el('clear-slack').onclick = () => clearProvider('slack').catch((error) => message(error.message, true));\n el('check-slack').onclick = () => checkSlack().catch((error) => message(error.message, true));\n el('save-google').onclick = () => saveGoogle().catch((error) => message(error.message, true));\n el('cancel-google-configuration').onclick = cancelConfigurationEdit;\n el('clear-google').onclick = () => clearProvider('google').catch((error) => message(error.message, true));\n el('check-google').onclick = () => checkGoogle().catch((error) => message(error.message, true));\n el('create-feishu-login-app').onclick = () => createRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('save-feishu-login-app').onclick = () => saveRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('cancel-feishu-configuration').onclick = cancelConfigurationEdit;\n el('clear-feishu').onclick = () => clearProvider('feishu').catch((error) => message(error.message, true));\n el('check-feishu-login-app').onclick = () => checkRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('create-lark-login-app').onclick = () => createRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('save-lark-login-app').onclick = () => saveRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('cancel-lark-configuration').onclick = cancelConfigurationEdit;\n el('clear-lark').onclick = () => clearProvider('lark').catch((error) => message(error.message, true));\n el('check-lark-login-app').onclick = () => checkRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('check-logto').onclick = () => checkLogto().catch((error) => message(error.message, true));\n el('reconcile-logto').onclick = () => reconcileLogto().then(load).catch((error) => message(error.message, true));\n el('logout').onclick = () => { sessionStorage.removeItem(tokenKey); load().catch((error) => message(error.message, true)); };\n el('save-options').onclick = () => saveOptions().catch((error) => message(error.message, true));\n for (const button of document.querySelectorAll('.edit-secret')) button.onclick = () => editSecret(button);\n for (const button of document.querySelectorAll('.edit-configuration')) button.onclick = () => beginConfigurationEdit(button.dataset.provider);\n updateBootstrapProvider();\n finishSignIn().then(load).catch((error) => message(error.message, true));\n <\/script>\n</body>\n</html>"], ["<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>AgentConnect deployment settings</title>\n <style>\n :root { color-scheme: light dark; font: 15px/1.5 system-ui, sans-serif; }\n body { max-width: 1280px; margin: 48px auto; padding: 0 20px 60px; }\n h1 { margin-bottom: 4px; } h2 { margin-top: 32px; } h3 { margin: 0 0 8px; }\n .muted { color: #777; } .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }\n button, .button { padding: 8px 13px; cursor: pointer; }\n .button { border: 1px solid #8888; border-radius: 4px; color: inherit; text-decoration: none; display: inline-block; }\n input, select { width: min(520px, 100%); padding: 7px; box-sizing: border-box; }\n input[type=\"checkbox\"] { width: auto; padding: 0; }\n .field { display: grid; gap: 4px; margin: 10px 0; }\n .provider-stack { display: grid; gap: 16px; }\n .panel { border: 1px solid #8886; border-radius: 8px; padding: 16px; }\n .section-head, .provider-head { display: flex; justify-content: space-between; gap: 16px; align-items: start; }\n .section-head h2, .provider-head h3 { margin: 0; }\n .provider-head p { margin: 4px 0 0; }\n .badge { flex: none; border: 1px solid #8886; border-radius: 999px; padding: 3px 9px; font-size: 13px; }\n .badge.pass { color: #198754; border-color: #19875466; background: #19875412; }\n .badge.warn { color: #a66b00; border-color: #d99b1366; background: #d99b1315; }\n .badge.fail { color: #c33; border-color: #c333; background: #c3331111; }\n .credentials { display: grid; grid-template-columns: minmax(160px, 220px) minmax(0, 1fr); gap: 7px 16px; margin: 16px 0; }\n .credentials dt { color: #777; }\n .credentials dd { margin: 0; overflow-wrap: anywhere; }\n .credentials code { user-select: all; }\n .redacted { font-family: ui-monospace, monospace; letter-spacing: .08em; }\n .secret-line, .value-line { display: flex; gap: 10px; align-items: center; min-height: 30px; }\n .edit-secret { padding: 2px 8px; font-size: 13px; }\n .secret-editor { display: flex; gap: 8px; flex-wrap: wrap; width: 100%; }\n .secret-editor input { width: min(420px, 100%); }\n .edit-configuration { padding: 2px 8px; font-size: 13px; }\n .startup-owned { color: #777; font-size: 13px; }\n .danger { color: #c33; border-color: #c336; }\n .subsection { margin-top: 16px; padding-top: 14px; border-top: 1px solid #8883; }\n textarea { width: min(720px, 100%); min-height: 100px; padding: 7px; box-sizing: border-box; }\n .uris { margin: 8px 0; padding-left: 20px; } .uris code { user-select: all; }\n .notice { border-left: 4px solid #d99b13; padding: 8px 12px; background: #d99b1315; }\n .diff-title { display: block; margin-bottom: 8px; }\n .config-diff { display: grid; gap: 1px; border: 1px solid #8884; border-radius: 6px; overflow: hidden; background: #8884; }\n .diff-row { display: grid; grid-template-columns: minmax(140px, .8fr) minmax(0, 1.2fr) minmax(0, 1.2fr); background: Canvas; }\n .diff-row > * { min-width: 0; padding: 7px 9px; overflow-wrap: anywhere; white-space: pre-wrap; }\n .diff-head { font-size: 12px; font-weight: 600; color: #777; }\n .diff-field { font-weight: 600; }\n .diff-value { font-family: ui-monospace, monospace; font-size: 13px; }\n pre { padding: 12px; border-radius: 6px; background: #8881; overflow-x: auto; user-select: all; }\n #message { white-space: pre-wrap; padding: 10px 0; min-height: 1.5em; }\n .error { color: #c33; } .ok { color: #198754; } .warn { color: #a66b00; }\n code { overflow-wrap: anywhere; }\n .setup-layout { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 32px; align-items: start; }\n .setup-nav { position: sticky; top: 24px; display: grid; gap: 3px; padding: 10px; border: 1px solid #8886; border-radius: 8px; background: Canvas; }\n .setup-nav a { padding: 7px 9px; border-radius: 5px; color: inherit; text-decoration: none; white-space: nowrap; }\n .setup-nav a:hover { background: #8882; }\n .setup-section { scroll-margin-top: 24px; }\n details.environment { margin: 0 0 24px; } details.environment summary { cursor: pointer; font-weight: 600; }\n @media (max-width: 760px) {\n body { margin-top: 24px; }\n .setup-layout { grid-template-columns: 1fr; gap: 18px; }\n .setup-nav { position: static; display: flex; overflow-x: auto; }\n .credentials { grid-template-columns: 1fr; gap: 2px; }\n .credentials dd { margin-bottom: 8px; }\n .diff-row { grid-template-columns: 1fr; }\n .diff-head { display: none; }\n .diff-value::before { display: block; margin-bottom: 2px; color: #777; font: 11px/1.4 system-ui, sans-serif; }\n .diff-current::before { content: 'Current'; }\n .diff-expected::before { content: 'Expected'; }\n }\n [hidden] { display: none !important; }\n </style>\n</head>\n<body>\n <section id=\"access\">\n <h1>AgentConnect Setup</h1>\n <p id=\"access-message\" class=\"muted\" aria-live=\"polite\">Checking Logto sign-in…</p>\n <div class=\"row\">\n <button id=\"login\" hidden>Sign in with Logto</button>\n <a id=\"open-logto\" class=\"button\" href=\"http://localhost:3002\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n <button id=\"show-bootstrap\" hidden>Continue setup</button>\n </div>\n\n <section id=\"bootstrap\" hidden>\n <h2>Set up sign-in</h2>\n <p id=\"bootstrap-progress\" class=\"muted\">Step 1 of 2</p>\n <div id=\"bootstrap-logto-step\" class=\"panel\">\n <h3>Connect Logto</h3>\n <p class=\"muted\">Enter the one-time Logto Management API credential. It is sealed in the deployment database and verified before continuing.</p>\n <label class=\"field\">Logto M2M App ID<input id=\"logto-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Logto M2M App Secret<input id=\"logto-app-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-management-api-resource\" type=\"url\" autocomplete=\"off\"></label>\n <p class=\"muted\">Logto Cloud uses <code>https://<tenant-id>.logto.app/api</code>. Keep <code>https://default.logto.app/api</code> for Logto OSS.</p>\n <button id=\"bootstrap-logto-submit\">Save Logto and continue</button>\n </div>\n <div id=\"bootstrap-provider-step\" hidden>\n <h3>Choose a sign-in provider</h3>\n <p class=\"muted\">Logto Management API access is ready. Configure one provider to enable sign-in.</p>\n <label class=\"field\">Sign-in provider\n <select id=\"bootstrap-provider\"><option value=\"google\">Google (works on localhost)</option><option value=\"github\">GitHub integration App</option><option id=\"bootstrap-slack-option\" value=\"slack\">Slack integration App</option></select>\n </label>\n <div id=\"bootstrap-google\" class=\"panel\">\n <h3>Google OAuth client</h3>\n <p class=\"muted\">Create a Web application client in Google Auth Platform, then paste its credentials here.</p>\n <p>Authorized JavaScript origin:</p><ul id=\"bootstrap-google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"bootstrap-google-redirects\" class=\"uris\"></ul>\n <a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a>\n <label class=\"field\">Client ID<input id=\"bootstrap-google-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client Secret<input id=\"bootstrap-google-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-google-submit\">Save Google OAuth and configure Logto</button>\n </div>\n <div id=\"bootstrap-github\" class=\"panel\" hidden>\n <h3>GitHub integration App</h3>\n <p id=\"bootstrap-github-note\" class=\"muted\">This creates one complete App for both GitHub sign-in and repository integration.</p>\n <label class=\"field\">Owner\n <select id=\"bootstrap-github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"bootstrap-github-org-field\" class=\"field\" hidden>Organization login<input id=\"bootstrap-github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"bootstrap-github-name\" value=\"AgentConnect\"></label>\n <button id=\"bootstrap-github-submit\">Create GitHub App and configure Logto</button>\n </div>\n <div id=\"bootstrap-slack\" class=\"panel\" hidden>\n <h3>Slack integration App</h3>\n <p id=\"bootstrap-slack-note\" class=\"muted\">Creates one complete Slack App for workspace installation and a separate Sign in with Slack OIDC flow.</p>\n <p>Logto redirect URI:</p><ul id=\"bootstrap-slack-redirects\" class=\"uris\"></ul>\n <label class=\"field\">App name<input id=\"bootstrap-slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"bootstrap-slack-token\" type=\"password\" autocomplete=\"new-password\"></label>\n <button id=\"bootstrap-slack-submit\">Create Slack App and configure Logto</button>\n </div>\n <button id=\"bootstrap-back\">Back to Logto credentials</button>\n </div>\n </section>\n </section>\n\n <main id=\"setup\" hidden>\n <div class=\"setup-layout\">\n <nav class=\"setup-nav\" aria-label=\"Deployment settings\">\n <a href=\"#startup-section\">Startup</a>\n <a href=\"#logto-section\">Logto</a>\n <a href=\"#github-section\">GitHub</a>\n <a href=\"#slack-section\">Slack</a>\n <a href=\"#google-section\">Google</a>\n <a href=\"#feishu-section\">Feishu</a>\n <a href=\"#lark-section\">Lark</a>\n <a href=\"#options-section\">Options</a>\n </nav>\n <div class=\"setup-content\">\n <h1>AgentConnect deployment settings</h1>\n <p class=\"muted\">Saved settings take effect after the stack is restarted.</p>\n <div class=\"row\">\n <button id=\"logout\">Log out</button>\n </div>\n <div id=\"message\" aria-live=\"polite\"></div>\n\n <section id=\"editor\" hidden>\n <details id=\"startup-section\" class=\"environment setup-section\" open>\n <summary>Startup environment</summary>\n <p class=\"notice\">Public service URLs come from <code>.env</code>. Provider callbacks below are derived from these values.</p>\n <pre id=\"startup-environment\"></pre>\n </details>\n\n <section id=\"logto-section\" class=\"setup-section\" aria-labelledby=\"logto-heading\">\n <div class=\"section-head\">\n <div><h2 id=\"logto-heading\">Logto</h2><p class=\"muted\">Authentication and administrator access.</p></div>\n <span id=\"logto-match\" class=\"badge\">Not checked</span>\n </div>\n <div class=\"panel\">\n <dl class=\"credentials\">\n <dt>Management endpoint</dt><dd class=\"value-line\"><code id=\"logto-management-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>Management App ID</dt><dd class=\"value-line\"><code id=\"logto-management-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management API resource</dt><dd class=\"value-line\"><code id=\"logto-management-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Management App secret</dt><dd class=\"secret-line\"><span id=\"logto-management-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.managementAppSecret\" data-secret-display=\"logto-management-secret-display\">Edit</button></dd>\n <dt>Sign-in endpoint</dt><dd class=\"value-line\"><code id=\"logto-browser-endpoint\">Not configured</code><span class=\"startup-owned\">startup environment</span></dd>\n <dt>SPA App ID</dt><dd class=\"value-line\"><code id=\"logto-browser-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n <dt>Browser API resource</dt><dd class=\"value-line\"><code id=\"logto-browser-resource\">Not configured</code><button class=\"edit-configuration\" data-provider=\"logto\">Edit</button></dd>\n </dl>\n <div id=\"logto-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Logto configuration</h3>\n <label class=\"field\">Management App ID<input id=\"logto-edit-management-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Management API resource<input id=\"logto-edit-management-resource\" autocomplete=\"off\"></label>\n <label class=\"field\">New Management App secret<input id=\"logto-edit-management-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when management identity changes\"></label>\n <label class=\"field\">SPA App ID<input id=\"logto-edit-browser-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Browser API resource<input id=\"logto-edit-browser-resource\" autocomplete=\"off\" placeholder=\"Optional\"></label>\n <div class=\"row\"><button id=\"save-logto-configuration\">Save configuration</button><button id=\"cancel-logto-configuration\">Cancel</button></div>\n </div>\n <p id=\"logto-status\" class=\"muted\">Checking redirects and sign-in settings…</p>\n <div id=\"logto-drift\" class=\"notice\" hidden></div>\n <div class=\"row\">\n <button id=\"check-logto\">Check match</button>\n <button id=\"reconcile-logto\">Apply expected settings</button>\n <a id=\"logto-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Logto Console</a>\n </div>\n </div>\n </section>\n\n <h2>Providers</h2>\n <div class=\"provider-stack\">\n <section id=\"github-section\" class=\"panel setup-section\" aria-labelledby=\"github-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"github-heading\">GitHub</h3><p class=\"muted\">Repository integration and optional Logto sign-in.</p></div>\n <span id=\"github-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"github-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>App slug</dt><dd class=\"value-line\"><code id=\"github-app-slug\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"github-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Webhook delivery</dt><dd class=\"value-line\"><code id=\"github-webhook-enabled\">Not configured</code><button class=\"edit-configuration\" data-provider=\"github\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"github-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.clientSecret\" data-secret-display=\"github-client-secret-display\">Edit</button></dd>\n <dt>Private key</dt><dd class=\"secret-line\"><span id=\"github-private-key-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.privateKeyB64\" data-secret-display=\"github-private-key-display\">Edit</button></dd>\n <dt>Webhook secret</dt><dd class=\"secret-line\"><span id=\"github-webhook-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"github.webhookSecret\" data-secret-display=\"github-webhook-secret-display\">Edit</button></dd>\n <dt>Logto connector secret</dt><dd class=\"secret-line\"><span id=\"github-logto-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.githubConnectorClientSecret\" data-secret-display=\"github-logto-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"github-status\" class=\"muted\"></p>\n <div id=\"github-drift\" class=\"notice\" hidden></div>\n <div id=\"github-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit GitHub App identity</h3>\n <label class=\"field\">App ID<input id=\"github-edit-app-id\" inputmode=\"numeric\" autocomplete=\"off\"></label>\n <label class=\"field\">App slug<input id=\"github-edit-slug\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"github-edit-client-id\" autocomplete=\"off\"></label>\n <label><input id=\"github-edit-webhook-enabled\" type=\"checkbox\"> Enable Relay webhook delivery</label>\n <label class=\"field\">New client secret<input id=\"github-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when App or Client ID changes\"></label>\n <label class=\"field\">New private key (base64)<textarea id=\"github-edit-private-key\" autocomplete=\"off\" placeholder=\"Required when App ID changes\"></textarea></label>\n <label class=\"field\">New webhook secret<input id=\"github-edit-webhook-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when an active webhook App ID changes\"></label>\n <label id=\"github-edit-logto-secret-field\" class=\"field\" hidden>New Logto connector client secret<input id=\"github-edit-logto-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when this App is also used for sign-in\"></label>\n <div class=\"row\"><button id=\"save-github-configuration\">Save configuration</button><button id=\"cancel-github-configuration\">Cancel</button></div>\n </div>\n <div id=\"github-create-controls\" class=\"subsection\">\n <label class=\"field\">Owner\n <select id=\"github-owner\"><option value=\"personal\">Personal account</option><option value=\"organization\">GitHub organization</option></select>\n </label>\n <label id=\"github-org-field\" class=\"field\" hidden>Organization login<input id=\"github-org\" autocomplete=\"off\"></label>\n <label class=\"field\">App name<input id=\"github-name\" value=\"AgentConnect\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-github\">Create GitHub App</button>\n <button id=\"connect-github-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-github\" hidden>Check match</button>\n <button id=\"clear-github\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"github-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open GitHub settings</a>\n </div>\n </section>\n\n <section id=\"slack-section\" class=\"panel setup-section\" aria-labelledby=\"slack-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"slack-heading\">Slack</h3><p class=\"muted\">One App for workspace integration and Logto sign-in.</p></div>\n <span id=\"slack-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"slack-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"slack-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"slack\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"slack-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.clientSecret\" data-secret-display=\"slack-client-secret-display\">Edit</button></dd>\n <dt>Signing secret</dt><dd class=\"secret-line\"><span id=\"slack-signing-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"slack.signingSecret\" data-secret-display=\"slack-signing-secret-display\">Edit</button></dd>\n <dt>Logto sign-in</dt><dd id=\"slack-logto-status\">Not configured</dd>\n </dl>\n <p id=\"slack-status\" class=\"muted\"></p>\n <div id=\"slack-drift\" class=\"notice\" hidden></div>\n <div id=\"slack-edit-controls\" class=\"subsection\" hidden>\n <h3>Edit Slack App identity</h3>\n <label class=\"field\">App ID<input id=\"slack-edit-app-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Client ID<input id=\"slack-edit-client-id\" autocomplete=\"off\"></label>\n <label class=\"field\">New client secret<input id=\"slack-edit-client-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <label class=\"field\">New signing secret<input id=\"slack-edit-signing-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when identity changes\"></label>\n <div class=\"row\"><button id=\"save-slack-configuration\">Save configuration</button><button id=\"cancel-slack-configuration\">Cancel</button></div>\n </div>\n <div class=\"subsection\">\n <label id=\"slack-name-field\" class=\"field\">App name<input id=\"slack-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Temporary App configuration token<input id=\"slack-token\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Used only for create or check\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-slack\">Create Slack App</button>\n <button id=\"connect-slack-login\" hidden>Use for Logto sign-in</button>\n <button id=\"check-slack\" hidden>Check match</button>\n <button id=\"clear-slack\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"slack-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Slack settings</a>\n </div>\n </section>\n\n <section id=\"google-section\" class=\"panel setup-section\" aria-labelledby=\"google-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"google-heading\">Google</h3><p class=\"muted\">OAuth client used by the Logto Google connector.</p></div>\n <span id=\"google-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>Client ID</dt><dd class=\"value-line\"><code id=\"google-client-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"google\">Edit</button></dd>\n <dt>Client secret</dt><dd class=\"secret-line\"><span id=\"google-client-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"logto.googleConnectorClientSecret\" data-secret-display=\"google-client-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"google-status\" class=\"muted\"></p>\n <div id=\"google-drift\" class=\"notice\" hidden></div>\n <p>Authorized JavaScript origin:</p><ul id=\"google-origins\" class=\"uris\"></ul>\n <p>Authorized redirect URIs:</p><ul id=\"google-redirects\" class=\"uris\"></ul>\n <p class=\"muted\">Google does not expose OAuth client redirect settings through an API. Copy these required values into Google Auth Platform; Setup can verify only the Logto connector.</p>\n <div class=\"row\"><a class=\"button\" href=\"https://console.cloud.google.com/auth/clients\" target=\"_blank\" rel=\"noopener\">Open Google settings</a></div>\n <div id=\"google-config-controls\" class=\"subsection\">\n <label class=\"field\">Client ID<input id=\"google-id\" autocomplete=\"off\"></label>\n <label id=\"google-initial-secret-field\" class=\"field\">Client secret<input id=\"google-secret\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Required when Client ID changes\"></label>\n </div>\n <div class=\"row\"><button id=\"save-google\">Save Google client</button><button id=\"cancel-google-configuration\" hidden>Cancel</button><button id=\"check-google\" hidden>Check match</button><button id=\"clear-google\" class=\"danger\" hidden>Clear configuration</button></div>\n </section>\n\n <section id=\"feishu-section\" class=\"panel setup-section\" aria-labelledby=\"feishu-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"feishu-heading\">Feishu</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Feishu, OAuth callbacks, and published App setup.</p></div>\n <span id=\"feishu-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"feishu-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"feishu\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"feishu-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"feishu.loginAppSecret\" data-secret-display=\"feishu-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"feishu-login-status\" class=\"muted\"></p>\n <div id=\"feishu-drift\" class=\"notice\" hidden></div>\n <div id=\"feishu-config-controls\" class=\"subsection\">\n <label id=\"feishu-create-name-field\" class=\"field\">New App name<input id=\"feishu-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"feishu-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"feishu-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-feishu-login-app\">Create Feishu App</button>\n <button id=\"save-feishu-login-app\">Save existing App</button>\n <button id=\"cancel-feishu-configuration\" hidden>Cancel</button>\n <button id=\"check-feishu-login-app\" hidden>Check setup</button>\n <button id=\"clear-feishu\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"feishu-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Feishu settings</a>\n </div>\n </section>\n\n <section id=\"lark-section\" class=\"panel setup-section\" aria-labelledby=\"lark-heading\">\n <div class=\"provider-head\">\n <div><h3 id=\"lark-heading\">Lark</h3><p class=\"muted\">Matches the Logto OAuth 2.0 connector named Lark, OAuth callbacks, and published App setup.</p></div>\n <span id=\"lark-match\" class=\"badge\">Not configured</span>\n </div>\n <dl class=\"credentials\">\n <dt>App ID</dt><dd class=\"value-line\"><code id=\"lark-app-id\">Not configured</code><button class=\"edit-configuration\" data-provider=\"lark\">Edit</button></dd>\n <dt>App secret</dt><dd class=\"secret-line\"><span id=\"lark-app-secret-display\" class=\"redacted\">Not configured</span><button class=\"edit-secret\" data-secret-key=\"lark.loginAppSecret\" data-secret-display=\"lark-app-secret-display\">Edit</button></dd>\n </dl>\n <p id=\"lark-login-status\" class=\"muted\"></p>\n <div id=\"lark-drift\" class=\"notice\" hidden></div>\n <div id=\"lark-config-controls\" class=\"subsection\">\n <label id=\"lark-create-name-field\" class=\"field\">New App name<input id=\"lark-create-name\" value=\"AgentConnect\"></label>\n <label class=\"field\">Existing App ID<input id=\"lark-login-id\" autocomplete=\"off\"></label>\n <label class=\"field\">Existing App secret<input id=\"lark-login-secret\" type=\"password\" autocomplete=\"new-password\"></label>\n </div>\n <div class=\"row\">\n <button id=\"create-lark-login-app\">Create Lark App</button>\n <button id=\"save-lark-login-app\">Save existing App</button>\n <button id=\"cancel-lark-configuration\" hidden>Cancel</button>\n <button id=\"check-lark-login-app\" hidden>Check setup</button>\n <button id=\"clear-lark\" class=\"danger\" hidden>Clear configuration</button>\n <a id=\"lark-settings\" class=\"button\" target=\"_blank\" rel=\"noopener\" hidden>Open Lark settings</a>\n </div>\n </section>\n </div>\n\n <section id=\"options-section\" class=\"setup-section\">\n <h2>Deployment options</h2>\n <div class=\"panel\">\n <label class=\"field\"><span><input id=\"preset-agents-enabled\" type=\"checkbox\"> Enable preset Agents</span></label>\n <div class=\"row\"><button id=\"save-options\">Save options</button></div>\n </div>\n </section>\n </section>\n </div>\n </div>\n </main>\n\n <script>\n const api = '/api/v1';\n const tokenKey = 'agentconnect.setup.token';\n const verifierKey = 'agentconnect.setup.pkce';\n const stateKey = 'agentconnect.setup.state';\n let currentRevision = 0;\n let currentStatus = null;\n let bootstrapInfo = null;\n const el = (id) => document.getElementById(id);\n const message = (text, error = false) => {\n const target = el('setup').hidden ? el('access-message') : el('message');\n target.textContent = text;\n target.className = error ? 'error' : 'ok';\n };\n const bearer = () => {\n const token = sessionStorage.getItem(tokenKey);\n return token ? { authorization: 'Bearer ' + token } : {};\n };\n const json = async (response) => {\n const body = await response.json().catch(() => ({}));\n if (!response.ok) throw Object.assign(new Error(body.message || ('HTTP ' + response.status)), { status: response.status, code: body.code });\n return body;\n };\n const base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n const random = () => base64url(crypto.getRandomValues(new Uint8Array(32)));\n const configured = (byKey, key) => Boolean(byKey.get(key) && byKey.get(key).configured);\n\n function showIdentityEditors(provider, show) {\n for (const button of document.querySelectorAll('.edit-configuration[data-provider=\"' + provider + '\"]')) {\n button.hidden = !show;\n }\n }\n\n function text(id, value) {\n el(id).textContent = value === null || value === undefined || value === '' ? 'Not configured' : String(value);\n }\n\n function secretText(id, byKey, key, editable) {\n const stored = configured(byKey, key);\n const display = el(id);\n const button = document.querySelector('[data-secret-display=\"' + id + '\"]');\n const row = display.closest('.secret-line');\n const editor = row && row.querySelector('.secret-editor');\n if (editor) editor.remove();\n display.hidden = false;\n display.textContent = stored ? '***' : 'Not configured';\n display.className = stored ? 'redacted' : 'muted';\n button.hidden = !editable;\n button.textContent = stored ? 'Edit' : 'Set';\n }\n\n function match(id, state, label) {\n const target = el(id);\n target.textContent = label;\n target.className = 'badge' + (state ? ' ' + state : '');\n }\n\n async function authConfig() { return json(await fetch(api + '/auth-config')); }\n\n async function signIn() {\n const config = await authConfig();\n if (config.mode !== 'oidc') throw new Error('Save an OIDC configuration first');\n const verifier = random();\n const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));\n const state = random();\n sessionStorage.setItem(verifierKey, verifier);\n sessionStorage.setItem(stateKey, state);\n const url = new URL(config.authorizationEndpoint);\n url.searchParams.set('client_id', config.appId);\n url.searchParams.set('redirect_uri', config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', 'openid profile email roles');\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n if (config.resource) url.searchParams.set('resource', config.resource);\n location.assign(url);\n }\n\n async function finishSignIn() {\n const url = new URL(location.href);\n const code = url.searchParams.get('code');\n if (!code) return;\n const state = url.searchParams.get('state');\n if (!state || state !== sessionStorage.getItem(stateKey)) throw new Error('OIDC state mismatch');\n const verifier = sessionStorage.getItem(verifierKey);\n if (!verifier) throw new Error('PKCE verifier is missing; start sign-in again');\n const config = await authConfig();\n const body = new URLSearchParams({\n grant_type: 'authorization_code', code, client_id: config.appId,\n redirect_uri: config.redirectUri, code_verifier: verifier\n });\n if (config.resource) body.set('resource', config.resource);\n const tokens = await json(await fetch(config.tokenEndpoint, {\n method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body\n }));\n if (typeof tokens.id_token !== 'string') throw new Error('Logto did not return an ID token');\n sessionStorage.setItem(tokenKey, tokens.id_token);\n sessionStorage.removeItem(verifierKey);\n sessionStorage.removeItem(stateKey);\n history.replaceState({}, '', '/');\n }\n\n function renderUriList(id, values) {\n el(id).replaceChildren(...(values || []).map((value) => {\n const item = document.createElement('li');\n const code = document.createElement('code'); code.textContent = value;\n item.append(code); return item;\n }));\n }\n\n function updateBootstrapProvider() {\n const provider = el('bootstrap-provider').value;\n el('bootstrap-google').hidden = provider !== 'google';\n el('bootstrap-github').hidden = provider !== 'github';\n el('bootstrap-slack').hidden = provider !== 'slack';\n }\n\n function showBootstrapStep(step) {\n const logto = step === 'logto';\n el('bootstrap-logto-step').hidden = !logto;\n el('bootstrap-provider-step').hidden = logto;\n el('bootstrap-progress').textContent = logto ? 'Step 1 of 2' : 'Step 2 of 2';\n }\n\n function updateOwner(prefix) {\n el(prefix + '-org-field').hidden = el(prefix + '-owner').value !== 'organization';\n }\n\n function ownership(prefix) {\n if (el(prefix + '-owner').value === 'personal') return { owner: 'personal', organization: null };\n const organization = el(prefix + '-org').value.trim();\n if (!organization) throw new Error('Enter the GitHub organization login');\n return { owner: 'organization', organization };\n }\n\n function formatDiffValue(value) {\n if (value === null || value === undefined || value === '') return 'Not configured';\n if (Array.isArray(value)) return value.length ? value.map(formatDiffValue).join('\\n') : '[]';\n if (typeof value === 'object') {\n return Object.entries(value).map(([key, item]) => key + ': ' + (Array.isArray(item) ? item.join(', ') : formatDiffValue(item))).join('\\n');\n }\n return String(value);\n }\n\n function showDiff(id, rows, label = 'Update required') {\n const box = el(id);\n box.hidden = rows.length === 0;\n box.replaceChildren();\n if (rows.length === 0) return;\n const title = document.createElement('strong');\n title.className = 'diff-title';\n title.textContent = label;\n const grid = document.createElement('div');\n grid.className = 'config-diff';\n const head = document.createElement('div');\n head.className = 'diff-row diff-head';\n for (const text of ['Field', 'Current', 'Expected']) {\n const cell = document.createElement('span'); cell.textContent = text; head.append(cell);\n }\n grid.append(head);\n for (const item of rows) {\n const row = document.createElement('div'); row.className = 'diff-row';\n const field = document.createElement('span'); field.className = 'diff-field'; field.textContent = item.field;\n const current = document.createElement('span'); current.className = 'diff-value diff-current'; current.textContent = formatDiffValue(item.current);\n const expected = document.createElement('span'); expected.className = 'diff-value diff-expected'; expected.textContent = formatDiffValue(item.expected);\n row.append(field, current, expected); grid.append(row);\n }\n box.append(title, grid);\n }\n\n function renderStartupEnvironment() {\n const services = bootstrapInfo.services;\n const environment = [\n ['AGENTCONNECT_PUBLIC_CP_URL', services.controlPlane],\n ['AGENTCONNECT_PUBLIC_RELAY_URL', services.relay],\n ['AGENTCONNECT_PUBLIC_WEB_URL', services.web],\n ['LOGTO_ENDPOINT', bootstrapInfo.logtoEndpoint],\n ['LOGTO_MGMT_ENDPOINT', bootstrapInfo.logtoManagementEndpoint]\n ];\n el('startup-environment').textContent = environment\n .filter(([, value]) => value)\n .map(([key, value]) => key + '=' + value)\n .join('\\n');\n }\n\n function renderApps(status) {\n currentStatus = status;\n const byKey = new Map((status.secrets || []).map((item) => [item.key, item]));\n const values = status.values;\n const expected = status.providerExpectations || { github: null, slack: null, google: { origins: [], redirects: [] } };\n renderStartupEnvironment();\n\n const logto = values.logto;\n text('logto-management-endpoint', logto && bootstrapInfo.logtoManagementEndpoint);\n text('logto-management-id', logto && logto.managementAppId);\n text('logto-management-resource', logto && logto.managementResource);\n secretText('logto-management-secret-display', byKey, 'logto.managementAppSecret', Boolean(logto));\n text('logto-browser-endpoint', logto && logto.browser && bootstrapInfo.logtoEndpoint);\n text('logto-browser-id', values.auth.mode === 'oidc' ? values.auth.browserClient.appId : null);\n text('logto-browser-resource', logto && logto.browser && logto.browser.apiResource);\n el('logto-edit-controls').hidden = true;\n showIdentityEditors('logto', Boolean(logto && logto.browser && values.auth.mode === 'oidc'));\n match(\n 'logto-match',\n logto && configured(byKey, 'logto.managementAppSecret') ? '' : 'warn',\n logto && configured(byKey, 'logto.managementAppSecret') ? 'Ready to check' : 'Not configured'\n );\n\n el('preset-agents-enabled').checked = values.features.presetAgentsEnabled;\n\n const github = values.github;\n const webhookStored = byKey.get('github.webhookSecret') && byKey.get('github.webhookSecret').configured;\n const webhookInactive = github && github.webhookEnabled === false;\n text('github-app-id', github && github.appId);\n text('github-app-slug', github && github.slug);\n text('github-client-id', github && github.clientId);\n text('github-webhook-enabled', github && (github.webhookEnabled === false ? 'Disabled' : 'Enabled'));\n el('github-edit-controls').hidden = true;\n showIdentityEditors('github', Boolean(github));\n secretText('github-client-secret-display', byKey, 'github.clientSecret', Boolean(github));\n secretText('github-private-key-display', byKey, 'github.privateKeyB64', Boolean(github));\n secretText('github-webhook-secret-display', byKey, 'github.webhookSecret', Boolean(github));\n secretText('github-logto-secret-display', byKey, 'logto.githubConnectorClientSecret', Boolean(values.logto && values.logto.githubConnector));\n el('github-status').textContent = github\n ? github.slug + ' is configured. Webhook secret: ' + (webhookStored ? 'stored' : webhookInactive ? 'not required yet' : 'missing') + '.' +\n (webhookInactive ? ' Relay webhook delivery is disabled.' : '')\n : 'Creates the complete App used for repository installation, webhooks, and optional GitHub sign-in.';\n const githubDrift = github\n ? expected.github\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'Valid Web, API, and ingress URLs' }]\n : [];\n match('github-match', !github ? '' : githubDrift.length ? 'warn' : '', !github ? 'Not configured' : githubDrift.length ? 'Expected URLs changed' : 'Ready to check');\n el('github-create-controls').hidden = Boolean(github);\n el('create-github').hidden = Boolean(github);\n el('clear-github').hidden = !github;\n el('connect-github-login').hidden = !github || !values.logto || Boolean(values.logto.githubConnector);\n el('check-github').hidden = !github;\n if (github) showDiff('github-drift', githubDrift, 'Expected URLs changed since App creation');\n else el('github-drift').hidden = true;\n\n const slack = values.slack;\n text('slack-app-id', slack && slack.appId);\n text('slack-client-id', slack && slack.clientId);\n el('slack-edit-controls').hidden = true;\n showIdentityEditors('slack', Boolean(slack));\n secretText('slack-client-secret-display', byKey, 'slack.clientSecret', Boolean(slack));\n secretText('slack-signing-secret-display', byKey, 'slack.signingSecret', Boolean(slack));\n el('slack-logto-status').textContent = values.logto && values.logto.slackConnector\n ? 'Enabled; reuses the Slack client secret above.'\n : 'Not enabled.';\n el('slack-status').textContent = slack ? slack.appId + ' is configured.' : 'Creates the default AgentConnect integration manifest.';\n const slackDrift = slack\n ? expected.slack\n ? []\n : [{ field: 'Startup public URLs', current: 'Unavailable', expected: 'HTTPS Web, API, and ingress URLs' }]\n : [];\n match('slack-match', !slack ? '' : slackDrift.length ? 'warn' : '', !slack ? 'Not configured' : slackDrift.length ? 'Update required' : 'Ready to check');\n el('slack-name-field').hidden = Boolean(slack);\n el('create-slack').hidden = Boolean(slack);\n el('connect-slack-login').hidden = !slack || !values.logto || Boolean(values.logto.slackConnector) || !bootstrapInfo?.slackAvailable;\n el('clear-slack').hidden = !slack;\n el('check-slack').hidden = !slack;\n el('slack-settings').hidden = !slack;\n if (slack) {\n el('slack-settings').href = 'https://api.slack.com/apps/' + encodeURIComponent(slack.appId);\n showDiff('slack-drift', slackDrift);\n } else el('slack-drift').hidden = true;\n\n const google = values.logto && values.logto.googleConnector;\n const googleSecret = configured(byKey, 'logto.googleConnectorClientSecret');\n const expectedGoogle = expected.google;\n renderUriList('google-origins', expectedGoogle.origins);\n renderUriList('google-redirects', expectedGoogle.redirects);\n text('google-client-id', google && google.clientId);\n secretText('google-client-secret-display', byKey, 'logto.googleConnectorClientSecret', Boolean(google));\n showIdentityEditors('google', Boolean(google));\n el('google-id').value = google ? google.clientId : '';\n el('google-status').textContent = google ? 'Google OAuth client is configured.' : 'Create a Web application OAuth client manually, then save it here.';\n showDiff('google-drift', []);\n match('google-match', google && googleSecret ? 'warn' : '', !google ? 'Not configured' : !googleSecret ? 'Missing secret' : \"Can't verify automatically\");\n el('google-initial-secret-field').hidden = googleSecret;\n el('save-google').textContent = google ? 'Confirm callback settings' : 'Save Google client';\n el('google-config-controls').hidden = Boolean(google);\n el('save-google').hidden = Boolean(google);\n el('cancel-google-configuration').hidden = true;\n el('check-google').hidden = !google || !googleSecret;\n el('clear-google').hidden = !google;\n\n const feishuSecret = byKey.get('feishu.loginAppSecret');\n const larkSecret = byKey.get('lark.loginAppSecret');\n el('feishu-login-id').value = values.feishu ? values.feishu.loginAppId : '';\n el('lark-login-id').value = values.lark ? values.lark.loginAppId : '';\n text('feishu-app-id', values.feishu && values.feishu.loginAppId);\n text('lark-app-id', values.lark && values.lark.loginAppId);\n showIdentityEditors('feishu', Boolean(values.feishu));\n showIdentityEditors('lark', Boolean(values.lark));\n secretText('feishu-app-secret-display', byKey, 'feishu.loginAppSecret', Boolean(values.feishu));\n secretText('lark-app-secret-display', byKey, 'lark.loginAppSecret', Boolean(values.lark));\n el('feishu-login-status').textContent = values.feishu && feishuSecret && feishuSecret.configured\n ? values.feishu.loginAppId + ' is configured.'\n : 'No Feishu tenant App is configured.';\n el('lark-login-status').textContent = values.lark && larkSecret && larkSecret.configured\n ? values.lark.loginAppId + ' is configured.'\n : 'No Lark tenant App is configured.';\n match('feishu-match', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? '' : '', values.feishu && configured(byKey, 'feishu.loginAppSecret') ? 'Ready to check' : 'Not configured');\n match('lark-match', values.lark && configured(byKey, 'lark.loginAppSecret') ? '' : '', values.lark && configured(byKey, 'lark.loginAppSecret') ? 'Ready to check' : 'Not configured');\n showDiff('feishu-drift', []);\n showDiff('lark-drift', []);\n el('feishu-config-controls').hidden = Boolean(values.feishu);\n el('lark-config-controls').hidden = Boolean(values.lark);\n el('feishu-create-name-field').hidden = Boolean(values.feishu);\n el('lark-create-name-field').hidden = Boolean(values.lark);\n el('create-feishu-login-app').hidden = Boolean(values.feishu);\n el('create-lark-login-app').hidden = Boolean(values.lark);\n el('save-feishu-login-app').hidden = Boolean(values.feishu);\n el('save-lark-login-app').hidden = Boolean(values.lark);\n el('cancel-feishu-configuration').hidden = true;\n el('cancel-lark-configuration').hidden = true;\n el('clear-feishu').hidden = !values.feishu;\n el('clear-lark').hidden = !values.lark;\n el('check-feishu-login-app').hidden = !values.feishu || !configured(byKey, 'feishu.loginAppSecret');\n el('check-lark-login-app').hidden = !values.lark || !configured(byKey, 'lark.loginAppSecret');\n el('feishu-settings').hidden = !values.feishu;\n el('lark-settings').hidden = !values.lark;\n if (values.feishu) el('feishu-settings').href = 'https://open.feishu.cn/app/' + encodeURIComponent(values.feishu.loginAppId);\n if (values.lark) el('lark-settings').href = 'https://open.larksuite.com/app/' + encodeURIComponent(values.lark.loginAppId);\n }\n\n function requiredInput(id, label) {\n const value = el(id).value.trim();\n if (!value) throw new Error('Enter ' + label);\n return value;\n }\n\n function githubConnectorUsesDeployment(values) {\n const github = values.github;\n const connector = values.logto && values.logto.githubConnector;\n return Boolean(\n github && connector &&\n connector.appId === github.appId &&\n connector.slug === github.slug &&\n connector.clientId === github.clientId\n );\n }\n\n function beginConfigurationEdit(provider) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const values = currentStatus.values;\n if (provider === 'logto') {\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') return;\n el('logto-edit-management-id').value = logto.managementAppId;\n el('logto-edit-management-resource').value = logto.managementResource;\n el('logto-edit-management-secret').value = '';\n el('logto-edit-browser-id').value = values.auth.browserClient.appId;\n el('logto-edit-browser-resource').value = logto.browser.apiResource || '';\n el('logto-edit-controls').hidden = false;\n el('logto-edit-management-id').focus();\n } else if (provider === 'github') {\n const github = values.github;\n if (!github) return;\n el('github-edit-app-id').value = String(github.appId);\n el('github-edit-slug').value = github.slug;\n el('github-edit-client-id').value = github.clientId || '';\n el('github-edit-webhook-enabled').checked = github.webhookEnabled !== false;\n for (const id of ['github-edit-client-secret', 'github-edit-private-key', 'github-edit-webhook-secret', 'github-edit-logto-secret']) el(id).value = '';\n el('github-edit-logto-secret-field').hidden = !githubConnectorUsesDeployment(values);\n el('github-edit-controls').hidden = false;\n el('clear-github').hidden = true;\n el('github-edit-app-id').focus();\n } else if (provider === 'slack') {\n const slack = values.slack;\n if (!slack) return;\n el('slack-edit-app-id').value = slack.appId;\n el('slack-edit-client-id').value = slack.clientId;\n el('slack-edit-client-secret').value = '';\n el('slack-edit-signing-secret').value = '';\n el('slack-edit-controls').hidden = false;\n el('clear-slack').hidden = true;\n el('slack-edit-app-id').focus();\n } else if (provider === 'google') {\n const google = values.logto && values.logto.googleConnector;\n if (!google) return;\n el('google-id').value = google.clientId;\n el('google-secret').value = '';\n el('google-config-controls').hidden = false;\n el('google-initial-secret-field').hidden = false;\n el('save-google').hidden = false;\n el('save-google').textContent = 'Save Google client';\n el('cancel-google-configuration').hidden = false;\n el('clear-google').hidden = true;\n el('google-id').focus();\n } else if (provider === 'feishu' || provider === 'lark') {\n el(provider + '-config-controls').hidden = false;\n el(provider + '-create-name-field').hidden = true;\n el('create-' + provider + '-login-app').hidden = true;\n el('save-' + provider + '-login-app').hidden = false;\n el('cancel-' + provider + '-configuration').hidden = false;\n el('clear-' + provider).hidden = true;\n el(provider + '-login-secret').value = '';\n el(provider + '-login-id').focus();\n }\n showIdentityEditors(provider, false);\n }\n\n function cancelConfigurationEdit() {\n if (currentStatus) renderApps(currentStatus);\n }\n\n async function replaceConfiguration(values, secrets, successMessage) {\n const response = await json(await fetch(api + '/deployment-config', {\n method: 'PUT',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values, ...(secrets ? { secrets } : {}) })\n }));\n await load();\n message(successMessage || 'Configuration saved. Restart AgentConnect to apply it.');\n return response;\n }\n\n async function saveLogtoConfiguration() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = currentStatus.values;\n const logto = values.logto;\n if (!logto || !logto.browser || values.auth.mode !== 'oidc') throw new Error('Logto is not configured');\n const managementAppId = requiredInput('logto-edit-management-id', 'the Management App ID');\n const managementResource = requiredInput('logto-edit-management-resource', 'the Management API resource');\n const browserAppId = requiredInput('logto-edit-browser-id', 'the SPA App ID');\n const browserResource = el('logto-edit-browser-resource').value.trim() || null;\n const managementIdentityChanged = managementAppId !== logto.managementAppId;\n const managementSecret = el('logto-edit-management-secret').value;\n if (managementIdentityChanged && !managementSecret) throw new Error('Enter the new Management App secret');\n const browser = { ...logto.browser, apiResource: browserResource };\n const auth = {\n ...values.auth,\n audience: browserResource || browserAppId,\n browserClient: { appId: browserAppId, apiResource: browserResource }\n };\n await replaceConfiguration(\n {\n ...values,\n auth,\n logto: { ...logto, managementAppId, managementResource, browser }\n },\n managementSecret ? { 'logto.managementAppSecret': managementSecret } : undefined,\n 'Logto configuration saved. Sign in again if the SPA identity changed, then restart AgentConnect.'\n );\n }\n\n async function saveGithubConfiguration() {\n if (!currentStatus || !currentStatus.values.github) throw new Error('GitHub is not configured');\n const values = currentStatus.values;\n const previous = values.github;\n const appId = Number(requiredInput('github-edit-app-id', 'the GitHub App ID'));\n if (!Number.isSafeInteger(appId) || appId <= 0) throw new Error('GitHub App ID must be a positive integer');\n const slug = requiredInput('github-edit-slug', 'the GitHub App slug');\n const clientId = requiredInput('github-edit-client-id', 'the GitHub Client ID');\n const appChanged = appId !== previous.appId;\n const clientChanged = clientId !== previous.clientId;\n const webhookEnabled = el('github-edit-webhook-enabled').checked;\n const webhookEnabling = webhookEnabled && previous.webhookEnabled === false;\n const connectorReused = githubConnectorUsesDeployment(values);\n const clientSecret = el('github-edit-client-secret').value;\n const privateKey = el('github-edit-private-key').value.trim();\n const webhookSecret = el('github-edit-webhook-secret').value;\n const connectorSecret = el('github-edit-logto-secret').value;\n const webhookSecretStored = currentStatus.secrets.some((secret) => secret.key === 'github.webhookSecret' && secret.configured);\n if ((appChanged || clientChanged) && !clientSecret) throw new Error('Enter the new GitHub client secret');\n if (appChanged && !privateKey) throw new Error('Enter the new GitHub private key as base64');\n if (webhookEnabled && ((appChanged && !webhookSecret) || (webhookEnabling && !webhookSecret && !webhookSecretStored))) throw new Error('Enter the new GitHub webhook secret');\n if (connectorReused && (appChanged || clientChanged) && !connectorSecret) throw new Error('Enter the new Logto connector client secret');\n const secrets = {};\n if (clientSecret) secrets['github.clientSecret'] = clientSecret;\n if (privateKey) secrets['github.privateKeyB64'] = privateKey;\n if (webhookSecret) secrets['github.webhookSecret'] = webhookSecret;\n if (connectorSecret) secrets['logto.githubConnectorClientSecret'] = connectorSecret;\n const nextLogto = connectorReused && values.logto\n ? { ...values.logto, githubConnector: { ...values.logto.githubConnector, appId, slug, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n github: { ...previous, appId, slug, clientId, webhookEnabled },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'GitHub App identity saved. Restart AgentConnect to apply it.'\n );\n if (connectorSecret) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function saveSlackConfiguration() {\n if (!currentStatus || !currentStatus.values.slack) throw new Error('Slack is not configured');\n const values = currentStatus.values;\n const previous = values.slack;\n const appId = requiredInput('slack-edit-app-id', 'the Slack App ID');\n const clientId = requiredInput('slack-edit-client-id', 'the Slack Client ID');\n const changed = appId !== previous.appId || clientId !== previous.clientId;\n const clientSecret = el('slack-edit-client-secret').value;\n const signingSecret = el('slack-edit-signing-secret').value;\n if (changed && (!clientSecret || !signingSecret)) throw new Error('Enter both the new Slack client secret and signing secret');\n const secrets = {};\n if (clientSecret) secrets['slack.clientSecret'] = clientSecret;\n if (signingSecret) secrets['slack.signingSecret'] = signingSecret;\n const nextLogto = values.logto && values.logto.slackConnector\n ? { ...values.logto, slackConnector: { ...values.logto.slackConnector, appId, clientId } }\n : values.logto;\n await replaceConfiguration(\n {\n ...values,\n slack: { ...previous, appId, clientId },\n ...(nextLogto ? { logto: nextLogto } : {})\n },\n Object.keys(secrets).length ? secrets : undefined,\n 'Slack App identity saved. Restart AgentConnect to apply it.'\n );\n if (clientSecret && nextLogto && nextLogto.slackConnector) {\n await reconcileLogto(true);\n await load();\n }\n }\n\n async function clearProvider(provider) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const label = provider === 'github' ? 'GitHub' : provider === 'slack' ? 'Slack' : provider === 'google' ? 'Google' : provider === 'feishu' ? 'Feishu' : 'Lark';\n if (!window.confirm('Clear the saved ' + label + ' configuration and secrets?')) return;\n const values = currentStatus.values;\n let next = values;\n let secrets = {};\n if (provider === 'github') {\n const connectorReused = githubConnectorUsesDeployment(values);\n const logto = connectorReused && values.logto\n ? {\n ...values.logto,\n githubConnector: null\n }\n : values.logto;\n next = { ...values, github: null, ...(logto ? { logto } : {}) };\n secrets = {\n 'github.clientSecret': null,\n 'github.privateKeyB64': null,\n 'github.webhookSecret': null,\n ...(connectorReused ? { 'logto.githubConnectorClientSecret': null } : {})\n };\n } else if (provider === 'slack') {\n const logto = values.logto\n ? {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'slack') }\n : values.logto.browser,\n slackConnector: null\n }\n : values.logto;\n next = { ...values, slack: null, ...(logto ? { logto } : {}) };\n secrets = { 'slack.clientSecret': null, 'slack.signingSecret': null };\n } else if (provider === 'google') {\n if (!values.logto) return;\n next = {\n ...values,\n logto: {\n ...values.logto,\n browser: values.logto.browser\n ? { ...values.logto.browser, socialProviders: values.logto.browser.socialProviders.filter((item) => item !== 'google') }\n : values.logto.browser,\n googleConnector: null\n }\n };\n secrets = { 'logto.googleConnectorClientSecret': null };\n } else if (provider === 'feishu' || provider === 'lark') {\n next = { ...values, [provider]: null };\n secrets = { [provider + '.loginAppSecret']: null };\n }\n await replaceConfiguration(next, secrets, label + ' configuration cleared. Its setup controls are available again.');\n }\n\n async function startGithub(prefix) {\n const result = await json(await fetch(api + '/create/github/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n purpose: 'deployment',\n name: el(prefix + '-name').value,\n ownership: ownership(prefix),\n connectLogto: prefix === 'bootstrap-github'\n })\n }));\n const form = document.createElement('form');\n form.method = 'post'; form.action = result.action;\n const manifest = document.createElement('input');\n manifest.type = 'hidden'; manifest.name = 'manifest'; manifest.value = JSON.stringify(result.manifest);\n form.append(manifest); document.body.append(form);\n message('Opening GitHub to review the complete integration App.');\n form.submit();\n }\n\n async function loadBootstrapInfo() {\n bootstrapInfo = await json(await fetch(api + '/bootstrap-info'));\n el('open-logto').href = bootstrapInfo.logtoAdminEndpoint;\n el('logto-settings').href = bootstrapInfo.logtoAdminEndpoint;\n if (!el('logto-app-id').value && bootstrapInfo.logtoManagementAppId) {\n el('logto-app-id').value = bootstrapInfo.logtoManagementAppId;\n }\n renderUriList('bootstrap-google-origins', bootstrapInfo.google.javascriptOrigins);\n renderUriList('bootstrap-google-redirects', bootstrapInfo.google.redirectUris);\n renderUriList('bootstrap-slack-redirects', [bootstrapInfo.slackLoginRedirectUrl]);\n el('logto-management-api-resource').value = bootstrapInfo.logtoManagementResource;\n el('bootstrap-github-submit').disabled = !bootstrapInfo.githubAvailable;\n if (!bootstrapInfo.githubAvailable) {\n el('bootstrap-github-note').textContent = 'GitHub App creation needs valid saved Web, API, and ingress URLs.';\n } else if (!bootstrapInfo.githubWebhookActive) {\n el('bootstrap-github-note').textContent = 'Creates the complete GitHub App now without submitting the localhost webhook URL. Add it after saving reachable HTTPS ingress.';\n } else {\n el('bootstrap-github-note').textContent = 'This creates one complete App for both GitHub sign-in and repository integration.';\n }\n el('bootstrap-slack-option').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-submit').disabled = !bootstrapInfo.slackAvailable;\n el('bootstrap-slack-note').textContent = bootstrapInfo.slackAvailable\n ? 'Creates one complete Slack App. Sign-in uses a separate openid profile email flow from workspace installation.'\n : 'Slack sign-in needs all public service URLs to use HTTPS. Use Google locally or expose the stack through trusted HTTPS endpoints.';\n if (!bootstrapInfo.slackAvailable && el('bootstrap-provider').value === 'slack') {\n el('bootstrap-provider').value = 'google';\n updateBootstrapProvider();\n }\n return bootstrapInfo;\n }\n\n async function logtoBootstrapFinding() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n for (const id of ['logto.client_credentials', 'logto.roles_read']) {\n const finding = report.findings.find((candidate) => candidate.id === id);\n if (!finding || finding.status !== 'pass') {\n return finding || { status: 'fail', message: 'Logto Management API permissions could not be verified.' };\n }\n }\n return { status: 'pass', message: 'Logto Management API access is ready.' };\n }\n\n async function showBootstrap() {\n el('bootstrap').hidden = false;\n el('show-bootstrap').hidden = true;\n const info = bootstrapInfo || await loadBootstrapInfo();\n if (!info.logtoConfigured) {\n showBootstrapStep('logto');\n return;\n }\n const finding = await logtoBootstrapFinding();\n if (finding && finding.status === 'pass') {\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n return;\n }\n showBootstrapStep('logto');\n message(finding?.message || 'Logto Management API credentials could not be verified.', true);\n }\n\n async function bootstrapLogto() {\n await json(await fetch(api + '/bootstrap/logto', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n managementAppId: el('logto-app-id').value,\n managementAppSecret: el('logto-app-secret').value,\n managementResource: el('logto-management-api-resource').value\n })\n }));\n el('logto-app-secret').value = '';\n await loadBootstrapInfo();\n const finding = await logtoBootstrapFinding();\n if (!finding || finding.status !== 'pass') {\n throw new Error(finding?.message || 'Logto Management API credentials could not be verified.');\n }\n showBootstrapStep('provider');\n message('Logto Management API access is ready. Choose a sign-in provider.');\n }\n\n async function bootstrapGoogle() {\n const secret = el('bootstrap-google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ clientId: el('bootstrap-google-id').value, clientSecret: secret })\n }));\n el('bootstrap-google-secret').value = '';\n await reconcileLogto(true);\n await load();\n }\n\n async function bootstrapGithub() {\n if (!bootstrapInfo || !bootstrapInfo.githubAvailable) throw new Error('GitHub App creation needs valid saved Web, API, and ingress URLs.');\n await startGithub('bootstrap-github');\n }\n\n async function bootstrapSlack() {\n if (!bootstrapInfo || !bootstrapInfo.slackAvailable) throw new Error('Slack sign-in requires all public service URLs to use HTTPS.');\n await createSlack('bootstrap-slack', true);\n await reconcileLogto();\n await load();\n }\n\n async function createSlack(prefix = 'slack', connectLogto = false) {\n const result = await json(await fetch(api + '/create/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n name: el(prefix + '-name').value,\n configToken: el(prefix + '-token').value,\n connectLogto\n })\n }));\n el(prefix + '-token').value = '';\n if (!connectLogto) await load();\n message('Slack App ' + result.app.id + ' was created with the default integration manifest. Restart AgentConnect to apply it.');\n }\n\n async function checkGithub() {\n const result = await json(await fetch(api + '/check/github', { headers: bearer() }));\n el('github-settings').href = result.settingsUrl;\n el('github-settings').hidden = false;\n const label = result.missing.length ? 'Update required' : \"Can't verify automatically\";\n showDiff('github-drift', result.diff || [], label);\n match('github-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : label);\n message(result.status === 'pass' ? 'GitHub App matches the expected integration manifest.' : result.missing.length ? 'GitHub App settings need an update.' : \"GitHub can't expose callback, setup, or webhook-active settings through its API; check them in GitHub.\", result.status === 'fail');\n }\n\n async function connectGithubLogin() {\n await json(await fetch(api + '/configure/github-login', { method: 'POST', headers: bearer() }));\n await reconcileLogto();\n await load();\n message('The deployment GitHub App is now also used for Logto sign-in. Update its displayed callback URLs.');\n }\n\n async function connectSlackLogin() {\n if (!currentStatus || !currentStatus.values.slack || !currentStatus.values.logto) {\n throw new Error('Save the Slack App and Logto configuration first');\n }\n const values = currentStatus.values;\n const slack = values.slack;\n const logto = values.logto;\n await replaceConfiguration(\n {\n ...values,\n logto: {\n ...logto,\n browser: logto.browser\n ? { ...logto.browser, socialProviders: [...new Set([...logto.browser.socialProviders, 'slack'])] }\n : logto.browser,\n slackConnector: {\n appId: slack.appId,\n clientId: slack.clientId\n }\n }\n },\n undefined,\n 'Slack App is ready to connect to Logto.'\n );\n await reconcileLogto();\n await load();\n message('The deployment Slack App is now also used for Logto sign-in.');\n }\n\n async function checkSlack() {\n const token = requiredInput('slack-token', 'the temporary Slack App configuration token');\n const result = await json(await fetch(api + '/check/slack', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ configToken: token })\n }));\n el('slack-token').value = '';\n showDiff('slack-drift', result.diff || []);\n match('slack-match', result.status === 'pass' ? 'pass' : 'warn', result.status === 'pass' ? 'Matches' : 'Update required');\n message(result.status === 'pass' ? 'Slack App matches the default integration manifest.' : 'Slack App settings need an update.', result.status !== 'pass');\n }\n\n async function checkGoogle() {\n const report = await checkLogto();\n const connector = report.findings.find((finding) => finding.id === 'logto.connectors');\n const google = currentStatus && currentStatus.values.logto && currentStatus.values.logto.googleConnector;\n const connectorDiff = connector && connector.diff\n ? connector.diff.filter((item) => item.field.toLowerCase().startsWith('google'))\n : [];\n const passed = Boolean(google && connector && connectorDiff.length === 0);\n showDiff('google-drift', connectorDiff);\n match('google-match', passed ? 'pass' : 'warn', passed ? 'Logto matches' : 'Update required');\n message(\n passed\n ? \"The Logto Google connector matches. Google OAuth redirect settings can't be verified automatically.\"\n : 'The Logto Google connector needs an update.',\n !passed\n );\n }\n\n async function checkRegionalLoginApp(region) {\n const result = await json(await fetch(api + '/check/regional-login-app/' + region, { headers: bearer() }));\n const label = region === 'feishu' ? 'Feishu' : 'Lark';\n showDiff(region + '-drift', result.diff || []);\n match(region + '-match', result.status === 'pass' ? 'pass' : result.status === 'fail' ? 'fail' : 'warn', result.status === 'pass' ? 'Matches' : result.status === 'fail' ? 'Update required' : 'Could not check');\n el(region + '-login-status').textContent = result.message;\n el(region + '-login-status').className = result.status === 'pass' ? 'ok' : result.status === 'fail' ? 'warn' : 'muted';\n message(result.message || (label + ' credential check completed.'), result.status === 'fail');\n }\n\n async function saveGoogle() {\n const secret = el('google-secret').value;\n await json(await fetch(api + '/configure/google', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({\n clientId: el('google-id').value,\n ...(secret ? { clientSecret: secret } : {})\n })\n }));\n el('google-secret').value = '';\n await reconcileLogto(Boolean(secret));\n await load();\n message('Google OAuth client and Logto connector are configured. Restart AgentConnect to apply the saved settings.');\n }\n\n function regionalLoginApp(prefix) {\n const appId = el(prefix + '-login-id').value.trim();\n const appSecret = el(prefix + '-login-secret').value;\n return appId ? { appId, ...(appSecret ? { appSecret } : {}) } : null;\n }\n\n async function saveRegionalLoginApp(region) {\n const app = regionalLoginApp(region);\n if (!app) throw new Error('Enter the ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App ID');\n await json(await fetch(api + '/configure/regional-login-app', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, app })\n }));\n el(region + '-login-secret').value = '';\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' tenant App is saved. Restart AgentConnect services to apply it.');\n }\n\n async function createRegionalLoginApp(region) {\n const popup = window.open('about:blank', '_blank');\n try {\n const started = await json(await fetch(api + '/create/regional-login-app/start', {\n method: 'POST', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ region, name: el(region + '-create-name').value })\n }));\n if (popup) popup.location.replace(started.authorizationUrl);\n else window.open(started.authorizationUrl, '_blank', 'noopener');\n message('Approve the new ' + (region === 'feishu' ? 'Feishu' : 'Lark') + ' App in the opened page. This page will finish saving it automatically.');\n while (true) {\n const result = await json(await fetch(api + '/create/regional-login-app/' + encodeURIComponent(started.id), { headers: bearer() }));\n if (result.status === 'completed') {\n await load();\n message((region === 'feishu' ? 'Feishu' : 'Lark') + ' App ' + result.appId + ' was created and saved. Restart AgentConnect services to apply it.');\n return;\n }\n if (result.status === 'failed') throw new Error('Regional App creation ' + result.reason + '. Start it again.');\n await new Promise((resolve) => setTimeout(resolve, Math.max(500, Math.min(result.retryAfterMs || 2000, 5000))));\n }\n } catch (error) {\n if (popup) popup.close();\n throw error;\n }\n }\n\n async function reconcileLogto(refreshConnectorSecrets) {\n return json(await fetch(api + '/reconcile/logto', {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ refreshConnectorSecrets: Boolean(refreshConnectorSecrets) })\n }));\n }\n\n async function refreshDeploymentConfig() {\n const status = await json(await fetch(api + '/deployment-config', { headers: bearer() }));\n el('access').hidden = true;\n el('setup').hidden = false;\n el('editor').hidden = false;\n currentRevision = status.revision;\n renderApps(status);\n return status;\n }\n\n async function checkLogto() {\n const report = await json(await fetch(api + '/check/logto', { headers: bearer() }));\n await refreshDeploymentConfig();\n const failures = report.findings.filter((finding) => finding.status !== 'pass');\n el('logto-status').textContent = failures.length === 0\n ? 'SPA redirects, connectors, and social-only sign-in match.'\n : failures.map((finding) => finding.message).join(' ');\n el('logto-status').className = failures.length === 0 ? 'ok' : 'warn';\n match('logto-match', failures.length === 0 ? 'pass' : 'warn', failures.length === 0 ? 'Matches' : 'Update required');\n showDiff(\n 'logto-drift',\n failures.flatMap((finding) => finding.diff && finding.diff.length\n ? finding.diff\n : [{ field: finding.id.replace(/^logto\\./, '').replaceAll('_', ' '), current: finding.message, expected: 'Matches expected configuration' }])\n );\n el('logto-settings').hidden = failures.length === 0;\n return report;\n }\n\n function githubNotice(value) {\n if (value === 'deployment-created') return 'GitHub integration App created. Its private key and webhook secret are stored.';\n if (value === 'deployment-login-created') return 'GitHub integration App created and connected to Logto sign-in.';\n if (value === 'cancelled') return 'GitHub App creation was cancelled.';\n if (value === 'expired') return 'GitHub App creation expired. Start it again.';\n if (value === 'invalid-callback') return 'GitHub returned an invalid App creation callback.';\n if (value === 'conversion-failed') return 'GitHub may have created the App, but did not return complete credentials. Delete the orphaned App and retry.';\n if (value === 'save-failed') return 'GitHub created the App, but its credentials could not be saved. Delete the orphaned App and retry.';\n return '';\n }\n\n async function load() {\n let publicAuth = await authConfig();\n const currentUrl = new URL(location.href);\n const githubResult = currentUrl.searchParams.get('github');\n const notice = githubNotice(githubResult);\n if (githubResult === 'deployment-login-created' && publicAuth.mode === 'none') {\n await reconcileLogto();\n publicAuth = await authConfig();\n }\n if (githubResult) history.replaceState({}, '', '/');\n const hasToken = Boolean(sessionStorage.getItem(tokenKey));\n el('access').hidden = false;\n el('setup').hidden = true;\n el('login').hidden = true;\n el('open-logto').hidden = true;\n el('show-bootstrap').hidden = true;\n el('editor').hidden = true;\n el('bootstrap').hidden = true;\n if (publicAuth.logtoAdminEndpoint) {\n el('open-logto').href = publicAuth.logtoAdminEndpoint;\n el('logto-settings').href = publicAuth.logtoAdminEndpoint;\n }\n if (publicAuth.mode === 'none') {\n sessionStorage.removeItem(tokenKey);\n await loadBootstrapInfo();\n el('open-logto').hidden = false;\n el('show-bootstrap').hidden = false;\n message(notice || 'Logto sign-in is not configured. Set up the initial Logto administrator first.');\n return;\n }\n if (publicAuth.mode === 'unavailable') {\n el('open-logto').hidden = false;\n message(publicAuth.message || 'Logto sign-in is unavailable. Open Logto Console to review its settings.', true);\n return;\n }\n if (!hasToken) {\n el('login').hidden = false;\n message(notice || 'Sign in with Logto to continue.');\n return;\n }\n if (publicAuth.claimAvailable) {\n try {\n await json(await fetch(api + '/bootstrap/claim', { method: 'POST', headers: bearer() }));\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('This first user is now an ADMIN. Sign in again to refresh the role claim.');\n return;\n } catch (error) {\n if (error.status !== 401 && error.status !== 403) throw error;\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with Logto to continue.', true);\n return;\n }\n }\n try {\n await loadBootstrapInfo();\n await refreshDeploymentConfig();\n message(notice);\n checkLogto().catch((error) => {\n el('logto-status').textContent = error.message;\n el('logto-status').className = 'warn';\n el('logto-settings').hidden = false;\n });\n } catch (error) {\n if (error.status === 401 || error.status === 403) {\n sessionStorage.removeItem(tokenKey);\n el('login').hidden = false;\n message('Sign in with a Logto ADMIN account.', true);\n }\n else throw error;\n }\n }\n\n async function saveSecretReplacement(key, value) {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n if (!value) throw new Error('Enter the replacement secret');\n const refreshLogto =\n (key === 'logto.githubConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.githubConnector) ||\n (key === 'logto.googleConnectorClientSecret' && currentStatus.values.logto && currentStatus.values.logto.googleConnector) ||\n (key === 'slack.clientSecret' && currentStatus.values.logto && currentStatus.values.logto.slackConnector);\n await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values: currentStatus.values, secrets: { [key]: value } })\n }));\n try {\n if (refreshLogto) await reconcileLogto(true);\n } finally {\n await load();\n }\n if (refreshLogto) {\n message('Secret replaced and applied to the Logto connector. Restart AgentConnect to apply deployment changes.');\n } else {\n message('Secret replaced. Restart AgentConnect to apply it.');\n }\n }\n\n function editSecret(button) {\n if (!currentStatus) return message('Deployment configuration is not loaded', true);\n const row = button.closest('.secret-line');\n const display = el(button.dataset.secretDisplay);\n if (!row || !display || row.querySelector('.secret-editor')) return;\n const editor = document.createElement('span'); editor.className = 'secret-editor';\n const input = document.createElement('input');\n input.type = 'password'; input.autocomplete = 'new-password'; input.placeholder = 'Enter replacement secret';\n const save = document.createElement('button'); save.textContent = 'Save';\n const cancel = document.createElement('button'); cancel.textContent = 'Cancel';\n const close = () => { editor.remove(); display.hidden = false; button.hidden = false; };\n save.onclick = async () => {\n save.disabled = true;\n try { await saveSecretReplacement(button.dataset.secretKey, input.value); }\n catch (error) { save.disabled = false; message(error.message, true); }\n };\n cancel.onclick = close;\n input.onkeydown = (event) => { if (event.key === 'Enter') save.click(); if (event.key === 'Escape') close(); };\n display.hidden = true; button.hidden = true;\n editor.append(input, save, cancel); row.append(editor); input.focus();\n }\n\n async function saveOptions() {\n if (!currentStatus) throw new Error('Deployment configuration is not loaded');\n const values = {\n ...currentStatus.values,\n features: {\n presetAgentsEnabled: el('preset-agents-enabled').checked\n }\n };\n const saved = await json(await fetch(api + '/deployment-config', {\n method: 'PUT', headers: { 'content-type': 'application/json', ...bearer() },\n body: JSON.stringify({ expectedRevision: currentRevision, values })\n }));\n message('Saved deployment options. Restart AgentConnect to apply them.');\n await load();\n }\n\n el('login').onclick = () => signIn().catch((error) => message(error.message, true));\n el('show-bootstrap').onclick = () => showBootstrap().catch((error) => message(error.message, true));\n el('bootstrap-provider').onchange = updateBootstrapProvider;\n el('bootstrap-github-owner').onchange = () => updateOwner('bootstrap-github');\n el('github-owner').onchange = () => updateOwner('github');\n el('bootstrap-logto-submit').onclick = () => bootstrapLogto().catch((error) => message(error.message, true));\n el('bootstrap-back').onclick = () => showBootstrapStep('logto');\n el('bootstrap-google-submit').onclick = () => bootstrapGoogle().catch((error) => message(error.message, true));\n el('bootstrap-github-submit').onclick = () => bootstrapGithub().catch((error) => message(error.message, true));\n el('bootstrap-slack-submit').onclick = () => bootstrapSlack().catch((error) => message(error.message, true));\n el('create-github').onclick = () => startGithub('github').catch((error) => message(error.message, true));\n el('save-logto-configuration').onclick = () => saveLogtoConfiguration().catch((error) => message(error.message, true));\n el('cancel-logto-configuration').onclick = cancelConfigurationEdit;\n el('save-github-configuration').onclick = () => saveGithubConfiguration().catch((error) => message(error.message, true));\n el('cancel-github-configuration').onclick = cancelConfigurationEdit;\n el('clear-github').onclick = () => clearProvider('github').catch((error) => message(error.message, true));\n el('connect-github-login').onclick = () => connectGithubLogin().catch((error) => message(error.message, true));\n el('check-github').onclick = () => checkGithub().catch((error) => message(error.message, true));\n el('create-slack').onclick = () => createSlack('slack').catch((error) => message(error.message, true));\n el('connect-slack-login').onclick = () => connectSlackLogin().catch((error) => message(error.message, true));\n el('save-slack-configuration').onclick = () => saveSlackConfiguration().catch((error) => message(error.message, true));\n el('cancel-slack-configuration').onclick = cancelConfigurationEdit;\n el('clear-slack').onclick = () => clearProvider('slack').catch((error) => message(error.message, true));\n el('check-slack').onclick = () => checkSlack().catch((error) => message(error.message, true));\n el('save-google').onclick = () => saveGoogle().catch((error) => message(error.message, true));\n el('cancel-google-configuration').onclick = cancelConfigurationEdit;\n el('clear-google').onclick = () => clearProvider('google').catch((error) => message(error.message, true));\n el('check-google').onclick = () => checkGoogle().catch((error) => message(error.message, true));\n el('create-feishu-login-app').onclick = () => createRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('save-feishu-login-app').onclick = () => saveRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('cancel-feishu-configuration').onclick = cancelConfigurationEdit;\n el('clear-feishu').onclick = () => clearProvider('feishu').catch((error) => message(error.message, true));\n el('check-feishu-login-app').onclick = () => checkRegionalLoginApp('feishu').catch((error) => message(error.message, true));\n el('create-lark-login-app').onclick = () => createRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('save-lark-login-app').onclick = () => saveRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('cancel-lark-configuration').onclick = cancelConfigurationEdit;\n el('clear-lark').onclick = () => clearProvider('lark').catch((error) => message(error.message, true));\n el('check-lark-login-app').onclick = () => checkRegionalLoginApp('lark').catch((error) => message(error.message, true));\n el('check-logto').onclick = () => checkLogto().catch((error) => message(error.message, true));\n el('reconcile-logto').onclick = () => reconcileLogto().then(load).catch((error) => message(error.message, true));\n el('logout').onclick = () => { sessionStorage.removeItem(tokenKey); load().catch((error) => message(error.message, true)); };\n el('save-options').onclick = () => saveOptions().catch((error) => message(error.message, true));\n for (const button of document.querySelectorAll('.edit-secret')) button.onclick = () => editSecret(button);\n for (const button of document.querySelectorAll('.edit-configuration')) button.onclick = () => beginConfigurationEdit(button.dataset.provider);\n updateBootstrapProvider();\n finishSignIn().then(load).catch((error) => message(error.message, true));\n <\/script>\n</body>\n</html>"])));
|
|
62206
62206
|
//#endregion
|
|
62207
|
-
//#region src/
|
|
62207
|
+
//#region src/server/logto-management.ts
|
|
62208
62208
|
/** Minimal Logto Management API client for setup reconciliation and ADMIN claim. */
|
|
62209
62209
|
const MANAGED_APP_TAG = "agentconnectSetup";
|
|
62210
62210
|
const MANAGED_APP_TAG_VALUE = {
|
|
@@ -62758,7 +62758,7 @@ var LogtoAdminClaimClient = class {
|
|
|
62758
62758
|
}
|
|
62759
62759
|
};
|
|
62760
62760
|
//#endregion
|
|
62761
|
-
//#region src/
|
|
62761
|
+
//#region src/server/index.ts
|
|
62762
62762
|
const PutDeploymentConfigBody = strictObject({
|
|
62763
62763
|
expectedRevision: number$1().int().nonnegative(),
|
|
62764
62764
|
values: DeploymentConfigValuesV1Schema,
|
|
@@ -62915,7 +62915,7 @@ function slackProviderAppConfig(services) {
|
|
|
62915
62915
|
requireProviderAppEndpoints(config);
|
|
62916
62916
|
return config;
|
|
62917
62917
|
}
|
|
62918
|
-
function logtoSetup(record,
|
|
62918
|
+
function logtoSetup(record, setupUrl, services, endpoint) {
|
|
62919
62919
|
const { auth, logto } = record.values;
|
|
62920
62920
|
if (!logto) return null;
|
|
62921
62921
|
const browser = logto.browser ?? (auth.mode === "oidc" ? {
|
|
@@ -62930,7 +62930,7 @@ function logtoSetup(record, tenantAdminUrl, services, endpoint) {
|
|
|
62930
62930
|
const slackSecret = record.secrets["slack.clientSecret"];
|
|
62931
62931
|
const applicationId = auth.mode === "oidc" ? auth.browserClient.appId : void 0;
|
|
62932
62932
|
const webOrigin = new URL(services.web).origin;
|
|
62933
|
-
const
|
|
62933
|
+
const setupOrigin = new URL(setupUrl).origin;
|
|
62934
62934
|
return {
|
|
62935
62935
|
endpoint: resolvedEndpoint,
|
|
62936
62936
|
apiResource: browser.apiResource,
|
|
@@ -62938,7 +62938,7 @@ function logtoSetup(record, tenantAdminUrl, services, endpoint) {
|
|
|
62938
62938
|
desired: {
|
|
62939
62939
|
...applicationId ? { applicationId } : {},
|
|
62940
62940
|
applicationName: browser.applicationName,
|
|
62941
|
-
redirectUris: [appendPath(webOrigin, "/auth/callback"), appendPath(
|
|
62941
|
+
redirectUris: [appendPath(webOrigin, "/auth/callback"), appendPath(setupOrigin, "/auth/callback")],
|
|
62942
62942
|
postLogoutRedirectUris: [appendPath(webOrigin, "/login")],
|
|
62943
62943
|
socialProviders: [...browser.socialProviders],
|
|
62944
62944
|
...logto.githubConnector && githubSecret ? { github: {
|
|
@@ -62958,7 +62958,7 @@ function logtoSetup(record, tenantAdminUrl, services, endpoint) {
|
|
|
62958
62958
|
};
|
|
62959
62959
|
}
|
|
62960
62960
|
/** Build the thin UI/API over the shared typed deployment-config service. */
|
|
62961
|
-
function
|
|
62961
|
+
function buildSetupServer(deps, options = {}) {
|
|
62962
62962
|
const app = (0, import_fastify.default)({
|
|
62963
62963
|
logger: false,
|
|
62964
62964
|
...options
|
|
@@ -62967,7 +62967,7 @@ function buildTenantAdminServer(deps, options = {}) {
|
|
|
62967
62967
|
const auditFeishuAppSetup = deps.auditFeishuAppSetup ?? createFeishuAppSetupAuditor(fetchImpl);
|
|
62968
62968
|
const slackConfigApi = deps.slackConfigApi ?? createSlackConfigApi({ fetch: fetchImpl });
|
|
62969
62969
|
const requireLocal = localOnly(deps.publicUrl, deps.allowContainerLoopbackProxy);
|
|
62970
|
-
const authenticator = new
|
|
62970
|
+
const authenticator = new SetupAuthenticator({ get: async () => oidcOf(await deps.store.getAdmin(), localAuthBootstrap.issuer, logtoManagementEndpoint) }, deps.verifyOidcToken);
|
|
62971
62971
|
const discovery = /* @__PURE__ */ new Map();
|
|
62972
62972
|
let mutationTail = Promise.resolve();
|
|
62973
62973
|
const defaultEnvironment = loadDeploymentEnvironment({});
|
|
@@ -63084,9 +63084,9 @@ function buildTenantAdminServer(deps, options = {}) {
|
|
|
63084
63084
|
const claimedFor = current ? deploymentAdminClaimKey(current.values, localAuthBootstrap.issuer) : null;
|
|
63085
63085
|
if (!current || current.values.auth.mode === "none" || !claimedFor || current.adminClaimedFor !== claimedFor) return requireLocal(request, reply);
|
|
63086
63086
|
try {
|
|
63087
|
-
request.
|
|
63087
|
+
request.setupPrincipal = await authenticator.authenticate(request.headers.authorization);
|
|
63088
63088
|
} catch (error) {
|
|
63089
|
-
if (!(error instanceof
|
|
63089
|
+
if (!(error instanceof SetupAuthError)) throw error;
|
|
63090
63090
|
authFailure(reply, error);
|
|
63091
63091
|
}
|
|
63092
63092
|
};
|
|
@@ -63099,12 +63099,12 @@ function buildTenantAdminServer(deps, options = {}) {
|
|
|
63099
63099
|
if (error instanceof DeploymentConfigSecretRefreshRequiredError) return problem(reply, 400, error.message, error.code);
|
|
63100
63100
|
if (error instanceof DeploymentConfigConflictError) return problem(reply, 409, error.message, error.code);
|
|
63101
63101
|
if (error instanceof LogtoManagementError) return problem(reply, error.code === "LOGTO_UNAVAILABLE" ? 502 : error.code === "SOCIAL_CONNECTOR_UNSUPPORTED" ? 400 : 409, error.message, error.code);
|
|
63102
|
-
request.log.error({ err: error }, "
|
|
63102
|
+
request.log.error({ err: error }, "setup-server request failed");
|
|
63103
63103
|
return problem(reply, 500, "internal server error");
|
|
63104
63104
|
});
|
|
63105
63105
|
app.get("/livez", async () => ({ status: "ok" }));
|
|
63106
|
-
app.get("/", async (_request, reply) => reply.type("text/html; charset=utf-8").send(
|
|
63107
|
-
app.get("/auth/callback", async (_request, reply) => reply.type("text/html; charset=utf-8").send(
|
|
63106
|
+
app.get("/", async (_request, reply) => reply.type("text/html; charset=utf-8").send(SETUP_HTML));
|
|
63107
|
+
app.get("/auth/callback", async (_request, reply) => reply.type("text/html; charset=utf-8").send(SETUP_HTML));
|
|
63108
63108
|
app.get("/api/v1/auth-config", async (_request, reply) => {
|
|
63109
63109
|
const current = await deps.store.getAdmin();
|
|
63110
63110
|
const auth = current?.values.auth;
|
|
@@ -63828,7 +63828,7 @@ function buildTenantAdminServer(deps, options = {}) {
|
|
|
63828
63828
|
} : {
|
|
63829
63829
|
id: "logto.admin_role",
|
|
63830
63830
|
status: "fail",
|
|
63831
|
-
message: adminRole.exists ? "A role named ADMIN exists, but it is not a non-default User role." : "The exact global User role ADMIN does not exist; the first
|
|
63831
|
+
message: adminRole.exists ? "A role named ADMIN exists, but it is not a non-default User role." : "The exact global User role ADMIN does not exist; the first administrator sign-in through Setup will create it.",
|
|
63832
63832
|
diff: [{
|
|
63833
63833
|
field: "ADMIN role",
|
|
63834
63834
|
current: adminRole.exists ? {
|
|
@@ -63917,7 +63917,7 @@ function buildTenantAdminServer(deps, options = {}) {
|
|
|
63917
63917
|
try {
|
|
63918
63918
|
principal = await authenticator.authenticate(request.headers.authorization, false);
|
|
63919
63919
|
} catch (error) {
|
|
63920
|
-
if (!(error instanceof
|
|
63920
|
+
if (!(error instanceof SetupAuthError)) throw error;
|
|
63921
63921
|
return authFailure(reply, error);
|
|
63922
63922
|
}
|
|
63923
63923
|
const logto = runtime.values.logto;
|
|
@@ -63940,14 +63940,14 @@ function buildTenantAdminServer(deps, options = {}) {
|
|
|
63940
63940
|
}));
|
|
63941
63941
|
return app;
|
|
63942
63942
|
}
|
|
63943
|
-
/** Run the
|
|
63944
|
-
async function
|
|
63945
|
-
const config =
|
|
63943
|
+
/** Run the Setup Server. */
|
|
63944
|
+
async function serveSetupServer(env = process.env) {
|
|
63945
|
+
const config = loadSetupServerProcessConfig(env);
|
|
63946
63946
|
const host = config.HOST;
|
|
63947
63947
|
const port = config.PORT;
|
|
63948
|
-
const publicUrl = env.
|
|
63948
|
+
const publicUrl = env.SETUP_SERVER_URL ?? `http://localhost:${port}`;
|
|
63949
63949
|
const deploymentEnvironment = loadDeploymentEnvironment(env);
|
|
63950
|
-
if (!isLoopbackHostname(host) && !config.
|
|
63950
|
+
if (!isLoopbackHostname(host) && !config.SETUP_SERVER_ALLOW_CONTAINER_PROXY) throw new Error("Setup Server may bind outside loopback only behind an isolated local port forward");
|
|
63951
63951
|
const handle = openDeploymentConfigStore({
|
|
63952
63952
|
databaseUrl: config.DATABASE_URL,
|
|
63953
63953
|
SECRET_CIPHER: config.SECRET_CIPHER,
|
|
@@ -63960,10 +63960,10 @@ async function serveTenantAdmin(env = process.env) {
|
|
|
63960
63960
|
...config.VAULT_TOKEN ? { VAULT_TOKEN: config.VAULT_TOKEN } : {},
|
|
63961
63961
|
...config.VAULT_JWT_ROLE ? { VAULT_JWT_ROLE: config.VAULT_JWT_ROLE } : {}
|
|
63962
63962
|
});
|
|
63963
|
-
const app =
|
|
63963
|
+
const app = buildSetupServer({
|
|
63964
63964
|
store: handle.store,
|
|
63965
63965
|
publicUrl,
|
|
63966
|
-
allowContainerLoopbackProxy: config.
|
|
63966
|
+
allowContainerLoopbackProxy: config.SETUP_SERVER_ALLOW_CONTAINER_PROXY,
|
|
63967
63967
|
localAuthBootstrap: {
|
|
63968
63968
|
issuer: deploymentEnvironment.issuer,
|
|
63969
63969
|
managementEndpoint: deploymentEnvironment.managementEndpoint,
|
|
@@ -63976,7 +63976,7 @@ async function serveTenantAdmin(env = process.env) {
|
|
|
63976
63976
|
host,
|
|
63977
63977
|
port
|
|
63978
63978
|
});
|
|
63979
|
-
app.log.info(`
|
|
63979
|
+
app.log.info(`Setup Server listening at ${publicUrl}`);
|
|
63980
63980
|
await new Promise((resolve) => {
|
|
63981
63981
|
const stop = () => resolve();
|
|
63982
63982
|
process.once("SIGINT", stop);
|
|
@@ -63995,9 +63995,9 @@ function loadDefaultEnvironment() {
|
|
|
63995
63995
|
if (path) loadEnvFile(path);
|
|
63996
63996
|
}
|
|
63997
63997
|
loadDefaultEnvironment();
|
|
63998
|
-
|
|
63998
|
+
serveSetupServer().catch((error) => {
|
|
63999
63999
|
const message = error instanceof Error ? error.message : String(error);
|
|
64000
|
-
console.error(`
|
|
64000
|
+
console.error(`setup-server: ${message}`);
|
|
64001
64001
|
process.exitCode = 1;
|
|
64002
64002
|
});
|
|
64003
64003
|
//#endregion
|