@frontmcp/auth 1.10.15 → 1.11.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/authorities/authorities.context.mjs +115 -0
- package/browser/authorities/authorities.engine.mjs +272 -0
- package/browser/authorities/authorities.errors.mjs +59 -0
- package/browser/authorities/authorities.evaluator.mjs +225 -0
- package/browser/authorities/authorities.metadata-augment.mjs +18 -0
- package/browser/authorities/authorities.registry.mjs +112 -0
- package/browser/authorities/authorities.schema.mjs +92 -0
- package/browser/authorities/authorities.scope-mapping.mjs +30 -0
- package/browser/authorities/authorities.validation.mjs +208 -0
- package/browser/authorities/index.mjs +43 -0
- package/browser/authorization/authorization.class.mjs +252 -0
- package/browser/authorization/authorization.types.mjs +64 -0
- package/browser/authorization/dcr-client.registry.mjs +222 -0
- package/browser/authorization/index.mjs +41 -0
- package/browser/authorization/orchestrated.accessor.mjs +86 -0
- package/browser/authorization/orchestrated.authorization.mjs +289 -0
- package/browser/authorization/public.authorization.mjs +124 -0
- package/browser/authorization/transparent.authorization.mjs +130 -0
- package/browser/cimd/cimd-redis.cache.mjs +231 -0
- package/browser/cimd/cimd.cache.mjs +215 -0
- package/browser/cimd/cimd.errors.mjs +142 -0
- package/browser/cimd/cimd.logger.mjs +15 -0
- package/browser/cimd/cimd.service.mjs +376 -0
- package/browser/cimd/cimd.types.mjs +228 -0
- package/browser/cimd/cimd.validator.mjs +217 -0
- package/browser/cimd/index.mjs +63 -0
- package/browser/common/auth-logger.interface.mjs +14 -0
- package/browser/common/index.mjs +6 -0
- package/browser/common/jwt.types.mjs +38 -0
- package/browser/common/session-user.types.mjs +0 -0
- package/browser/common/session.types.mjs +58 -0
- package/browser/common/zod-utils.mjs +0 -0
- package/browser/consent/consent.types.mjs +94 -0
- package/browser/consent/index.mjs +17 -0
- package/browser/context/frontmcp-auth-context.factory.mjs +29 -0
- package/browser/context/frontmcp-auth-context.impl.mjs +112 -0
- package/browser/context/index.mjs +7 -0
- package/browser/detection/auth-provider-detection.mjs +176 -0
- package/browser/detection/index.mjs +19 -0
- package/browser/errors/auth-internal.error.mjs +42 -0
- package/browser/errors/auth-internal.errors.mjs +150 -0
- package/browser/errors/index.mjs +53 -0
- package/browser/fetch/fetch-credential-middleware.mjs +91 -0
- package/browser/fetch/index.mjs +19 -0
- package/browser/index.mjs +573 -0
- package/browser/jwks/index.mjs +9 -0
- package/browser/jwks/jose.loader.mjs +28 -0
- package/browser/jwks/jwks.service.mjs +521 -0
- package/browser/jwks/jwks.utils.mjs +33 -0
- package/browser/options/app-auth.schema.mjs +18 -0
- package/browser/options/index.mjs +89 -0
- package/browser/options/orchestrated.schema.mjs +128 -0
- package/browser/options/public.schema.mjs +39 -0
- package/browser/options/schema.mjs +16 -0
- package/browser/options/shared.schemas.mjs +506 -0
- package/browser/options/static.schema.mjs +41 -0
- package/browser/options/transparent.schema.mjs +43 -0
- package/browser/options/utils.mjs +47 -0
- package/browser/people/index.mjs +15 -0
- package/browser/people/loopback-token.auth.mjs +115 -0
- package/browser/people/people-auth.options.mjs +37 -0
- package/browser/session/authorization-vault.mjs +279 -0
- package/browser/session/authorization.store.mjs +336 -0
- package/browser/session/consent.store.mjs +34 -0
- package/browser/session/credential-resume-link.mjs +43 -0
- package/browser/session/credentials-accessor.impl.mjs +54 -0
- package/browser/session/credentials-accessor.mjs +5 -0
- package/browser/session/encrypted-authorization-vault.mjs +405 -0
- package/browser/session/federated-auth.session.mjs +176 -0
- package/browser/session/incremental-auth-ticket.mjs +42 -0
- package/browser/session/index.mjs +266 -0
- package/browser/session/orchestrated-token.crypto.mjs +47 -0
- package/browser/session/orchestrated-token.store.mjs +286 -0
- package/browser/session/pending-login.mjs +37 -0
- package/browser/session/redis-session.store.mjs +255 -0
- package/browser/session/secure-store-accessor.impl.mjs +78 -0
- package/browser/session/secure-store-accessor.mjs +5 -0
- package/browser/session/secure-store-backends.mjs +111 -0
- package/browser/session/secure-store.factory.mjs +77 -0
- package/browser/session/session-credential-vault.factory.mjs +18 -0
- package/browser/session/session-credential-vault.mjs +243 -0
- package/browser/session/session-crypto.mjs +39 -0
- package/browser/session/session-rate-limiter.mjs +126 -0
- package/browser/session/session.transport.mjs +16 -0
- package/browser/session/signin-binding.mjs +30 -0
- package/browser/session/storage/in-memory-authorization-vault.mjs +28 -0
- package/browser/session/storage/index.mjs +14 -0
- package/browser/session/storage/storage-authorization-vault.mjs +301 -0
- package/browser/session/storage/storage-token-store.mjs +83 -0
- package/browser/session/storage-authorization.store.mjs +134 -0
- package/browser/session/storage-consent.store.mjs +33 -0
- package/browser/session/storage-federated-auth.session.mjs +55 -0
- package/browser/session/storage-orchestrated-token.store.mjs +197 -0
- package/browser/session/token-storage.factory.mjs +58 -0
- package/browser/session/token.refresh.mjs +36 -0
- package/browser/session/token.vault.mjs +61 -0
- package/browser/session/transport-session.types.mjs +108 -0
- package/browser/session/utils/auth-token.utils.mjs +58 -0
- package/browser/session/utils/index.mjs +23 -0
- package/browser/session/utils/session-crypto.utils.mjs +49 -0
- package/browser/session/utils/tiny-ttl-cache.mjs +46 -0
- package/browser/session/vault-encryption.mjs +187 -0
- package/browser/session/vercel-kv-session.store.mjs +230 -0
- package/browser/shared-server/index.mjs +17 -0
- package/browser/shared-server/shared-server-identity.mjs +55 -0
- package/browser/ui/base-layout.mjs +184 -0
- package/browser/ui/index.mjs +51 -0
- package/browser/ui/local-login.helper.mjs +46 -0
- package/browser/ui/templates.mjs +482 -0
- package/browser/utils/audience.validator.mjs +110 -0
- package/browser/utils/authorization-id.utils.mjs +10 -0
- package/browser/utils/index.mjs +45 -0
- package/browser/utils/redirect-uri.utils.mjs +22 -0
- package/browser/utils/scope-grant.utils.mjs +36 -0
- package/browser/utils/token-scopes.utils.mjs +12 -0
- package/browser/utils/www-authenticate.utils.mjs +164 -0
- package/browser/vault/auth-providers.accessor.impl.mjs +236 -0
- package/browser/vault/auth-providers.accessor.mjs +5 -0
- package/browser/vault/auth-providers.registry.mjs +140 -0
- package/browser/vault/auth-providers.types.mjs +47 -0
- package/browser/vault/auth-providers.vault.mjs +206 -0
- package/browser/vault/credential-cache.mjs +187 -0
- package/browser/vault/credential-helpers.mjs +15 -0
- package/browser/vault/credential-loaders/eager-loader.mjs +91 -0
- package/browser/vault/credential-loaders/lazy-loader.mjs +120 -0
- package/browser/vault/index.mjs +35 -0
- package/cimd/cimd.types.d.ts +1 -1
- package/cimd/cimd.types.d.ts.map +1 -1
- package/common/jwt.types.d.ts +1 -1
- package/common/jwt.types.d.ts.map +1 -1
- package/common/session.types.d.ts +9 -1
- package/common/session.types.d.ts.map +1 -1
- package/consent/consent.types.d.ts +1 -1
- package/consent/consent.types.d.ts.map +1 -1
- package/detection/auth-provider-detection.d.ts +1 -1
- package/detection/auth-provider-detection.d.ts.map +1 -1
- package/errors/auth-internal.errors.d.ts +6 -0
- package/errors/auth-internal.errors.d.ts.map +1 -1
- package/errors/index.d.ts +1 -1
- package/errors/index.d.ts.map +1 -1
- package/esm/index.mjs +2 -2
- package/esm/package.json +21 -5
- package/index.d.ts +5 -1
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/options/app-auth.schema.d.ts +19 -1
- package/options/app-auth.schema.d.ts.map +1 -1
- package/options/orchestrated.schema.d.ts +37 -1
- package/options/orchestrated.schema.d.ts.map +1 -1
- package/options/public.schema.d.ts +1 -1
- package/options/public.schema.d.ts.map +1 -1
- package/options/schema.d.ts +19 -1
- package/options/schema.d.ts.map +1 -1
- package/options/shared.schemas.d.ts +10 -14
- package/options/shared.schemas.d.ts.map +1 -1
- package/options/static.schema.d.ts +1 -1
- package/options/static.schema.d.ts.map +1 -1
- package/options/transparent.schema.d.ts +1 -1
- package/options/transparent.schema.d.ts.map +1 -1
- package/package.json +21 -5
- package/people/index.d.ts +3 -0
- package/people/index.d.ts.map +1 -0
- package/people/loopback-token.auth.d.ts +75 -0
- package/people/loopback-token.auth.d.ts.map +1 -0
- package/people/people-auth.options.d.ts +36 -0
- package/people/people-auth.options.d.ts.map +1 -0
- package/session/authorization-vault.d.ts +1 -1
- package/session/authorization-vault.d.ts.map +1 -1
- package/session/encrypted-authorization-vault.d.ts +1 -1
- package/session/encrypted-authorization-vault.d.ts.map +1 -1
- package/session/secure-store.factory.d.ts.map +1 -1
- package/shared-server/index.d.ts +3 -0
- package/shared-server/index.d.ts.map +1 -0
- package/shared-server/shared-server-identity.d.ts +26 -0
- package/shared-server/shared-server-identity.d.ts.map +1 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// libs/auth/src/ui/index.ts
|
|
2
|
+
import {
|
|
3
|
+
CDN,
|
|
4
|
+
DEFAULT_THEME,
|
|
5
|
+
baseLayout,
|
|
6
|
+
createLayout,
|
|
7
|
+
authLayout,
|
|
8
|
+
centeredCardLayout,
|
|
9
|
+
wideLayout,
|
|
10
|
+
extraWideLayout,
|
|
11
|
+
escapeHtml,
|
|
12
|
+
BUILT_IN_AUTH_PAGE_CSP,
|
|
13
|
+
builtInAuthPageHeaders
|
|
14
|
+
} from "./base-layout.mjs";
|
|
15
|
+
import {
|
|
16
|
+
buildConsentPage,
|
|
17
|
+
buildIncrementalAuthPage,
|
|
18
|
+
buildFederatedLoginPage,
|
|
19
|
+
buildToolConsentPage,
|
|
20
|
+
buildLoginPage,
|
|
21
|
+
buildConnectPage,
|
|
22
|
+
buildConnectSuccessPage,
|
|
23
|
+
buildErrorPage,
|
|
24
|
+
renderToHtml
|
|
25
|
+
} from "./templates.mjs";
|
|
26
|
+
import { renderLocalLoginPage, toLoginExtraFields, withoutSecretFieldValues } from "./local-login.helper.mjs";
|
|
27
|
+
export {
|
|
28
|
+
BUILT_IN_AUTH_PAGE_CSP,
|
|
29
|
+
CDN,
|
|
30
|
+
DEFAULT_THEME,
|
|
31
|
+
authLayout,
|
|
32
|
+
baseLayout,
|
|
33
|
+
buildConnectPage,
|
|
34
|
+
buildConnectSuccessPage,
|
|
35
|
+
buildConsentPage,
|
|
36
|
+
buildErrorPage,
|
|
37
|
+
buildFederatedLoginPage,
|
|
38
|
+
buildIncrementalAuthPage,
|
|
39
|
+
buildLoginPage,
|
|
40
|
+
buildToolConsentPage,
|
|
41
|
+
builtInAuthPageHeaders,
|
|
42
|
+
centeredCardLayout,
|
|
43
|
+
createLayout,
|
|
44
|
+
escapeHtml,
|
|
45
|
+
extraWideLayout,
|
|
46
|
+
renderLocalLoginPage,
|
|
47
|
+
renderToHtml,
|
|
48
|
+
toLoginExtraFields,
|
|
49
|
+
wideLayout,
|
|
50
|
+
withoutSecretFieldValues
|
|
51
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// libs/auth/src/ui/local-login.helper.ts
|
|
2
|
+
import { buildLoginPage } from "./templates.mjs";
|
|
3
|
+
function toLoginExtraFields(fields, values) {
|
|
4
|
+
if (!fields) return [];
|
|
5
|
+
return Object.entries(fields).map(([name, def]) => ({
|
|
6
|
+
name,
|
|
7
|
+
type: def.type,
|
|
8
|
+
label: def.label,
|
|
9
|
+
required: def.required,
|
|
10
|
+
placeholder: def.placeholder,
|
|
11
|
+
options: def.options,
|
|
12
|
+
value: values?.[name]
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
function withoutSecretFieldValues(values, fields) {
|
|
16
|
+
if (!values) return void 0;
|
|
17
|
+
const kept = {};
|
|
18
|
+
for (const [name, value] of Object.entries(values)) {
|
|
19
|
+
if (fields?.[name]?.type === "password") continue;
|
|
20
|
+
kept[name] = value;
|
|
21
|
+
}
|
|
22
|
+
return kept;
|
|
23
|
+
}
|
|
24
|
+
function renderLocalLoginPage(login, ctx, submitted) {
|
|
25
|
+
const values = withoutSecretFieldValues(submitted, login?.fields);
|
|
26
|
+
if (login?.render) {
|
|
27
|
+
return login.render({ ...ctx, values });
|
|
28
|
+
}
|
|
29
|
+
const extraFields = login?.fields ? toLoginExtraFields(login.fields, values) : void 0;
|
|
30
|
+
return buildLoginPage({
|
|
31
|
+
clientName: ctx.clientName,
|
|
32
|
+
scope: ctx.scopes.join(" "),
|
|
33
|
+
pendingAuthId: ctx.pendingAuthId,
|
|
34
|
+
callbackPath: ctx.callbackPath,
|
|
35
|
+
title: login?.title,
|
|
36
|
+
subtitle: login?.subtitle,
|
|
37
|
+
logoUri: login?.logoUri ?? ctx.logoUri,
|
|
38
|
+
error: ctx.error,
|
|
39
|
+
extraFields
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
renderLocalLoginPage,
|
|
44
|
+
toLoginExtraFields,
|
|
45
|
+
withoutSecretFieldValues
|
|
46
|
+
};
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
// libs/auth/src/ui/templates.ts
|
|
2
|
+
import { escapeHtml as baseEscapeHtml, centeredCardLayout, extraWideLayout, wideLayout } from "./base-layout.mjs";
|
|
3
|
+
var escapeHtml = baseEscapeHtml;
|
|
4
|
+
function buildConsentPage(params) {
|
|
5
|
+
const { apps, clientName, pendingAuthId, csrfToken, callbackPath } = params;
|
|
6
|
+
const appCards = apps.map((app) => buildAppCardHtml(app, pendingAuthId, csrfToken, callbackPath)).join("\n");
|
|
7
|
+
const content = `
|
|
8
|
+
<h1 class="text-3xl font-bold text-gray-900 mb-4">Authorize ${escapeHtml(clientName)}</h1>
|
|
9
|
+
<p class="text-gray-600 mb-8">
|
|
10
|
+
Select which apps you want to authorize. You can skip apps and authorize them later when needed.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<div class="space-y-4" id="app-list">
|
|
14
|
+
${appCards}
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800">
|
|
18
|
+
Skipped apps can be authorized later when you try to use their tools (progressive authorization).
|
|
19
|
+
</div>`;
|
|
20
|
+
return wideLayout(content, { title: `Authorize ${clientName}` });
|
|
21
|
+
}
|
|
22
|
+
function buildAppCardHtml(app, pendingAuthId, csrfToken, callbackPath) {
|
|
23
|
+
const icon = app.iconUrl ? `<img src="${escapeHtml(app.iconUrl)}" alt="${escapeHtml(
|
|
24
|
+
app.appName
|
|
25
|
+
)}" class="w-12 h-12 rounded-lg object-cover">` : `<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg">${escapeHtml(
|
|
26
|
+
app.appName.charAt(0).toUpperCase()
|
|
27
|
+
)}</div>`;
|
|
28
|
+
const description = app.description ? `<p class="text-sm text-gray-500">${escapeHtml(app.description)}</p>` : "";
|
|
29
|
+
const scopes = app.requiredScopes?.length ? `<div class="mb-4">
|
|
30
|
+
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2">Permissions</p>
|
|
31
|
+
<div class="flex flex-wrap gap-2">
|
|
32
|
+
${app.requiredScopes.map(
|
|
33
|
+
(scope) => `<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">${escapeHtml(
|
|
34
|
+
scope
|
|
35
|
+
)}</span>`
|
|
36
|
+
).join("")}
|
|
37
|
+
</div>
|
|
38
|
+
</div>` : "";
|
|
39
|
+
return `<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow" data-app-id="${escapeHtml(
|
|
40
|
+
app.appId
|
|
41
|
+
)}">
|
|
42
|
+
<div class="flex items-center gap-4 mb-4">
|
|
43
|
+
${icon}
|
|
44
|
+
<div class="flex-1">
|
|
45
|
+
<h3 class="font-semibold text-gray-900">${escapeHtml(app.appName)}</h3>
|
|
46
|
+
${description}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
${scopes}
|
|
51
|
+
|
|
52
|
+
<form method="POST" action="${escapeHtml(callbackPath)}" class="flex gap-3 pt-4 border-t border-gray-100">
|
|
53
|
+
<input type="hidden" name="csrf" value="${escapeHtml(csrfToken)}">
|
|
54
|
+
<input type="hidden" name="pending_auth_id" value="${escapeHtml(pendingAuthId)}">
|
|
55
|
+
<input type="hidden" name="app" value="${escapeHtml(app.appId)}">
|
|
56
|
+
<button type="submit" name="action" value="authorize"
|
|
57
|
+
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors">
|
|
58
|
+
Authorize
|
|
59
|
+
</button>
|
|
60
|
+
<button type="submit" name="action" value="skip"
|
|
61
|
+
class="px-4 py-2.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 font-medium rounded-lg transition-colors">
|
|
62
|
+
Skip
|
|
63
|
+
</button>
|
|
64
|
+
</form>
|
|
65
|
+
</div>`;
|
|
66
|
+
}
|
|
67
|
+
function buildIncrementalAuthPage(params) {
|
|
68
|
+
const { app, toolId, sessionHint, callbackPath } = params;
|
|
69
|
+
const description = app.description ? `<p class="text-gray-500 mt-2">${escapeHtml(app.description)}</p>` : "";
|
|
70
|
+
const content = `
|
|
71
|
+
<!-- Warning icon -->
|
|
72
|
+
<div class="flex justify-center mb-6">
|
|
73
|
+
<div class="w-16 h-16 rounded-full bg-amber-100 flex items-center justify-center">
|
|
74
|
+
<svg class="w-8 h-8 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
75
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
|
76
|
+
</svg>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<h1 class="text-2xl font-bold text-gray-900 text-center mb-2">Authorization Required</h1>
|
|
81
|
+
<p class="text-gray-600 text-center mb-8">
|
|
82
|
+
To use "<span class="font-mono text-sm bg-gray-100 px-1 rounded">${escapeHtml(
|
|
83
|
+
toolId
|
|
84
|
+
)}</span>", you need to authorize ${escapeHtml(app.appName)}.
|
|
85
|
+
</p>
|
|
86
|
+
|
|
87
|
+
<!-- App card -->
|
|
88
|
+
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
|
|
89
|
+
<div class="flex items-center gap-4 mb-4">
|
|
90
|
+
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg">
|
|
91
|
+
${escapeHtml(app.appName.charAt(0).toUpperCase())}
|
|
92
|
+
</div>
|
|
93
|
+
<div class="flex-1">
|
|
94
|
+
<h3 class="font-semibold text-gray-900">${escapeHtml(app.appName)}</h3>
|
|
95
|
+
${description}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<form method="POST" action="${escapeHtml(callbackPath)}" class="flex gap-3 pt-4 border-t border-gray-100">
|
|
100
|
+
<input type="hidden" name="pending_auth_id" value="${escapeHtml(sessionHint)}">
|
|
101
|
+
<button type="button" onclick="window.close()"
|
|
102
|
+
class="flex-1 px-4 py-2.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 font-medium rounded-lg transition-colors">
|
|
103
|
+
Cancel
|
|
104
|
+
</button>
|
|
105
|
+
<button type="submit"
|
|
106
|
+
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors">
|
|
107
|
+
Authorize
|
|
108
|
+
</button>
|
|
109
|
+
</form>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div class="p-4 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-800 text-center">
|
|
113
|
+
This is an incremental authorization. Your existing session will be expanded to include ${escapeHtml(
|
|
114
|
+
app.appName
|
|
115
|
+
)}.
|
|
116
|
+
</div>`;
|
|
117
|
+
return centeredCardLayout(content, { title: `Authorize ${app.appName}` });
|
|
118
|
+
}
|
|
119
|
+
function buildFederatedLoginPage(params) {
|
|
120
|
+
const { providers, clientName, pendingAuthId, callbackPath } = params;
|
|
121
|
+
const providerCards = providers.map((provider) => {
|
|
122
|
+
const isPrimaryBadge = provider.isPrimary ? `<span class="px-2 py-0.5 text-xs font-medium bg-blue-600 text-white rounded-full">Primary</span>` : "";
|
|
123
|
+
const providerUrl = provider.providerUrl ? `<p class="text-xs text-gray-400 font-mono truncate">${escapeHtml(provider.providerUrl)}</p>` : "";
|
|
124
|
+
const appIds = provider.appIds.length > 0 ? `<p class="text-xs text-gray-500 mt-1">Apps: ${provider.appIds.map((id) => escapeHtml(id)).join(", ")}</p>` : "";
|
|
125
|
+
return `<label class="flex items-start gap-4 p-4 bg-white border-2 border-gray-200 rounded-xl cursor-pointer hover:border-blue-300 transition-colors has-[:checked]:border-blue-500 has-[:checked]:bg-blue-50">
|
|
126
|
+
<input type="checkbox" name="providers" value="${escapeHtml(provider.providerId)}"
|
|
127
|
+
class="mt-1 w-5 h-5 rounded border-gray-300" ${provider.isPrimary ? "checked" : ""}>
|
|
128
|
+
<div class="flex-1">
|
|
129
|
+
<div class="flex items-center gap-2 mb-1">
|
|
130
|
+
<span class="font-semibold text-gray-900">${escapeHtml(provider.providerName)}</span>
|
|
131
|
+
${isPrimaryBadge}
|
|
132
|
+
</div>
|
|
133
|
+
<p class="text-sm text-gray-500">Mode: ${escapeHtml(provider.mode)}</p>
|
|
134
|
+
${providerUrl}
|
|
135
|
+
${appIds}
|
|
136
|
+
</div>
|
|
137
|
+
</label>`;
|
|
138
|
+
}).join("\n");
|
|
139
|
+
const content = `
|
|
140
|
+
<h1 class="text-3xl font-bold text-gray-900 mb-4">Select Authorization Providers</h1>
|
|
141
|
+
<p class="text-gray-600 mb-8">
|
|
142
|
+
${escapeHtml(clientName)} uses multiple authentication providers. Select which ones you want to authorize.
|
|
143
|
+
</p>
|
|
144
|
+
|
|
145
|
+
<form method="POST" action="${escapeHtml(callbackPath)}" id="federated-form">
|
|
146
|
+
<input type="hidden" name="pending_auth_id" value="${escapeHtml(pendingAuthId)}">
|
|
147
|
+
<input type="hidden" name="federated" value="true">
|
|
148
|
+
|
|
149
|
+
<!-- Select all toggle -->
|
|
150
|
+
<label class="flex items-center gap-3 mb-6 cursor-pointer">
|
|
151
|
+
<input type="checkbox" id="select-all" class="w-5 h-5 rounded border-gray-300"
|
|
152
|
+
onchange="document.querySelectorAll('input[name=providers]').forEach(cb => cb.checked = this.checked)">
|
|
153
|
+
<span class="text-gray-700">Select all providers</span>
|
|
154
|
+
</label>
|
|
155
|
+
|
|
156
|
+
<!-- Provider cards -->
|
|
157
|
+
<div class="space-y-4 mb-8">
|
|
158
|
+
${providerCards}
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!-- Email input -->
|
|
162
|
+
<div class="mb-6">
|
|
163
|
+
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
|
|
164
|
+
<input type="email" id="email" name="email" required placeholder="you@example.com"
|
|
165
|
+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<!-- Buttons -->
|
|
169
|
+
<div class="flex gap-4">
|
|
170
|
+
<button type="button"
|
|
171
|
+
class="flex-1 px-6 py-3 text-gray-700 bg-gray-100 hover:bg-gray-200 font-medium rounded-lg transition-colors"
|
|
172
|
+
onclick="document.querySelectorAll('input[name=providers]').forEach(cb => cb.checked = false); document.getElementById('federated-form').submit();">
|
|
173
|
+
Skip All
|
|
174
|
+
</button>
|
|
175
|
+
<button type="submit"
|
|
176
|
+
class="flex-1 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
|
|
177
|
+
Continue
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
</form>
|
|
181
|
+
|
|
182
|
+
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800">
|
|
183
|
+
Skipped providers can be authorized later when you try to use their tools (progressive authorization).
|
|
184
|
+
</div>`;
|
|
185
|
+
return wideLayout(content, { title: "Select Providers" });
|
|
186
|
+
}
|
|
187
|
+
function buildToolConsentPage(params) {
|
|
188
|
+
const {
|
|
189
|
+
tools,
|
|
190
|
+
clientName,
|
|
191
|
+
pendingAuthId,
|
|
192
|
+
csrfToken,
|
|
193
|
+
callbackPath,
|
|
194
|
+
userName,
|
|
195
|
+
userEmail,
|
|
196
|
+
groupByApp = true,
|
|
197
|
+
showDescriptions = true,
|
|
198
|
+
customMessage,
|
|
199
|
+
allowSelectAll = true,
|
|
200
|
+
requireSelection = true,
|
|
201
|
+
defaultSelectedTools,
|
|
202
|
+
error,
|
|
203
|
+
hiddenFields = []
|
|
204
|
+
} = params;
|
|
205
|
+
const defaultSet = defaultSelectedTools ? new Set(defaultSelectedTools) : void 0;
|
|
206
|
+
const isChecked = (toolId) => defaultSet ? defaultSet.has(toolId) : true;
|
|
207
|
+
const checkedCount = tools.filter((t) => isChecked(t.toolId)).length;
|
|
208
|
+
const renderTool = (tool) => {
|
|
209
|
+
const desc = showDescriptions && tool.description ? `<p class="text-sm text-gray-500 mt-0.5">${escapeHtml(tool.description)}</p>` : "";
|
|
210
|
+
return `<label class="flex items-start gap-3 p-3 bg-white rounded-lg cursor-pointer hover:bg-gray-50">
|
|
211
|
+
<input type="checkbox" name="tools" value="${escapeHtml(tool.toolId)}" class="mt-0.5 w-5 h-5 rounded border-gray-300"${isChecked(tool.toolId) ? " checked" : ""}>
|
|
212
|
+
<div>
|
|
213
|
+
<span class="font-medium text-gray-900">${escapeHtml(tool.toolName)}</span>
|
|
214
|
+
${desc}
|
|
215
|
+
</div>
|
|
216
|
+
</label>`;
|
|
217
|
+
};
|
|
218
|
+
let toolSection;
|
|
219
|
+
if (groupByApp) {
|
|
220
|
+
const toolsByApp = {};
|
|
221
|
+
for (const tool of tools) {
|
|
222
|
+
if (!toolsByApp[tool.appId]) {
|
|
223
|
+
toolsByApp[tool.appId] = { appName: tool.appName, tools: [] };
|
|
224
|
+
}
|
|
225
|
+
toolsByApp[tool.appId].tools.push(tool);
|
|
226
|
+
}
|
|
227
|
+
const toggleAllButton = allowSelectAll ? `<button type="button" class="text-sm text-blue-600 hover:text-blue-800"
|
|
228
|
+
onclick="const container = this.closest('.bg-gray-50').querySelector('[data-app]'); const cbs = container.querySelectorAll('input[name=tools]'); const allChecked = [...cbs].every(cb => cb.checked); cbs.forEach(cb => cb.checked = !allChecked); updateCount();">
|
|
229
|
+
Toggle All
|
|
230
|
+
</button>` : "";
|
|
231
|
+
toolSection = Object.entries(toolsByApp).map(([appId, { appName, tools: appTools }]) => {
|
|
232
|
+
const toolItems = appTools.map(renderTool).join("\n");
|
|
233
|
+
return `<div class="bg-gray-50 rounded-xl overflow-hidden">
|
|
234
|
+
<div class="flex items-center justify-between px-4 py-3 bg-gray-100">
|
|
235
|
+
<div class="flex items-center gap-3">
|
|
236
|
+
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm">
|
|
237
|
+
${escapeHtml(appName.charAt(0).toUpperCase())}
|
|
238
|
+
</div>
|
|
239
|
+
<span class="font-semibold text-gray-900">${escapeHtml(appName)}</span>
|
|
240
|
+
</div>
|
|
241
|
+
${toggleAllButton}
|
|
242
|
+
</div>
|
|
243
|
+
<div class="p-4 space-y-2" data-app="${escapeHtml(appId)}">
|
|
244
|
+
${toolItems}
|
|
245
|
+
</div>
|
|
246
|
+
</div>`;
|
|
247
|
+
}).join("\n");
|
|
248
|
+
} else {
|
|
249
|
+
toolSection = `<div class="bg-gray-50 rounded-xl p-4 space-y-2" data-app="__all__">
|
|
250
|
+
${tools.map(renderTool).join("\n")}
|
|
251
|
+
</div>`;
|
|
252
|
+
}
|
|
253
|
+
const userInfo = userName || userEmail ? `<div class="p-3 bg-gray-50 rounded-lg mb-6 text-sm">
|
|
254
|
+
<span class="text-gray-500">Signed in as: </span>
|
|
255
|
+
<span class="font-medium text-gray-900">${escapeHtml(userName || userEmail || "")}</span>
|
|
256
|
+
</div>` : "";
|
|
257
|
+
const errorBanner = error ? `<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-sm text-red-800">${escapeHtml(error)}</div>` : "";
|
|
258
|
+
const hiddenInputs = hiddenFields.map((f) => `<input type="hidden" name="${escapeHtml(f.name)}" value="${escapeHtml(f.value)}">`).join("\n ");
|
|
259
|
+
const selectAllControl = allowSelectAll ? `<label class="flex items-center gap-3 cursor-pointer">
|
|
260
|
+
<input type="checkbox" id="select-all" class="w-5 h-5 rounded border-gray-300"${checkedCount === tools.length && tools.length > 0 ? " checked" : ""}
|
|
261
|
+
onchange="document.querySelectorAll('input[name=tools]').forEach(cb => cb.checked = this.checked); updateCount();">
|
|
262
|
+
<span class="text-gray-700">Select all tools</span>
|
|
263
|
+
</label>` : "<span></span>";
|
|
264
|
+
const requireSelectionScript = requireSelection ? `
|
|
265
|
+
const submitBtn = document.getElementById('consent-submit');
|
|
266
|
+
if (submitBtn) submitBtn.disabled = checked.length === 0;` : "";
|
|
267
|
+
const updateCountScript = `
|
|
268
|
+
<script>
|
|
269
|
+
function updateCount() {
|
|
270
|
+
const all = document.querySelectorAll('input[name="tools"]');
|
|
271
|
+
const checked = document.querySelectorAll('input[name="tools"]:checked');
|
|
272
|
+
const countEl = document.getElementById('selection-count');
|
|
273
|
+
if (countEl) countEl.textContent = checked.length + ' of ' + all.length + ' selected';
|
|
274
|
+
const selectAll = document.getElementById('select-all');
|
|
275
|
+
if (selectAll) selectAll.checked = all.length > 0 && all.length === checked.length;${requireSelectionScript}
|
|
276
|
+
}
|
|
277
|
+
document.querySelectorAll('input[name="tools"]').forEach(cb => cb.addEventListener('change', updateCount));
|
|
278
|
+
updateCount();
|
|
279
|
+
</script>`;
|
|
280
|
+
const subtitle = customMessage ? escapeHtml(customMessage) : `Choose which tools ${escapeHtml(clientName)} can access. You can change this later.`;
|
|
281
|
+
const content = `
|
|
282
|
+
<h1 class="text-3xl font-bold text-gray-900 mb-4">Select Tools to Enable</h1>
|
|
283
|
+
<p class="text-gray-600 mb-6">
|
|
284
|
+
${subtitle}
|
|
285
|
+
</p>
|
|
286
|
+
|
|
287
|
+
${errorBanner}
|
|
288
|
+
${userInfo}
|
|
289
|
+
|
|
290
|
+
<form method="POST" action="${escapeHtml(callbackPath)}" id="consent-form">
|
|
291
|
+
<input type="hidden" name="csrf" value="${escapeHtml(csrfToken)}">
|
|
292
|
+
<input type="hidden" name="pending_auth_id" value="${escapeHtml(pendingAuthId)}">
|
|
293
|
+
<input type="hidden" name="consent_submitted" value="1">
|
|
294
|
+
${hiddenInputs}
|
|
295
|
+
|
|
296
|
+
<!-- Select all toggle -->
|
|
297
|
+
<div class="flex items-center justify-between mb-6">
|
|
298
|
+
${selectAllControl}
|
|
299
|
+
<span id="selection-count" class="text-sm text-gray-500">${checkedCount} of ${tools.length} selected</span>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<!-- Tool groups -->
|
|
303
|
+
<div class="space-y-6 mb-8">
|
|
304
|
+
${toolSection}
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<!-- Buttons -->
|
|
308
|
+
<div class="flex gap-4">
|
|
309
|
+
<button type="button" onclick="history.back()"
|
|
310
|
+
class="flex-1 px-6 py-3 text-gray-700 bg-gray-100 hover:bg-gray-200 font-medium rounded-lg transition-colors">
|
|
311
|
+
Cancel
|
|
312
|
+
</button>
|
|
313
|
+
<button type="submit" id="consent-submit"
|
|
314
|
+
class="flex-1 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
|
315
|
+
Confirm Selection
|
|
316
|
+
</button>
|
|
317
|
+
</div>
|
|
318
|
+
</form>
|
|
319
|
+
${updateCountScript}`;
|
|
320
|
+
return extraWideLayout(content, { title: "Select Tools" });
|
|
321
|
+
}
|
|
322
|
+
function buildLoginFieldHtml(field) {
|
|
323
|
+
const id = `field-${field.name}`;
|
|
324
|
+
const safeName = escapeHtml(field.name);
|
|
325
|
+
const requiredAttr = field.required ? " required" : "";
|
|
326
|
+
const labelText = escapeHtml(field.label ?? field.name);
|
|
327
|
+
const safeValue = field.value !== void 0 ? escapeHtml(field.value) : "";
|
|
328
|
+
if (field.type === "hidden") {
|
|
329
|
+
return `<input type="hidden" name="${safeName}" value="${safeValue}">`;
|
|
330
|
+
}
|
|
331
|
+
const inputClass = "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
332
|
+
let control;
|
|
333
|
+
if (field.type === "select") {
|
|
334
|
+
const opts = (field.options ?? []).map(
|
|
335
|
+
(o) => `<option value="${escapeHtml(o.value)}"${o.value === field.value ? " selected" : ""}>${escapeHtml(
|
|
336
|
+
o.label
|
|
337
|
+
)}</option>`
|
|
338
|
+
).join("");
|
|
339
|
+
control = `<select id="${escapeHtml(id)}" name="${safeName}"${requiredAttr} class="${inputClass}">${opts}</select>`;
|
|
340
|
+
} else {
|
|
341
|
+
const placeholder = field.placeholder ? ` placeholder="${escapeHtml(field.placeholder)}"` : "";
|
|
342
|
+
control = `<input type="${escapeHtml(field.type)}" id="${escapeHtml(
|
|
343
|
+
id
|
|
344
|
+
)}" name="${safeName}"${requiredAttr}${placeholder} value="${safeValue}" class="${inputClass}">`;
|
|
345
|
+
}
|
|
346
|
+
return `<div class="mb-4">
|
|
347
|
+
<label for="${escapeHtml(id)}" class="block text-sm font-medium text-gray-700 mb-2">${labelText}</label>
|
|
348
|
+
${control}
|
|
349
|
+
</div>`;
|
|
350
|
+
}
|
|
351
|
+
function buildLoginPage(params) {
|
|
352
|
+
const { clientName, scope, pendingAuthId, callbackPath, title, subtitle, logoUri, error, extraFields } = params;
|
|
353
|
+
const scopesHtml = scope ? `<div class="p-4 bg-gray-50 rounded-lg mb-6">
|
|
354
|
+
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2">Requested permissions</p>
|
|
355
|
+
<p class="text-gray-700">${scope.split(" ").map((s) => escapeHtml(s)).join(", ") || "Default access"}</p>
|
|
356
|
+
</div>` : "";
|
|
357
|
+
const defaultFieldsHtml = `
|
|
358
|
+
<!-- Email -->
|
|
359
|
+
<div class="mb-4">
|
|
360
|
+
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
|
|
361
|
+
<input type="email" id="email" name="email" required placeholder="you@example.com"
|
|
362
|
+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<!-- Name (optional) -->
|
|
366
|
+
<div class="mb-6">
|
|
367
|
+
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">Name (optional)</label>
|
|
368
|
+
<input type="text" id="name" name="name" placeholder="Your name"
|
|
369
|
+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
370
|
+
</div>`;
|
|
371
|
+
const fieldsHtml = extraFields && extraFields.length > 0 ? extraFields.map((f) => buildLoginFieldHtml(f)).join("\n") : defaultFieldsHtml;
|
|
372
|
+
const logoHtml = logoUri ? `<div class="flex justify-center mb-4"><img src="${escapeHtml(
|
|
373
|
+
logoUri
|
|
374
|
+
)}" alt="${escapeHtml(clientName)}" class="h-12 w-12 rounded-lg object-cover"></div>` : "";
|
|
375
|
+
const errorHtml = error ? `<div class="p-3 mb-6 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">${escapeHtml(error)}</div>` : "";
|
|
376
|
+
const headingText = escapeHtml(title ?? "Sign In");
|
|
377
|
+
const subtitleText = subtitle ? escapeHtml(subtitle) : `Authorize access to ${escapeHtml(clientName)}`;
|
|
378
|
+
const content = `
|
|
379
|
+
<div class="bg-white rounded-2xl shadow-xl p-8">
|
|
380
|
+
${logoHtml}
|
|
381
|
+
<h1 class="text-3xl font-bold text-gray-900 mb-2 text-center">${headingText}</h1>
|
|
382
|
+
<p class="text-gray-600 mb-8 text-center">${subtitleText}</p>
|
|
383
|
+
|
|
384
|
+
${errorHtml}
|
|
385
|
+
|
|
386
|
+
${scopesHtml}
|
|
387
|
+
|
|
388
|
+
<form method="POST" action="${escapeHtml(callbackPath)}">
|
|
389
|
+
<input type="hidden" name="pending_auth_id" value="${escapeHtml(pendingAuthId)}">
|
|
390
|
+
${fieldsHtml}
|
|
391
|
+
|
|
392
|
+
<!-- Submit -->
|
|
393
|
+
<button type="submit"
|
|
394
|
+
class="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
|
|
395
|
+
Authorize
|
|
396
|
+
</button>
|
|
397
|
+
</form>
|
|
398
|
+
|
|
399
|
+
<p class="text-center text-sm text-gray-500 mt-6">Client: ${escapeHtml(clientName)}</p>
|
|
400
|
+
</div>`;
|
|
401
|
+
return centeredCardLayout(content, { title: title ?? "Sign In" });
|
|
402
|
+
}
|
|
403
|
+
function buildConnectPage(params) {
|
|
404
|
+
const { token, field, action, title, subtitle, logoUri, error } = params;
|
|
405
|
+
const logoHtml = logoUri ? `<div class="flex justify-center mb-4"><img src="${escapeHtml(
|
|
406
|
+
logoUri
|
|
407
|
+
)}" alt="Connect" class="h-12 w-12 rounded-lg object-cover"></div>` : "";
|
|
408
|
+
const errorHtml = error ? `<div class="p-3 mb-6 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">${escapeHtml(error)}</div>` : "";
|
|
409
|
+
const headingText = escapeHtml(title ?? "Connect credential");
|
|
410
|
+
const subtitleText = subtitle ? escapeHtml(subtitle) : `Connect "${escapeHtml(field.name)}" to continue`;
|
|
411
|
+
const content = `
|
|
412
|
+
<div class="bg-white rounded-2xl shadow-xl p-8">
|
|
413
|
+
${logoHtml}
|
|
414
|
+
<h1 class="text-3xl font-bold text-gray-900 mb-2 text-center">${headingText}</h1>
|
|
415
|
+
<p class="text-gray-600 mb-8 text-center">${subtitleText}</p>
|
|
416
|
+
|
|
417
|
+
${errorHtml}
|
|
418
|
+
|
|
419
|
+
<form method="POST" action="${escapeHtml(action)}">
|
|
420
|
+
<input type="hidden" name="token" value="${escapeHtml(token)}">
|
|
421
|
+
${buildLoginFieldHtml(field)}
|
|
422
|
+
|
|
423
|
+
<button type="submit"
|
|
424
|
+
class="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
|
|
425
|
+
Connect
|
|
426
|
+
</button>
|
|
427
|
+
</form>
|
|
428
|
+
</div>`;
|
|
429
|
+
return centeredCardLayout(content, { title: title ?? "Connect credential" });
|
|
430
|
+
}
|
|
431
|
+
function buildConnectSuccessPage(params) {
|
|
432
|
+
const { key, message } = params;
|
|
433
|
+
const content = `
|
|
434
|
+
<div class="bg-white rounded-2xl shadow-xl p-8 text-center">
|
|
435
|
+
<div class="flex justify-center mb-6">
|
|
436
|
+
<div class="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center">
|
|
437
|
+
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
438
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
439
|
+
</svg>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
<h1 class="text-2xl font-bold text-gray-900 mb-4">Credential connected</h1>
|
|
443
|
+
<p class="text-gray-600">${escapeHtml(message ?? `"${key}" is now connected. You can return to your session.`)}</p>
|
|
444
|
+
</div>`;
|
|
445
|
+
return centeredCardLayout(content, { title: "Connected" });
|
|
446
|
+
}
|
|
447
|
+
function buildErrorPage(params) {
|
|
448
|
+
const { error, description } = params;
|
|
449
|
+
const content = `
|
|
450
|
+
<div class="bg-white rounded-2xl shadow-xl p-8 text-center">
|
|
451
|
+
<!-- Error icon -->
|
|
452
|
+
<div class="flex justify-center mb-6">
|
|
453
|
+
<div class="w-16 h-16 rounded-full bg-red-100 flex items-center justify-center">
|
|
454
|
+
<svg class="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
455
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
456
|
+
</svg>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
<h1 class="text-2xl font-bold text-gray-900 mb-4">Authorization Error</h1>
|
|
461
|
+
<p class="mb-4">
|
|
462
|
+
<code class="px-2 py-1 bg-gray-100 rounded text-red-600 font-mono text-sm">${escapeHtml(error)}</code>
|
|
463
|
+
</p>
|
|
464
|
+
<p class="text-gray-600">${escapeHtml(description)}</p>
|
|
465
|
+
</div>`;
|
|
466
|
+
return centeredCardLayout(content, { title: "Error" });
|
|
467
|
+
}
|
|
468
|
+
function renderToHtml(html, _options) {
|
|
469
|
+
return html;
|
|
470
|
+
}
|
|
471
|
+
export {
|
|
472
|
+
buildConnectPage,
|
|
473
|
+
buildConnectSuccessPage,
|
|
474
|
+
buildConsentPage,
|
|
475
|
+
buildErrorPage,
|
|
476
|
+
buildFederatedLoginPage,
|
|
477
|
+
buildIncrementalAuthPage,
|
|
478
|
+
buildLoginPage,
|
|
479
|
+
buildToolConsentPage,
|
|
480
|
+
escapeHtml,
|
|
481
|
+
renderToHtml
|
|
482
|
+
};
|