@tidecloak/js 0.14.32-staging → 0.14.33

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,227 @@
1
+ # Front-Channel Mode
2
+
3
+ The simplest way to add TideCloak to your web app. Everything happens in the browser.
4
+
5
+ ---
6
+
7
+ ## What You'll Build
8
+
9
+ Click login, your users go to TideCloak, they log in, they come back authenticated. That's it.
10
+
11
+ ---
12
+
13
+ ## Quick Start
14
+
15
+ ### 1. Get Your Config File
16
+
17
+ Download `tidecloak.json` (your client adapter config) from your TideCloak admin console and put it in your app.
18
+
19
+ ### 2. Add Silent SSO Check File
20
+
21
+ This file is required for silent session checks. It should be auto-copied when you install `@tidecloak/js`, but if it's missing, create `public/silent-check-sso.html`:
22
+
23
+ ```html
24
+ <html><body><script>parent.postMessage(location.href, location.origin)</script></body></html>
25
+ ```
26
+
27
+ ### 3. Create a Redirect Page
28
+
29
+ Create `public/auth/redirect.html`:
30
+
31
+ ```html
32
+ <!DOCTYPE html>
33
+ <html>
34
+ <head><title>Redirecting...</title></head>
35
+ <body>
36
+ <p>Redirecting...</p>
37
+ <script>window.location.href = "/";</script>
38
+ </body>
39
+ </html>
40
+ ```
41
+
42
+ ### 4. Initialize the SDK
43
+
44
+ ```js
45
+ import { IAMService } from "@tidecloak/js";
46
+ import config from "./tidecloak.json";
47
+
48
+ // Listen for events. Handlers get the event name first, then its arguments.
49
+ IAMService
50
+ .on("ready", (_event, loggedIn) => {
51
+ console.log("Ready! Logged in:", loggedIn);
52
+ updateUI(loggedIn);
53
+ })
54
+ .on("authSuccess", () => {
55
+ console.log("Login successful");
56
+ })
57
+ .on("logout", () => {
58
+ console.log("Logged out");
59
+ });
60
+
61
+ // Start the SDK
62
+ await IAMService.initIAM(config);
63
+ ```
64
+
65
+ ### DPoP (opt-in)
66
+
67
+ DPoP (sender-constrained tokens, [RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449)) binds an access token to a per-session key, so a stolen token can't be replayed. It is **opt-in**: set `useDPoP` and the SDK turns it on; omit it and the SDK requests an ordinary (unbound) token.
68
+
69
+ > **DPoP is not a flag you can flip in isolation.** Asking for it appends `dpop_jkt` to the authorization request, and the realm then issues a token carrying `typ: "DPoP"` and `cnf.jkt`. Such a token is **invalid** if it is later presented as a plain `Authorization: Bearer …`, and the resource server answers a bare `401`. So enable DPoP only if every call that carries the token attaches a `DPoP:` proof (use `IAMService.secureFetch`, which does this for you). This is why the SDK will not turn DPoP on for you.
70
+
71
+ ```js
72
+ // Opt in, enforced: init fails if the realm doesn't advertise DPoP support
73
+ await IAMService.initIAM({ ...config, useDPoP: { mode: "strict" } });
74
+
75
+ // Use DPoP only when the realm supports it, otherwise fall back to bearer:
76
+ await IAMService.initIAM({ ...config, useDPoP: { mode: "auto" } });
77
+
78
+ // Pick the proof signing algorithm (default "ES256"):
79
+ await IAMService.initIAM({ ...config, useDPoP: { mode: "strict", alg: "EdDSA" } });
80
+
81
+ // No DPoP, a plain unbound access token (the default)
82
+ await IAMService.initIAM({ ...config });
83
+ ```
84
+
85
+ | `useDPoP` value | Behavior |
86
+ | -------------------------- | ------------------------------------------------------------------------ |
87
+ | *(omitted)* / `false` | **Default.** No DPoP. No `dpop_jkt`, plain unbound access token. |
88
+ | `{ mode: "auto" }` | Use DPoP when the realm advertises it; otherwise fall back to bearer. |
89
+ | `{ mode: "strict" }` | Enforce DPoP; init fails if the realm lacks DPoP support. |
90
+ | `{ mode: …, alg }` | As above, with a specific proof algorithm (`ES256` default). |
91
+
92
+ > Your **resource server** must validate DPoP proofs for the binding to be meaningful. See [`lib/README.md`](../lib/README.md#dpop-resource-server-setup) for serving `tide_dpop_auth.html`.
93
+
94
+ ### 5. Add Login/Logout Buttons
95
+
96
+ ```js
97
+ document.getElementById("login-btn").onclick = () => IAMService.doLogin();
98
+ document.getElementById("logout-btn").onclick = () => IAMService.doLogout();
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Everything You Can Do
104
+
105
+ ```js
106
+ // Check auth state
107
+ IAMService.isLoggedIn(); // Is user logged in?
108
+
109
+ // Get tokens
110
+ await IAMService.getToken(); // Access token (for API calls)
111
+ IAMService.getIDToken(); // ID token
112
+
113
+ // Get user info
114
+ IAMService.getName(); // Username
115
+ IAMService.getValueFromToken("email");
116
+ IAMService.getValueFromIDToken("name"); // getValueFromIdToken also works
117
+
118
+ // Check roles
119
+ IAMService.hasRealmRole("admin");
120
+ IAMService.hasClientRole("editor");
121
+
122
+ // Auth actions
123
+ IAMService.doLogin();
124
+ IAMService.doLogout();
125
+ await IAMService.updateIAMToken(); // Refresh token
126
+
127
+ // Encryption (if configured)
128
+ await IAMService.doEncrypt([{ data: "secret", tags: ["personal"] }]);
129
+ await IAMService.doDecrypt([{ encrypted: "...", tags: ["personal"] }]);
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Events
135
+
136
+ Every handler is called with the event name first, then the event's arguments.
137
+
138
+ ```js
139
+ IAMService
140
+ .on("ready", (_event, loggedIn) => {
141
+ // SDK is ready - loggedIn is true/false
142
+ })
143
+ .on("authSuccess", () => {
144
+ // User logged in
145
+ })
146
+ .on("authError", (_event, err) => {
147
+ // Login failed
148
+ })
149
+ .on("logout", () => {
150
+ // User logged out
151
+ })
152
+ .on("tokenExpired", () => {
153
+ // Token expired - SDK will try to refresh
154
+ });
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Encryption
160
+
161
+ Protect sensitive data with tag-based encryption:
162
+
163
+ ```js
164
+ // Encrypt one or more items
165
+ const encrypted = await IAMService.doEncrypt([
166
+ { data: "10 Smith Street", tags: ["address"] },
167
+ { data: "john@example.com", tags: ["email"] },
168
+ ]);
169
+
170
+ // Decrypt
171
+ const decrypted = await IAMService.doDecrypt([
172
+ { encrypted: encrypted[0], tags: ["address"] },
173
+ { encrypted: encrypted[1], tags: ["email"] },
174
+ ]);
175
+ ```
176
+
177
+ **Important:**
178
+ - `data` must be a string or `Uint8Array` (not an object - use `JSON.stringify()` first)
179
+ - Users need `_tide_<tag>.selfencrypt` / `_tide_<tag>.selfdecrypt` roles
180
+ - Output order matches input order
181
+ - Both methods take an optional signed decryption policy (`Uint8Array`) as a second argument. See [Policy-Protected Encryption](../lib/README.md#policy-protected-encryption).
182
+
183
+ ### Encrypt Objects
184
+
185
+ ```js
186
+ // Wrong - objects not allowed
187
+ await IAMService.doEncrypt([{ data: { name: "John" }, tags: ["user"] }]);
188
+
189
+ // Right - stringify first
190
+ await IAMService.doEncrypt([{ data: JSON.stringify({ name: "John" }), tags: ["user"] }]);
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Custom Redirect Path
196
+
197
+ By default, users go to `/auth/redirect` after login. To change this:
198
+
199
+ ```js
200
+ await IAMService.initIAM({
201
+ ...config,
202
+ redirectUri: "https://myapp.com/callback"
203
+ });
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Troubleshooting
209
+
210
+ **Blank page after login**
211
+
212
+ Make sure you have a page at `/auth/redirect` and your redirect URI is registered in TideCloak.
213
+
214
+ **"silent-check-sso.html not found" or silent SSO fails**
215
+
216
+ Create `public/silent-check-sso.html` with this content:
217
+ ```html
218
+ <html><body><script>parent.postMessage(location.href, location.origin)</script></body></html>
219
+ ```
220
+
221
+ **Token not available**
222
+
223
+ Wait for the `ready` event before using tokens.
224
+
225
+ **Encryption fails**
226
+
227
+ Make sure your `tidecloak.json` includes `vendorId` and the `client-origin-auth-{origin}` for your app's origin.
@@ -0,0 +1,185 @@
1
+ # Hybrid/BFF Mode
2
+
3
+ For apps where security is critical. Your backend handles tokens - they never touch the browser.
4
+
5
+ ---
6
+
7
+ ## What You'll Build
8
+
9
+ User clicks login, goes to TideCloak, logs in, comes back. But instead of tokens going to the browser, they go to your server. Your server creates a session, and the browser just gets a session cookie.
10
+
11
+ ---
12
+
13
+ ## When to Use This
14
+
15
+ - You're handling sensitive data (financial, medical, etc.)
16
+ - You don't want tokens in the browser at all
17
+ - Your backend needs to make API calls on behalf of users
18
+ - You need server-side session control
19
+
20
+ ---
21
+
22
+ ## Quick Start
23
+
24
+ ### 1. Set Up Your Config
25
+
26
+ ```js
27
+ const hybridConfig = {
28
+ authMode: "hybrid",
29
+ oidc: {
30
+ authorizationEndpoint: "https://auth.example.com/realms/myrealm/protocol/openid-connect/auth",
31
+ clientId: "my-app",
32
+ redirectUri: "https://myapp.com/auth/callback",
33
+ scope: "openid profile email"
34
+ },
35
+ tokenExchange: {
36
+ endpoint: "/api/authenticate" // Your backend will handle this
37
+ }
38
+ };
39
+ ```
40
+
41
+ ### 2. Login Page
42
+
43
+ ```js
44
+ import { IAMService } from "@tidecloak/js";
45
+
46
+ await IAMService.loadConfig(hybridConfig);
47
+
48
+ document.getElementById("login-btn").onclick = () => {
49
+ IAMService.doLogin("/dashboard"); // Where to go after login
50
+ };
51
+ ```
52
+
53
+ ### 3. Callback Page
54
+
55
+ ```js
56
+ import { IAMService } from "@tidecloak/js";
57
+
58
+ const loggedIn = await IAMService.initIAM(hybridConfig);
59
+
60
+ if (loggedIn) {
61
+ // Success - redirect to where user wanted to go
62
+ window.location.href = IAMService.getReturnUrl() || "/";
63
+ } else {
64
+ // Failed
65
+ document.getElementById("error").textContent = "Login failed";
66
+ }
67
+ ```
68
+
69
+ ### 4. Build Your Backend Endpoint
70
+
71
+ Your `/api/authenticate` endpoint receives:
72
+
73
+ ```json
74
+ {
75
+ "accessToken": "{\"code\":\"AUTH_CODE\",\"code_verifier\":\"PKCE_VERIFIER\",\"redirect_uri\":\"...\"}",
76
+ "provider": "tidecloak-auth"
77
+ }
78
+ ```
79
+
80
+ Your backend should:
81
+ 1. Parse the JSON in `accessToken`
82
+ 2. Exchange the code with TideCloak
83
+ 3. Store tokens server-side
84
+ 4. Create a session for the user
85
+
86
+ **Express.js example:**
87
+
88
+ ```js
89
+ app.post('/api/authenticate', async (req, res) => {
90
+ const authData = JSON.parse(req.body.accessToken);
91
+
92
+ // Exchange code for tokens
93
+ const tokenResponse = await fetch(
94
+ 'https://auth.example.com/realms/myrealm/protocol/openid-connect/token',
95
+ {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
98
+ body: new URLSearchParams({
99
+ grant_type: 'authorization_code',
100
+ client_id: 'my-app',
101
+ code: authData.code,
102
+ code_verifier: authData.code_verifier,
103
+ redirect_uri: authData.redirect_uri,
104
+ }),
105
+ }
106
+ );
107
+
108
+ if (!tokenResponse.ok) {
109
+ return res.status(401).json({ error: 'Token exchange failed' });
110
+ }
111
+
112
+ const tokens = await tokenResponse.json();
113
+
114
+ // Store tokens and create session
115
+ req.session.tokens = tokens;
116
+ req.session.userId = tokens.sub;
117
+
118
+ res.json({ success: true });
119
+ });
120
+ ```
121
+
122
+ ---
123
+
124
+ ## What You Can Do (Client-Side)
125
+
126
+ Since tokens are on your server, most methods won't work in the browser. You can use:
127
+
128
+ ```js
129
+ // Did user complete login?
130
+ IAMService.isLoggedIn();
131
+
132
+ // Where did user want to go?
133
+ IAMService.getReturnUrl();
134
+ ```
135
+
136
+ ---
137
+
138
+ ## What You Can't Do (Client-Side)
139
+
140
+ These won't work because tokens are on your server:
141
+
142
+ - `getToken()`, `getIDToken()`
143
+ - `getName()`, `hasRealmRole()`, `hasClientRole()`
144
+ - `getValueFromToken()`, `getValueFromIDToken()` (and its alias `getValueFromIdToken()`)
145
+ - `doEncrypt()`, `doDecrypt()`
146
+
147
+ Your backend should provide user info via your own API endpoints.
148
+
149
+ ---
150
+
151
+ ## Front-Channel vs Hybrid
152
+
153
+ | Question | Front-Channel | Hybrid |
154
+ |----------|---------------|--------|
155
+ | Where are tokens? | Browser | Server |
156
+ | Can I use encryption client-side? | Yes | No |
157
+ | Can I check roles client-side? | Yes | No |
158
+ | Is it simpler? | Yes | No |
159
+ | Is it more secure? | Good | Better |
160
+
161
+ **Use Front-Channel if:**
162
+ - You're building a simple web app
163
+ - You need client-side encryption
164
+ - You want the easiest setup
165
+
166
+ **Use Hybrid if:**
167
+ - Security is critical
168
+ - You don't trust the browser with tokens
169
+ - Your backend makes API calls on behalf of users
170
+
171
+ ---
172
+
173
+ ## Troubleshooting
174
+
175
+ **Callback fails**
176
+
177
+ Check that your `redirect_uri` matches exactly what's registered in TideCloak.
178
+
179
+ **Session not persisting**
180
+
181
+ Make sure your backend is setting cookies correctly and they're not being blocked by CORS or same-site policies.
182
+
183
+ **Backend token exchange fails**
184
+
185
+ Double-check your TideCloak token endpoint URL and client ID.
@@ -0,0 +1,284 @@
1
+ # Native Mode
2
+
3
+ Build desktop and mobile apps with TideCloak. Works with Electron, Tauri, and React Native.
4
+
5
+ ---
6
+
7
+ ## What You'll Build
8
+
9
+ Your users click "Login" in your app, their browser opens, they log in, and they're back in your app - authenticated and ready to go. Tokens are stored securely on their device.
10
+
11
+ ---
12
+
13
+ ## Quick Start
14
+
15
+ ### 1. Get Your Config File
16
+
17
+ Download `tidecloak.json` (your client adapter config) from your TideCloak admin console. It has all your TideCloak settings.
18
+
19
+ ### 2. Create Your Adapter
20
+
21
+ The adapter handles platform-specific stuff - opening login windows, storing tokens. Here's one for Electron:
22
+
23
+ ```js
24
+ // electronAdapter.js
25
+ export function createElectronAdapter() {
26
+ return {
27
+ // Where should TideCloak redirect after login?
28
+ getRedirectUri: () => 'myapp://auth/callback',
29
+
30
+ // Open login in a popup window (NOT external browser - see note below)
31
+ openExternalUrl: async (url) => {
32
+ await window.ipcRenderer.invoke('open-auth-popup', url);
33
+ },
34
+
35
+ // Listen for the auth callback
36
+ onAuthCallback: (callback) => {
37
+ const handler = (_event, data) => callback(data);
38
+ window.ipcRenderer.on('auth-callback', handler);
39
+ return () => window.ipcRenderer.off('auth-callback', handler);
40
+ },
41
+
42
+ // Store tokens securely
43
+ saveTokens: async (tokens) => {
44
+ await window.ipcRenderer.invoke('save-tokens', tokens);
45
+ return true;
46
+ },
47
+
48
+ // Get stored tokens
49
+ getTokens: async () => {
50
+ return await window.ipcRenderer.invoke('get-tokens');
51
+ },
52
+
53
+ // Clear tokens on logout
54
+ deleteTokens: async () => {
55
+ await window.ipcRenderer.invoke('delete-tokens');
56
+ return true;
57
+ },
58
+ };
59
+ }
60
+ ```
61
+
62
+ That's it for the adapter. Just 6 functions, all platform-specific. The SDK handles everything else.
63
+
64
+ > **Popup Window vs External Browser**
65
+ >
66
+ > If you need **encryption/decryption**, you **must** use a popup window (Electron BrowserWindow) instead of the system's external browser. This keeps session cookies inside your Electron app, which is required for the encryption/decryption session key to work.
67
+ >
68
+ > | Approach | Session Cookies | Encryption/Decryption |
69
+ > |----------|-----------------|------------|
70
+ > | Popup window (BrowserWindow) | Stays in Electron | Works |
71
+ > | External browser (shell.openExternal) | Separate from Electron | Fails with "session key mismatch" |
72
+ >
73
+ > Without the session cookies, `doEncrypt()` and `doDecrypt()` will fail.
74
+ >
75
+ > Your main process should create a BrowserWindow for login, not call `shell.openExternal()`.
76
+
77
+ ### 3. Initialize the SDK
78
+
79
+ ```js
80
+ import { IAMService } from "@tidecloak/js";
81
+ import adapterConfig from "./tidecloak.json";
82
+ import { createElectronAdapter } from "./electronAdapter";
83
+
84
+ const config = {
85
+ authMode: "native",
86
+ adapter: createElectronAdapter(),
87
+ ...adapterConfig, // Spread your tidecloak.json config
88
+ };
89
+
90
+ // Listen for events. Handlers get the event name first, then its arguments.
91
+ IAMService
92
+ .on("authSuccess", () => console.log("Logged in!"))
93
+ .on("authError", (_event, err) => console.error("Login failed:", err))
94
+ .on("logout", () => console.log("Logged out"));
95
+
96
+ // Start the SDK
97
+ await IAMService.initIAM(config);
98
+ ```
99
+
100
+ ### Session Mode
101
+
102
+ Control how the SDK handles tokens on startup:
103
+
104
+ ```js
105
+ const config = {
106
+ authMode: "native",
107
+ adapter: createElectronAdapter(),
108
+ sessionMode: "offline", // or "online"
109
+ ...adapterConfig,
110
+ };
111
+ ```
112
+
113
+ | Mode | Behavior | Best For |
114
+ |------|----------|----------|
115
+ | `"online"` | Validates tokens with server, refreshes if needed, requires login if invalid | Always-connected apps |
116
+ | `"offline"` | Accepts stored tokens without server validation, even if expired | Offline-first apps |
117
+
118
+ **Offline mode** is great for apps that need to work without internet. Users can access the app with expired tokens, and you only prompt for re-login when an API call fails with 401.
119
+
120
+ ### 4. Login and Logout
121
+
122
+ ```js
123
+ // Login - opens system browser
124
+ IAMService.doLogin();
125
+
126
+ // Logout - clears tokens
127
+ IAMService.doLogout();
128
+
129
+ // Check login state
130
+ if (IAMService.isLoggedIn()) {
131
+ console.log("User is logged in");
132
+ }
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Everything You Can Do
138
+
139
+ ```js
140
+ // Check auth state
141
+ IAMService.isLoggedIn(); // Is user logged in?
142
+
143
+ // Get tokens
144
+ await IAMService.getToken(); // Access token
145
+ IAMService.getIDToken(); // ID token
146
+
147
+ // Get user info
148
+ IAMService.getName(); // Username
149
+ IAMService.getValueFromToken("email");
150
+ IAMService.getValueFromIDToken("name"); // getValueFromIdToken also works
151
+
152
+ // Check roles
153
+ IAMService.hasRealmRole("admin");
154
+ IAMService.hasClientRole("editor");
155
+
156
+ // Auth actions
157
+ IAMService.doLogin();
158
+ IAMService.doLogout();
159
+ await IAMService.updateIAMToken(); // Refresh token
160
+
161
+ // Encryption (if configured)
162
+ await IAMService.doEncrypt([{ data: "secret", tags: ["personal"] }]);
163
+ await IAMService.doDecrypt([{ encrypted: "...", tags: ["personal"] }]);
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Events
169
+
170
+ Know when things happen:
171
+
172
+ ```js
173
+ IAMService
174
+ .on("authSuccess", () => {
175
+ // User logged in successfully
176
+ })
177
+ .on("authError", (_event, err) => {
178
+ // Login failed
179
+ })
180
+ .on("logout", () => {
181
+ // User logged out
182
+ })
183
+ .on("tokenExpired", () => {
184
+ // Token expired - SDK will try to refresh
185
+ });
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Encryption
191
+
192
+ If your TideCloak is set up for encryption, you can protect sensitive data:
193
+
194
+ ```js
195
+ // Encrypt
196
+ const [encrypted] = await IAMService.doEncrypt([
197
+ { data: "sensitive info", tags: ["personal"] }
198
+ ]);
199
+
200
+ // Decrypt
201
+ const [decrypted] = await IAMService.doDecrypt([
202
+ { encrypted, tags: ["personal"] }
203
+ ]);
204
+ ```
205
+
206
+ Users need the right roles (`_tide_<tag>.selfencrypt` / `_tide_<tag>.selfdecrypt`) to encrypt/decrypt with specific tags.
207
+
208
+ Native mode doesn't support decryption policies. Passing a policy to `doEncrypt` or `doDecrypt` throws, and the draft/commit encryption methods aren't available.
209
+
210
+ ---
211
+
212
+ ## Platform Setup
213
+
214
+ ### Electron
215
+
216
+ You'll need to set up your main process to:
217
+
218
+ 1. **Create a popup window for login** (required for encryption):
219
+ ```js
220
+ // main.js
221
+ ipcMain.handle('open-auth-popup', async (event, url) => {
222
+ const authWindow = new BrowserWindow({
223
+ width: 500,
224
+ height: 700,
225
+ webPreferences: {
226
+ nodeIntegration: false,
227
+ contextIsolation: true,
228
+ },
229
+ });
230
+
231
+ // Listen for redirect
232
+ authWindow.webContents.on('will-redirect', (event, redirectUrl) => {
233
+ if (redirectUrl.startsWith('myapp://')) {
234
+ const url = new URL(redirectUrl);
235
+ const code = url.searchParams.get('code');
236
+ const error = url.searchParams.get('error');
237
+
238
+ // Send back to renderer
239
+ mainWindow.webContents.send('auth-callback', { code, error });
240
+ authWindow.close();
241
+ }
242
+ });
243
+
244
+ authWindow.loadURL(url);
245
+ });
246
+ ```
247
+
248
+ 2. **Register a custom protocol** (e.g., `myapp://`)
249
+
250
+ 3. **Store tokens securely** (use `safeStorage` or similar)
251
+
252
+ 4. **Handle token storage IPC calls**
253
+
254
+ ### Tauri
255
+
256
+ Similar to Electron - use a WebView window for login, register a protocol and handle callbacks.
257
+
258
+ ### React Native
259
+
260
+ Use deep links for the callback URL and secure storage for tokens.
261
+
262
+ ---
263
+
264
+ ## Troubleshooting
265
+
266
+ **Login opens but nothing happens after**
267
+
268
+ Make sure your redirect URI is registered in TideCloak and your app is handling the callback.
269
+
270
+ **Tokens not persisting**
271
+
272
+ Check that your `saveTokens` and `getTokens` functions are working correctly.
273
+
274
+ **Encryption/decryption fails with "session key mismatch"**
275
+
276
+ You're probably using an external browser instead of a popup window. The session cookies from login must stay in your Electron app for encryption/decryption to work.
277
+
278
+ Fix: Use `new BrowserWindow()` to open the login page, not `shell.openExternal()`.
279
+
280
+ **Encryption/decryption fails with missing config**
281
+
282
+ Make sure your `tidecloak.json` includes:
283
+ - `vendorId` - Your Tide vendor ID
284
+ - `client-origin-auth-{origin}` - Auth signature for your app's origin (e.g., `client-origin-auth-http://localhost:5174`)