@bnbagent/deploy-provider-bnb 0.3.0

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/src/session.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Session storage for the trial platform: short-lived access token + single-use
3
+ * refresh token, persisted at ~/.bnbagent-deploy/bnb/session.json (mode 0600).
4
+ *
5
+ * ★ Refresh tokens are SINGLE-USE (the platform rotates them and revokes the
6
+ * whole session family on reuse) — always persist the newest pair atomically.
7
+ */
8
+ import { existsSync } from "node:fs";
9
+ import { chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+
13
+ export interface Session {
14
+ /** The API base URL this session was minted against. */
15
+ apiUrl: string;
16
+ access_token: string;
17
+ refresh_token: string;
18
+ }
19
+
20
+ function sessionPath(): string {
21
+ const home = process.env.BNBAGENT_DEPLOY_HOME || join(homedir(), ".bnbagent-deploy");
22
+ return join(home, "bnb", "session.json");
23
+ }
24
+
25
+ export async function withSessionLock<T>(
26
+ fn: () => Promise<T>,
27
+ opts: { timeoutMs?: number; staleMs?: number; retryMs?: number } = {},
28
+ ): Promise<T> {
29
+ const p = `${sessionPath()}.lock`;
30
+ const timeoutMs = opts.timeoutMs ?? 60_000;
31
+ const staleMs = opts.staleMs ?? 5 * 60_000;
32
+ const retryMs = opts.retryMs ?? 50;
33
+ const deadline = Date.now() + timeoutMs;
34
+ await mkdir(dirname(p), { recursive: true });
35
+
36
+ for (;;) {
37
+ try {
38
+ const handle = await open(p, "wx", 0o600);
39
+ try {
40
+ await handle.writeFile(JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString() }));
41
+ return await fn();
42
+ } finally {
43
+ await handle.close().catch(() => {});
44
+ await rm(p, { force: true }).catch(() => {});
45
+ }
46
+ } catch (e) {
47
+ if ((e as NodeJS.ErrnoException).code !== "EEXIST") throw e;
48
+ try {
49
+ const lock = await stat(p);
50
+ if (Date.now() - lock.mtimeMs > staleMs) {
51
+ await rm(p, { force: true });
52
+ continue;
53
+ }
54
+ } catch (statError) {
55
+ if ((statError as NodeJS.ErrnoException).code === "ENOENT") continue;
56
+ throw statError;
57
+ }
58
+ if (Date.now() >= deadline) {
59
+ throw new Error(`Timed out waiting for the BNB session lock (${p}).`);
60
+ }
61
+ await Bun.sleep(retryMs);
62
+ }
63
+ }
64
+ }
65
+
66
+ export async function loadSession(): Promise<Session | undefined> {
67
+ const p = sessionPath();
68
+ if (!existsSync(p)) return undefined;
69
+ try {
70
+ const s = JSON.parse(await readFile(p, "utf8")) as Session;
71
+ return s.access_token && s.refresh_token ? s : undefined;
72
+ } catch {
73
+ return undefined; // corrupt file → treat as signed out
74
+ }
75
+ }
76
+
77
+ export async function saveSession(s: Session): Promise<void> {
78
+ const p = sessionPath();
79
+ await mkdir(dirname(p), { recursive: true });
80
+ // Atomic + never world-readable: write to a 0600 temp file, then rename over
81
+ // the target (rename is atomic on the same filesystem). Avoids both the torn-
82
+ // write and the create-0644-then-chmod window that would briefly expose the token.
83
+ const tmp = `${p}.${process.pid}.${crypto.randomUUID()}.tmp`;
84
+ await writeFile(tmp, JSON.stringify(s, null, 2) + "\n", { mode: 0o600 });
85
+ await chmod(tmp, 0o600); // umask can mask the create mode; enforce it explicitly
86
+ await rename(tmp, p);
87
+ }
88
+
89
+ export async function clearSession(): Promise<boolean> {
90
+ const p = sessionPath();
91
+ if (!existsSync(p)) return false;
92
+ await rm(p, { force: true });
93
+ return true;
94
+ }