diff --git a/README.md b/README.md
index e1ac7b30..1bff5ab4 100644
--- a/README.md
+++ b/README.md
@@ -35,8 +35,6 @@
Termix is free and open source. If you find it useful, consider [donating](https://donate.termix.site/) to help cover server costs and development time.
-
-
@@ -392,6 +390,10 @@ See [Projects](https://github.com/orgs/Termix-SSH/projects/5) for all planned fe
+
+
+
+
diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts
index 9c73a7f8..d730379d 100644
--- a/src/backend/utils/auth-manager.ts
+++ b/src/backend/utils/auth-manager.ts
@@ -881,6 +881,41 @@ class AuthManager {
});
});
+ // Auto-unlock the per-user Data Encryption Key for OIDC-backed users so
+ // API-key (headless / automation) requests can access encrypted
+ // credentials/hosts. Without this, every encrypted-table op throws
+ // "User data not unlocked" because the API-key path never established a
+ // DEK session. OIDC DEKs are wrapped with a server-derived system key
+ // (deriveOIDCSystemKey), so no user password is required. Password-based
+ // users are deliberately NOT unlocked here (their KEK is password-derived
+ // and cannot be reproduced server-side).
+ //
+ // Opt-in and OFF by default: enabling this widens the blast radius of a
+ // leaked API key (it can then read that user's stored SSH secrets in
+ // clear text), so operators must turn it on explicitly.
+ if (
+ process.env.ALLOW_APIKEY_DATA_UNLOCK === "true" &&
+ !this.userCrypto.isUserUnlocked(matchedKey.userId)
+ ) {
+ try {
+ const { users } = await import("../database/db/schema.js");
+ const oidcRows = await db
+ .select()
+ .from(users)
+ .where(eq(users.id, matchedKey.userId))
+ .limit(1);
+ if (oidcRows[0]?.isOidc) {
+ await this.authenticateOIDCUser(matchedKey.userId);
+ }
+ } catch (err) {
+ databaseLogger.warn("API-key OIDC auto-unlock failed", {
+ operation: "api_key_oidc_unlock_failed",
+ userId: matchedKey.userId,
+ error: err instanceof Error ? err.message : "Unknown",
+ });
+ }
+ }
+
req.userId = matchedKey.userId;
req.apiKeyId = matchedKey.id;
next();