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.
- package/package.json +1 -1
- package/src/northbase.mjs +127 -22
package/package.json
CHANGED
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
|
-
|
|
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();
|
|
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
|
|
69
|
-
const
|
|
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 (
|
|
72
|
-
|
|
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)
|
|
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")
|
|
365
|
-
if (cmd === "logout")
|
|
366
|
-
if (cmd === "whoami")
|
|
367
|
-
if (cmd === "
|
|
368
|
-
if (cmd === "
|
|
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>");
|