edsger 0.72.2 → 0.72.3

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.
@@ -19,5 +19,16 @@ export declare function getSupabase(): SupabaseClient;
19
19
  * usable for direct-SDK calls.
20
20
  */
21
21
  export declare function hasSupabaseSession(): boolean;
22
+ /**
23
+ * Wait for the most recent setSession to finish applying, then report whether
24
+ * the client actually holds a usable user session.
25
+ *
26
+ * Returns false when the synced token was stale/expired and supabase-js
27
+ * dropped the session — in that state REST calls run as `anon` (auth.uid() is
28
+ * NULL) and any insert into an RLS table with `WITH CHECK (auth.uid() =
29
+ * user_id)` is rejected. Writers should gate the direct-SDK path on this and
30
+ * fall back to the MCP edge function (service-role) when it returns false.
31
+ */
32
+ export declare function ensureSupabaseSession(): Promise<boolean>;
22
33
  /** Reset module state. Test-only. */
23
34
  export declare function resetSupabaseClient(): void;
@@ -14,6 +14,8 @@ const AUTH_FILE = join(homedir(), '.edsger', 'auth.json');
14
14
  let _client = null;
15
15
  let _watcherInstalled = false;
16
16
  let _lastAppliedAccessToken;
17
+ /** The in-flight setSession promise, awaited by ensureSupabaseSession(). */
18
+ let _sessionReady = null;
17
19
  /**
18
20
  * Get (or lazily create) the shared SupabaseClient.
19
21
  *
@@ -39,7 +41,11 @@ export function getSupabase() {
39
41
  detectSessionInUrl: false,
40
42
  },
41
43
  });
42
- void _client.auth.setSession({
44
+ // Capture the setSession promise so writers can await it (via
45
+ // ensureSupabaseSession) before issuing inserts. Without this, the first
46
+ // REST call can race ahead of setSession and go out as `anon` (no user
47
+ // JWT), tripping the `auth.uid() = user_id` RLS check.
48
+ _sessionReady = _client.auth.setSession({
43
49
  access_token: accessToken,
44
50
  refresh_token: refreshToken ?? '',
45
51
  });
@@ -54,6 +60,32 @@ export function getSupabase() {
54
60
  export function hasSupabaseSession() {
55
61
  return Boolean(getSupabaseUrl() && getSupabaseAnonKey() && getAccessToken());
56
62
  }
63
+ /**
64
+ * Wait for the most recent setSession to finish applying, then report whether
65
+ * the client actually holds a usable user session.
66
+ *
67
+ * Returns false when the synced token was stale/expired and supabase-js
68
+ * dropped the session — in that state REST calls run as `anon` (auth.uid() is
69
+ * NULL) and any insert into an RLS table with `WITH CHECK (auth.uid() =
70
+ * user_id)` is rejected. Writers should gate the direct-SDK path on this and
71
+ * fall back to the MCP edge function (service-role) when it returns false.
72
+ */
73
+ export async function ensureSupabaseSession() {
74
+ if (!hasSupabaseSession()) {
75
+ return false;
76
+ }
77
+ try {
78
+ const client = getSupabase();
79
+ if (_sessionReady) {
80
+ await _sessionReady;
81
+ }
82
+ const { data } = await client.auth.getSession();
83
+ return Boolean(data.session?.access_token);
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
57
89
  function installAuthWatcher() {
58
90
  if (_watcherInstalled) {
59
91
  return;
@@ -70,7 +102,7 @@ function installAuthWatcher() {
70
102
  return;
71
103
  }
72
104
  _lastAppliedAccessToken = nextAccess;
73
- void _client.auth.setSession({
105
+ _sessionReady = _client.auth.setSession({
74
106
  access_token: nextAccess,
75
107
  refresh_token: nextRefresh ?? '',
76
108
  });
@@ -87,4 +119,5 @@ export function resetSupabaseClient() {
87
119
  _client = null;
88
120
  _watcherInstalled = false;
89
121
  _lastAppliedAccessToken = undefined;
122
+ _sessionReady = null;
90
123
  }
@@ -10,7 +10,7 @@ import { hostname } from 'os';
10
10
  import { callMcpEndpoint } from '../api/mcp-client.js';
11
11
  import { getUserId } from '../auth/auth-store.js';
12
12
  import { getVersion } from '../constants.js';
13
- import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
13
+ import { ensureSupabaseSession, getSupabase, hasSupabaseSession, } from '../supabase/client.js';
14
14
  import { initLogSync, logInfo, logWarning, stopLogSync, } from '../utils/logger.js';
15
15
  let currentSessionId = null;
16
16
  let heartbeatTimer;
@@ -63,7 +63,13 @@ export async function registerSession(options) {
63
63
  const invocation = process.argv.slice(2).join(' ') || undefined;
64
64
  try {
65
65
  const userId = getUserId();
66
- if (hasSupabaseSession() && userId) {
66
+ // ensureSupabaseSession() awaits the in-flight setSession and confirms the
67
+ // client holds a live user session — so a token that hasn't applied yet
68
+ // (race) or was dropped (stale → anon) doesn't silently write as anon and
69
+ // trip the cli_sessions RLS check. The desktop refreshes the token into
70
+ // auth.json before spawning the CLI, so this should hold for app-spawned
71
+ // runs; the MCP branch stays for the legacy no-Supabase-session path.
72
+ if (userId && (await ensureSupabaseSession())) {
67
73
  const row = {
68
74
  session_id: sessionId,
69
75
  user_id: userId,
@@ -1,7 +1,7 @@
1
1
  import { callMcpEndpoint } from '../api/mcp-client.js';
2
2
  import { getUserId } from '../auth/auth-store.js';
3
3
  import { getVersion } from '../constants.js';
4
- import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
4
+ import { ensureSupabaseSession, getSupabase } from '../supabase/client.js';
5
5
  export const colors = {
6
6
  reset: '\x1b[0m',
7
7
  bright: '\x1b[1m',
@@ -49,13 +49,16 @@ export async function flushLogs() {
49
49
  return;
50
50
  }
51
51
  const batch = _logBuffer.splice(0);
52
+ const sessionId = _logSyncSessionId;
52
53
  try {
53
54
  const userId = getUserId();
54
- if (hasSupabaseSession() && userId) {
55
+ if (userId && (await ensureSupabaseSession())) {
55
56
  // Direct-SDK path. user_id is set explicitly per row to satisfy the
56
- // cli_logs RLS check (auth.uid() = user_id).
57
+ // cli_logs RLS check (auth.uid() = user_id). Gating on a confirmed
58
+ // session keeps a stale token (→ anon) from writing as anon and tripping
59
+ // RLS — desktop refreshes the token into auth.json before spawning.
57
60
  const rows = batch.slice(0, 200).map((log) => ({
58
- session_id: _logSyncSessionId,
61
+ session_id: sessionId,
59
62
  user_id: userId,
60
63
  level: log.level,
61
64
  message: log.message,
@@ -68,7 +71,7 @@ export async function flushLogs() {
68
71
  }
69
72
  else {
70
73
  await callMcpEndpoint('cli_logs/batch', {
71
- session_id: _logSyncSessionId,
74
+ session_id: sessionId,
72
75
  logs: batch,
73
76
  });
74
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.72.2",
3
+ "version": "0.72.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"