@edcalderon/auth 1.2.2 → 1.4.0

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.
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Enhanced Authentik callback handler with blocking provisioning support.
3
+ *
4
+ * This module handles the OIDC authorization code exchange and optionally
5
+ * calls a provisioning adapter **before** allowing the app to redirect
6
+ * into the protected product.
7
+ *
8
+ * Reference: CIG apps/dashboard/app/auth/callback/page.tsx
9
+ */
10
+ /* ------------------------------------------------------------------ */
11
+ /* Token exchange */
12
+ /* ------------------------------------------------------------------ */
13
+ /**
14
+ * Exchange an authorization code for tokens using PKCE.
15
+ *
16
+ * This is a pure server-safe function — it does not touch sessionStorage.
17
+ */
18
+ export async function exchangeCode(config, code, codeVerifier) {
19
+ const tokenUrl = config.tokenEndpoint;
20
+ const fetchFn = config.fetchFn || fetch;
21
+ const body = new URLSearchParams({
22
+ grant_type: "authorization_code",
23
+ code,
24
+ redirect_uri: config.redirectUri,
25
+ client_id: config.clientId,
26
+ code_verifier: codeVerifier,
27
+ });
28
+ const response = await fetchFn(tokenUrl, {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
31
+ body,
32
+ });
33
+ if (!response.ok) {
34
+ throw new Error(`NETWORK_ERROR: Token exchange failed (HTTP ${response.status})`);
35
+ }
36
+ const json = (await response.json());
37
+ if (!json.access_token || typeof json.access_token !== "string") {
38
+ throw new Error("SESSION_ERROR: Token response missing access_token");
39
+ }
40
+ return {
41
+ access_token: json.access_token,
42
+ token_type: json.token_type ?? undefined,
43
+ refresh_token: json.refresh_token ?? undefined,
44
+ id_token: json.id_token ?? undefined,
45
+ expires_in: typeof json.expires_in === "number" ? json.expires_in : undefined,
46
+ scope: json.scope ?? undefined,
47
+ };
48
+ }
49
+ /* ------------------------------------------------------------------ */
50
+ /* Userinfo */
51
+ /* ------------------------------------------------------------------ */
52
+ /**
53
+ * Fetch OIDC claims from the Authentik userinfo endpoint.
54
+ */
55
+ export async function fetchClaims(config, accessToken) {
56
+ const userinfoUrl = config.userinfoEndpoint;
57
+ const fetchFn = config.fetchFn || fetch;
58
+ const response = await fetchFn(userinfoUrl, {
59
+ method: "GET",
60
+ headers: { Authorization: `Bearer ${accessToken}` },
61
+ });
62
+ if (!response.ok) {
63
+ throw new Error(`NETWORK_ERROR: Userinfo request failed (HTTP ${response.status})`);
64
+ }
65
+ const claims = (await response.json());
66
+ if (!claims.sub || !claims.iss) {
67
+ throw new Error("SESSION_ERROR: Userinfo response missing required claims (sub, iss)");
68
+ }
69
+ return claims;
70
+ }
71
+ /**
72
+ * Process an Authentik OIDC callback end-to-end:
73
+ *
74
+ * 1. Validate state matches
75
+ * 2. Exchange the authorization code for tokens
76
+ * 3. Fetch OIDC claims from userinfo
77
+ * 4. (Optional) Run the provisioning adapter — blocks until complete
78
+ * 5. Return the combined result
79
+ *
80
+ * If any step fails the function returns `{ success: false, error }`.
81
+ * It does **not** throw so that callers can present structured error UI.
82
+ */
83
+ export async function processCallback(options) {
84
+ // 1. State validation
85
+ if (options.state !== options.expectedState) {
86
+ return {
87
+ success: false,
88
+ error: "Invalid callback state — possible CSRF or expired session",
89
+ errorCode: "state_mismatch",
90
+ };
91
+ }
92
+ // 2. Token exchange
93
+ let tokens;
94
+ try {
95
+ tokens = await exchangeCode(options.config, options.code, options.codeVerifier);
96
+ }
97
+ catch (err) {
98
+ return {
99
+ success: false,
100
+ error: err instanceof Error ? err.message : "Token exchange failed",
101
+ errorCode: "token_exchange_failed",
102
+ };
103
+ }
104
+ // 3. Fetch claims
105
+ let claims;
106
+ try {
107
+ claims = await fetchClaims(options.config, tokens.access_token);
108
+ }
109
+ catch (err) {
110
+ return {
111
+ success: false,
112
+ error: err instanceof Error ? err.message : "Userinfo fetch failed",
113
+ errorCode: "userinfo_failed",
114
+ };
115
+ }
116
+ const callbackResult = {
117
+ tokens,
118
+ claims,
119
+ provider: options.provider,
120
+ };
121
+ // 4. Provisioning gate
122
+ if (options.provisioningAdapter) {
123
+ const payload = {
124
+ sub: claims.sub,
125
+ iss: claims.iss,
126
+ email: (claims.email || "").toLowerCase(),
127
+ emailVerified: claims.email_verified,
128
+ name: claims.name || claims.preferred_username,
129
+ picture: claims.picture,
130
+ provider: options.provider,
131
+ rawClaims: claims,
132
+ };
133
+ let provisioningResult;
134
+ try {
135
+ provisioningResult = await options.provisioningAdapter.sync(payload);
136
+ }
137
+ catch (err) {
138
+ return {
139
+ success: false,
140
+ callbackResult,
141
+ error: err instanceof Error ? err.message : "Provisioning failed",
142
+ errorCode: "provisioning_error",
143
+ };
144
+ }
145
+ if (!provisioningResult.synced) {
146
+ return {
147
+ success: false,
148
+ callbackResult,
149
+ provisioningResult,
150
+ error: provisioningResult.error || "User provisioning did not complete",
151
+ errorCode: provisioningResult.errorCode || "provisioning_failed",
152
+ };
153
+ }
154
+ return {
155
+ success: true,
156
+ callbackResult,
157
+ provisioningResult,
158
+ };
159
+ }
160
+ // No adapter — exchange-only mode
161
+ return { success: true, callbackResult };
162
+ }
163
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Configuration validation / doctor helpers.
3
+ *
4
+ * These helpers detect missing or invalid configuration **before** the
5
+ * first real user login, so misconfigured environments fail fast.
6
+ *
7
+ * Reference: CIG `supabase_not_configured` callback error code.
8
+ */
9
+ import type { ConfigValidationResult, AuthentikCallbackConfig, AuthentikEndpoints, SupabaseSyncConfig } from "./types";
10
+ /**
11
+ * Validate that the core Authentik OIDC configuration is present and
12
+ * syntactically correct.
13
+ *
14
+ * This does **not** make network requests — it only checks that the
15
+ * required values are set and look reasonable.
16
+ */
17
+ export declare function validateAuthentikConfig(config: Partial<AuthentikCallbackConfig>): ConfigValidationResult;
18
+ /**
19
+ * Validate the server-side Supabase sync configuration.
20
+ *
21
+ * Returns a diagnostic result with the exact error code
22
+ * `supabase_not_configured` when the Supabase URL or service role key
23
+ * is missing, matching the CIG convention.
24
+ */
25
+ export declare function validateSupabaseSyncConfig(config: Partial<SupabaseSyncConfig>): ConfigValidationResult;
26
+ /**
27
+ * Validate both Authentik and Supabase sync configuration in one call.
28
+ *
29
+ * Useful as a startup / health-check / deploy-time validation gate.
30
+ */
31
+ export declare function validateFullConfig(authentikConfig: Partial<AuthentikCallbackConfig>, supabaseConfig: Partial<SupabaseSyncConfig>): ConfigValidationResult;
32
+ /**
33
+ * Discover OIDC endpoint URLs from an Authentik issuer's
34
+ * `.well-known/openid-configuration`.
35
+ *
36
+ * **Important:** Authentik places most OIDC endpoints (token, userinfo,
37
+ * authorize, revocation) at the `/application/o/` level, *not* under the
38
+ * per-app issuer path. For example, with:
39
+ * issuer = `https://auth.example.com/application/o/my-app/`
40
+ * the token endpoint is:
41
+ * `https://auth.example.com/application/o/token/`
42
+ *
43
+ * This function fetches the correct endpoint URLs from the well-known
44
+ * document so callers never need to guess.
45
+ *
46
+ * ```ts
47
+ * const endpoints = await discoverEndpoints("https://auth.example.com/application/o/my-app/");
48
+ * // endpoints.token → "https://auth.example.com/application/o/token/"
49
+ * // endpoints.userinfo → "https://auth.example.com/application/o/userinfo/"
50
+ * // endpoints.endSession → "https://auth.example.com/application/o/my-app/end-session/"
51
+ * ```
52
+ */
53
+ export declare function discoverEndpoints(issuer: string, fetchFn?: typeof fetch): Promise<AuthentikEndpoints>;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Configuration validation / doctor helpers.
3
+ *
4
+ * These helpers detect missing or invalid configuration **before** the
5
+ * first real user login, so misconfigured environments fail fast.
6
+ *
7
+ * Reference: CIG `supabase_not_configured` callback error code.
8
+ */
9
+ /* ------------------------------------------------------------------ */
10
+ /* Authentik config validation */
11
+ /* ------------------------------------------------------------------ */
12
+ /**
13
+ * Validate that the core Authentik OIDC configuration is present and
14
+ * syntactically correct.
15
+ *
16
+ * This does **not** make network requests — it only checks that the
17
+ * required values are set and look reasonable.
18
+ */
19
+ export function validateAuthentikConfig(config) {
20
+ const checks = [];
21
+ // issuer
22
+ checks.push(config.issuer
23
+ ? isValidUrl(config.issuer)
24
+ ? { name: "issuer", passed: true, message: "Issuer URL is valid", severity: "error" }
25
+ : { name: "issuer", passed: false, message: `Issuer is not a valid URL: ${config.issuer}`, severity: "error" }
26
+ : { name: "issuer", passed: false, message: "Authentik issuer URL is required", severity: "error" });
27
+ // clientId
28
+ checks.push(config.clientId
29
+ ? { name: "clientId", passed: true, message: "Client ID is set", severity: "error" }
30
+ : { name: "clientId", passed: false, message: "Authentik client_id is required", severity: "error" });
31
+ // redirectUri
32
+ checks.push(config.redirectUri
33
+ ? isValidUrl(config.redirectUri)
34
+ ? { name: "redirectUri", passed: true, message: "Redirect URI is valid", severity: "error" }
35
+ : { name: "redirectUri", passed: false, message: `Redirect URI is not a valid URL: ${config.redirectUri}`, severity: "error" }
36
+ : { name: "redirectUri", passed: false, message: "Redirect URI is required", severity: "error" });
37
+ // tokenEndpoint
38
+ checks.push(config.tokenEndpoint
39
+ ? isValidUrl(config.tokenEndpoint)
40
+ ? { name: "tokenEndpoint", passed: true, message: "Token endpoint URL is valid", severity: "error" }
41
+ : { name: "tokenEndpoint", passed: false, message: `Token endpoint is not a valid URL: ${config.tokenEndpoint}`, severity: "error" }
42
+ : { name: "tokenEndpoint", passed: false, message: "Token endpoint URL is required — use discoverEndpoints() or supply manually", severity: "error" });
43
+ // userinfoEndpoint
44
+ checks.push(config.userinfoEndpoint
45
+ ? isValidUrl(config.userinfoEndpoint)
46
+ ? { name: "userinfoEndpoint", passed: true, message: "Userinfo endpoint URL is valid", severity: "error" }
47
+ : { name: "userinfoEndpoint", passed: false, message: `Userinfo endpoint is not a valid URL: ${config.userinfoEndpoint}`, severity: "error" }
48
+ : { name: "userinfoEndpoint", passed: false, message: "Userinfo endpoint URL is required — use discoverEndpoints() or supply manually", severity: "error" });
49
+ return {
50
+ valid: checks.every((c) => c.passed),
51
+ checks,
52
+ };
53
+ }
54
+ /* ------------------------------------------------------------------ */
55
+ /* Supabase sync config validation */
56
+ /* ------------------------------------------------------------------ */
57
+ /**
58
+ * Validate the server-side Supabase sync configuration.
59
+ *
60
+ * Returns a diagnostic result with the exact error code
61
+ * `supabase_not_configured` when the Supabase URL or service role key
62
+ * is missing, matching the CIG convention.
63
+ */
64
+ export function validateSupabaseSyncConfig(config) {
65
+ const checks = [];
66
+ // supabaseUrl
67
+ checks.push(config.supabaseUrl
68
+ ? isValidUrl(config.supabaseUrl)
69
+ ? { name: "supabaseUrl", passed: true, message: "Supabase URL is valid", severity: "error" }
70
+ : { name: "supabaseUrl", passed: false, message: `Supabase URL is not a valid URL: ${config.supabaseUrl}`, severity: "error" }
71
+ : { name: "supabaseUrl", passed: false, message: "supabase_not_configured: Supabase URL is required for sync", severity: "error" });
72
+ // supabaseServiceRoleKey
73
+ checks.push(config.supabaseServiceRoleKey
74
+ ? config.supabaseServiceRoleKey.length >= 20
75
+ ? { name: "supabaseServiceRoleKey", passed: true, message: "Service role key is set", severity: "error" }
76
+ : { name: "supabaseServiceRoleKey", passed: false, message: "Service role key appears too short", severity: "warning" }
77
+ : { name: "supabaseServiceRoleKey", passed: false, message: "supabase_not_configured: Supabase service_role key is required for sync", severity: "error" });
78
+ // upsertRpcName (optional, warn if customised)
79
+ if (config.upsertRpcName && config.upsertRpcName !== "upsert_oidc_user") {
80
+ checks.push({
81
+ name: "upsertRpcName",
82
+ passed: true,
83
+ message: `Custom RPC name: ${config.upsertRpcName}`,
84
+ severity: "warning",
85
+ });
86
+ }
87
+ return {
88
+ valid: checks.filter((c) => c.severity === "error").every((c) => c.passed),
89
+ checks,
90
+ };
91
+ }
92
+ /* ------------------------------------------------------------------ */
93
+ /* Combined validation */
94
+ /* ------------------------------------------------------------------ */
95
+ /**
96
+ * Validate both Authentik and Supabase sync configuration in one call.
97
+ *
98
+ * Useful as a startup / health-check / deploy-time validation gate.
99
+ */
100
+ export function validateFullConfig(authentikConfig, supabaseConfig) {
101
+ const authentik = validateAuthentikConfig(authentikConfig);
102
+ const supabase = validateSupabaseSyncConfig(supabaseConfig);
103
+ const checks = [...authentik.checks, ...supabase.checks];
104
+ return {
105
+ valid: checks.filter((c) => c.severity === "error").every((c) => c.passed),
106
+ checks,
107
+ };
108
+ }
109
+ /* ------------------------------------------------------------------ */
110
+ /* Helpers */
111
+ /* ------------------------------------------------------------------ */
112
+ function isValidUrl(value) {
113
+ try {
114
+ new URL(value);
115
+ return true;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ /* ------------------------------------------------------------------ */
122
+ /* Endpoint discovery */
123
+ /* ------------------------------------------------------------------ */
124
+ /**
125
+ * Discover OIDC endpoint URLs from an Authentik issuer's
126
+ * `.well-known/openid-configuration`.
127
+ *
128
+ * **Important:** Authentik places most OIDC endpoints (token, userinfo,
129
+ * authorize, revocation) at the `/application/o/` level, *not* under the
130
+ * per-app issuer path. For example, with:
131
+ * issuer = `https://auth.example.com/application/o/my-app/`
132
+ * the token endpoint is:
133
+ * `https://auth.example.com/application/o/token/`
134
+ *
135
+ * This function fetches the correct endpoint URLs from the well-known
136
+ * document so callers never need to guess.
137
+ *
138
+ * ```ts
139
+ * const endpoints = await discoverEndpoints("https://auth.example.com/application/o/my-app/");
140
+ * // endpoints.token → "https://auth.example.com/application/o/token/"
141
+ * // endpoints.userinfo → "https://auth.example.com/application/o/userinfo/"
142
+ * // endpoints.endSession → "https://auth.example.com/application/o/my-app/end-session/"
143
+ * ```
144
+ */
145
+ export async function discoverEndpoints(issuer, fetchFn = fetch) {
146
+ const base = issuer.endsWith("/") ? issuer : `${issuer}/`;
147
+ const wellKnownUrl = `${base}.well-known/openid-configuration`;
148
+ const response = await fetchFn(wellKnownUrl);
149
+ if (!response.ok) {
150
+ throw new Error(`Failed to fetch .well-known/openid-configuration from ${wellKnownUrl} (HTTP ${response.status})`);
151
+ }
152
+ const doc = (await response.json());
153
+ const authorization = doc.authorization_endpoint;
154
+ const token = doc.token_endpoint;
155
+ const userinfo = doc.userinfo_endpoint;
156
+ if (typeof authorization !== "string" ||
157
+ typeof token !== "string" ||
158
+ typeof userinfo !== "string") {
159
+ throw new Error("Well-known document missing required endpoints (authorization_endpoint, token_endpoint, userinfo_endpoint)");
160
+ }
161
+ return {
162
+ authorization,
163
+ token,
164
+ userinfo,
165
+ revocation: typeof doc.revocation_endpoint === "string" ? doc.revocation_endpoint : undefined,
166
+ endSession: typeof doc.end_session_endpoint === "string" ? doc.end_session_endpoint : undefined,
167
+ };
168
+ }
169
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @edcalderon/auth — Authentik flow + provisioning kit.
3
+ *
4
+ * Barrel export that assembles all Authentik-specific modules into a
5
+ * single importable surface area.
6
+ *
7
+ * Usage:
8
+ * import { processCallback, orchestrateLogout, ... } from "@edcalderon/auth/authentik";
9
+ */
10
+ export type { AuthentikProvider, AuthentikEndpoints, AuthentikRelayConfig, RelayIncomingParams, RelayHandlerResult, AuthentikCallbackConfig, AuthentikTokenResponse, AuthentikClaims, AuthentikCallbackResult, AuthentikLogoutConfig, AuthentikLogoutResult, ProvisioningPayload, ProvisioningResult, ProvisioningAdapter, SupabaseSyncConfig, ConfigValidationResult, ConfigCheck, SafeRedirectConfig, } from "./types";
11
+ export { createRelayPageHtml, parseRelayParams, readRelayStorage, clearRelayStorage, } from "./relay";
12
+ export { exchangeCode, fetchClaims, processCallback, } from "./callback";
13
+ export type { ProcessCallbackOptions, ProcessCallbackResult, } from "./callback";
14
+ export { revokeToken, buildEndSessionUrl, orchestrateLogout, } from "./logout";
15
+ export { NoopProvisioningAdapter, createProvisioningAdapter, normalizePayload, SupabaseSyncAdapter, createSupabaseSyncAdapter, } from "./provisioning";
16
+ export { validateAuthentikConfig, validateSupabaseSyncConfig, validateFullConfig, discoverEndpoints, } from "./config";
17
+ export { resolveSafeRedirect } from "./redirect";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @edcalderon/auth — Authentik flow + provisioning kit.
3
+ *
4
+ * Barrel export that assembles all Authentik-specific modules into a
5
+ * single importable surface area.
6
+ *
7
+ * Usage:
8
+ * import { processCallback, orchestrateLogout, ... } from "@edcalderon/auth/authentik";
9
+ */
10
+ // Relay
11
+ export { createRelayPageHtml, parseRelayParams, readRelayStorage, clearRelayStorage, } from "./relay";
12
+ // Callback
13
+ export { exchangeCode, fetchClaims, processCallback, } from "./callback";
14
+ // Logout
15
+ export { revokeToken, buildEndSessionUrl, orchestrateLogout, } from "./logout";
16
+ // Provisioning
17
+ export { NoopProvisioningAdapter, createProvisioningAdapter, normalizePayload, SupabaseSyncAdapter, createSupabaseSyncAdapter, } from "./provisioning";
18
+ // Config validation
19
+ export { validateAuthentikConfig, validateSupabaseSyncConfig, validateFullConfig, discoverEndpoints, } from "./config";
20
+ // Safe redirect
21
+ export { resolveSafeRedirect } from "./redirect";
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Authentik logout orchestrator.
3
+ *
4
+ * A correct Authentik logout requires all of these steps in sequence:
5
+ * 1. Clear app-local session state
6
+ * 2. Revoke the access token (best-effort)
7
+ * 3. Build the RP-initiated logout URL with id_token_hint
8
+ * 4. Navigate the browser to the end-session URL
9
+ *
10
+ * Reference: CIG apps/landing/components/AuthProvider.tsx signOut()
11
+ */
12
+ import type { AuthentikLogoutConfig, AuthentikLogoutResult } from "./types";
13
+ /**
14
+ * Revoke an OAuth2 token at the Authentik revocation endpoint.
15
+ *
16
+ * This is a **best-effort** operation — revocation failures do not
17
+ * prevent logout from completing. Returns `true` if the server
18
+ * responded with 2xx.
19
+ *
20
+ * If `config.revocationEndpoint` is not set, revocation is skipped
21
+ * and the function returns `false`.
22
+ */
23
+ export declare function revokeToken(config: AuthentikLogoutConfig, accessToken: string): Promise<boolean>;
24
+ /**
25
+ * Build the Authentik RP-initiated logout URL.
26
+ *
27
+ * The resulting URL, when navigated to, will:
28
+ * 1. Clear the Authentik browser session
29
+ * 2. Redirect back to `postLogoutRedirectUri`
30
+ *
31
+ * Requires that the Authentik provider has an **invalidation flow**
32
+ * configured with a "User Logout" stage and a redirect stage.
33
+ */
34
+ export declare function buildEndSessionUrl(config: AuthentikLogoutConfig, idToken?: string): string;
35
+ /**
36
+ * Execute the full Authentik logout sequence:
37
+ *
38
+ * 1. Revoke the access token (best-effort)
39
+ * 2. Build the RP-initiated end-session URL
40
+ *
41
+ * The caller is responsible for:
42
+ * - Clearing app-local session state **before** calling this function
43
+ * - Navigating the browser to `result.endSessionUrl` **after** this returns
44
+ *
45
+ * This design keeps the orchestrator framework-agnostic.
46
+ */
47
+ export declare function orchestrateLogout(config: AuthentikLogoutConfig, tokens: {
48
+ accessToken?: string;
49
+ idToken?: string;
50
+ }): Promise<AuthentikLogoutResult>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Authentik logout orchestrator.
3
+ *
4
+ * A correct Authentik logout requires all of these steps in sequence:
5
+ * 1. Clear app-local session state
6
+ * 2. Revoke the access token (best-effort)
7
+ * 3. Build the RP-initiated logout URL with id_token_hint
8
+ * 4. Navigate the browser to the end-session URL
9
+ *
10
+ * Reference: CIG apps/landing/components/AuthProvider.tsx signOut()
11
+ */
12
+ /* ------------------------------------------------------------------ */
13
+ /* Token revocation */
14
+ /* ------------------------------------------------------------------ */
15
+ /**
16
+ * Revoke an OAuth2 token at the Authentik revocation endpoint.
17
+ *
18
+ * This is a **best-effort** operation — revocation failures do not
19
+ * prevent logout from completing. Returns `true` if the server
20
+ * responded with 2xx.
21
+ *
22
+ * If `config.revocationEndpoint` is not set, revocation is skipped
23
+ * and the function returns `false`.
24
+ */
25
+ export async function revokeToken(config, accessToken) {
26
+ if (!config.revocationEndpoint) {
27
+ return false;
28
+ }
29
+ const revocationUrl = config.revocationEndpoint;
30
+ const fetchFn = config.fetchFn || fetch;
31
+ const body = new URLSearchParams({
32
+ token: accessToken,
33
+ token_type_hint: "access_token",
34
+ });
35
+ if (config.clientId) {
36
+ body.set("client_id", config.clientId);
37
+ }
38
+ try {
39
+ const response = await fetchFn(revocationUrl, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
42
+ body,
43
+ });
44
+ return response.ok;
45
+ }
46
+ catch {
47
+ // Revocation is best-effort
48
+ return false;
49
+ }
50
+ }
51
+ /* ------------------------------------------------------------------ */
52
+ /* RP-initiated logout URL */
53
+ /* ------------------------------------------------------------------ */
54
+ /**
55
+ * Build the Authentik RP-initiated logout URL.
56
+ *
57
+ * The resulting URL, when navigated to, will:
58
+ * 1. Clear the Authentik browser session
59
+ * 2. Redirect back to `postLogoutRedirectUri`
60
+ *
61
+ * Requires that the Authentik provider has an **invalidation flow**
62
+ * configured with a "User Logout" stage and a redirect stage.
63
+ */
64
+ export function buildEndSessionUrl(config, idToken) {
65
+ const endSessionUrl = config.endSessionEndpoint;
66
+ const url = new URL(endSessionUrl);
67
+ if (idToken) {
68
+ url.searchParams.set("id_token_hint", idToken);
69
+ }
70
+ url.searchParams.set("post_logout_redirect_uri", config.postLogoutRedirectUri);
71
+ return url.toString();
72
+ }
73
+ /* ------------------------------------------------------------------ */
74
+ /* Full logout orchestration */
75
+ /* ------------------------------------------------------------------ */
76
+ /**
77
+ * Execute the full Authentik logout sequence:
78
+ *
79
+ * 1. Revoke the access token (best-effort)
80
+ * 2. Build the RP-initiated end-session URL
81
+ *
82
+ * The caller is responsible for:
83
+ * - Clearing app-local session state **before** calling this function
84
+ * - Navigating the browser to `result.endSessionUrl` **after** this returns
85
+ *
86
+ * This design keeps the orchestrator framework-agnostic.
87
+ */
88
+ export async function orchestrateLogout(config, tokens) {
89
+ let tokenRevoked = false;
90
+ if (tokens.accessToken) {
91
+ tokenRevoked = await revokeToken(config, tokens.accessToken);
92
+ }
93
+ const endSessionUrl = buildEndSessionUrl(config, tokens.idToken);
94
+ return { endSessionUrl, tokenRevoked };
95
+ }
96
+ //# sourceMappingURL=logout.js.map