@naisys/common-node 3.0.0-beta.4 → 3.0.0-beta.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.
@@ -4,77 +4,90 @@ import yaml from "js-yaml";
4
4
  import * as path from "path";
5
5
  /** Loads agent yaml configs from a file or directory path, returns a map of userId → UserEntry */
6
6
  export function loadAgentConfigs(startupPath) {
7
- const configEntries = [];
8
- const usernameToPath = new Map();
9
- const resolvedPath = path.resolve(startupPath);
10
- if (fs.statSync(resolvedPath).isDirectory()) {
11
- processDirectory(resolvedPath, undefined, configEntries, usernameToPath);
12
- }
13
- else {
14
- processFile(resolvedPath, undefined, configEntries, usernameToPath);
15
- }
16
- // Add admin if not present
17
- const hasAdmin = configEntries.some((e) => e.username === adminAgentConfig.username);
18
- if (!hasAdmin) {
19
- configEntries.push({
20
- username: adminAgentConfig.username,
21
- leadEntryIndex: undefined,
22
- config: adminAgentConfig,
23
- });
24
- }
25
- // Build userId map (1-based sequential IDs)
26
- const userMap = new Map();
27
- for (let i = 0; i < configEntries.length; i++) {
28
- const entry = configEntries[i];
29
- const userId = i + 1;
30
- const leadUserId = entry.leadEntryIndex !== undefined ? entry.leadEntryIndex + 1 : undefined;
31
- userMap.set(userId, {
32
- userId,
33
- username: entry.username,
34
- enabled: true,
35
- leadUserId,
36
- config: entry.config,
37
- });
38
- }
39
- return userMap;
7
+ const configEntries = [];
8
+ const usernameToPath = new Map();
9
+ const resolvedPath = path.resolve(startupPath);
10
+ if (fs.statSync(resolvedPath).isDirectory()) {
11
+ processDirectory(resolvedPath, undefined, configEntries, usernameToPath);
12
+ } else {
13
+ processFile(resolvedPath, undefined, configEntries, usernameToPath);
14
+ }
15
+ // Add admin if not present
16
+ const hasAdmin = configEntries.some(
17
+ (e) => e.username === adminAgentConfig.username,
18
+ );
19
+ if (!hasAdmin) {
20
+ configEntries.push({
21
+ username: adminAgentConfig.username,
22
+ leadEntryIndex: undefined,
23
+ config: adminAgentConfig,
24
+ });
25
+ }
26
+ // Build userId map (1-based sequential IDs)
27
+ const userMap = new Map();
28
+ for (let i = 0; i < configEntries.length; i++) {
29
+ const entry = configEntries[i];
30
+ const userId = i + 1;
31
+ const leadUserId =
32
+ entry.leadEntryIndex !== undefined ? entry.leadEntryIndex + 1 : undefined;
33
+ userMap.set(userId, {
34
+ userId,
35
+ username: entry.username,
36
+ enabled: true,
37
+ leadUserId,
38
+ config: entry.config,
39
+ });
40
+ }
41
+ return userMap;
40
42
  }
