@notis_ai/cli 0.2.0-beta.92.1 → 0.2.0-beta.99.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.
@@ -3,11 +3,11 @@ import { createReadStream } from 'node:fs';
3
3
  import { CliError, EXIT_CODES } from './errors.js';
4
4
  import {
5
5
  DEFAULT_PROFILE,
6
- ensureProfile,
7
6
  getProfile,
7
+ isJwtExpired,
8
8
  loadConfig,
9
- saveConfig,
10
9
  } from './profiles.js';
10
+ import { createExpiredAuthError, createInvalidAuthHints } from './desktop-auth.js';
11
11
 
12
12
  function escapeMultipartHeaderValue(value) {
13
13
  return String(value ?? '')
@@ -76,7 +76,7 @@ function createMultipartFileUpload(payload, bindingMetadata, fileBindings) {
76
76
  };
77
77
  }
78
78
 
79
- function normalizeBackendError(status, payload) {
79
+ function normalizeBackendError(status, payload, runtime) {
80
80
  const backendError = payload?.error;
81
81
  const message =
82
82
  backendError?.message ||
@@ -91,10 +91,7 @@ function normalizeBackendError(status, payload) {
91
91
  message,
92
92
  exitCode: EXIT_CODES.auth,
93
93
  hints: [
94
- {
95
- command: 'Open the Notis desktop app and sign in',
96
- reason: 'Open the Notis desktop app to refresh CLI auth, or set NOTIS_JWT',
97
- },
94
+ ...createInvalidAuthHints(runtime),
98
95
  ],
99
96
  details: payload || {},
100
97
  });
@@ -145,46 +142,10 @@ function normalizeBackendError(status, payload) {
145
142
  });
146
143
  }
147
144
 
148
- function shouldAttemptDevPortalRefresh(runtime, { force = false } = {}) {
149
- if (runtime.authMode !== 'dev_portal' || !runtime.refreshToken || !runtime.apiBase) {
150
- return false;
151
- }
152
-
153
- if (force) {
154
- return true;
155
- }
156
-
157
- if (typeof runtime.accessExpiresAt !== 'number' || runtime.accessExpiresAt <= 0) {
145
+ function reloadJwtFromConfig(runtime) {
146
+ if (runtime.credentialSource === 'env') {
158
147
  return false;
159
148
  }
160
-
161
- const thresholdSeconds = 60;
162
- return runtime.accessExpiresAt - Math.floor(Date.now() / 1000) <= thresholdSeconds;
163
- }
164
-
165
- function persistDevPortalRuntimeAuth(runtime, session) {
166
- const config = ensureProfile(loadConfig(), runtime.profileName || DEFAULT_PROFILE);
167
- const profileName = runtime.profileName || DEFAULT_PROFILE;
168
- config.current_profile = profileName;
169
- config.profiles[profileName] = {
170
- ...config.profiles[profileName],
171
- jwt: session.access_token,
172
- api_base: runtime.apiBase,
173
- auth_mode: 'dev_portal',
174
- refresh_token: session.refresh_token,
175
- access_expires_at: session.access_expires_at,
176
- refresh_expires_at: session.refresh_expires_at,
177
- };
178
- saveConfig(config);
179
-
180
- runtime.jwt = session.access_token;
181
- runtime.authMode = 'dev_portal';
182
- runtime.refreshToken = session.refresh_token;
183
- runtime.accessExpiresAt = session.access_expires_at;
184
- runtime.refreshExpiresAt = session.refresh_expires_at;
185
- }
186
-
187
- function reloadJwtFromConfig(runtime) {
188
149
  const profileName = runtime.profileName || DEFAULT_PROFILE;
189
150
  let profile;
190
151
  try {
@@ -197,39 +158,8 @@ function reloadJwtFromConfig(runtime) {
197
158
  return false;
198
159
  }
199
160
  runtime.jwt = nextJwt;
200
- runtime.authMode = profile.auth_mode;
201
- runtime.refreshToken = profile.refresh_token;
202
- runtime.accessExpiresAt = profile.access_expires_at;
203
- runtime.refreshExpiresAt = profile.refresh_expires_at;
204
- return true;
205
- }
206
-
207
- async function maybeRefreshDevPortalAuth(runtime, { force = false } = {}) {
208
- if (!shouldAttemptDevPortalRefresh(runtime, { force })) {
209
- return false;
210
- }
211
-
212
- const response = await fetch(`${runtime.apiBase}/portal_auth/dev-refresh`, {
213
- method: 'POST',
214
- headers: {
215
- 'Content-Type': 'application/json',
216
- 'X-Notis-CLI-Version': runtime.cliVersion,
217
- },
218
- body: JSON.stringify({ refresh_token: runtime.refreshToken }),
219
- });
220
-
221
- let payload = null;
222
- try {
223
- payload = await response.json();
224
- } catch {
225
- payload = null;
226
- }
227
-
228
- if (!response.ok || !payload?.session?.access_token || !payload?.session?.refresh_token) {
229
- throw normalizeBackendError(response.status, payload);
230
- }
231
-
232
- persistDevPortalRuntimeAuth(runtime, payload.session);
161
+ runtime.desktopAppName = profile.desktop_app_name;
162
+ runtime.desktopPid = profile.desktop_pid;
233
163
  return true;
234
164
  }
235
165
 
@@ -241,7 +171,14 @@ export async function httpRequest({
241
171
  multipart = false,
242
172
  requireAuth = true,
243
173
  }) {
244
- await maybeRefreshDevPortalAuth(runtime);
174
+ // The desktop renderer is the single owner of the rotating Supabase refresh
175
+ // token. Each CLI request only consumes the newest access token it synced to
176
+ // disk, avoiding refresh-token races between independent CLI processes and
177
+ // the running desktop session.
178
+ reloadJwtFromConfig(runtime);
179
+ if (requireAuth && isJwtExpired(runtime.jwt)) {
180
+ throw createExpiredAuthError(runtime);
181
+ }
245
182
 
246
183
  let controller = new AbortController();
247
184
  let timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
@@ -298,8 +235,7 @@ export async function httpRequest({
298
235
  }
299
236
 
300
237
  if (response.status === 401) {
301
- const refreshed = (await maybeRefreshDevPortalAuth(runtime, { force: true }))
302
- || reloadJwtFromConfig(runtime);
238
+ const refreshed = reloadJwtFromConfig(runtime) && !isJwtExpired(runtime.jwt);
303
239
  if (refreshed) {
304
240
  if (requireAuth && runtime.jwt) {
305
241
  headers.Authorization = `Bearer ${runtime.jwt}`;
@@ -324,7 +260,10 @@ export async function httpRequest({
324
260
  clearTimeout(timeout);
325
261
 
326
262
  if (!response.ok) {
327
- throw normalizeBackendError(response.status, payload);
263
+ if (response.status === 401 && isJwtExpired(runtime.jwt)) {
264
+ throw createExpiredAuthError(runtime);
265
+ }
266
+ throw normalizeBackendError(response.status, payload, runtime);
328
267
  }
329
268
 
330
269
  return {
Binary file