@edcalderon/auth 1.3.0 โ†’ 1.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.1] - 2026-03-23
4
+
5
+ ### Added
6
+
7
+ - ๐Ÿ“š **Documentation**: Added `packages/auth/docs/` with five guides: `authentik-integration-guide.md`, `provisioning-model.md`, `upgrade-migration.md`, `nextjs-examples.md`, `cig-reference-map.md`.
8
+ - Updated README with documentation table and `@edcalderon/auth/authentik` subpath listing.
9
+
10
+ ## [1.4.0] - 2026-03-23
11
+
12
+ ### Added
13
+
14
+ - โœจ **Authentik flow + provisioning kit** (`@edcalderon/auth/authentik`) โ€” a reusable set of helpers generalised from the production CIG Authentik implementation.
15
+ - ๐Ÿ”€ **Cross-origin PKCE relay** โ€” `createRelayPageHtml()`, `parseRelayParams()`, `readRelayStorage()`, `clearRelayStorage()` for apps where login UI and callback handler live on different origins.
16
+ - ๐Ÿ”„ **Enhanced callback handler** โ€” `exchangeCode()`, `fetchClaims()`, `processCallback()` with blocking provisioning gate that prevents redirect until user sync completes.
17
+ - ๐Ÿšช **Logout orchestrator** โ€” `revokeToken()`, `buildEndSessionUrl()`, `orchestrateLogout()` implementing the full RP-initiated logout flow.
18
+ - ๐Ÿ”Œ **Provisioning adapter layer** โ€” pluggable adapters: `NoopProvisioningAdapter`, `createProvisioningAdapter()`, `SupabaseSyncAdapter` with identity-first matching and rollback on failure.
19
+ - ๐Ÿฅ **Config validation / doctor** โ€” `validateAuthentikConfig()`, `validateSupabaseSyncConfig()`, `validateFullConfig()` for startup / deploy-time validation (detects `supabase_not_configured`).
20
+ - ๐Ÿ›ก๏ธ **Safe redirect resolver** โ€” `resolveSafeRedirect()` with origin allowlist to prevent open-redirect vulnerabilities.
21
+ - ๐Ÿ“ฆ **New subpath export** โ€” `@edcalderon/auth/authentik` barrel export for all Authentik-specific modules.
22
+ - ๐Ÿ—„๏ธ **SQL migration 003** โ€” `003_authentik_shadow_auth_users.sql` adds shadow auth user linkage columns and `link_shadow_auth_user()` RPC.
23
+ - ๐Ÿงช **96 tests** across 6 test suites covering relay, callback, logout, provisioning (incl. paginated page-2 lookups, shadow linkage RPC, rollback), config validation (incl. endpoint discovery), and redirect safety.
24
+
3
25
  ## [1.3.0] - 2026-03-19
4
26
 
5
27
  ### Added
package/README.md CHANGED
@@ -11,13 +11,12 @@ Swap between Supabase, Firebase, Hybrid, or any custom provider without changing
11
11
 
12
12
  ---
13
13
 
14
- ## ๐Ÿ“‹ Latest Changes (v1.3.0)
14
+ ## ๐Ÿ“‹ Latest Changes (v1.4.1)
15
15
 
16
16
  ### Added
17
17
 
18
- - Added canonical `AuthentikOidcClient` browser helpers with PKCE-only OAuth flow utilities (`isAuthentikConfigured`, `startAuthentikOAuthFlow`, `handleAuthentikCallback`, `readOidcSession`, `clearOidcSession`, `hasPendingAuthentikCallback`, `OIDC_INITIAL_SEARCH`).
19
- - Added exported Authentik OIDC types: `OidcClaims`, `OidcSession`, `OidcProvider`.
20
- - Added README guidance for Authentik setup and the known Authentik `2026.2.1` social re-link bug workaround.
18
+ - ๐Ÿ“š **Documentation**: Added `packages/auth/docs/` with five guides: `authentik-integration-guide.md`, `provisioning-model.md`, `upgrade-migration.md`, `nextjs-examples.md`, `cig-reference-map.md`.
19
+ - Updated README with documentation table and `@edcalderon/auth/authentik` subpath listing.
21
20
 