41
- function processDirectory(dirPath, leadEntryIndex, configEntries, usernameToPath) {
42
- const files = fs.readdirSync(dirPath);
43
- for (const file of files) {
44
- if (file.endsWith(".yaml") || file.endsWith(".yml")) {
45
- processFile(path.join(dirPath, file), leadEntryIndex, configEntries, usernameToPath);
46
- }
43
+ function processDirectory(
44
+ dirPath,
45
+ leadEntryIndex,
46
+ configEntries,
47
+ usernameToPath,
48
+ ) {
49
+ const files = fs.readdirSync(dirPath);
50
+ for (const file of files) {
51
+ if (file.endsWith(".yaml") || file.endsWith(".yml")) {
52
+ processFile(
53
+ path.join(dirPath, file),
54
+ leadEntryIndex,
55
+ configEntries,
56
+ usernameToPath,
57
+ );
47
58
  }
59
+ }
48
60
  }
49
61
  function processFile(filePath, leadEntryIndex, configEntries, usernameToPath) {
50
- const absolutePath = path.resolve(filePath);
51
- try {
52
- const configYaml = fs.readFileSync(absolutePath, "utf8");
53
- const configObj = yaml.load(configYaml);
54
- const agentConfig = AgentConfigFileSchema.parse(configObj);
55
- const username = agentConfig.username;
56
- // Check for duplicate usernames from different files
57
- const existingPath = usernameToPath.get(username);
58
- if (existingPath && existingPath !== absolutePath) {
59
- throw new Error(`Duplicate username "${username}" found in multiple files:\n ${existingPath}\n ${absolutePath}`);
60
- }
61
- usernameToPath.set(username, absolutePath);
62
- const currentIndex = configEntries.length;
63
- configEntries.push({
64
- username,
65
- leadEntryIndex,
66
- config: agentConfig,
67
- });
68
- console.log(`Loaded user: ${username} from ${filePath}`);
69
- // Check for a subdirectory matching the filename (without extension)
70
- const ext = path.extname(absolutePath);
71
- const baseName = path.basename(absolutePath, ext);
72
- const subDir = path.join(path.dirname(absolutePath), baseName);
73
- if (fs.existsSync(subDir) && fs.statSync(subDir).isDirectory()) {
74
- processDirectory(subDir, currentIndex, configEntries, usernameToPath);
75
- }
62
+ const absolutePath = path.resolve(filePath);
63
+ try {
64
+ const configYaml = fs.readFileSync(absolutePath, "utf8");
65
+ const configObj = yaml.load(configYaml);
66
+ const agentConfig = AgentConfigFileSchema.parse(configObj);
67
+ const username = agentConfig.username;
68
+ // Check for duplicate usernames from different files
69
+ const existingPath = usernameToPath.get(username);
70
+ if (existingPath && existingPath !== absolutePath) {
71
+ throw new Error(
72
+ `Duplicate username "${username}" found in multiple files:\n ${existingPath}\n ${absolutePath}`,
73
+ );
76
74
  }
77
- catch (e) {
78
- throw new Error(`Failed to process agent config at ${filePath}: ${e}`);
75
+ usernameToPath.set(username, absolutePath);
76
+ const currentIndex = configEntries.length;
77
+ configEntries.push({
78
+ username,
79
+ leadEntryIndex,
80
+ config: agentConfig,
81
+ });
82
+ console.log(`Loaded user: ${username} from ${filePath}`);
83
+ // Check for a subdirectory matching the filename (without extension)
84
+ const ext = path.extname(absolutePath);
85
+ const baseName = path.basename(absolutePath, ext);
86
+ const subDir = path.join(path.dirname(absolutePath), baseName);
87
+ if (fs.existsSync(subDir) && fs.statSync(subDir).isDirectory()) {
88
+ processDirectory(subDir, currentIndex, configEntries, usernameToPath);
79
89
  }
90
+ } catch (e) {
91
+ throw new Error(`Failed to process agent config at ${filePath}: ${e}`);
92
+ }
80
93
  }
@@ -3,7 +3,6 @@
3
3
  * Returns undefined if the header is missing or not in Bearer format.
4
4
  */
5
5
  export function extractBearerToken(authHeader) {
6
- if (!authHeader?.startsWith("Bearer "))
7
- return undefined;
8
- return authHeader.slice(7);
6
+ if (!authHeader?.startsWith("Bearer ")) return undefined;
7
+ return authHeader.slice(7);
9
8
  }
@@ -3,37 +3,37 @@ import fs from "fs";
3
3
  import yaml from "js-yaml";
4
4
  import path from "path";
5
5
  export function loadCustomModels(folder) {
6
- if (!folder) {
7
- return { llmModels: [], imageModels: [] };
8
- }
9
- const filePath = path.join(folder, "custom-models.yaml");
10
- if (!fs.existsSync(filePath)) {
11
- return { llmModels: [], imageModels: [] };
12
- }
13
- const raw = fs.readFileSync(filePath, "utf-8");
14
- const parsed = yaml.load(raw);
15
- const result = CustomModelsFileSchema.parse(parsed);
16
- return {
17
- llmModels: result.llmModels ?? [],
18
- imageModels: result.imageModels ?? [],
19
- };
6
+ if (!folder) {
7
+ return { llmModels: [], imageModels: [] };
8
+ }
9
+ const filePath = path.join(folder, "custom-models.yaml");
10
+ if (!fs.existsSync(filePath)) {
11
+ return { llmModels: [], imageModels: [] };
12
+ }
13
+ const raw = fs.readFileSync(filePath, "utf-8");
14
+ const parsed = yaml.load(raw);
15
+ const result = CustomModelsFileSchema.parse(parsed);
16
+ return {
17
+ llmModels: result.llmModels ?? [],
18
+ imageModels: result.imageModels ?? [],
19
+ };
20
20
  }
21
21
  export function saveCustomModels(data) {
22
- const folder = process.env.NAISYS_FOLDER;
23
- if (!folder) {
24
- throw new Error("NAISYS_FOLDER environment variable is not set");
25
- }
26
- // Validate before writing
27
- CustomModelsFileSchema.parse(data);
28
- // Omit empty arrays from output
29
- const output = {};
30
- if (data.llmModels && data.llmModels.length > 0) {
31
- output.llmModels = data.llmModels;
32
- }
33
- if (data.imageModels && data.imageModels.length > 0) {
34
- output.imageModels = data.imageModels;
35
- }
36
- const filePath = path.join(folder, "custom-models.yaml");
37
- const yamlStr = yaml.dump(output, { lineWidth: -1 });
38
- fs.writeFileSync(filePath, yamlStr, "utf-8");
22
+ const folder = process.env.NAISYS_FOLDER;
23
+ if (!folder) {
24
+ throw new Error("NAISYS_FOLDER environment variable is not set");
25
+ }
26
+ // Validate before writing
27
+ CustomModelsFileSchema.parse(data);
28
+ // Omit empty arrays from output
29
+ const output = {};
30
+ if (data.llmModels && data.llmModels.length > 0) {
31
+ output.llmModels = data.llmModels;
32
+ }
33
+ if (data.imageModels && data.imageModels.length > 0) {
34
+ output.imageModels = data.imageModels;
35
+ }
36
+ const filePath = path.join(folder, "custom-models.yaml");
37
+ const yamlStr = yaml.dump(output, { lineWidth: -1 });
38
+ fs.writeFileSync(filePath, yamlStr, "utf-8");
39
39
  }
package/dist/expandEnv.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import os from "os";
2
2
  /** Expand ~ to the user's home directory in NAISYS_FOLDER */
3
3
  export function expandNaisysFolder() {
4
- if (process.env.NAISYS_FOLDER?.startsWith("~")) {
5
- process.env.NAISYS_FOLDER = process.env.NAISYS_FOLDER.replace("~", os.homedir());
6
- }
4
+ if (process.env.NAISYS_FOLDER?.startsWith("~")) {
5
+ process.env.NAISYS_FOLDER = process.env.NAISYS_FOLDER.replace(
6
+ "~",
7
+ os.homedir(),
8
+ );
9
+ }
7
10
  }
package/dist/hashToken.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "crypto";
2
2
  /** Hash a token (session cookie, API key) with SHA-256 for safe cache keys / DB lookup. */
3
3
  export function hashToken(token) {
4
- return createHash("sha256").update(token).digest("hex");
4
+ return createHash("sha256").update(token).digest("hex");
5
5
  }
@@ -8,33 +8,34 @@ import tls from "tls";
8
8
  * Returns undefined if the file does not exist.
9
9
  */
10
10
  export function readHubAccessKeyFile() {
11
- const naisysFolder = process.env.NAISYS_FOLDER || "";
12
- const accessKeyPath = join(naisysFolder, "cert", "hub-access-key");
13
- if (!existsSync(accessKeyPath))
14
- return undefined;
15
- return readFileSync(accessKeyPath, "utf-8").trim();
11
+ const naisysFolder = process.env.NAISYS_FOLDER || "";
12
+ const accessKeyPath = join(naisysFolder, "cert", "hub-access-key");
13
+ if (!existsSync(accessKeyPath)) return undefined;
14
+ return readFileSync(accessKeyPath, "utf-8").trim();
16
15
  }
17
16
  /**
18
17
  * Resolve the hub access key from environment variable or local cert file.
19
18
  * Returns undefined if neither is available.
20
19
  */
21
20
  export function resolveHubAccessKey() {
22
- return process.env.HUB_ACCESS_KEY || readHubAccessKeyFile();
21
+ return process.env.HUB_ACCESS_KEY || readHubAccessKeyFile();
23
22
  }
24
23
  /** Parse a hub access key in format "<fingerprintPrefix>+<secret>" */
25
24
  export function parseHubAccessKey(accessKey) {
26
- const plusIndex = accessKey.indexOf("+");
27
- if (plusIndex === -1) {
28
- throw new Error(`Invalid hub access key format, expected <fingerprint>+<secret>, got ${accessKey}`);
29
- }
30
- return {
31
- fingerprintPrefix: accessKey.substring(0, plusIndex),
32
- secret: accessKey.substring(plusIndex + 1),
33
- };
25
+ const plusIndex = accessKey.indexOf("+");
26
+ if (plusIndex === -1) {
27
+ throw new Error(
28
+ `Invalid hub access key format, expected <fingerprint>+<secret>, got ${accessKey}`,
29
+ );
30
+ }
31
+ return {
32
+ fingerprintPrefix: accessKey.substring(0, plusIndex),
33
+ secret: accessKey.substring(plusIndex + 1),
34
+ };
34
35
  }
35
36
  /** Compute SHA-256 fingerprint of a DER-encoded certificate */
36
37
  export function computeCertFingerprint(derCert) {
37
- return createHash("sha256").update(derCert).digest("hex");
38
+ return createHash("sha256").update(derCert).digest("hex");
38
39
  }
39
40
  /**
40
41
  * Connect to a TLS server, verify that the certificate fingerprint matches
@@ -42,27 +43,32 @@ export function computeCertFingerprint(derCert) {
42
43
  * trusted CA in subsequent connections.
43
44
  */
44
45
  export async function verifyHubCertificate(host, port, fingerprintPrefix) {
45
- return new Promise((resolve, reject) => {
46
- const sock = tls.connect(port, host, { rejectUnauthorized: false }, () => {
47
- const cert = sock.getPeerCertificate(true);
48
- sock.destroy();
49
- if (!cert?.raw) {
50
- reject(new Error("No certificate received from hub"));
51
- return;
52
- }
53
- const fingerprint = computeCertFingerprint(cert.raw);
54
- if (!fingerprint.startsWith(fingerprintPrefix)) {
55
- reject(new Error(`Hub certificate fingerprint mismatch: expected prefix ${fingerprintPrefix}, got ${fingerprint.substring(0, fingerprintPrefix.length)}`));
56
- return;
57
- }
58
- // Convert DER to PEM so it can be used as a trusted CA
59
- const pem = "-----BEGIN CERTIFICATE-----\n" +
60
- cert.raw.toString("base64") +
61
- "\n-----END CERTIFICATE-----\n";
62
- resolve(pem);
63
- });
64
- sock.on("error", reject);
46
+ return new Promise((resolve, reject) => {
47
+ const sock = tls.connect(port, host, { rejectUnauthorized: false }, () => {
48
+ const cert = sock.getPeerCertificate(true);
49
+ sock.destroy();
50
+ if (!cert?.raw) {
51
+ reject(new Error("No certificate received from hub"));
52
+ return;
53
+ }
54
+ const fingerprint = computeCertFingerprint(cert.raw);
55
+ if (!fingerprint.startsWith(fingerprintPrefix)) {
56
+ reject(
57
+ new Error(
58
+ `Hub certificate fingerprint mismatch: expected prefix ${fingerprintPrefix}, got ${fingerprint.substring(0, fingerprintPrefix.length)}`,
59
+ ),
60
+ );
61
+ return;
62
+ }
63
+ // Convert DER to PEM so it can be used as a trusted CA
64
+ const pem =
65
+ "-----BEGIN CERTIFICATE-----\n" +
66
+ cert.raw.toString("base64") +
67
+ "\n-----END CERTIFICATE-----\n";
68
+ resolve(pem);
65
69
  });
70
+ sock.on("error", reject);
71
+ });
66
72
  }
67
73
  /**
68
74
  * Create an https.Agent that trusts only the given PEM certificate.
@@ -70,9 +76,9 @@ export async function verifyHubCertificate(host, port, fingerprintPrefix) {
70
76
  * to the same cert (no TOCTOU gap since Node's TLS layer enforces it).
71
77
  */
72
78
  export function createPinnedHttpsAgent(certPem) {
73
- return new https.Agent({
74
- ca: certPem,
75
- // Skip hostname check — cert is already pinned by fingerprint
76
- checkServerIdentity: () => undefined,
77
- });
79
+ return new https.Agent({
80
+ ca: certPem,
81
+ // Skip hostname check — cert is already pinned by fingerprint
82
+ checkServerIdentity: () => undefined,
83
+ });
78
84
  }
@@ -1,58 +1,55 @@
1
1
  import fsp from "node:fs/promises";
2
2
  const MAX_READ_BYTES = 256 * 1024;
3
3
  export async function tailLogFile(filePath, lineCount, minLevel) {
4
- let stat;
4
+ let stat;
5
+ try {
6
+ stat = await fsp.stat(filePath);
7
+ } catch {
8
+ return { entries: [], fileSize: 0 };
9
+ }
10
+ const fileSize = stat.size;
11
+ if (fileSize === 0) {
12
+ return { entries: [], fileSize: 0 };
13
+ }
14
+ const readSize = Math.min(fileSize, MAX_READ_BYTES);
15
+ const position = fileSize - readSize;
16
+ const buffer = Buffer.alloc(readSize);
17
+ const handle = await fsp.open(filePath, "r");
18
+ try {
19
+ await handle.read(buffer, 0, readSize, position);
20
+ } finally {
21
+ await handle.close();
22
+ }
23
+ const text = buffer.toString("utf-8");
24
+ const lines = text.split("\n").filter((line) => line.trim().length > 0);
25
+ // If we didn't read from the start, drop the first line (likely partial)
26
+ if (position > 0 && lines.length > 0) {
27
+ lines.shift();
28
+ }
29
+ const OMIT_KEYS = new Set(["level", "time", "msg", "pid", "hostname"]);
30
+ const entries = [];
31
+ for (const line of lines) {
5
32
  try {
6
- stat = await fsp.stat(filePath);
7
- }
8
- catch {
9
- return { entries: [], fileSize: 0 };
10
- }
11
- const fileSize = stat.size;
12
- if (fileSize === 0) {
13
- return { entries: [], fileSize: 0 };
14
- }
15
- const readSize = Math.min(fileSize, MAX_READ_BYTES);
16
- const position = fileSize - readSize;
17
- const buffer = Buffer.alloc(readSize);
18
- const handle = await fsp.open(filePath, "r");
19
- try {
20
- await handle.read(buffer, 0, readSize, position);
21
- }
22
- finally {
23
- await handle.close();
24
- }
25
- const text = buffer.toString("utf-8");
26
- const lines = text.split("\n").filter((line) => line.trim().length > 0);
27
- // If we didn't read from the start, drop the first line (likely partial)
28
- if (position > 0 && lines.length > 0) {
29
- lines.shift();
30
- }
31
- const OMIT_KEYS = new Set(["level", "time", "msg", "pid", "hostname"]);
32
- const entries = [];
33
- for (const line of lines) {
34
- try {
35
- const parsed = JSON.parse(line);
36
- const level = parsed.level ?? 30;
37
- if (minLevel != null && level < minLevel)
38
- continue;
39
- const extra = {};
40
- for (const key of Object.keys(parsed)) {
41
- if (!OMIT_KEYS.has(key)) {
42
- extra[key] = parsed[key];
43
- }
44
- }
45
- const detail = Object.keys(extra).length > 0 ? JSON.stringify(extra) : undefined;
46
- entries.push({
47
- level,
48
- time: parsed.time ?? 0,
49
- msg: parsed.msg ?? "",
50
- detail,
51
- });
52
- }
53
- catch {
54
- // skip malformed lines
33
+ const parsed = JSON.parse(line);
34
+ const level = parsed.level ?? 30;
35
+ if (minLevel != null && level < minLevel) continue;
36
+ const extra = {};
37
+ for (const key of Object.keys(parsed)) {
38
+ if (!OMIT_KEYS.has(key)) {
39
+ extra[key] = parsed[key];
55
40
  }
41
+ }
42
+ const detail =
43
+ Object.keys(extra).length > 0 ? JSON.stringify(extra) : undefined;
44
+ entries.push({
45
+ level,
46
+ time: parsed.time ?? 0,
47
+ msg: parsed.msg ?? "",
48
+ detail,
49
+ });
50
+ } catch {
51
+ // skip malformed lines
56
52
  }
57
- return { entries: entries.slice(-lineCount), fileSize };
53
+ }
54
+ return { entries: entries.slice(-lineCount), fileSize };
58
55
  }
@@ -9,113 +9,121 @@ const execAsync = promisify(exec);
9
9
  * Uses `better-sqlite3` directly (synchronous, no Prisma dependency) for the version check.
10
10
  */
11
11
  export async function deployPrismaMigrations(options) {
12
- const { packageDir, databasePath, expectedVersion, envOverrides } = options;
13
- // Ensure database directory exists
14
- const databaseDir = dirname(databasePath);
15
- if (!existsSync(databaseDir)) {
16
- mkdirSync(databaseDir, { recursive: true });
12
+ const { packageDir, databasePath, expectedVersion, envOverrides } = options;
13
+ // Ensure database directory exists
14
+ const databaseDir = dirname(databasePath);
15
+ if (!existsSync(databaseDir)) {
16
+ mkdirSync(databaseDir, { recursive: true });
17
+ }
18
+ let currentVersion;
19
+ // Check version if database file already exists
20
+ if (existsSync(databasePath)) {
21
+ const db = new Database(databasePath);
22
+ try {
23
+ const row = db
24
+ .prepare("SELECT version FROM schema_version WHERE id = 1")
25
+ .get();
26
+ currentVersion = row?.version;
27
+ } catch {
28
+ // "no such table" → treat as new DB, proceed with migration
17
29
  }
18
- let currentVersion;
19
- // Check version if database file already exists
20
- if (existsSync(databasePath)) {
21
- const db = new Database(databasePath);
22
- try {
23
- const row = db
24
- .prepare("SELECT version FROM schema_version WHERE id = 1")
25
- .get();
26
- currentVersion = row?.version;
27
- }
28
- catch {
29
- // "no such table" → treat as new DB, proceed with migration
30
- }
31
- // Switch from WAL to DELETE journal mode before closing. This merges any
32
- // pending WAL data and removes the -wal/-shm files entirely. Without this,
33
- // prisma migrate (a separate process) sees the leftover SHM file and fails
34
- // with "database is locked".
35
- try {
36
- db.pragma("journal_mode=DELETE");
37
- }
38
- catch {
39
- // Failed — another process may genuinely hold the lock
40
- }
41
- db.close();
42
- if (currentVersion === expectedVersion) {
43
- return; // Fast path — already at expected version
44
- }
30
+ // Switch from WAL to DELETE journal mode before closing. This merges any
31
+ // pending WAL data and removes the -wal/-shm files entirely. Without this,
32
+ // prisma migrate (a separate process) sees the leftover SHM file and fails
33
+ // with "database is locked".
34
+ try {
35
+ db.pragma("journal_mode=DELETE");
36
+ } catch {
37
+ // Failed — another process may genuinely hold the lock
45
38
  }
46
- // Log migration status
47
- if (currentVersion !== undefined) {
48
- if (currentVersion > expectedVersion) {
49
- throw new Error(`Database version ${currentVersion} is newer than expected ${expectedVersion}. Manual intervention required.`);
50
- }
51
- console.log(`Migrating database from version ${currentVersion} to ${expectedVersion}...`);
39
+ db.close();
40
+ if (currentVersion === expectedVersion) {
41
+ return; // Fast path — already at expected version
52
42
  }
53
- else {
54
- console.log(`Creating new database with schema version ${expectedVersion}...`);
43
+ }
44
+ // Log migration status
45
+ if (currentVersion !== undefined) {
46
+ if (currentVersion > expectedVersion) {
47
+ throw new Error(
48
+ `Database version ${currentVersion} is newer than expected ${expectedVersion}. Manual intervention required.`,
49
+ );
55
50
  }
56
- // Run prisma migrate deploy
57
- const schemaPath = join(packageDir, "prisma", "schema.prisma");
58
- const absoluteDbPath = resolve(databasePath).replace(/\\/g, "/");
59
- let stdout;
60
- let stderr;
61
- try {
62
- ({ stdout, stderr } = await execAsync(`npx prisma migrate deploy --schema="${schemaPath}"`, {
51
+ console.log(
52
+ `Migrating database from version ${currentVersion} to ${expectedVersion}...`,
53
+ );
54
+ } else {
55
+ console.log(
56
+ `Creating new database with schema version ${expectedVersion}...`,
57
+ );
58
+ }
59
+ // Run prisma migrate deploy
60
+ const schemaPath = join(packageDir, "prisma", "schema.prisma");
61
+ const absoluteDbPath = resolve(databasePath).replace(/\\/g, "/");
62
+ let stdout;
63
+ let stderr;
64
+ try {
65
+ ({ stdout, stderr } = await execAsync(
66
+ `npx prisma migrate deploy --schema="${schemaPath}"`,
67
+ {
68
+ cwd: packageDir,
69
+ env: {
70
+ ...process.env,
71
+ // Resolve to absolute so prisma.config.ts gets a correct path
72
+ // regardless of this subprocess's cwd (which is packageDir)
73
+ NAISYS_FOLDER: resolve(process.env.NAISYS_FOLDER || ""),
74
+ ...envOverrides,
75
+ },
76
+ },
77
+ ));
78
+ } catch (error) {
79
+ const msg = error instanceof Error ? error.message : String(error);
80
+ if (msg.includes("database is locked")) {
81
+ // Stale WAL/SHM files from a crashed process — remove and retry
82
+ const walPath = absoluteDbPath + "-wal";
83
+ const shmPath = absoluteDbPath + "-shm";
84
+ let removed = false;
85
+ for (const staleFile of [walPath, shmPath]) {
86
+ if (existsSync(staleFile)) {
87
+ console.log(`Removing stale file: ${staleFile}`);
88
+ unlinkSync(staleFile);
89
+ removed = true;
90
+ }
91
+ }
92
+ if (removed) {
93
+ console.log("Retrying migration after removing stale WAL files...");
94
+ ({ stdout, stderr } = await execAsync(
95
+ `npx prisma migrate deploy --schema="${schemaPath}"`,
96
+ {
63
97
  cwd: packageDir,
64
98
  env: {
65
- ...process.env,
66
- // Resolve to absolute so prisma.config.ts gets a correct path
67
- // regardless of this subprocess's cwd (which is packageDir)
68
- NAISYS_FOLDER: resolve(process.env.NAISYS_FOLDER || ""),
69
- ...envOverrides,
99
+ ...process.env,
100
+ NAISYS_FOLDER: resolve(process.env.NAISYS_FOLDER || ""),
101
+ ...envOverrides,
70
102
  },
71
- }));
72
- }
73
- catch (error) {
74
- const msg = error instanceof Error ? error.message : String(error);
75
- if (msg.includes("database is locked")) {
76
- // Stale WAL/SHM files from a crashed process — remove and retry
77
- const walPath = absoluteDbPath + "-wal";
78
- const shmPath = absoluteDbPath + "-shm";
79
- let removed = false;
80
- for (const staleFile of [walPath, shmPath]) {
81
- if (existsSync(staleFile)) {
82
- console.log(`Removing stale file: ${staleFile}`);
83
- unlinkSync(staleFile);
84
- removed = true;
85
- }
86
- }
87
- if (removed) {
88
- console.log("Retrying migration after removing stale WAL files...");
89
- ({ stdout, stderr } = await execAsync(`npx prisma migrate deploy --schema="${schemaPath}"`, {
90
- cwd: packageDir,
91
- env: {
92
- ...process.env,
93
- NAISYS_FOLDER: resolve(process.env.NAISYS_FOLDER || ""),
94
- ...envOverrides,
95
- },
96
- }));
97
- }
98
- else {
99
- throw new Error(`Database is locked: ${absoluteDbPath}\n` +
100
- `Another process may be using the database.`);
101
- }
102
- }
103
- else {
104
- throw error;
105
- }
106
- }
107
- if (stdout)
108
- console.log(stdout);
109
- if (stderr && !stderr.includes("Loaded Prisma config")) {
110
- console.error(stderr);
111
- }
112
- // Upsert schema_version row via raw SQL
113
- const db = new Database(absoluteDbPath);
114
- try {
115
- db.prepare("INSERT OR REPLACE INTO schema_version (id, version, updated) VALUES (1, ?, ?)").run(expectedVersion, new Date().toISOString());
116
- }
117
- finally {
118
- db.close();
103
+ },
104
+ ));
105
+ } else {
106
+ throw new Error(
107
+ `Database is locked: ${absoluteDbPath}\n` +
108
+ `Another process may be using the database.`,
109
+ );
110
+ }
111
+ } else {
112
+ throw error;
119
113
  }
120
- console.log("Database migration completed.");
114
+ }
115
+ if (stdout) console.log(stdout);
116
+ if (stderr && !stderr.includes("Loaded Prisma config")) {
117
+ console.error(stderr);
118
+ }
119
+ // Upsert schema_version row via raw SQL
120
+ const db = new Database(absoluteDbPath);
121
+ try {
122
+ db.prepare(
123
+ "INSERT OR REPLACE INTO schema_version (id, version, updated) VALUES (1, ?, ?)",
124
+ ).run(expectedVersion, new Date().toISOString());
125
+ } finally {
126
+ db.close();
127
+ }
128
+ console.log("Database migration completed.");
121
129
  }
@@ -1,10 +1,10 @@
1
1
  export const SESSION_COOKIE_NAME = "naisys_session";
2
2
  export function sessionCookieOptions(expiresAt) {
3
- return {
4
- path: "/",
5
- httpOnly: true,
6
- sameSite: "lax",
7
- secure: process.env.NODE_ENV === "production",
8
- maxAge: Math.floor((expiresAt.getTime() - Date.now()) / 1000),
9
- };
3
+ return {
4
+ path: "/",
5
+ httpOnly: true,
6
+ sameSite: "lax",
7
+ secure: process.env.NODE_ENV === "production",
8
+ maxAge: Math.floor((expiresAt.getTime() - Date.now()) / 1000),
9
+ };
10
10
  }
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@naisys/common-node",
3
- "version": "3.0.0-beta.4",
3
+ "version": "3.0.0-beta.5",
4
4
  "type": "module",
5
5
  "description": "[internal] Node-only utilities for NAISYS",
6
6
  "files": [
7
7
  "dist",
8
- "!dist/**/*.map"
8
+ "!dist/**/*.map",
9
+ "!dist/**/*.d.ts",
10
+ "!dist/**/*.d.ts.map"
9
11
  ],
10
12
  "scripts": {
11
13
  "clean": "rimraf dist",
@@ -14,7 +16,7 @@
14
16
  "npm:publish": "npm publish --access public"
15
17
  },
16
18
  "dependencies": {
17
- "@naisys/common": "3.0.0-beta.4",
19
+ "@naisys/common": "3.0.0-beta.5",
18
20
  "better-sqlite3": "^12.6.2",
19
21
  "js-yaml": "^4.1.1"
20
22
  },
@@ -1,4 +0,0 @@
1
- import type { UserEntry } from "@naisys/common";
2
- /** Loads agent yaml configs from a file or directory path, returns a map of userId → UserEntry */
3
- export declare function loadAgentConfigs(startupPath: string): Map<number, UserEntry>;
4
- //# sourceMappingURL=agentConfigLoader.d.ts.map
@@ -1,6 +0,0 @@
1
- /**
2
- * Extract the API key from an Authorization: Bearer header value.
3
- * Returns undefined if the header is missing or not in Bearer format.
4
- */
5
- export declare function extractBearerToken(authHeader: string | undefined): string | undefined;
6
- //# sourceMappingURL=bearerToken.d.ts.map
@@ -1,4 +0,0 @@
1
- import { type CustomModelsFile } from "@naisys/common";
2
- export declare function loadCustomModels(folder: string): CustomModelsFile;
3
- export declare function saveCustomModels(data: CustomModelsFile): void;
4
- //# sourceMappingURL=customModelsLoader.d.ts.map
@@ -1,3 +0,0 @@
1
- /** Expand ~ to the user's home directory in NAISYS_FOLDER */
2
- export declare function expandNaisysFolder(): void;
3
- //# sourceMappingURL=expandEnv.d.ts.map
@@ -1,3 +0,0 @@
1
- /** Hash a token (session cookie, API key) with SHA-256 for safe cache keys / DB lookup. */
2
- export declare function hashToken(token: string): string;
3
- //# sourceMappingURL=hashToken.d.ts.map
@@ -1,31 +0,0 @@
1
- import https from "https";
2
- /**
3
- * Read the hub access key from the local cert file at NAISYS_FOLDER/cert/hub-access-key.
4
- * Returns undefined if the file does not exist.
5
- */
6
- export declare function readHubAccessKeyFile(): string | undefined;
7
- /**
8
- * Resolve the hub access key from environment variable or local cert file.
9
- * Returns undefined if neither is available.
10
- */
11
- export declare function resolveHubAccessKey(): string | undefined;
12
- /** Parse a hub access key in format "<fingerprintPrefix>+<secret>" */
13
- export declare function parseHubAccessKey(accessKey: string): {
14
- fingerprintPrefix: string;
15
- secret: string;
16
- };
17
- /** Compute SHA-256 fingerprint of a DER-encoded certificate */
18
- export declare function computeCertFingerprint(derCert: Buffer): string;
19
- /**
20
- * Connect to a TLS server, verify that the certificate fingerprint matches
21
- * the expected prefix, and return the PEM-encoded certificate for use as a
22
- * trusted CA in subsequent connections.
23
- */
24
- export declare function verifyHubCertificate(host: string, port: number, fingerprintPrefix: string): Promise<string>;
25
- /**
26
- * Create an https.Agent that trusts only the given PEM certificate.
27
- * Used after verifyHubCertificate to pin all subsequent connections
28
- * to the same cert (no TOCTOU gap since Node's TLS layer enforces it).
29
- */
30
- export declare function createPinnedHttpsAgent(certPem: string): https.Agent;
31
- //# sourceMappingURL=hubCertVerification.d.ts.map
package/dist/index.d.ts DELETED
@@ -1,10 +0,0 @@
1
- export * from "./agentConfigLoader.js";
2
- export * from "./bearerToken.js";
3
- export * from "./expandEnv.js";
4
- export * from "./customModelsLoader.js";
5
- export * from "./hashToken.js";
6
- export * from "./hubCertVerification.js";
7
- export * from "./logFileService.js";
8
- export * from "./migrationHelper.js";
9
- export * from "./sessionCookie.js";
10
- //# sourceMappingURL=index.d.ts.map
@@ -1,11 +0,0 @@
1
- export interface PinoLogEntry {
2
- level: number;
3
- time: number;
4
- msg: string;
5
- detail?: string;
6
- }
7
- export declare function tailLogFile(filePath: string, lineCount: number, minLevel?: number): Promise<{
8
- entries: PinoLogEntry[];
9
- fileSize: number;
10
- }>;
11
- //# sourceMappingURL=logFileService.d.ts.map
@@ -1,15 +0,0 @@
1
- /**
2
- * Shared helper that runs `prisma migrate deploy` with a version-checked fast path.
3
- * Uses `better-sqlite3` directly (synchronous, no Prisma dependency) for the version check.
4
- */
5
- export declare function deployPrismaMigrations(options: {
6
- /** Directory containing prisma.config.ts and prisma/ folder */
7
- packageDir: string;
8
- /** Absolute path to the .db file */
9
- databasePath: string;
10
- /** Skip migrations if DB already at this version */
11
- expectedVersion: number;
12
- /** Extra env vars forwarded to `prisma migrate deploy` */
13
- envOverrides?: Record<string, string>;
14
- }): Promise<void>;
15
- //# sourceMappingURL=migrationHelper.d.ts.map
@@ -1,9 +0,0 @@
1
- export declare const SESSION_COOKIE_NAME = "naisys_session";
2
- export declare function sessionCookieOptions(expiresAt: Date): {
3
- path: string;
4
- httpOnly: boolean;
5
- sameSite: "lax";
6
- secure: boolean;
7
- maxAge: number;
8
- };
9
- //# sourceMappingURL=sessionCookie.d.ts.map