assistme 0.3.0 → 0.3.2

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.
Files changed (44) hide show
  1. package/PLAN.md +14 -3
  2. package/dist/{chunk-UWE5WVQI.js → chunk-KX7ITO55.js} +20 -11
  3. package/dist/index.js +1791 -572
  4. package/dist/{job-runner-N4XAAWLJ.js → job-runner-P2L6MOOX.js} +1 -1
  5. package/package.json +5 -3
  6. package/src/agent/job-runner.ts +9 -13
  7. package/src/agent/mcp-servers.ts +6 -1020
  8. package/src/agent/memory.ts +2 -11
  9. package/src/agent/processor.ts +18 -108
  10. package/src/agent/scheduler.ts +2 -3
  11. package/src/agent/session.ts +20 -36
  12. package/src/agent/skills.ts +167 -61
  13. package/src/agent/system-prompt.ts +126 -0
  14. package/src/browser/chrome-launcher.ts +555 -0
  15. package/src/browser/controller.ts +1386 -0
  16. package/src/browser/types.ts +70 -0
  17. package/src/commands/credential.ts +190 -0
  18. package/src/commands/job.ts +14 -45
  19. package/src/commands/memory.ts +16 -29
  20. package/src/commands/schedule.ts +15 -37
  21. package/src/commands/start.ts +11 -43
  22. package/src/credentials/credential-store.test.ts +162 -0
  23. package/src/credentials/credential-store.ts +266 -0
  24. package/src/credentials/encryption.test.ts +98 -0
  25. package/src/credentials/encryption.ts +82 -0
  26. package/src/credentials/index.ts +15 -0
  27. package/src/credentials/local-store.ts +89 -0
  28. package/src/db/action.ts +19 -0
  29. package/src/db/api-client.ts +3 -32
  30. package/src/db/auth-store.ts +41 -0
  31. package/src/db/auth.ts +38 -0
  32. package/src/db/conversation.ts +39 -0
  33. package/src/db/event.ts +52 -0
  34. package/src/db/job-poll.ts +18 -0
  35. package/src/db/session.ts +60 -0
  36. package/src/db/supabase.ts +40 -383
  37. package/src/db/task.ts +69 -0
  38. package/src/db/types.ts +54 -0
  39. package/src/index.ts +2 -0
  40. package/src/mcp/agent-tools-server.ts +1047 -0
  41. package/src/mcp/browser-server.ts +258 -0
  42. package/src/tools/browser.ts +28 -1208
  43. package/src/tools/index.ts +32 -263
  44. package/src/tools/web.ts +0 -73
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, rmSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { CredentialStore } from "./credential-store.js";
6
+ import { resetLocalStore } from "./local-store.js";
7
+
8
+ describe("CredentialStore", () => {
9
+ let tempDir: string;
10
+ let store: CredentialStore;
11
+
12
+ beforeEach(() => {
13
+ resetLocalStore();
14
+ tempDir = mkdtempSync(join(tmpdir(), "cred-test-"));
15
+ store = new CredentialStore(tempDir);
16
+ });
17
+
18
+ afterEach(() => {
19
+ resetLocalStore();
20
+ rmSync(tempDir, { recursive: true, force: true });
21
+ });
22
+
23
+ describe("save and get", () => {
24
+ it("saves and retrieves a credential by ID", () => {
25
+ const meta = store.save("test-key", "api_key", { api_key: "sk-abc123" });
26
+ expect(meta.name).toBe("test-key");
27
+ expect(meta.type).toBe("api_key");
28
+ expect(meta.id).toBeTruthy();
29
+
30
+ const cred = store.get(meta.id);
31
+ expect(cred).not.toBeNull();
32
+ expect(cred!.data.api_key).toBe("sk-abc123");
33
+ });
34
+
35
+ it("retrieves a credential by name", () => {
36
+ store.save("my-login", "login", { username: "user", password: "pass" });
37
+
38
+ const cred = store.getByName("my-login");
39
+ expect(cred).not.toBeNull();
40
+ expect(cred!.data.username).toBe("user");
41
+ expect(cred!.data.password).toBe("pass");
42
+ });
43
+
44
+ it("updates existing credential if name matches", () => {
45
+ store.save("my-key", "api_key", { api_key: "old" });
46
+ store.save("my-key", "api_key", { api_key: "new" });
47
+
48
+ const all = store.list();
49
+ expect(all).toHaveLength(1);
50
+
51
+ const cred = store.getByName("my-key");
52
+ expect(cred!.data.api_key).toBe("new");
53
+ });
54
+
55
+ it("stores with optional skill and tags", () => {
56
+ const meta = store.save("cred", "secret", { token: "abc" }, {
57
+ skillName: "my-skill",
58
+ tags: ["tag1", "tag2"],
59
+ });
60
+
61
+ expect(meta.skillName).toBe("my-skill");
62
+ expect(meta.tags).toEqual(["tag1", "tag2"]);
63
+ });
64
+ });
65
+
66
+ describe("update", () => {
67
+ it("merges data with existing credential", () => {
68
+ const meta = store.save("test", "custom", { a: "1", b: "2" });
69
+ store.update(meta.id, { b: "updated", c: "3" });
70
+
71
+ const cred = store.get(meta.id);
72
+ expect(cred!.data).toEqual({ a: "1", b: "updated", c: "3" });
73
+ });
74
+
75
+ it("returns null for non-existent ID", () => {
76
+ expect(store.update("nonexistent", { key: "val" })).toBeNull();
77
+ });
78
+ });
79
+
80
+ describe("remove", () => {
81
+ it("removes a credential by ID", () => {
82
+ const meta = store.save("to-remove", "secret", { key: "val" });
83
+ expect(store.remove(meta.id)).toBe(true);
84
+ expect(store.get(meta.id)).toBeNull();
85
+ expect(store.list()).toHaveLength(0);
86
+ });
87
+
88
+ it("removes a credential by name", () => {
89
+ store.save("by-name", "api_key", { key: "val" });
90
+ expect(store.removeByName("by-name")).toBe(true);
91
+ expect(store.getByName("by-name")).toBeNull();
92
+ });
93
+
94
+ it("returns false for non-existent credential", () => {
95
+ expect(store.remove("nonexistent")).toBe(false);
96
+ expect(store.removeByName("nonexistent")).toBe(false);
97
+ });
98
+ });
99
+
100
+ describe("query", () => {
101
+ beforeEach(() => {
102
+ store.save("amazon-login", "login", { user: "a" }, { skillName: "price-check", tags: ["amazon", "shopping"] });
103
+ store.save("openai-key", "api_key", { key: "b" }, { skillName: "ai-tools", tags: ["ai"] });
104
+ store.save("github-token", "oauth_token", { token: "c" }, { skillName: "price-check", tags: ["github"] });
105
+ });
106
+
107
+ it("lists all credentials", () => {
108
+ expect(store.list()).toHaveLength(3);
109
+ });
110
+
111
+ it("finds by skill", () => {
112
+ const results = store.findBySkill("price-check");
113
+ expect(results).toHaveLength(2);
114
+ expect(results.map((m) => m.name).sort()).toEqual(["amazon-login", "github-token"]);
115
+ });
116
+
117
+ it("finds by tag", () => {
118
+ const results = store.findByTag("amazon");
119
+ expect(results).toHaveLength(1);
120
+ expect(results[0].name).toBe("amazon-login");
121
+ });
122
+
123
+ it("finds by type", () => {
124
+ const results = store.findByType("login");
125
+ expect(results).toHaveLength(1);
126
+ expect(results[0].name).toBe("amazon-login");
127
+ });
128
+ });
129
+
130
+ describe("bulk operations", () => {
131
+ it("removes all credentials for a skill", () => {
132
+ store.save("a", "secret", { x: "1" }, { skillName: "s1" });
133
+ store.save("b", "secret", { x: "2" }, { skillName: "s1" });
134
+ store.save("c", "secret", { x: "3" }, { skillName: "s2" });
135
+
136
+ const removed = store.removeBySkill("s1");
137
+ expect(removed).toBe(2);
138
+ expect(store.list()).toHaveLength(1);
139
+ expect(store.list()[0].name).toBe("c");
140
+ });
141
+
142
+ it("clears all credentials", () => {
143
+ store.save("a", "secret", { x: "1" });
144
+ store.save("b", "secret", { x: "2" });
145
+
146
+ store.clear();
147
+ expect(store.list()).toHaveLength(0);
148
+ });
149
+ });
150
+
151
+ describe("persistence", () => {
152
+ it("survives store recreation", () => {
153
+ store.save("persist-test", "api_key", { key: "persistent-value" });
154
+
155
+ // Create new store instance with same DB path (same SQLite file)
156
+ const store2 = new CredentialStore(tempDir);
157
+ const cred = store2.getByName("persist-test");
158
+ expect(cred).not.toBeNull();
159
+ expect(cred!.data.key).toBe("persistent-value");
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,266 @@
1
+ import { randomUUID } from "crypto";
2
+ import { dirname } from "path";
3
+ import { deriveKey, encrypt, decrypt } from "./encryption.js";
4
+ import { getLocalStore, type LocalStore } from "./local-store.js";
5
+ import { log } from "../utils/logger.js";
6
+
7
+ // ── Types ───────────────────────────────────────────────────────────
8
+
9
+ export type CredentialType = "api_key" | "oauth_token" | "login" | "secret" | "custom";
10
+
11
+ /** Metadata — contains NO secret data. */
12
+ export interface CredentialMeta {
13
+ id: string;
14
+ name: string;
15
+ type: CredentialType;
16
+ skillName?: string;
17
+ tags: string[];
18
+ createdAt: string;
19
+ updatedAt: string;
20
+ }
21
+
22
+ /** The actual credential payload (key-value pairs). */
23
+ export type CredentialData = Record<string, string>;
24
+
25
+ /** Full credential = metadata + secret data. */
26
+ export interface Credential {
27
+ meta: CredentialMeta;
28
+ data: CredentialData;
29
+ }
30
+
31
+ // ── Row shape from SQLite ───────────────────────────────────────────
32
+
33
+ interface CredentialRow {
34
+ id: string;
35
+ name: string;
36
+ type: string;
37
+ skill_name: string | null;
38
+ tags: string;
39
+ encrypted_data: string;
40
+ created_at: string;
41
+ updated_at: string;
42
+ }
43
+
44
+ // ── Store Implementation ────────────────────────────────────────────
45
+
46
+ export class CredentialStore {
47
+ private store: LocalStore;
48
+ private encryptionKey: Buffer;
49
+
50
+ constructor(dbPath?: string) {
51
+ this.store = getLocalStore(dbPath);
52
+ this.encryptionKey = deriveKey(dirname(this.store.dbPath));
53
+ }
54
+
55
+ // ── CRUD ────────────────────────────────────────────────────────
56
+
57
+ save(
58
+ name: string,
59
+ type: CredentialType,
60
+ data: CredentialData,
61
+ opts?: { skillName?: string; tags?: string[] },
62
+ ): CredentialMeta {
63
+ const db = this.store.getDb();
64
+ const now = new Date().toISOString();
65
+ const encryptedData = this.encryptData(data);
66
+ const tags = JSON.stringify(opts?.tags || []);
67
+
68
+ // Upsert: insert or update on name conflict
69
+ const existing = db.prepare("SELECT id FROM credentials WHERE name = ?").get(name) as { id: string } | undefined;
70
+
71
+ if (existing) {
72
+ db.prepare(`
73
+ UPDATE credentials
74
+ SET type = ?, skill_name = ?, tags = ?, encrypted_data = ?, updated_at = ?
75
+ WHERE id = ?
76
+ `).run(type, opts?.skillName ?? null, tags, encryptedData, now, existing.id);
77
+
78
+ log.debug(`Credential "${name}" updated (${existing.id})`);
79
+ return this.toMeta({ id: existing.id, name, type, skill_name: opts?.skillName ?? null, tags, encrypted_data: encryptedData, created_at: now, updated_at: now });
80
+ }
81
+
82
+ const id = randomUUID();
83
+ db.prepare(`
84
+ INSERT INTO credentials (id, name, type, skill_name, tags, encrypted_data, created_at, updated_at)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
86
+ `).run(id, name, type, opts?.skillName ?? null, tags, encryptedData, now, now);
87
+
88
+ log.debug(`Credential "${name}" saved (${id})`);
89
+ return { id, name, type, skillName: opts?.skillName, tags: opts?.tags || [], createdAt: now, updatedAt: now };
90
+ }
91
+
92
+ get(id: string): Credential | null {
93
+ const row = this.store.getDb()
94
+ .prepare("SELECT * FROM credentials WHERE id = ?")
95
+ .get(id) as CredentialRow | undefined;
96
+
97
+ return row ? this.toCredential(row) : null;
98
+ }
99
+
100
+ getByName(name: string): Credential | null {
101
+ const row = this.store.getDb()
102
+ .prepare("SELECT * FROM credentials WHERE name = ?")
103
+ .get(name) as CredentialRow | undefined;
104
+
105
+ return row ? this.toCredential(row) : null;
106
+ }
107
+
108
+ update(id: string, data: Partial<CredentialData>): CredentialMeta | null {
109
+ const row = this.store.getDb()
110
+ .prepare("SELECT * FROM credentials WHERE id = ?")
111
+ .get(id) as CredentialRow | undefined;
112
+
113
+ if (!row) return null;
114
+
115
+ // Merge with existing data
116
+ const existing = this.decryptData(row.encrypted_data);
117
+ const merged: CredentialData = { ...existing };
118
+ for (const [key, value] of Object.entries(data)) {
119
+ if (value !== undefined) {
120
+ merged[key] = value;
121
+ }
122
+ }
123
+
124
+ const now = new Date().toISOString();
125
+ const encryptedData = this.encryptData(merged);
126
+
127
+ this.store.getDb()
128
+ .prepare("UPDATE credentials SET encrypted_data = ?, updated_at = ? WHERE id = ?")
129
+ .run(encryptedData, now, id);
130
+
131
+ log.debug(`Credential "${row.name}" updated`);
132
+ return this.toMeta({ ...row, encrypted_data: encryptedData, updated_at: now });
133
+ }
134
+
135
+ remove(id: string): boolean {
136
+ const result = this.store.getDb()
137
+ .prepare("DELETE FROM credentials WHERE id = ?")
138
+ .run(id);
139
+
140
+ if (result.changes > 0) {
141
+ log.debug(`Credential ${id} removed`);
142
+ return true;
143
+ }
144
+ return false;
145
+ }
146
+
147
+ removeByName(name: string): boolean {
148
+ const result = this.store.getDb()
149
+ .prepare("DELETE FROM credentials WHERE name = ?")
150
+ .run(name);
151
+
152
+ if (result.changes > 0) {
153
+ log.debug(`Credential "${name}" removed`);
154
+ return true;
155
+ }
156
+ return false;
157
+ }
158
+
159
+ // ── Query ───────────────────────────────────────────────────────
160
+
161
+ list(): CredentialMeta[] {
162
+ const rows = this.store.getDb()
163
+ .prepare("SELECT * FROM credentials ORDER BY updated_at DESC")
164
+ .all() as CredentialRow[];
165
+
166
+ return rows.map((r) => this.toMeta(r));
167
+ }
168
+
169
+ findBySkill(skillName: string): CredentialMeta[] {
170
+ const rows = this.store.getDb()
171
+ .prepare("SELECT * FROM credentials WHERE skill_name = ? ORDER BY name")
172
+ .all(skillName) as CredentialRow[];
173
+
174
+ return rows.map((r) => this.toMeta(r));
175
+ }
176
+
177
+ findByTag(tag: string): CredentialMeta[] {
178
+ // SQLite JSON: search within the tags JSON array (case-insensitive)
179
+ const rows = this.store.getDb()
180
+ .prepare("SELECT * FROM credentials WHERE tags LIKE ? ORDER BY name")
181
+ .all(`%${tag.toLowerCase()}%`) as CredentialRow[];
182
+
183
+ // Filter precisely in JS since LIKE is coarse
184
+ return rows
185
+ .filter((r) => {
186
+ const tags: string[] = JSON.parse(r.tags);
187
+ return tags.some((t) => t.toLowerCase() === tag.toLowerCase());
188
+ })
189
+ .map((r) => this.toMeta(r));
190
+ }
191
+
192
+ findByType(type: CredentialType): CredentialMeta[] {
193
+ const rows = this.store.getDb()
194
+ .prepare("SELECT * FROM credentials WHERE type = ? ORDER BY name")
195
+ .all(type) as CredentialRow[];
196
+
197
+ return rows.map((r) => this.toMeta(r));
198
+ }
199
+
200
+ // ── Bulk ────────────────────────────────────────────────────────
201
+
202
+ removeBySkill(skillName: string): number {
203
+ const result = this.store.getDb()
204
+ .prepare("DELETE FROM credentials WHERE skill_name = ?")
205
+ .run(skillName);
206
+
207
+ return result.changes;
208
+ }
209
+
210
+ clear(): void {
211
+ this.store.getDb().prepare("DELETE FROM credentials").run();
212
+ }
213
+
214
+ // ── Internal ────────────────────────────────────────────────────
215
+
216
+ private encryptData(data: CredentialData): string {
217
+ const payload = encrypt(JSON.stringify(data), this.encryptionKey);
218
+ return JSON.stringify(payload);
219
+ }
220
+
221
+ private decryptData(encrypted: string): CredentialData {
222
+ const payload = JSON.parse(encrypted);
223
+ const decrypted = decrypt(payload, this.encryptionKey);
224
+ return JSON.parse(decrypted);
225
+ }
226
+
227
+ private toMeta(row: CredentialRow): CredentialMeta {
228
+ return {
229
+ id: row.id,
230
+ name: row.name,
231
+ type: row.type as CredentialType,
232
+ skillName: row.skill_name || undefined,
233
+ tags: JSON.parse(row.tags),
234
+ createdAt: row.created_at,
235
+ updatedAt: row.updated_at,
236
+ };
237
+ }
238
+
239
+ private toCredential(row: CredentialRow): Credential | null {
240
+ try {
241
+ return {
242
+ meta: this.toMeta(row),
243
+ data: this.decryptData(row.encrypted_data),
244
+ };
245
+ } catch (err) {
246
+ log.debug(`Failed to decrypt credential ${row.id}: ${err}`);
247
+ return null;
248
+ }
249
+ }
250
+ }
251
+
252
+ // ── Singleton ────────────────────────────────────────────────────────
253
+
254
+ let _instance: CredentialStore | null = null;
255
+
256
+ export function getCredentialStore(): CredentialStore {
257
+ if (!_instance) {
258
+ _instance = new CredentialStore();
259
+ }
260
+ return _instance;
261
+ }
262
+
263
+ /** Reset singleton (for tests). */
264
+ export function resetCredentialStore(): void {
265
+ _instance = null;
266
+ }
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, rmSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { deriveKey, encrypt, decrypt } from "./encryption.js";
6
+
7
+ describe("encryption", () => {
8
+ let tempDir: string;
9
+
10
+ beforeEach(() => {
11
+ tempDir = mkdtempSync(join(tmpdir(), "enc-test-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ rmSync(tempDir, { recursive: true, force: true });
16
+ });
17
+
18
+ describe("deriveKey", () => {
19
+ it("returns a 32-byte key", () => {
20
+ const key = deriveKey(tempDir);
21
+ expect(key).toBeInstanceOf(Buffer);
22
+ expect(key.length).toBe(32);
23
+ });
24
+
25
+ it("returns the same key for the same path", () => {
26
+ const key1 = deriveKey(tempDir);
27
+ const key2 = deriveKey(tempDir);
28
+ expect(key1.equals(key2)).toBe(true);
29
+ });
30
+
31
+ it("returns different keys for different paths", () => {
32
+ const tempDir2 = mkdtempSync(join(tmpdir(), "enc-test2-"));
33
+ try {
34
+ const key1 = deriveKey(tempDir);
35
+ const key2 = deriveKey(tempDir2);
36
+ expect(key1.equals(key2)).toBe(false);
37
+ } finally {
38
+ rmSync(tempDir2, { recursive: true, force: true });
39
+ }
40
+ });
41
+ });
42
+
43
+ describe("encrypt / decrypt", () => {
44
+ it("round-trips plaintext correctly", () => {
45
+ const key = deriveKey(tempDir);
46
+ const plaintext = "my secret api key 12345";
47
+
48
+ const encrypted = encrypt(plaintext, key);
49
+ expect(encrypted.iv).toBeTruthy();
50
+ expect(encrypted.data).toBeTruthy();
51
+ expect(encrypted.tag).toBeTruthy();
52
+
53
+ const decrypted = decrypt(encrypted, key);
54
+ expect(decrypted).toBe(plaintext);
55
+ });
56
+
57
+ it("handles unicode and JSON content", () => {
58
+ const key = deriveKey(tempDir);
59
+ const plaintext = JSON.stringify({ username: "用户", password: "密码123" });
60
+
61
+ const encrypted = encrypt(plaintext, key);
62
+ const decrypted = decrypt(encrypted, key);
63
+ expect(decrypted).toBe(plaintext);
64
+ });
65
+
66
+ it("produces different ciphertext for same plaintext (random IV)", () => {
67
+ const key = deriveKey(tempDir);
68
+ const plaintext = "same text";
69
+
70
+ const enc1 = encrypt(plaintext, key);
71
+ const enc2 = encrypt(plaintext, key);
72
+ expect(enc1.iv).not.toBe(enc2.iv);
73
+ expect(enc1.data).not.toBe(enc2.data);
74
+ });
75
+
76
+ it("fails to decrypt with wrong key", () => {
77
+ const key1 = deriveKey(tempDir);
78
+ const tempDir2 = mkdtempSync(join(tmpdir(), "enc-test-wrong-"));
79
+ const key2 = deriveKey(tempDir2);
80
+
81
+ try {
82
+ const encrypted = encrypt("secret", key1);
83
+ expect(() => decrypt(encrypted, key2)).toThrow();
84
+ } finally {
85
+ rmSync(tempDir2, { recursive: true, force: true });
86
+ }
87
+ });
88
+
89
+ it("fails if ciphertext is tampered", () => {
90
+ const key = deriveKey(tempDir);
91
+ const encrypted = encrypt("secret", key);
92
+
93
+ // Tamper with the data
94
+ const tampered = { ...encrypted, data: encrypted.data.slice(0, -2) + "AA" };
95
+ expect(() => decrypt(tampered, key)).toThrow();
96
+ });
97
+ });
98
+ });
@@ -0,0 +1,82 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash } from "crypto";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir, hostname, userInfo } from "os";
5
+
6
+ const ALGORITHM = "aes-256-gcm";
7
+ const KEY_LENGTH = 32;
8
+ const IV_LENGTH = 12;
9
+ const AUTH_TAG_LENGTH = 16;
10
+ const SALT_FILE = "encryption.salt";
11
+
12
+ /**
13
+ * Derive a stable machine-specific encryption key.
14
+ *
15
+ * Uses hostname + username + homedir as a machine identifier,
16
+ * combined with a random salt persisted to disk. This means:
17
+ * - Same machine = same key (credentials stay readable)
18
+ * - Different machine = different key (stolen files are useless)
19
+ * - Reinstalling the app preserves the salt (it's in ~/.config/assistme/)
20
+ */
21
+ export function deriveKey(basePath: string): Buffer {
22
+ const saltPath = join(basePath, SALT_FILE);
23
+
24
+ let salt: Buffer;
25
+ if (existsSync(saltPath)) {
26
+ salt = readFileSync(saltPath);
27
+ } else {
28
+ salt = randomBytes(32);
29
+ if (!existsSync(basePath)) {
30
+ mkdirSync(basePath, { recursive: true, mode: 0o700 });
31
+ }
32
+ writeFileSync(saltPath, salt, { mode: 0o600 });
33
+ }
34
+
35
+ // Build a stable machine identifier
36
+ const machineId = createHash("sha256")
37
+ .update(hostname())
38
+ .update(userInfo().username)
39
+ .update(homedir())
40
+ .digest();
41
+
42
+ return scryptSync(machineId, salt, KEY_LENGTH);
43
+ }
44
+
45
+ export interface EncryptedPayload {
46
+ /** Base64-encoded IV */
47
+ iv: string;
48
+ /** Base64-encoded encrypted data */
49
+ data: string;
50
+ /** Base64-encoded GCM auth tag */
51
+ tag: string;
52
+ }
53
+
54
+ export function encrypt(plaintext: string, key: Buffer): EncryptedPayload {
55
+ const iv = randomBytes(IV_LENGTH);
56
+ const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
57
+
58
+ const encrypted = Buffer.concat([
59
+ cipher.update(plaintext, "utf-8"),
60
+ cipher.final(),
61
+ ]);
62
+
63
+ return {
64
+ iv: iv.toString("base64"),
65
+ data: encrypted.toString("base64"),
66
+ tag: cipher.getAuthTag().toString("base64"),
67
+ };
68
+ }
69
+
70
+ export function decrypt(payload: EncryptedPayload, key: Buffer): string {
71
+ const iv = Buffer.from(payload.iv, "base64");
72
+ const data = Buffer.from(payload.data, "base64");
73
+ const tag = Buffer.from(payload.tag, "base64");
74
+
75
+ const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
76
+ decipher.setAuthTag(tag);
77
+
78
+ return Buffer.concat([
79
+ decipher.update(data),
80
+ decipher.final(),
81
+ ]).toString("utf-8");
82
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ CredentialStore,
3
+ getCredentialStore,
4
+ resetCredentialStore,
5
+ type Credential,
6
+ type CredentialData,
7
+ type CredentialMeta,
8
+ type CredentialType,
9
+ } from "./credential-store.js";
10
+
11
+ export {
12
+ LocalStore,
13
+ getLocalStore,
14
+ resetLocalStore,
15
+ } from "./local-store.js";