@naisys/common-node 3.0.0-beta.5 → 3.0.0-beta.7

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,90 +4,77 @@ 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
- } 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;
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;
42
40
  }
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
- );
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
+ }
58
47
  }
59
- }
60
48
  }
61
49
  function processFile(filePath, leadEntryIndex, configEntries, usernameToPath) {
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
- );
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
+ }
74
76
  }
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);
77
+ catch (e) {
78
+ throw new Error(`Failed to process agent config at ${filePath}: ${e}`);
89
79
  }
90
- } catch (e) {
91
- throw new Error(`Failed to process agent config at ${filePath}: ${e}`);
92
- }
93
80
  }
@@ -3,6 +3,7 @@
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 ")) return undefined;
7
- return authHeader.slice(7);
6
+ if (!authHeader?.startsWith("Bearer "))
7
+ return undefined;
8
+ return authHeader.slice(7);
8
9
  }
@@ -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,10 +1,7 @@
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(
6
- "~",
7
- os.homedir(),
8
- );
9
- }
4
+ if (process.env.NAISYS_FOLDER?.startsWith("~")) {
5
+ process.env.NAISYS_FOLDER = process.env.NAISYS_FOLDER.replace("~", os.homedir());
6
+ }
10
7
  }
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,34 +8,33 @@ 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)) return undefined;
14
- 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))
14
+ return undefined;
15
+ return readFileSync(accessKeyPath, "utf-8").trim();
15
16
  }
16
17
  /**
17
18
  * Resolve the hub access key from environment variable or local cert file.
18
19
  * Returns undefined if neither is available.
19
20
  */
20
21
  export function resolveHubAccessKey() {
21
- return process.env.HUB_ACCESS_KEY || readHubAccessKeyFile();
22
+ return process.env.HUB_ACCESS_KEY || readHubAccessKeyFile();
22
23
  }
23
24
  /** Parse a hub access key in format "<fingerprintPrefix>+<secret>" */
24
25
  export function parseHubAccessKey(accessKey) {
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
- };
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
+ };
35
34
  }
36
35
  /** Compute SHA-256 fingerprint of a DER-encoded certificate */
37
36
  export function computeCertFingerprint(derCert) {
38
- return createHash("sha256").update(derCert).digest("hex");
37
+ return createHash("sha256").update(derCert).digest("hex");
39
38
  }
40
39
  /**
41
40
  * Connect to a TLS server, verify that the certificate fingerprint matches
@@ -43,32 +42,27 @@ export function computeCertFingerprint(derCert) {
43
42
  * trusted CA in subsequent connections.
44
43
  */
45
44
  export async function verifyHubCertificate(host, port, fingerprintPrefix) {
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);
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);
69
65
  });
70
- sock.on("error", reject);
71
- });
72
66
  }
73
67
  /**
74
68
  * Create an https.Agent that trusts only the given PEM certificate.
@@ -76,9 +70,9 @@ export async function verifyHubCertificate(host, port, fingerprintPrefix) {
76
70
  * to the same cert (no TOCTOU gap since Node's TLS layer enforces it).
77
71
  */
78
72
  export function createPinnedHttpsAgent(certPem) {
79
- return new https.Agent({
80
- ca: certPem,
81
- // Skip hostname check — cert is already pinned by fingerprint
82
- checkServerIdentity: () => undefined,
83
- });
73
+ return new https.Agent({
74
+ ca: certPem,
75
+ // Skip hostname check — cert is already pinned by fingerprint
76
+ checkServerIdentity: () => undefined,
77
+ });
84
78
  }
@@ -1,55 +1,58 @@
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;
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) {
4
+ let stat;
32
5
  try {
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];
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
40
55
  }
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
52
56
  }
53
- }
54
- return { entries: entries.slice(-lineCount), fileSize };
57
+ return { entries: entries.slice(-lineCount), fileSize };
55
58
  }
@@ -9,121 +9,113 @@ 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 });
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
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 });
29
17
  }
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
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
+ }
38
45
  }
39
- db.close();
40
- if (currentVersion === expectedVersion) {
41
- return; // Fast path — already at expected version
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}...`);
42
52
  }
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
- );
53
+ else {
54
+ console.log(`Creating new database with schema version ${expectedVersion}...`);
50
55
  }
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
- {
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}"`, {
97
63
  cwd: packageDir,
98
64
  env: {
99
- ...process.env,
100
- NAISYS_FOLDER: resolve(process.env.NAISYS_FOLDER || ""),
101
- ...envOverrides,
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,
102
70
  },
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;
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();
113
119
  }
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.");
120
+ console.log("Database migration completed.");
129
121
  }
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@naisys/common-node",
3
- "version": "3.0.0-beta.5",
3
+ "version": "3.0.0-beta.7",
4
4
  "type": "module",
5
5
  "description": "[internal] Node-only utilities for NAISYS",
6
6
  "files": [
@@ -16,7 +16,7 @@
16
16
  "npm:publish": "npm publish --access public"
17
17
  },
18
18
  "dependencies": {
19
- "@naisys/common": "3.0.0-beta.5",
19
+ "@naisys/common": "3.0.0-beta.7",
20
20
  "better-sqlite3": "^12.6.2",
21
21
  "js-yaml": "^4.1.1"
22
22
  },