@snapback/cli 1.0.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.
@@ -0,0 +1,257 @@
1
+ import { __name } from './chunk-WCQVDF3K.js';
2
+ import { scryptSync, randomBytes, createCipheriv, createDecipheriv } from 'crypto';
3
+ import { unlink, readFile, mkdir, writeFile } from 'fs/promises';
4
+ import { homedir, hostname, platform, userInfo } from 'os';
5
+ import { join, dirname } from 'path';
6
+
7
+ var SERVICE_NAME = "snapback-cli";
8
+ var ACCOUNT_NAME = "default";
9
+ var ENCRYPTION_ALGORITHM = "aes-256-gcm";
10
+ var KEY_LENGTH = 32;
11
+ var IV_LENGTH = 12;
12
+ var AUTH_TAG_LENGTH = 16;
13
+ var SALT_LENGTH = 32;
14
+ var GLOBAL_DIR = join(homedir(), ".snapback");
15
+ var CREDENTIALS_FILE = join(GLOBAL_DIR, "credentials.json");
16
+ var ENCRYPTED_CREDENTIALS_FILE = join(GLOBAL_DIR, "credentials.enc");
17
+ async function createKeytarProvider() {
18
+ try {
19
+ const keytar = await import('keytar');
20
+ return {
21
+ name: "keytar",
22
+ async isAvailable() {
23
+ try {
24
+ await keytar.getPassword("__snapback_test__", "__test__");
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ },
30
+ async getPassword(service, account) {
31
+ return keytar.getPassword(service, account);
32
+ },
33
+ async setPassword(service, account, password) {
34
+ await keytar.setPassword(service, account, password);
35
+ },
36
+ async deletePassword(service, account) {
37
+ return keytar.deletePassword(service, account);
38
+ }
39
+ };
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ __name(createKeytarProvider, "createKeytarProvider");
45
+ function deriveMachineKey(salt) {
46
+ const machineData = [
47
+ hostname(),
48
+ platform(),
49
+ userInfo().username,
50
+ homedir(),
51
+ // Add some entropy from process info
52
+ process.arch,
53
+ process.platform
54
+ ].join("|");
55
+ return scryptSync(machineData, salt, KEY_LENGTH);
56
+ }
57
+ __name(deriveMachineKey, "deriveMachineKey");
58
+ function encryptCredentials(credentials, salt) {
59
+ const key = deriveMachineKey(salt);
60
+ const iv = randomBytes(IV_LENGTH);
61
+ const cipher = createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
62
+ const plaintext = JSON.stringify(credentials);
63
+ const encrypted = Buffer.concat([
64
+ cipher.update(plaintext, "utf8"),
65
+ cipher.final()
66
+ ]);
67
+ const authTag = cipher.getAuthTag();
68
+ return Buffer.concat([
69
+ salt,
70
+ iv,
71
+ authTag,
72
+ encrypted
73
+ ]);
74
+ }
75
+ __name(encryptCredentials, "encryptCredentials");
76
+ function decryptCredentials(data) {
77
+ const salt = data.subarray(0, SALT_LENGTH);
78
+ const iv = data.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
79
+ const authTag = data.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
80
+ const encrypted = data.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
81
+ const key = deriveMachineKey(salt);
82
+ const decipher = createDecipheriv(ENCRYPTION_ALGORITHM, key, iv);
83
+ decipher.setAuthTag(authTag);
84
+ const decrypted = Buffer.concat([
85
+ decipher.update(encrypted),
86
+ decipher.final()
87
+ ]);
88
+ return JSON.parse(decrypted.toString("utf8"));
89
+ }
90
+ __name(decryptCredentials, "decryptCredentials");
91
+ function createEncryptedFileProvider() {
92
+ return {
93
+ name: "encrypted-file",
94
+ async isAvailable() {
95
+ return true;
96
+ },
97
+ async getPassword(_service, _account) {
98
+ try {
99
+ const data = await readFile(ENCRYPTED_CREDENTIALS_FILE);
100
+ const credentials = decryptCredentials(data);
101
+ return JSON.stringify(credentials);
102
+ } catch {
103
+ return null;
104
+ }
105
+ },
106
+ async setPassword(_service, _account, password) {
107
+ const credentials = JSON.parse(password);
108
+ const salt = randomBytes(SALT_LENGTH);
109
+ const encrypted = encryptCredentials(credentials, salt);
110
+ await mkdir(dirname(ENCRYPTED_CREDENTIALS_FILE), {
111
+ recursive: true
112
+ });
113
+ await writeFile(ENCRYPTED_CREDENTIALS_FILE, encrypted);
114
+ },
115
+ async deletePassword(_service, _account) {
116
+ try {
117
+ await unlink(ENCRYPTED_CREDENTIALS_FILE);
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+ };
124
+ }
125
+ __name(createEncryptedFileProvider, "createEncryptedFileProvider");
126
+ var SecureCredentialsManager = class SecureCredentialsManager2 {
127
+ static {
128
+ __name(this, "SecureCredentialsManager");
129
+ }
130
+ provider = null;
131
+ initialized = false;
132
+ /**
133
+ * Initialize the credentials manager
134
+ * Selects the best available provider
135
+ */
136
+ async initialize() {
137
+ if (this.initialized) return;
138
+ const keytarProvider = await createKeytarProvider();
139
+ if (keytarProvider && await keytarProvider.isAvailable()) {
140
+ this.provider = keytarProvider;
141
+ this.initialized = true;
142
+ return;
143
+ }
144
+ this.provider = createEncryptedFileProvider();
145
+ this.initialized = true;
146
+ }
147
+ /**
148
+ * Get the name of the active provider
149
+ */
150
+ getProviderName() {
151
+ return this.provider?.name ?? "none";
152
+ }
153
+ /**
154
+ * Get stored credentials
155
+ */
156
+ async getCredentials() {
157
+ await this.initialize();
158
+ if (!this.provider) return null;
159
+ const stored = await this.provider.getPassword(SERVICE_NAME, ACCOUNT_NAME);
160
+ if (!stored) {
161
+ const legacy = await this.getLegacyCredentials();
162
+ if (legacy) {
163
+ await this.setCredentials(legacy);
164
+ try {
165
+ await unlink(CREDENTIALS_FILE);
166
+ } catch {
167
+ }
168
+ return legacy;
169
+ }
170
+ return null;
171
+ }
172
+ try {
173
+ return JSON.parse(stored);
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+ /**
179
+ * Get legacy plain-text credentials for migration
180
+ */
181
+ async getLegacyCredentials() {
182
+ try {
183
+ const content = await readFile(CREDENTIALS_FILE, "utf8");
184
+ return JSON.parse(content);
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ /**
190
+ * Save credentials securely
191
+ */
192
+ async setCredentials(credentials) {
193
+ await this.initialize();
194
+ if (!this.provider) {
195
+ throw new Error("No credentials provider available");
196
+ }
197
+ await this.provider.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials));
198
+ }
199
+ /**
200
+ * Clear stored credentials
201
+ */
202
+ async clearCredentials() {
203
+ await this.initialize();
204
+ if (!this.provider) return;
205
+ await this.provider.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
206
+ try {
207
+ await unlink(CREDENTIALS_FILE);
208
+ } catch {
209
+ }
210
+ try {
211
+ await unlink(ENCRYPTED_CREDENTIALS_FILE);
212
+ } catch {
213
+ }
214
+ }
215
+ /**
216
+ * Check if user is logged in
217
+ */
218
+ async isLoggedIn() {
219
+ const credentials = await this.getCredentials();
220
+ if (!credentials?.accessToken) return false;
221
+ if (credentials.expiresAt) {
222
+ const expiresAt = new Date(credentials.expiresAt);
223
+ if (expiresAt < /* @__PURE__ */ new Date()) {
224
+ return false;
225
+ }
226
+ }
227
+ return true;
228
+ }
229
+ };
230
+ var secureCredentialsManager = null;
231
+ function getSecureCredentials() {
232
+ if (!secureCredentialsManager) {
233
+ secureCredentialsManager = new SecureCredentialsManager();
234
+ }
235
+ return secureCredentialsManager;
236
+ }
237
+ __name(getSecureCredentials, "getSecureCredentials");
238
+ async function getCredentialsSecure() {
239
+ return getSecureCredentials().getCredentials();
240
+ }
241
+ __name(getCredentialsSecure, "getCredentialsSecure");
242
+ async function saveCredentialsSecure(credentials) {
243
+ return getSecureCredentials().setCredentials(credentials);
244
+ }
245
+ __name(saveCredentialsSecure, "saveCredentialsSecure");
246
+ async function clearCredentialsSecure() {
247
+ return getSecureCredentials().clearCredentials();
248
+ }
249
+ __name(clearCredentialsSecure, "clearCredentialsSecure");
250
+ async function isLoggedInSecure() {
251
+ return getSecureCredentials().isLoggedIn();
252
+ }
253
+ __name(isLoggedInSecure, "isLoggedInSecure");
254
+
255
+ export { SecureCredentialsManager, clearCredentialsSecure, getCredentialsSecure, getSecureCredentials, isLoggedInSecure, saveCredentialsSecure };
256
+ //# sourceMappingURL=secure-credentials-YKZHAZNB.js.map
257
+ //# sourceMappingURL=secure-credentials-YKZHAZNB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/services/secure-credentials.ts"],"names":["SERVICE_NAME","ACCOUNT_NAME","ENCRYPTION_ALGORITHM","KEY_LENGTH","IV_LENGTH","AUTH_TAG_LENGTH","SALT_LENGTH","GLOBAL_DIR","join","homedir","CREDENTIALS_FILE","ENCRYPTED_CREDENTIALS_FILE","createKeytarProvider","keytar","name","isAvailable","getPassword","service","account","setPassword","password","deletePassword","deriveMachineKey","salt","machineData","hostname","platform","userInfo","username","process","arch","scryptSync","encryptCredentials","credentials","key","iv","randomBytes","cipher","createCipheriv","plaintext","JSON","stringify","encrypted","Buffer","concat","update","final","authTag","getAuthTag","decryptCredentials","data","subarray","decipher","createDecipheriv","setAuthTag","decrypted","parse","toString","createEncryptedFileProvider","_service","_account","readFile","mkdir","dirname","recursive","writeFile","unlink","SecureCredentialsManager","provider","initialized","initialize","keytarProvider","getProviderName","getCredentials","stored","legacy","getLegacyCredentials","setCredentials","content","Error","clearCredentials","isLoggedIn","accessToken","expiresAt","Date","secureCredentialsManager","getSecureCredentials","getCredentialsSecure","saveCredentialsSecure","clearCredentialsSecure","isLoggedInSecure"],"mappings":"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","file":"secure-credentials-YKZHAZNB.js","sourcesContent":["/**\n * Secure Credentials Storage for SnapBack CLI\n *\n * FIX 4: Implements OS keychain storage with file fallback\n *\n * Security Hierarchy:\n * 1. OS Keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service)\n * 2. Encrypted file fallback (AES-256-GCM with machine-derived key)\n * 3. Plain text fallback (development only, with warning)\n *\n * @module services/secure-credentials\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from \"node:crypto\";\nimport { mkdir, readFile, unlink, writeFile } from \"node:fs/promises\";\nimport { homedir, hostname, platform, userInfo } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { GlobalCredentials } from \"./snapback-dir\";\n\n// =============================================================================\n// CONSTANTS\n// =============================================================================\n\nconst SERVICE_NAME = \"snapback-cli\";\nconst ACCOUNT_NAME = \"default\";\nconst ENCRYPTION_ALGORITHM = \"aes-256-gcm\";\nconst KEY_LENGTH = 32;\nconst IV_LENGTH = 12;\nconst AUTH_TAG_LENGTH = 16;\nconst SALT_LENGTH = 32;\n\n// File paths\nconst GLOBAL_DIR = join(homedir(), \".snapback\");\nconst CREDENTIALS_FILE = join(GLOBAL_DIR, \"credentials.json\");\nconst ENCRYPTED_CREDENTIALS_FILE = join(GLOBAL_DIR, \"credentials.enc\");\n\n// =============================================================================\n// KEYCHAIN INTERFACE\n// =============================================================================\n\n/**\n * Keychain abstraction interface\n * Allows for different implementations based on availability\n */\ninterface KeychainProvider {\n\tname: string;\n\tisAvailable(): Promise<boolean>;\n\tgetPassword(service: string, account: string): Promise<string | null>;\n\tsetPassword(service: string, account: string, password: string): Promise<void>;\n\tdeletePassword(service: string, account: string): Promise<boolean>;\n}\n\n// =============================================================================\n// KEYTAR PROVIDER (OS KEYCHAIN)\n// =============================================================================\n\n/**\n * Keytar-based keychain provider\n * Uses OS-level secure credential storage\n */\nasync function createKeytarProvider(): Promise<KeychainProvider | null> {\n\ttry {\n\t\t// Dynamic import to handle missing keytar gracefully\n\t\t// @ts-expect-error - keytar is optional dependency\n\t\tconst keytar = await import(\"keytar\");\n\n\t\treturn {\n\t\t\tname: \"keytar\",\n\t\t\tasync isAvailable(): Promise<boolean> {\n\t\t\t\ttry {\n\t\t\t\t\t// Test availability by trying a no-op operation\n\t\t\t\t\tawait keytar.getPassword(\"__snapback_test__\", \"__test__\");\n\t\t\t\t\treturn true;\n\t\t\t\t} catch {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\t\t\tasync getPassword(service: string, account: string): Promise<string | null> {\n\t\t\t\treturn keytar.getPassword(service, account);\n\t\t\t},\n\t\t\tasync setPassword(service: string, account: string, password: string): Promise<void> {\n\t\t\t\tawait keytar.setPassword(service, account, password);\n\t\t\t},\n\t\t\tasync deletePassword(service: string, account: string): Promise<boolean> {\n\t\t\t\treturn keytar.deletePassword(service, account);\n\t\t\t},\n\t\t};\n\t} catch {\n\t\t// keytar not available (not installed or native module issues)\n\t\treturn null;\n\t}\n}\n\n// =============================================================================\n// ENCRYPTED FILE PROVIDER\n// =============================================================================\n\n/**\n * Derive an encryption key from machine-specific data\n * This provides defense-in-depth even if the file is copied to another machine\n */\nfunction deriveMachineKey(salt: Buffer): Buffer {\n\t// Combine machine-specific values for key derivation\n\tconst machineData = [\n\t\thostname(),\n\t\tplatform(),\n\t\tuserInfo().username,\n\t\thomedir(),\n\t\t// Add some entropy from process info\n\t\tprocess.arch,\n\t\tprocess.platform,\n\t].join(\"|\");\n\n\treturn scryptSync(machineData, salt, KEY_LENGTH);\n}\n\n/**\n * Encrypt credentials with machine-derived key\n */\nfunction encryptCredentials(credentials: GlobalCredentials, salt: Buffer): Buffer {\n\tconst key = deriveMachineKey(salt);\n\tconst iv = randomBytes(IV_LENGTH);\n\tconst cipher = createCipheriv(ENCRYPTION_ALGORITHM, key, iv);\n\n\tconst plaintext = JSON.stringify(credentials);\n\tconst encrypted = Buffer.concat([cipher.update(plaintext, \"utf8\"), cipher.final()]);\n\tconst authTag = cipher.getAuthTag();\n\n\t// Format: salt (32) + iv (12) + authTag (16) + encrypted data\n\treturn Buffer.concat([salt, iv, authTag, encrypted]);\n}\n\n/**\n * Decrypt credentials with machine-derived key\n */\nfunction decryptCredentials(data: Buffer): GlobalCredentials {\n\t// Extract components\n\tconst salt = data.subarray(0, SALT_LENGTH);\n\tconst iv = data.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n\tconst authTag = data.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);\n\tconst encrypted = data.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);\n\n\tconst key = deriveMachineKey(salt);\n\tconst decipher = createDecipheriv(ENCRYPTION_ALGORITHM, key, iv);\n\tdecipher.setAuthTag(authTag);\n\n\tconst decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);\n\treturn JSON.parse(decrypted.toString(\"utf8\")) as GlobalCredentials;\n}\n\n/**\n * Encrypted file provider\n * Uses AES-256-GCM with machine-derived key\n */\nfunction createEncryptedFileProvider(): KeychainProvider {\n\treturn {\n\t\tname: \"encrypted-file\",\n\t\tasync isAvailable(): Promise<boolean> {\n\t\t\treturn true; // Always available as fallback\n\t\t},\n\t\tasync getPassword(_service: string, _account: string): Promise<string | null> {\n\t\t\ttry {\n\t\t\t\tconst data = await readFile(ENCRYPTED_CREDENTIALS_FILE);\n\t\t\t\tconst credentials = decryptCredentials(data);\n\t\t\t\treturn JSON.stringify(credentials);\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\t\tasync setPassword(_service: string, _account: string, password: string): Promise<void> {\n\t\t\tconst credentials = JSON.parse(password) as GlobalCredentials;\n\t\t\tconst salt = randomBytes(SALT_LENGTH);\n\t\t\tconst encrypted = encryptCredentials(credentials, salt);\n\n\t\t\tawait mkdir(dirname(ENCRYPTED_CREDENTIALS_FILE), { recursive: true });\n\t\t\tawait writeFile(ENCRYPTED_CREDENTIALS_FILE, encrypted);\n\t\t},\n\t\tasync deletePassword(_service: string, _account: string): Promise<boolean> {\n\t\t\ttry {\n\t\t\t\tawait unlink(ENCRYPTED_CREDENTIALS_FILE);\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t};\n}\n\n// =============================================================================\n// PLAIN FILE PROVIDER (DEVELOPMENT FALLBACK)\n// =============================================================================\n\n/**\n * Plain file provider (legacy, development only)\n * Shows warning when used in production\n * @deprecated Use encrypted file provider instead\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nfunction _createPlainFileProvider(): KeychainProvider {\n\tlet warningShown = false;\n\n\treturn {\n\t\tname: \"plain-file\",\n\t\tasync isAvailable(): Promise<boolean> {\n\t\t\treturn true;\n\t\t},\n\t\tasync getPassword(_service: string, _account: string): Promise<string | null> {\n\t\t\ttry {\n\t\t\t\tconst content = await readFile(CREDENTIALS_FILE, \"utf8\");\n\t\t\t\treturn content;\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\t\tasync setPassword(_service: string, _account: string, password: string): Promise<void> {\n\t\t\tif (process.env.NODE_ENV === \"production\" && !warningShown) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t\"\\n⚠️ Warning: Storing credentials in plain text. \" +\n\t\t\t\t\t\t\"Install 'keytar' for OS keychain support: pnpm add keytar\\n\",\n\t\t\t\t);\n\t\t\t\twarningShown = true;\n\t\t\t}\n\n\t\t\tawait mkdir(dirname(CREDENTIALS_FILE), { recursive: true });\n\t\t\tawait writeFile(CREDENTIALS_FILE, password, { mode: 0o600 }); // Restrict file permissions\n\t\t},\n\t\tasync deletePassword(_service: string, _account: string): Promise<boolean> {\n\t\t\ttry {\n\t\t\t\tawait unlink(CREDENTIALS_FILE);\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t};\n}\n\n// =============================================================================\n// SECURE CREDENTIALS MANAGER\n// =============================================================================\n\n/**\n * Secure Credentials Manager\n *\n * Automatically selects the most secure available storage:\n * 1. OS Keychain (via keytar)\n * 2. Encrypted file\n * 3. Plain file (with warning)\n */\nclass SecureCredentialsManager {\n\tprivate provider: KeychainProvider | null = null;\n\tprivate initialized = false;\n\n\t/**\n\t * Initialize the credentials manager\n\t * Selects the best available provider\n\t */\n\tasync initialize(): Promise<void> {\n\t\tif (this.initialized) return;\n\n\t\t// Try keytar first (OS keychain)\n\t\tconst keytarProvider = await createKeytarProvider();\n\t\tif (keytarProvider && (await keytarProvider.isAvailable())) {\n\t\t\tthis.provider = keytarProvider;\n\t\t\tthis.initialized = true;\n\t\t\treturn;\n\t\t}\n\n\t\t// Fall back to encrypted file\n\t\tthis.provider = createEncryptedFileProvider();\n\t\tthis.initialized = true;\n\t}\n\n\t/**\n\t * Get the name of the active provider\n\t */\n\tgetProviderName(): string {\n\t\treturn this.provider?.name ?? \"none\";\n\t}\n\n\t/**\n\t * Get stored credentials\n\t */\n\tasync getCredentials(): Promise<GlobalCredentials | null> {\n\t\tawait this.initialize();\n\t\tif (!this.provider) return null;\n\n\t\tconst stored = await this.provider.getPassword(SERVICE_NAME, ACCOUNT_NAME);\n\t\tif (!stored) {\n\t\t\t// Check legacy plain file as migration fallback\n\t\t\tconst legacy = await this.getLegacyCredentials();\n\t\t\tif (legacy) {\n\t\t\t\t// Migrate to secure storage\n\t\t\t\tawait this.setCredentials(legacy);\n\t\t\t\t// Delete legacy file after successful migration\n\t\t\t\ttry {\n\t\t\t\t\tawait unlink(CREDENTIALS_FILE);\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore if file doesn't exist\n\t\t\t\t}\n\t\t\t\treturn legacy;\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\treturn JSON.parse(stored) as GlobalCredentials;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Get legacy plain-text credentials for migration\n\t */\n\tprivate async getLegacyCredentials(): Promise<GlobalCredentials | null> {\n\t\ttry {\n\t\t\tconst content = await readFile(CREDENTIALS_FILE, \"utf8\");\n\t\t\treturn JSON.parse(content) as GlobalCredentials;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Save credentials securely\n\t */\n\tasync setCredentials(credentials: GlobalCredentials): Promise<void> {\n\t\tawait this.initialize();\n\t\tif (!this.provider) {\n\t\t\tthrow new Error(\"No credentials provider available\");\n\t\t}\n\n\t\tawait this.provider.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials));\n\t}\n\n\t/**\n\t * Clear stored credentials\n\t */\n\tasync clearCredentials(): Promise<void> {\n\t\tawait this.initialize();\n\t\tif (!this.provider) return;\n\n\t\tawait this.provider.deletePassword(SERVICE_NAME, ACCOUNT_NAME);\n\n\t\t// Also clean up any legacy files\n\t\ttry {\n\t\t\tawait unlink(CREDENTIALS_FILE);\n\t\t} catch {\n\t\t\t// Ignore\n\t\t}\n\t\ttry {\n\t\t\tawait unlink(ENCRYPTED_CREDENTIALS_FILE);\n\t\t} catch {\n\t\t\t// Ignore\n\t\t}\n\t}\n\n\t/**\n\t * Check if user is logged in\n\t */\n\tasync isLoggedIn(): Promise<boolean> {\n\t\tconst credentials = await this.getCredentials();\n\t\tif (!credentials?.accessToken) return false;\n\n\t\t// Check if token is expired\n\t\tif (credentials.expiresAt) {\n\t\t\tconst expiresAt = new Date(credentials.expiresAt);\n\t\t\tif (expiresAt < new Date()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n}\n\n// =============================================================================\n// EXPORTS\n// =============================================================================\n\n// Singleton instance\nlet secureCredentialsManager: SecureCredentialsManager | null = null;\n\n/**\n * Get the secure credentials manager singleton\n */\nexport function getSecureCredentials(): SecureCredentialsManager {\n\tif (!secureCredentialsManager) {\n\t\tsecureCredentialsManager = new SecureCredentialsManager();\n\t}\n\treturn secureCredentialsManager;\n}\n\n/**\n * Secure versions of credential functions (drop-in replacements)\n */\nexport async function getCredentialsSecure(): Promise<GlobalCredentials | null> {\n\treturn getSecureCredentials().getCredentials();\n}\n\nexport async function saveCredentialsSecure(credentials: GlobalCredentials): Promise<void> {\n\treturn getSecureCredentials().setCredentials(credentials);\n}\n\nexport async function clearCredentialsSecure(): Promise<void> {\n\treturn getSecureCredentials().clearCredentials();\n}\n\nexport async function isLoggedInSecure(): Promise<boolean> {\n\treturn getSecureCredentials().isLoggedIn();\n}\n\nexport { SecureCredentialsManager };\n"]}
@@ -0,0 +1,5 @@
1
+ export { appendSnapbackJsonl, clearCredentials, createGlobalDirectory, createSnapbackDirectory, deleteGlobalJson, endCurrentSession, findWorkspaceRoot, getCredentials, getCurrentSession, getGlobalConfig, getGlobalDir, getGlobalPath, getLearnings, getProtectedFiles, getStats, getViolations, getWorkspaceConfig, getWorkspaceDir, getWorkspacePath, getWorkspaceVitals, isLoggedIn, isSnapbackInitialized, loadSnapbackJsonl, pathExists, readGlobalJson, readSnapbackJson, recordLearning, recordViolation, saveCredentials, saveCurrentSession, saveGlobalConfig, saveProtectedFiles, saveWorkspaceConfig, saveWorkspaceVitals, writeGlobalJson, writeSnapbackJson } from './chunk-KSPLKCVF.js';
2
+ export { generateId } from './chunk-BCIXMIPW.js';
3
+ import './chunk-WCQVDF3K.js';
4
+ //# sourceMappingURL=snapback-dir-4QRR2IPV.js.map
5
+ //# sourceMappingURL=snapback-dir-4QRR2IPV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"snapback-dir-4QRR2IPV.js"}
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@snapback/cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for managing SnapBack snapshots and file protection",
5
+ "homepage": "https://snapback.dev",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/snapback-dev/snapback-cli.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/snapback-dev/snapback-cli/issues"
12
+ },
13
+ "license": "Apache-2.0",
14
+ "author": {
15
+ "name": "SnapBack Team",
16
+ "url": "https://snapback.dev"
17
+ },
18
+ "keywords": [
19
+ "snapback",
20
+ "cli",
21
+ "file-protection",
22
+ "snapshots",
23
+ "backup"
24
+ ],
25
+ "bin": {
26
+ "snapback": "dist/index.js",
27
+ "snap": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "dependencies": {
35
+ "@inquirer/prompts": "catalog:",
36
+ "@modelcontextprotocol/sdk": "catalog:",
37
+ "@snapback/contracts": "workspace:*",
38
+ "@snapback/core": "workspace:*",
39
+ "@snapback/core-runtime": "workspace:*",
40
+ "@snapback/mcp": "workspace:*",
41
+ "@snapback/mcp-config": "workspace:*",
42
+ "@snapback/engine": "workspace:*",
43
+ "@snapback/intelligence": "workspace:*",
44
+ "@snapback/sdk": "workspace:*",
45
+ "boxen": "catalog:",
46
+ "chalk": "catalog:",
47
+ "chokidar": "catalog:",
48
+ "cli-table3": "catalog:",
49
+ "commander": "catalog:",
50
+ "conf": "catalog:",
51
+ "esprima": "catalog:",
52
+ "execa": "catalog:",
53
+ "log-update": "catalog:",
54
+ "ora": "catalog:",
55
+ "zod": "catalog:"
56
+ },
57
+ "devDependencies": {
58
+ "@vitest/coverage-v8": "catalog:",
59
+ "tsup": "catalog:",
60
+ "tsx": "catalog:",
61
+ "typescript": "catalog:",
62
+ "vitest": "catalog:"
63
+ },
64
+ "scripts": {
65
+ "build": "tsup && tsc --build tsconfig.build.json --emitDeclarationOnly",
66
+ "check": "biome check .",
67
+ "dev": "tsup --watch",
68
+ "format": "biome format --write .",
69
+ "lint": "biome lint .",
70
+ "lint:fix": "biome lint --fix .",
71
+ "prepublishOnly": "pnpm run build && pnpm run test",
72
+ "test": "vitest run",
73
+ "test:coverage": "vitest run --coverage",
74
+ "test:watch": "vitest",
75
+ "type-check": "tsc -p tsconfig.json --noEmit",
76
+ "cli:install": "pnpm build && pnpm link --global && echo '✅ CLI installed globally. Run: snap --help or snapback --help'",
77
+ "cli:uninstall": "pnpm unlink --global && echo '✅ CLI uninstalled globally'",
78
+ "cli:reinstall": "pnpm run cli:uninstall && pnpm run cli:install",
79
+ "mcp:configure": "node dist/index.js tools configure --yes",
80
+ "mcp:configure:dev": "node dist/index.js tools configure --yes --dev",
81
+ "mcp:status": "node dist/index.js tools configure --list"
82
+ },
83
+ "publishConfig": {
84
+ "access": "public",
85
+ "registry": "https://registry.npmjs.org/"
86
+ },
87
+ "exports": {
88
+ ".": "./dist/index.js",
89
+ "./services/snapback-dir": "./dist/services/snapback-dir.js"
90
+ },
91
+ "type": "module",
92
+ "engines": {
93
+ "node": ">=18.17.0",
94
+ "npm": ">=8.0.0",
95
+ "pnpm": ">=8.0.0"
96
+ }
97
+ }