northbase 0.1.4 → 0.1.6

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/northbase.mjs +127 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "northbase",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Local-first CLI for reading and writing text files stored in Supabase",
5
5
  "type": "module",
6
6
  "bin": {
package/src/northbase.mjs CHANGED
@@ -16,6 +16,11 @@ const INDEX_PATH = path.join(NORTHBASE_DIR, "index.json");
16
16
  const SESSION_PATH = path.join(NORTHBASE_DIR, "session.json");
17
17
  const MAX_BYTES = 500_000;
18
18
 
19
+ const DEBUG = !!process.env.NORTHBASE_DEBUG;
20
+ function debug(...args) {
21
+ if (DEBUG) console.error("NORTHBASE DEBUG", ...args);
22
+ }
23
+
19
24
  // ── directory / index helpers ─────────────────────────────────────────────────
20
25
 
21
26
  function ensureDir(p) {
@@ -37,6 +42,13 @@ function saveIndex(idx) {
37
42
 
38
43
  // ── session helpers ───────────────────────────────────────────────────────────
39
44
 
45
+ function normalizeSession(s) {
46
+ let expiresAt = s.expires_at;
47
+ if (expiresAt && expiresAt > 1e12) expiresAt = Math.floor(expiresAt / 1000); // ms → s
48
+ if (!expiresAt && s.expires_in) expiresAt = Math.floor(Date.now() / 1000) + s.expires_in;
49
+ return { ...s, expires_at: expiresAt ?? 0 };
50
+ }
51
+
40
52
  function loadSession() {
41
53
  try {
42
54
  const s = JSON.parse(fs.readFileSync(SESSION_PATH, "utf8"));
@@ -49,7 +61,11 @@ function loadSession() {
49
61
 
50
62
  function saveSession(session) {
51
63
  ensureDir(NORTHBASE_DIR);
52
- fs.writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2), { mode: 0o600 });
64
+ const normalized = normalizeSession(session);
65
+ const tmp = SESSION_PATH + ".tmp";
66
+ fs.writeFileSync(tmp, JSON.stringify(normalized, null, 2), { mode: 0o600 });
67
+ fs.renameSync(tmp, SESSION_PATH);
68
+ debug("session.json written expires_at=" + normalized.expires_at);
53
69
  }
54
70
 
55
71
  function deleteSession() {
@@ -58,34 +74,62 @@ function deleteSession() {
58
74
 
59
75
  // ── authenticated supabase client ─────────────────────────────────────────────
60
76
 
77
+ async function doRefresh(supabase, stored) {
78
+ debug("refresh starting");
79
+ console.error("NORTHBASE session refreshing");
80
+ const { data, error } = await supabase.auth.refreshSession({
81
+ refresh_token: stored.refresh_token,
82
+ });
83
+ if (error) {
84
+ const revoked = error.message?.includes("invalid_grant")
85
+ || error.error === "invalid_grant"
86
+ || error.code === "invalid_grant";
87
+ if (revoked) {
88
+ console.error("NORTHBASE session revoked — deleting session.json");
89
+ deleteSession();
90
+ throw new Error("Session revoked. Run `northbase login`.");
91
+ }
92
+ // Transient error (network, rate limit, etc.) — do NOT delete session.json
93
+ throw new Error(`Session refresh failed (${error.message}) — session preserved, will retry next command.`);
94
+ }
95
+ const newSession = data.session;
96
+ if (!newSession?.access_token || !newSession?.refresh_token) {
97
+ throw new Error("Refresh returned incomplete session. Run `northbase login`.");
98
+ }
99
+ debug(`refresh ok refresh_token_rotated=${newSession.refresh_token !== stored.refresh_token}`);
100
+ saveSession(newSession);
101
+ const { error: setErr } = await supabase.auth.setSession({
102
+ access_token: newSession.access_token,
103
+ refresh_token: newSession.refresh_token,
104
+ });
105
+ if (setErr) throw setErr;
106
+ }
107
+
61
108
  async function getAuthenticatedClient() {
62
- const stored = loadSession(); // throws "Not logged in" if missing/corrupt
109
+ const stored = loadSession();
110
+ debug(`session loaded expires_at=${stored.expires_at}`);
63
111
 
64
112
  const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
65
113
  auth: { persistSession: false, autoRefreshToken: false },
66
114
  });
67
115
 
68
- const nowSec = Math.floor(Date.now() / 1000);
69
- const isExpired = stored.expires_at && nowSec >= stored.expires_at - 60;
116
+ const nowSec = Math.floor(Date.now() / 1000);
117
+ const expiresAt = stored.expires_at ?? 0;
118
+ const secsRemaining = expiresAt - nowSec;
119
+ const needsRefresh = expiresAt === 0 || secsRemaining <= 60;
120
+ debug(`seconds_remaining=${secsRemaining} needs_refresh=${needsRefresh}`);
70
121
 
71
- if (isExpired) {
72
- console.error("NORTHBASE session refreshing");
73
- const { data, error } = await supabase.auth.refreshSession({
74
- refresh_token: stored.refresh_token,
75
- });
76
- if (error) throw new Error("Session expired. Run `northbase login` again.");
77
- saveSession(data.session);
78
- const { error: setErr } = await supabase.auth.setSession({
79
- access_token: data.session.access_token,
80
- refresh_token: data.session.refresh_token,
81
- });
82
- if (setErr) throw setErr;
122
+ if (needsRefresh) {
123
+ await doRefresh(supabase, stored);
83
124
  } else {
84
125
  const { error } = await supabase.auth.setSession({
85
126
  access_token: stored.access_token,
86
127
  refresh_token: stored.refresh_token,
87
128
  });
88
- if (error) throw new Error("Session invalid. Run `northbase login` again.");
129
+ if (error) {
130
+ debug(`setSession failed (${error.message}) — falling back to refresh`);
131
+ await doRefresh(supabase, stored);
132
+ }
89
133
  }
90
134
 
91
135
  return supabase;
@@ -356,16 +400,75 @@ async function cmdWhoami() {
356
400
  console.log(`Logged in as ${email} (${id})`);
357
401
  }
358
402
 
403
+ async function cmdSession() {
404
+ let stored;
405
+ try { stored = loadSession(); } catch {
406
+ console.log("Not logged in.");
407
+ return;
408
+ }
409
+ const nowSec = Math.floor(Date.now() / 1000);
410
+ const expiresAt = stored.expires_at ?? 0;
411
+ const secsRemaining = expiresAt - nowSec;
412
+ console.log(`now=${nowSec}`);
413
+ console.log(`expires_at=${expiresAt}`);
414
+ console.log(`seconds_remaining=${secsRemaining}`);
415
+ console.log(`will_refresh_soon=${secsRemaining <= 60}`);
416
+ console.log(`email=${stored.user?.email ?? "(unknown)"}`);
417
+ }
418
+
419
+ async function cmdAuthDebug() {
420
+ const exists = fs.existsSync(SESSION_PATH);
421
+ console.log(`session_file_exists=${exists}`);
422
+ if (!exists) { console.log("status=not_logged_in"); return; }
423
+
424
+ let stored;
425
+ try {
426
+ stored = JSON.parse(fs.readFileSync(SESSION_PATH, "utf8"));
427
+ } catch (e) {
428
+ console.log(`session_file_parse_error=${e.message}`);
429
+ return;
430
+ }
431
+
432
+ const nowSec = Math.floor(Date.now() / 1000);
433
+ const expiresAt = stored.expires_at ?? 0;
434
+ const secsRemaining = expiresAt - nowSec;
435
+
436
+ console.log(`has_access_token=${!!stored.access_token}`);
437
+ console.log(`has_refresh_token=${!!stored.refresh_token}`);
438
+ console.log(`expires_at=${expiresAt}`);
439
+ console.log(`seconds_remaining=${secsRemaining}`);
440
+ console.log(`will_refresh_soon=${secsRemaining <= 60}`);
441
+ console.log(`email=${stored.user?.email ?? "(unknown)"}`);
442
+
443
+ let refreshAttempted = false, refreshSucceeded = false, userFetchSucceeded = false;
444
+ try {
445
+ const supabase = await getAuthenticatedClient();
446
+ const after = JSON.parse(fs.readFileSync(SESSION_PATH, "utf8"));
447
+ refreshAttempted = secsRemaining <= 60 || after.expires_at !== expiresAt;
448
+ refreshSucceeded = !!after.access_token;
449
+ const { data, error } = await supabase.auth.getUser();
450
+ userFetchSucceeded = !error && !!data?.user;
451
+ } catch (e) {
452
+ console.log(`auth_error=${e.message}`);
453
+ }
454
+
455
+ console.log(`refresh_attempted=${refreshAttempted}`);
456
+ console.log(`refresh_succeeded=${refreshSucceeded}`);
457
+ console.log(`user_fetch_succeeded=${userFetchSucceeded}`);
458
+ }
459
+
359
460
  // ── main ──────────────────────────────────────────────────────────────────────
360
461
 
361
462
  async function main() {
362
463
  const [cmd, ...args] = process.argv.slice(2);
363
464
 
364
- if (cmd === "login") { await cmdLogin(); return; }
365
- if (cmd === "logout") { await cmdLogout(); return; }
366
- if (cmd === "whoami") { await cmdWhoami(); return; }
367
- if (cmd === "list") { await cmdList(args[0]); return; }
368
- if (cmd === "pull") { await cmdPull(args[0]); return; }
465
+ if (cmd === "login") { await cmdLogin(); return; }
466
+ if (cmd === "logout") { await cmdLogout(); return; }
467
+ if (cmd === "whoami") { await cmdWhoami(); return; }
468
+ if (cmd === "session") { await cmdSession(); return; }
469
+ if (cmd === "auth-debug") { await cmdAuthDebug(); return; }
470
+ if (cmd === "list") { await cmdList(args[0]); return; }
471
+ if (cmd === "pull") { await cmdPull(args[0]); return; }
369
472
 
370
473
  if (cmd === "get") {
371
474
  const rel = args[0];
@@ -390,6 +493,8 @@ async function main() {
390
493
  console.log(" northbase login");
391
494
  console.log(" northbase logout");
392
495
  console.log(" northbase whoami");
496
+ console.log(" northbase session");
497
+ console.log(" northbase auth-debug");
393
498
  console.log(" northbase list [prefix]");
394
499
  console.log(" northbase pull [prefix]");
395
500
  console.log(" northbase get <path>");