northbase 0.1.4 → 0.1.5

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 +80 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "northbase",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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,58 @@ 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") || error.status === 400;
85
+ if (revoked) deleteSession();
86
+ throw new Error(revoked
87
+ ? "Session revoked. Run `northbase login`."
88
+ : `Session refresh failed (${error.message}). Run \`northbase login\`.`
89
+ );
90
+ }
91
+ const newSession = data.session;
92
+ if (!newSession?.access_token || !newSession?.refresh_token) {
93
+ throw new Error("Refresh returned incomplete session. Run `northbase login`.");
94
+ }
95
+ debug(`refresh ok refresh_token_rotated=${newSession.refresh_token !== stored.refresh_token}`);
96
+ saveSession(newSession);
97
+ const { error: setErr } = await supabase.auth.setSession({
98
+ access_token: newSession.access_token,
99
+ refresh_token: newSession.refresh_token,
100
+ });
101
+ if (setErr) throw setErr;
102
+ }
103
+
61
104
  async function getAuthenticatedClient() {
62
- const stored = loadSession(); // throws "Not logged in" if missing/corrupt
105
+ const stored = loadSession();
106
+ debug(`session loaded expires_at=${stored.expires_at}`);
63
107
 
64
108
  const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
65
109
  auth: { persistSession: false, autoRefreshToken: false },
66
110
  });
67
111
 
68
- const nowSec = Math.floor(Date.now() / 1000);
69
- const isExpired = stored.expires_at && nowSec >= stored.expires_at - 60;
112
+ const nowSec = Math.floor(Date.now() / 1000);
113
+ const expiresAt = stored.expires_at ?? 0;
114
+ const secsRemaining = expiresAt - nowSec;
115
+ const needsRefresh = expiresAt === 0 || secsRemaining <= 60;
116
+ debug(`seconds_remaining=${secsRemaining} needs_refresh=${needsRefresh}`);
70
117
 
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;
118
+ if (needsRefresh) {
119
+ await doRefresh(supabase, stored);
83
120
  } else {
84
121
  const { error } = await supabase.auth.setSession({
85
122
  access_token: stored.access_token,
86
123
  refresh_token: stored.refresh_token,
87
124
  });
88
- if (error) throw new Error("Session invalid. Run `northbase login` again.");
125
+ if (error) {
126
+ debug(`setSession failed (${error.message}) — falling back to refresh`);
127
+ await doRefresh(supabase, stored);
128
+ }
89
129
  }
90
130
 
91
131
  return supabase;
@@ -356,16 +396,33 @@ async function cmdWhoami() {
356
396
  console.log(`Logged in as ${email} (${id})`);
357
397
  }
358
398
 
399
+ async function cmdSession() {
400
+ let stored;
401
+ try { stored = loadSession(); } catch {
402
+ console.log("Not logged in.");
403
+ return;
404
+ }
405
+ const nowSec = Math.floor(Date.now() / 1000);
406
+ const expiresAt = stored.expires_at ?? 0;
407
+ const secsRemaining = expiresAt - nowSec;
408
+ console.log(`now=${nowSec}`);
409
+ console.log(`expires_at=${expiresAt}`);
410
+ console.log(`seconds_remaining=${secsRemaining}`);
411
+ console.log(`will_refresh_soon=${secsRemaining <= 60}`);
412
+ console.log(`email=${stored.user?.email ?? "(unknown)"}`);
413
+ }
414
+
359
415
  // ── main ──────────────────────────────────────────────────────────────────────
360
416
 
361
417
  async function main() {
362
418
  const [cmd, ...args] = process.argv.slice(2);
363
419
 
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; }
420
+ if (cmd === "login") { await cmdLogin(); return; }
421
+ if (cmd === "logout") { await cmdLogout(); return; }
422
+ if (cmd === "whoami") { await cmdWhoami(); return; }
423
+ if (cmd === "session") { await cmdSession(); return; }
424
+ if (cmd === "list") { await cmdList(args[0]); return; }
425
+ if (cmd === "pull") { await cmdPull(args[0]); return; }
369
426
 
370
427
  if (cmd === "get") {
371
428
  const rel = args[0];
@@ -390,6 +447,7 @@ async function main() {
390
447
  console.log(" northbase login");
391
448
  console.log(" northbase logout");
392
449
  console.log(" northbase whoami");
450
+ console.log(" northbase session");
393
451
  console.log(" northbase list [prefix]");
394
452
  console.log(" northbase pull [prefix]");
395
453
  console.log(" northbase get <path>");