@rigour-labs/core 6.1.0-beta.2 → 6.1.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.
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create the local encryption key without exposing a partially-written key to
|
|
3
|
+
* another Rigour process. A fully-written temporary file is linked into place,
|
|
4
|
+
* so exactly one concurrent process wins and every other process reads it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function loadOrCreateFileKey(keyPath: string): Promise<Buffer>;
|
|
1
7
|
export declare function encryptLocalPayload(value: unknown): Promise<string>;
|
|
2
8
|
export declare function decryptLocalPayload<T>(value: string): Promise<T>;
|
|
@@ -1,27 +1,68 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
|
2
|
+
import { chmod, link, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
3
|
import fs from 'fs-extra';
|
|
3
4
|
import os from 'os';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
const KEY_PATH = path.join(os.homedir(), '.rigour', 'team-cache.key');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return key;
|
|
7
|
+
let fileKeyPromise;
|
|
8
|
+
function decodeKey(value, source) {
|
|
9
|
+
const normalized = value.trim();
|
|
10
|
+
const key = Buffer.from(normalized, 'base64');
|
|
11
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(normalized) || key.length !== 32) {
|
|
12
|
+
throw new Error(`${source} must contain a base64-encoded 32-byte key.`);
|
|
13
13
|
}
|
|
14
|
+
return key;
|
|
15
|
+
}
|
|
16
|
+
async function readFileKey(keyPath) {
|
|
17
|
+
return decodeKey(await readFile(keyPath, 'utf8'), keyPath);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create the local encryption key without exposing a partially-written key to
|
|
21
|
+
* another Rigour process. A fully-written temporary file is linked into place,
|
|
22
|
+
* so exactly one concurrent process wins and every other process reads it.
|
|
23
|
+
*/
|
|
24
|
+
export async function loadOrCreateFileKey(keyPath) {
|
|
14
25
|
try {
|
|
15
|
-
|
|
16
|
-
if (key.length === 32)
|
|
17
|
-
return key;
|
|
26
|
+
return await readFileKey(keyPath);
|
|
18
27
|
}
|
|
19
|
-
catch {
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== 'ENOENT')
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
await fs.ensureDir(path.dirname(keyPath));
|
|
20
33
|
const key = randomBytes(32);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
const temporaryPath = `${keyPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
35
|
+
try {
|
|
36
|
+
await writeFile(temporaryPath, key.toString('base64'), { mode: 0o600, flag: 'wx' });
|
|
37
|
+
await chmod(temporaryPath, 0o600);
|
|
38
|
+
try {
|
|
39
|
+
await link(temporaryPath, keyPath);
|
|
40
|
+
await chmod(keyPath, 0o600);
|
|
41
|
+
return key;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code !== 'EEXIST')
|
|
45
|
+
throw error;
|
|
46
|
+
return await readFileKey(keyPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await unlink(temporaryPath).catch(error => {
|
|
51
|
+
if (error.code !== 'ENOENT')
|
|
52
|
+
throw error;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function getKey() {
|
|
57
|
+
const configured = process.env.RIGOUR_LOCAL_CACHE_KEY;
|
|
58
|
+
if (configured) {
|
|
59
|
+
return decodeKey(configured, 'RIGOUR_LOCAL_CACHE_KEY');
|
|
60
|
+
}
|
|
61
|
+
fileKeyPromise ??= loadOrCreateFileKey(KEY_PATH).catch(error => {
|
|
62
|
+
fileKeyPromise = undefined;
|
|
63
|
+
throw error;
|
|
64
|
+
});
|
|
65
|
+
return fileKeyPromise;
|
|
25
66
|
}
|
|
26
67
|
export async function encryptLocalPayload(value) {
|
|
27
68
|
const key = await getKey();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { loadOrCreateFileKey } from './local-encryption.js';
|
|
6
|
+
const temporaryDirectories = [];
|
|
7
|
+
afterEach(async () => {
|
|
8
|
+
await Promise.all(temporaryDirectories.splice(0).map(directory => fs.remove(directory)));
|
|
9
|
+
});
|
|
10
|
+
async function temporaryKeyPath() {
|
|
11
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-key-test-'));
|
|
12
|
+
temporaryDirectories.push(directory);
|
|
13
|
+
return path.join(directory, 'team-cache.key');
|
|
14
|
+
}
|
|
15
|
+
describe('local encryption key storage', () => {
|
|
16
|
+
it('returns one key when concurrent processes initialize the same path', async () => {
|
|
17
|
+
const keyPath = await temporaryKeyPath();
|
|
18
|
+
const keys = await Promise.all(Array.from({ length: 32 }, () => loadOrCreateFileKey(keyPath)));
|
|
19
|
+
expect(new Set(keys.map(key => key.toString('base64')))).toHaveLength(1);
|
|
20
|
+
expect(await fs.readFile(keyPath, 'utf8')).toBe(keys[0].toString('base64'));
|
|
21
|
+
});
|
|
22
|
+
it('rejects an invalid existing key without replacing it', async () => {
|
|
23
|
+
const keyPath = await temporaryKeyPath();
|
|
24
|
+
await fs.writeFile(keyPath, 'not-a-valid-key', 'utf8');
|
|
25
|
+
await expect(loadOrCreateFileKey(keyPath)).rejects.toThrow('must contain a base64-encoded 32-byte key');
|
|
26
|
+
expect(await fs.readFile(keyPath, 'utf8')).toBe('not-a-valid-key');
|
|
27
|
+
});
|
|
28
|
+
it('accepts an existing key without base64 padding for backward compatibility', async () => {
|
|
29
|
+
const keyPath = await temporaryKeyPath();
|
|
30
|
+
const encodedKey = Buffer.alloc(32, 7).toString('base64').replace(/=+$/, '');
|
|
31
|
+
await fs.writeFile(keyPath, encodedKey, 'utf8');
|
|
32
|
+
expect((await loadOrCreateFileKey(keyPath)).equals(Buffer.alloc(32, 7))).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "6.1.0
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "AI-native quality gate engine with local Bayesian learning. AST analysis, drift detection, Fix Packet generation, and agent self-healing across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -67,11 +67,11 @@
|
|
|
67
67
|
"pg": "^8.16.3",
|
|
68
68
|
"sqlite3": "^5.1.7",
|
|
69
69
|
"openai": "^4.104.0",
|
|
70
|
-
"@rigour-labs/brain-darwin-arm64": "6.1.0
|
|
71
|
-
"@rigour-labs/brain-
|
|
72
|
-
"@rigour-labs/brain-linux-x64": "6.1.0
|
|
73
|
-
"@rigour-labs/brain-
|
|
74
|
-
"@rigour-labs/brain-linux-arm64": "6.1.0
|
|
70
|
+
"@rigour-labs/brain-darwin-arm64": "6.1.0",
|
|
71
|
+
"@rigour-labs/brain-darwin-x64": "6.1.0",
|
|
72
|
+
"@rigour-labs/brain-linux-x64": "6.1.0",
|
|
73
|
+
"@rigour-labs/brain-win-x64": "6.1.0",
|
|
74
|
+
"@rigour-labs/brain-linux-arm64": "6.1.0"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@types/fs-extra": "^11.0.4",
|