diff --git a/src/InnerProvider.svelte b/src/InnerProvider.svelte
new file mode 100644
index 0000000..1a5403e
--- /dev/null
+++ b/src/InnerProvider.svelte
@@ -0,0 +1,71 @@
+
+
+{#if $db.identity}
+ {@render children()}
+{:else}
+
+
+
+
Connecting to SpacetimeDB...
+
Establishing a secure connection to the chat server.
+
+
+
+ {#if onCancel}
+
+ {/if}
+
+
+{/if}
diff --git a/src/InnerSpacetimeProvider.svelte b/src/InnerSpacetimeProvider.svelte
index f836aa4..a5049d4 100644
--- a/src/InnerSpacetimeProvider.svelte
+++ b/src/InnerSpacetimeProvider.svelte
@@ -1,57 +1,36 @@
-{#if $db.identity}
- {@render children()}
+{#if builder}
+
+ {@render children()}
+
{:else}
-
Connecting to SpacetimeDB...
-
Establishing a secure connection to the chat server.
-
-
-
- {#if onCancel}
-
- {/if}
+
Authenticating...
+
Waiting for identity provider callback.
{/if}
diff --git a/src/auth/AuthGate.svelte b/src/auth/AuthGate.svelte
index 4dd11e2..6a4617b 100644
--- a/src/auth/AuthGate.svelte
+++ b/src/auth/AuthGate.svelte
@@ -30,14 +30,29 @@
combinedConnection = `${stdbHost.replace(/^(https?|wss?):\/\//, "")}:${stdbDbName}`;
const isChanging = localStorage.getItem("zep_changing_server") === "true";
+ const isInCallback = window.location.search.includes("code=") && window.location.search.includes("state=");
+
if (isChanging) {
userWantsToConnect = false;
- } else if (TokenStore.get(stdbHost, stdbDbName)) {
- // Auto-connect if we have a token and NOT changing server
+ } else if (TokenStore.get(stdbHost, stdbDbName) || isInCallback) {
+ // Auto-connect ONLY if we have a SpacetimeDB token OR are in a redirect callback.
+ // We don't auto-connect just because of auth.isAuthenticated to prevent logout loops.
userWantsToConnect = true;
}
});
+ // Ensure we transition to connecting if we just finished an OIDC redirect
+ $effect(() => {
+ if (auth.isAuthenticated && !auth.isLoading) {
+ const isInCallback = window.location.search.includes("code=") && window.location.search.includes("state=");
+ if (isInCallback) {
+ untrack(() => {
+ userWantsToConnect = true;
+ });
+ }
+ }
+ });
+
// Split combined connection if it changes
$effect(() => {
if (combinedConnection.includes(":")) {
@@ -59,7 +74,13 @@
combinedConnection = `${hostPart}:${stdbDbName}`;
});
- const hasStoredToken = $derived(!!TokenStore.get(stdbHost, stdbDbName));
+ let hasStoredToken = $state(false);
+
+ $effect(() => {
+ // Check for token on mount and when connection params change
+ const _ = combinedConnection;
+ hasStoredToken = !!TokenStore.get(stdbHost, stdbDbName);
+ });
$effect(() => {
if (stdbHost) localStorage.setItem(HOST_KEY, stdbHost);
diff --git a/src/auth/auth.svelte.ts b/src/auth/auth.svelte.ts
index b90a31d..3953da1 100644
--- a/src/auth/auth.svelte.ts
+++ b/src/auth/auth.svelte.ts
@@ -7,8 +7,8 @@ import { getEnv } from "../env";
// OIDC Configuration - User should replace these with their own provider values
export const oidcConfig: UserManagerSettings = {
- authority: getEnv("VITE_OIDC_AUTHORITY", "https://accounts.google.com"),
- client_id: getEnv("VITE_OIDC_CLIENT_ID", "REPLACE_ME"),
+ authority: getEnv("VITE_OIDC_AUTHORITY", "https://sso.adamlamers.com/application/o/truenas-zep/"),
+ client_id: getEnv("VITE_OIDC_CLIENT_ID", "Omb5jIeYY2rJqkxK52qZgLRGejOBCK5Km2BcA5RO"),
redirect_uri: window.location.origin,
scope: "openid profile email",
response_type: "code",
@@ -18,6 +18,7 @@ class AuthStore {
#userManager: UserManager;
#user = $state(null);
#isLoading = $state(true);
+ #isProcessingCallback = false;
constructor(settings: UserManagerSettings) {
this.#userManager = new UserManager(settings);
@@ -27,8 +28,19 @@ class AuthStore {
window.location.search.includes("code=") &&
window.location.search.includes("state=")
) {
- this.signinCallback();
+ if (!this.#isProcessingCallback) {
+ this.signinCallback();
+ }
} else {
+ // Not in a callback, safe to clear any stale/redundant OIDC state keys
+ // that might have accumulated from partial attempts
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (key && key.startsWith("oidc.")) {
+ localStorage.removeItem(key);
+ }
+ }
+
this.#userManager
.getUser()
.then((user) => {
@@ -71,10 +83,19 @@ class AuthStore {
}
async signinCallback() {
+ if (this.#isProcessingCallback) return;
+ this.#isProcessingCallback = true;
this.#isLoading = true;
try {
const user = await this.#userManager.signinCallback();
this.#user = user;
+
+ // Stage the id_token so SpacetimeDB can find it even if OIDC state is cleared
+ if (user.id_token) {
+ console.log("AuthStore: Staging OIDC id_token for SpacetimeDB handshake");
+ localStorage.setItem("zep_oidc_staging_token", user.id_token);
+ }
+
window.history.replaceState({}, document.title, window.location.pathname);
} catch (error) {
console.error("Signin callback error:", error);
@@ -101,7 +122,7 @@ class AuthStore {
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
- if (key && key.includes("auth_token")) {
+ if (key && (key.includes("stdb_token:") || key.includes("auth_token") || key === "zep_oidc_staging_token")) {
keysToRemove.push(key);
}
}
diff --git a/src/config.ts b/src/config.ts
index 16e2c04..3441d9c 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -45,35 +45,20 @@ export const TokenStore = {
const key = localStorage.key(i);
if (!key) continue;
- let connStr: string | null = null;
-
if (key.startsWith("stdb_token:")) {
- // New format: stdb_token:protocol://host[:port]:database
const parts = key.split(":");
if (parts.length >= 4) {
const dbName = parts[parts.length - 1];
const hostParts = parts.slice(2, parts.length - 1);
const host = hostParts.join(":").replace(/^\/\//, "");
- connStr = `${host}:${dbName}`;
- }
- } else if (key.endsWith("/auth_token")) {
- // Legacy format: protocol://host[:port]/database/auth_token
- // Example: http://localhost:3000/zep/auth_token
- const parts = key.split("/");
- // parts: ["http:", "", "localhost:3000", "zep", "auth_token"]
- if (parts.length >= 5) {
- const dbName = parts[parts.length - 2];
- const host = parts[2];
- if (dbName && host) {
- connStr = `${host}:${dbName}`;
+ const connStr = `${host}:${dbName}`;
+
+ if (!seen.has(connStr)) {
+ connections.push(connStr);
+ seen.add(connStr);
}
}
}
-
- if (connStr && !seen.has(connStr)) {
- connections.push(connStr);
- seen.add(connStr);
- }
}
return connections;
}
@@ -102,6 +87,39 @@ export const stopActiveConnection = () => {
}
};
+export const handleConnect = (conn: DbConnection, identity: any, token: string) => {
+ const host = getStdbHost();
+ const dbName = getStdbDbName();
+
+ console.log(`handleConnect: Connection established! Token received (len: ${token?.length || 0})`);
+
+ _connection = conn;
+ if (token) {
+ console.log("handleConnect: Persisting token to TokenStore...");
+ TokenStore.set(host, dbName, token);
+
+ // Clear staging token now that we have a permanent one
+ localStorage.removeItem("zep_oidc_staging_token");
+ } else {
+ console.warn("handleConnect: No token received from SpacetimeDB!");
+ }
+
+ connectionState.status = "connected";
+ connectionState.hasConnectedOnce = true;
+ connectionState.error = null;
+
+ // Call the auth update reducer to ensure OIDC info is synced
+ setTimeout(() => {
+ if (_connection) {
+ console.log("handleConnect: Requesting auth info update...");
+ // Use the typed reducer call if available
+ if ((_connection.reducers as any).updateAuthInfo) {
+ (_connection.reducers as any).updateAuthInfo({});
+ }
+ }
+ }, 100);
+};
+
class ConnectionManager {
#retryCount = 0;
#reconnectTimeout: any = null;
@@ -197,50 +215,29 @@ export const connectionBuilder = (oidcToken?: string) => {
const host = normalizeHost(rawHost);
const dbName = getStdbDbName();
- console.log(`connectionBuilder: Using host: ${host} (raw: ${rawHost}), database: ${dbName}`);
-
- connectionState.status = "connecting";
-
- if (activeManager) {
- console.log("connectionBuilder: Stopping previous activeManager");
- activeManager.stop();
- }
-
- const manager = new ConnectionManager(host, dbName);
- activeManager = manager;
+ console.log(`connectionBuilder: Creating builder for host: ${host}, database: ${dbName}`);
const builder = DbConnection.builder()
.withUri(host)
.withDatabaseName(dbName);
const storedToken = TokenStore.get(host, dbName);
- console.log("connectionBuilder: oidcToken:", oidcToken ? "present" : "absent");
- console.log("connectionBuilder: storedToken:", storedToken ? "present" : "absent");
+ const stagingToken = localStorage.getItem("zep_oidc_staging_token");
+
+ console.log("connectionBuilder: tokens -> oidc:", oidcToken ? "yes" : "no", "staging:", stagingToken ? "yes" : "no", "stored:", storedToken ? "yes" : "no");
if (oidcToken) {
- console.log("connectionBuilder: Calling withToken with oidcToken");
builder.withToken(oidcToken);
+ } else if (stagingToken) {
+ console.log("connectionBuilder: Using staged OIDC token");
+ builder.withToken(stagingToken);
} else if (storedToken) {
- console.log("connectionBuilder: Calling withToken with storedToken");
builder.withToken(storedToken);
- } else {
- console.log("connectionBuilder: No token, will connect anonymously");
}
- // Register our manager's listeners directly on the connection object.
- // This ensures they coexist with the Svelte provider's listeners.
- builder.onConnect((c: any, id: any, token: string) => {
- console.log("connectionBuilder: CONNECTION onConnect TRIGGERED!");
- manager.onConnect(c as any, id, token);
- });
- builder.onDisconnect((_ctx: any, err: any) => {
- console.log("connectionBuilder: CONNECTION onDisconnect TRIGGERED!", err);
- manager.onDisconnect();
- });
- builder.onConnectError((ctx: any, err: any) => {
- console.log("connectionBuilder: CONNECTION onConnectError TRIGGERED!", err);
- manager.onConnectError(ctx, err);
- });
+ builder.onConnect((conn: any, identity: any, token: string) => {
+ handleConnect(conn, identity, token);
+ });
return builder;
};