@goodea/olimpyx 0.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.
- package/data/skill/playbook.md +196 -0
- package/data/skill/starter.md +19 -0
- package/package.json +37 -0
- package/src/budget.js +348 -0
- package/src/characters.js +149 -0
- package/src/cli.js +1140 -0
- package/src/client.js +477 -0
- package/src/index.js +9 -0
- package/src/init-apply.js +207 -0
- package/src/init.js +167 -0
- package/src/install-skill.js +14 -0
- package/src/redaction.js +21 -0
- package/src/session.js +22 -0
- package/src/skill-install.js +51 -0
- package/src/state.js +151 -0
- package/src/vault.js +99 -0
package/src/vault.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
const KEY_BYTES = 32;
|
|
7
|
+
const IV_BYTES = 12;
|
|
8
|
+
const VERSION = 1;
|
|
9
|
+
|
|
10
|
+
export function ownerHome(env = process.env) {
|
|
11
|
+
return env.OLIMPYX_OWNER_HOME || join(env.HOME || homedir(), '.olimpyx');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function keyPath(env = process.env) {
|
|
15
|
+
return env.OLIMPYX_MASTER_KEY || join(env.HOME || homedir(), '.config', 'olimpyx', 'master.key');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function vaultPath(env = process.env) {
|
|
19
|
+
return join(ownerHome(env), 'vault.enc');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function configPath(env = process.env) {
|
|
23
|
+
return join(ownerHome(env), 'config.json');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function loadOrCreateKey(path = keyPath()) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await readFile(path);
|
|
29
|
+
if (raw.length !== KEY_BYTES) throw new Error('Olimpyx master key is the wrong size');
|
|
30
|
+
return raw;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code !== 'ENOENT') throw error;
|
|
33
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
34
|
+
const key = randomBytes(KEY_BYTES);
|
|
35
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
36
|
+
await writeFile(temp, key, { mode: 0o600 });
|
|
37
|
+
await chmod(temp, 0o600);
|
|
38
|
+
await rename(temp, path);
|
|
39
|
+
await chmod(path, 0o600);
|
|
40
|
+
return key;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function encryptVault(payload, key) {
|
|
45
|
+
const iv = randomBytes(IV_BYTES);
|
|
46
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
47
|
+
const plaintext = Buffer.from(JSON.stringify(payload), 'utf8');
|
|
48
|
+
const data = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
49
|
+
const tag = cipher.getAuthTag();
|
|
50
|
+
return `${JSON.stringify({
|
|
51
|
+
v: VERSION,
|
|
52
|
+
iv: iv.toString('base64'),
|
|
53
|
+
tag: tag.toString('base64'),
|
|
54
|
+
data: data.toString('base64')
|
|
55
|
+
})}\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function decryptVault(serialized, key) {
|
|
59
|
+
const envelope = typeof serialized === 'string' ? JSON.parse(serialized) : serialized;
|
|
60
|
+
if (envelope?.v !== VERSION || !envelope.iv || !envelope.tag || !envelope.data) {
|
|
61
|
+
throw new Error('Vault is unreadable');
|
|
62
|
+
}
|
|
63
|
+
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(envelope.iv, 'base64'));
|
|
64
|
+
decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'));
|
|
65
|
+
const plaintext = Buffer.concat([
|
|
66
|
+
decipher.update(Buffer.from(envelope.data, 'base64')),
|
|
67
|
+
decipher.final()
|
|
68
|
+
]);
|
|
69
|
+
return JSON.parse(plaintext.toString('utf8'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function readVault(env = process.env) {
|
|
73
|
+
const key = await loadOrCreateKey(keyPath(env));
|
|
74
|
+
const serialized = await readFile(vaultPath(env), 'utf8');
|
|
75
|
+
return decryptVault(serialized, key);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function writeVault(payload, env = process.env) {
|
|
79
|
+
const home = ownerHome(env);
|
|
80
|
+
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
81
|
+
const key = await loadOrCreateKey(keyPath(env));
|
|
82
|
+
const path = vaultPath(env);
|
|
83
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
84
|
+
await writeFile(temp, encryptVault(payload, key), { mode: 0o600 });
|
|
85
|
+
await chmod(temp, 0o600);
|
|
86
|
+
await rename(temp, path);
|
|
87
|
+
await chmod(path, 0o600);
|
|
88
|
+
return path;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function vaultExists(env = process.env) {
|
|
92
|
+
try {
|
|
93
|
+
await readFile(vaultPath(env));
|
|
94
|
+
return true;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error.code === 'ENOENT') return false;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|