22
21
  For full version history, see [CHANGELOG.md](./CHANGELOG.md) and [GitHub releases](https://github.com/edcalderon/my-second-brain/releases)
23
22
 
@@ -150,6 +149,7 @@ The package avoids bleeding `window` or `document` objects into Expo bundles or
150
149
  - `@edcalderon/auth/firebase-native`
151
150
  - `@edcalderon/auth/hybrid-web`
152
151
  - `@edcalderon/auth/hybrid-native`
152
+ - `@edcalderon/auth/authentik` (Authentik flow + provisioning kit โ€” [docs](./docs/authentik-integration-guide.md))
153
153
 
154
154
  ---
155
155
 
@@ -32,8 +32,10 @@ function resolveEndpoint(issuer, explicitPath, fallbackPath) {
32
32
  if (explicitPath) {
33
33
  return new URL(explicitPath, `${issuerUrl.origin}/`).toString();
34
34
  }
35
- const normalizedBase = ensurePathSuffix(issuerUrl.pathname);
36
- return new URL(`${normalizedBase}${fallbackPath}`, issuerUrl.origin).toString();
35
+ // Authentik OAuth endpoints (authorize, token, userinfo) live at the parent
36
+ // of the issuer path: issuer = /application/o/<slug>/, endpoint = /application/o/<ep>/.
37
+ const base = ensurePathSuffix(issuer);
38
+ return new URL(`../${fallbackPath}`, base).toString();
37
39
  }
38
40
  function getSessionStorage(config) {
39
41
  if (config.sessionStorage) {
@@ -0,0 +1,71 @@
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
+ import type { AuthentikCallbackConfig, AuthentikCallbackResult, AuthentikTokenResponse, AuthentikClaims, ProvisioningAdapter, ProvisioningResult } from "./types";
11
+ /**
12
+ * Exchange an authorization code for tokens using PKCE.
13
+ *
14
+ * This is a pure server-safe function โ€” it does not touch sessionStorage.
15
+ */
16
+ export declare function exchangeCode(config: AuthentikCallbackConfig, code: string, codeVerifier: string): Promise<AuthentikTokenResponse>;
17
+ /**
18
+ * Fetch OIDC claims from the Authentik userinfo endpoint.
19
+ */
20
+ export declare function fetchClaims(config: AuthentikCallbackConfig, accessToken: string): Promise<AuthentikClaims>;
21
+ /**
22
+ * Options for the full callback handler.
23
+ */
24
+ export interface ProcessCallbackOptions {
25
+ /** Callback config (issuer, clientId, redirectUri, etc.). */
26
+ config: AuthentikCallbackConfig;
27
+ /** The authorization code from the callback query string. */
28
+ code: string;
29
+ /** The PKCE code verifier stored by the relay or startOAuthFlow. */
30
+ codeVerifier: string;
31
+ /** The state token from the callback query string. */
32
+ state: string;
33
+ /** The expected state token (from sessionStorage / relay). */
34
+ expectedState: string;
35
+ /** The social-login provider that initiated the flow. */
36
+ provider: string;
37
+ /**
38
+ * Optional provisioning adapter.
39
+ * When provided the callback will **block** until provisioning succeeds.
40
+ * If provisioning fails the result will contain the error.
41
+ */
42
+ provisioningAdapter?: ProvisioningAdapter;
43
+ }
44
+ /**
45
+ * Result of the full callback flow.
46
+ */
47
+ export interface ProcessCallbackResult {
48
+ /** Whether the entire flow (exchange + provisioning) succeeded. */
49
+ success: boolean;
50
+ /** Tokens + claims from the exchange step. */
51
+ callbackResult?: AuthentikCallbackResult;
52
+ /** Provisioning result (only present when an adapter was provided). */
53
+ provisioningResult?: ProvisioningResult;
54
+ /** Error message on failure. */
55
+ error?: string;
56
+ /** Machine-readable error code on failure. */
57
+ errorCode?: string;
58
+ }
59
+ /**
60
+ * Process an Authentik OIDC callback end-to-end:
61
+ *
62
+ * 1. Validate state matches
63
+ * 2. Exchange the authorization code for tokens
64
+ * 3. Fetch OIDC claims from userinfo
65
+ * 4. (Optional) Run the provisioning adapter โ€” blocks until complete
66
+ * 5. Return the combined result
67
+ *
68
+ * If any step fails the function returns `{ success: false, error }`.
69
+ * It does **not** throw so that callers can present structured error UI.
70
+ */
71
+ export declare function processCallback(options: ProcessCallbackOptions): Promise<ProcessCallbackResult>;
@@ -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>;