lsh-framework 3.2.5 → 3.5.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/LICENSE +21 -0
- package/README.md +72 -34
- package/dist/commands/ipfs.js +7 -12
- package/dist/commands/sync.js +49 -38
- package/dist/constants/config.js +3 -0
- package/dist/lib/floating-point-arithmetic.js +2 -2
- package/dist/lib/ipfs-client-manager.js +51 -13
- package/dist/lib/ipfs-secrets-storage.js +21 -16
- package/dist/lib/ipfs-sync.js +88 -14
- package/dist/lib/secrets-manager.js +117 -47
- package/dist/lib/sync-key-store.js +87 -0
- package/dist/services/secrets/secrets.js +77 -39
- package/package.json +16 -16
- package/dist/__tests__/fixtures/job-fixtures.js +0 -204
- package/dist/__tests__/fixtures/supabase-mocks.js +0 -252
- package/dist/daemon/job-registry.js +0 -556
- package/dist/daemon/lshd.js +0 -968
- package/dist/daemon/saas-api-routes.js +0 -599
- package/dist/daemon/saas-api-server.js +0 -231
- package/dist/examples/supabase-integration.js +0 -106
- package/dist/lib/api-response.js +0 -226
- package/dist/lib/base-command-registrar.js +0 -287
- package/dist/lib/base-job-manager.js +0 -295
- package/dist/lib/cloud-config-manager.js +0 -348
- package/dist/lib/cron-job-manager.js +0 -368
- package/dist/lib/daemon-client-helper.js +0 -145
- package/dist/lib/daemon-client.js +0 -513
- package/dist/lib/database-persistence.js +0 -727
- package/dist/lib/database-schema.js +0 -259
- package/dist/lib/database-types.js +0 -90
- package/dist/lib/enhanced-history-system.js +0 -247
- package/dist/lib/history-system.js +0 -246
- package/dist/lib/job-manager.js +0 -436
- package/dist/lib/job-storage-database.js +0 -164
- package/dist/lib/job-storage-memory.js +0 -73
- package/dist/lib/local-storage-adapter.js +0 -507
- package/dist/lib/optimized-job-scheduler.js +0 -356
- package/dist/lib/saas-audit.js +0 -215
- package/dist/lib/saas-auth.js +0 -465
- package/dist/lib/saas-billing.js +0 -503
- package/dist/lib/saas-email.js +0 -403
- package/dist/lib/saas-encryption.js +0 -221
- package/dist/lib/saas-organizations.js +0 -662
- package/dist/lib/saas-secrets.js +0 -408
- package/dist/lib/saas-types.js +0 -165
- package/dist/lib/supabase-client.js +0 -125
- package/dist/lib/supabase-utils.js +0 -396
- package/dist/services/cron/cron-registrar.js +0 -240
- package/dist/services/cron/cron.js +0 -9
- package/dist/services/daemon/daemon-registrar.js +0 -585
- package/dist/services/daemon/daemon.js +0 -9
- package/dist/services/supabase/supabase-registrar.js +0 -375
- package/dist/services/supabase/supabase.js +0 -9
|
@@ -2,53 +2,25 @@
|
|
|
2
2
|
* Secrets Management Commands
|
|
3
3
|
* Sync .env files across development environments
|
|
4
4
|
*/
|
|
5
|
-
import SecretsManager from '../../lib/secrets-manager.js';
|
|
5
|
+
import SecretsManager, { findEncryptionKey } from '../../lib/secrets-manager.js';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import * as readline from 'readline';
|
|
9
9
|
import { getGitRepoInfo } from '../../lib/git-utils.js';
|
|
10
10
|
import { ENV_VARS } from '../../constants/index.js';
|
|
11
11
|
import { IPFSClientManager } from '../../lib/ipfs-client-manager.js';
|
|
12
|
+
import { SyncKeyStore } from '../../lib/sync-key-store.js';
|
|
13
|
+
function maskKey(key) {
|
|
14
|
+
if (key.length <= 12)
|
|
15
|
+
return '*'.repeat(key.length);
|
|
16
|
+
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
17
|
+
}
|
|
12
18
|
/**
|
|
13
19
|
* Type guard to check if a string is a valid OutputFormat.
|
|
14
20
|
*/
|
|
15
21
|
function isOutputFormat(value) {
|
|
16
22
|
return ['env', 'json', 'yaml', 'toml', 'export'].includes(value);
|
|
17
23
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Find existing LSH_SECRETS_KEY from environment, local .env, or global ~/.env
|
|
20
|
-
*/
|
|
21
|
-
function findExistingKey() {
|
|
22
|
-
// 1. Check environment variable
|
|
23
|
-
const envKey = process.env[ENV_VARS.LSH_SECRETS_KEY];
|
|
24
|
-
if (envKey)
|
|
25
|
-
return envKey;
|
|
26
|
-
// 2. Check local .env
|
|
27
|
-
const localEnvPath = path.join(process.cwd(), '.env');
|
|
28
|
-
const localKey = readKeyFromEnvFile(localEnvPath);
|
|
29
|
-
if (localKey)
|
|
30
|
-
return localKey;
|
|
31
|
-
// 3. Check global ~/.env
|
|
32
|
-
const globalEnvPath = path.join(process.env.HOME || '~', '.env');
|
|
33
|
-
const globalKey = readKeyFromEnvFile(globalEnvPath);
|
|
34
|
-
if (globalKey)
|
|
35
|
-
return globalKey;
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
function readKeyFromEnvFile(envPath) {
|
|
39
|
-
try {
|
|
40
|
-
if (fs.existsSync(envPath)) {
|
|
41
|
-
const content = fs.readFileSync(envPath, 'utf-8');
|
|
42
|
-
const match = content.match(/^LSH_SECRETS_KEY=['"]?([^'"\n]+)['"]?/m);
|
|
43
|
-
if (match)
|
|
44
|
-
return match[1];
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// Ignore read errors
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
24
|
export async function init_secrets(program) {
|
|
53
25
|
// Push secrets to cloud
|
|
54
26
|
program
|
|
@@ -305,7 +277,7 @@ export async function init_secrets(program) {
|
|
|
305
277
|
// Default action: show existing key or prompt to generate
|
|
306
278
|
keyCmd
|
|
307
279
|
.action(async () => {
|
|
308
|
-
const existingKey =
|
|
280
|
+
const existingKey = findEncryptionKey();
|
|
309
281
|
if (existingKey) {
|
|
310
282
|
const masked = existingKey.slice(0, 8) + '...' + existingKey.slice(-4);
|
|
311
283
|
console.log(`\n🔑 LSH_SECRETS_KEY=${masked}\n`);
|
|
@@ -326,7 +298,7 @@ export async function init_secrets(program) {
|
|
|
326
298
|
.option('--no-mask', 'Show the full key (default: masked)')
|
|
327
299
|
.option('--export', 'Output in export format for shell evaluation')
|
|
328
300
|
.action(async (options) => {
|
|
329
|
-
const existingKey =
|
|
301
|
+
const existingKey = findEncryptionKey();
|
|
330
302
|
if (!existingKey) {
|
|
331
303
|
console.error('❌ No encryption key found. Run: lsh key generate');
|
|
332
304
|
process.exit(1);
|
|
@@ -350,7 +322,7 @@ export async function init_secrets(program) {
|
|
|
350
322
|
.option('--force', 'Overwrite existing key')
|
|
351
323
|
.option('--export', 'Output in export format for shell evaluation')
|
|
352
324
|
.action(async (options) => {
|
|
353
|
-
const existingKey =
|
|
325
|
+
const existingKey = findEncryptionKey();
|
|
354
326
|
if (existingKey && !options.force) {
|
|
355
327
|
console.error('❌ An encryption key already exists. Use --force to overwrite.');
|
|
356
328
|
console.error('⚠️ Warning: existing secrets will NOT be decryptable with a new key.\n');
|
|
@@ -400,7 +372,7 @@ export async function init_secrets(program) {
|
|
|
400
372
|
process.exit(1);
|
|
401
373
|
}
|
|
402
374
|
// Check for existing key
|
|
403
|
-
const existingKey =
|
|
375
|
+
const existingKey = findEncryptionKey();
|
|
404
376
|
if (existingKey) {
|
|
405
377
|
if (existingKey === keyValue) {
|
|
406
378
|
console.log('\n✅ This key is already configured.\n');
|
|
@@ -447,6 +419,72 @@ export async function init_secrets(program) {
|
|
|
447
419
|
process.exit(1);
|
|
448
420
|
}
|
|
449
421
|
});
|
|
422
|
+
// lsh key gen — generate + persist to ~/.config/lsh/sync_key.json
|
|
423
|
+
keyCmd
|
|
424
|
+
.command('gen')
|
|
425
|
+
.description('Generate a new key and persist it to the LSH sync key store')
|
|
426
|
+
.option('-f, --force', 'Overwrite an existing stored key')
|
|
427
|
+
.option('--show', 'Print the full key (default masks it)')
|
|
428
|
+
.action((options) => {
|
|
429
|
+
const store = new SyncKeyStore();
|
|
430
|
+
let key;
|
|
431
|
+
try {
|
|
432
|
+
key = store.generate(options.force === true);
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
const e = err;
|
|
436
|
+
console.error(`❌ ${e.message}`);
|
|
437
|
+
console.error("💡 Use --force to overwrite, or run 'lsh key show' to view the existing key.");
|
|
438
|
+
process.exit(1);
|
|
439
|
+
return; // for the type-checker
|
|
440
|
+
}
|
|
441
|
+
console.log(`✅ Generated sync key at ${store.path}`);
|
|
442
|
+
if (options.show) {
|
|
443
|
+
console.log(`🔑 ${key}`);
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
console.log(`🔑 ${maskKey(key)} (use --show to print full)`);
|
|
447
|
+
}
|
|
448
|
+
console.log("💡 Share this key with teammates / your other hosts. Then run `lsh key set <key>` on each peer.");
|
|
449
|
+
});
|
|
450
|
+
// lsh key set — paste a 64-char hex key from another host into the store
|
|
451
|
+
keyCmd
|
|
452
|
+
.command('set <key>')
|
|
453
|
+
.description('Persist an existing 64-char hex key to the LSH sync key store')
|
|
454
|
+
.action((keyValue) => {
|
|
455
|
+
const store = new SyncKeyStore();
|
|
456
|
+
try {
|
|
457
|
+
store.set(keyValue.trim());
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
const e = err;
|
|
461
|
+
console.error(`❌ ${e.message}`);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
console.log('✅ Sync key stored.');
|
|
465
|
+
});
|
|
466
|
+
// lsh key clear — delete the persisted key (env var, if set, is untouched)
|
|
467
|
+
keyCmd
|
|
468
|
+
.command('clear')
|
|
469
|
+
.description('Delete the persisted LSH sync key (env var is untouched)')
|
|
470
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
471
|
+
.action(async (options) => {
|
|
472
|
+
if (!options.yes) {
|
|
473
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
474
|
+
const answer = await new Promise((resolve) => {
|
|
475
|
+
rl.question('Remove the stored sync key? [y/N] ', (ans) => {
|
|
476
|
+
rl.close();
|
|
477
|
+
resolve(ans.trim().toLowerCase());
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
481
|
+
console.log('Aborted.');
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
new SyncKeyStore().clear();
|
|
486
|
+
console.log('✅ Stored sync key removed.');
|
|
487
|
+
});
|
|
450
488
|
// Create .env file
|
|
451
489
|
program
|
|
452
490
|
.command('create')
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lsh-framework",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Simple, cross-platform encrypted secrets manager with automatic sync, IPFS audit logs, and multi-environment support. Just run lsh sync and start managing your secrets.",
|
|
5
|
-
"main": "dist/
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"lsh": "./dist/cli.js"
|
|
8
8
|
},
|
|
@@ -64,39 +64,39 @@
|
|
|
64
64
|
],
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@supabase/supabase-js": "^2.57.4",
|
|
67
|
-
"@types/proper-lockfile": "^4.1.4",
|
|
68
67
|
"bcrypt": "^6.0.0",
|
|
69
68
|
"chalk": "^5.3.0",
|
|
70
69
|
"chokidar": "^5.0.0",
|
|
71
|
-
"commander": "^
|
|
70
|
+
"commander": "^15.0.0",
|
|
72
71
|
"cors": "^2.8.5",
|
|
73
72
|
"dotenv": "^17.2.3",
|
|
74
|
-
"express": "^
|
|
73
|
+
"express": "^5.2.1",
|
|
75
74
|
"express-rate-limit": "^8.2.1",
|
|
76
75
|
"glob": "^13.0.0",
|
|
77
|
-
"inquirer": "^
|
|
76
|
+
"inquirer": "^14.0.1",
|
|
78
77
|
"js-yaml": "^4.1.0",
|
|
79
78
|
"jsonwebtoken": "^9.0.2",
|
|
80
79
|
"node-cron": "^4.2.1",
|
|
81
80
|
"ora": "^9.0.0",
|
|
82
81
|
"pg": "^8.16.3",
|
|
83
82
|
"proper-lockfile": "^4.1.2",
|
|
84
|
-
"smol-toml": "^1.3.1"
|
|
85
|
-
"uuid": "^13.0.0"
|
|
83
|
+
"smol-toml": "^1.3.1"
|
|
86
84
|
},
|
|
87
85
|
"devDependencies": {
|
|
88
|
-
"@
|
|
86
|
+
"@eslint/js": "^10.0.1",
|
|
87
|
+
"@types/bcrypt": "^6.0.0",
|
|
89
88
|
"@types/cors": "^2.8.17",
|
|
90
|
-
"@types/express": "^
|
|
89
|
+
"@types/express": "^5.0.6",
|
|
91
90
|
"@types/jest": "^30.0.0",
|
|
92
91
|
"@types/js-yaml": "^4.0.9",
|
|
93
92
|
"@types/jsonwebtoken": "^9.0.5",
|
|
94
|
-
"@types/node": "^
|
|
95
|
-
"@
|
|
96
|
-
"@typescript-eslint/
|
|
97
|
-
"eslint": "^
|
|
98
|
-
"
|
|
93
|
+
"@types/node": "^25.9.1",
|
|
94
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
95
|
+
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
|
96
|
+
"@typescript-eslint/parser": "^8.60.0",
|
|
97
|
+
"eslint": "^10.4.1",
|
|
98
|
+
"jest": "^30.4.2",
|
|
99
99
|
"ts-jest": "^29.2.5",
|
|
100
|
-
"typescript": "^
|
|
100
|
+
"typescript": "^6.0.3"
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Job Fixtures for Testing
|
|
3
|
-
*
|
|
4
|
-
* Provides factory functions and sample data for testing job-related functionality.
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```typescript
|
|
8
|
-
* import { createTestJob, SAMPLE_JOBS } from '../fixtures/job-fixtures';
|
|
9
|
-
*
|
|
10
|
-
* const job = createTestJob({ name: 'my-test-job', command: 'echo test' });
|
|
11
|
-
* ```
|
|
12
|
-
*/
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// FACTORY FUNCTIONS
|
|
15
|
-
// ============================================================================
|
|
16
|
-
/**
|
|
17
|
-
* Create a test job specification with sensible defaults.
|
|
18
|
-
*/
|
|
19
|
-
export function createTestJob(overrides = {}) {
|
|
20
|
-
const id = `job_${Math.random().toString(36).substr(2, 9)}`;
|
|
21
|
-
return {
|
|
22
|
-
id,
|
|
23
|
-
name: 'test-job',
|
|
24
|
-
command: 'echo "test"',
|
|
25
|
-
args: [],
|
|
26
|
-
status: 'created',
|
|
27
|
-
createdAt: new Date(),
|
|
28
|
-
tags: [],
|
|
29
|
-
priority: 5,
|
|
30
|
-
maxRetries: 3,
|
|
31
|
-
retryCount: 0,
|
|
32
|
-
databaseSync: false, // Disable DB sync in tests by default
|
|
33
|
-
...overrides,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Create a test job execution record.
|
|
38
|
-
*/
|
|
39
|
-
export function createTestExecution(overrides = {}) {
|
|
40
|
-
const executionId = `exec_${Math.random().toString(36).substr(2, 9)}`;
|
|
41
|
-
return {
|
|
42
|
-
executionId,
|
|
43
|
-
jobId: `job_${Math.random().toString(36).substr(2, 9)}`,
|
|
44
|
-
jobName: 'test-job',
|
|
45
|
-
command: 'echo "test"',
|
|
46
|
-
startTime: new Date(),
|
|
47
|
-
status: 'completed',
|
|
48
|
-
exitCode: 0,
|
|
49
|
-
...overrides,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Create a scheduled job with cron expression.
|
|
54
|
-
*/
|
|
55
|
-
export function createScheduledJob(cronExpression, overrides = {}) {
|
|
56
|
-
return createTestJob({
|
|
57
|
-
name: 'scheduled-job',
|
|
58
|
-
command: './scripts/scheduled.sh',
|
|
59
|
-
schedule: {
|
|
60
|
-
cron: cronExpression,
|
|
61
|
-
},
|
|
62
|
-
...overrides,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Create a job with interval scheduling.
|
|
67
|
-
*/
|
|
68
|
-
export function createIntervalJob(intervalMs, overrides = {}) {
|
|
69
|
-
return createTestJob({
|
|
70
|
-
name: 'interval-job',
|
|
71
|
-
command: './scripts/interval.sh',
|
|
72
|
-
schedule: {
|
|
73
|
-
interval: intervalMs,
|
|
74
|
-
},
|
|
75
|
-
...overrides,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
// ============================================================================
|
|
79
|
-
// SAMPLE DATA
|
|
80
|
-
// ============================================================================
|
|
81
|
-
/**
|
|
82
|
-
* Collection of sample jobs for different test scenarios.
|
|
83
|
-
*/
|
|
84
|
-
export const SAMPLE_JOBS = {
|
|
85
|
-
/** Simple echo job */
|
|
86
|
-
simple: createTestJob({
|
|
87
|
-
id: 'job_simple',
|
|
88
|
-
name: 'simple-job',
|
|
89
|
-
command: 'echo "hello"',
|
|
90
|
-
}),
|
|
91
|
-
/** Job with environment variables */
|
|
92
|
-
withEnv: createTestJob({
|
|
93
|
-
id: 'job_with_env',
|
|
94
|
-
name: 'env-job',
|
|
95
|
-
command: 'printenv',
|
|
96
|
-
env: {
|
|
97
|
-
MY_VAR: 'test_value',
|
|
98
|
-
ANOTHER_VAR: '123',
|
|
99
|
-
},
|
|
100
|
-
}),
|
|
101
|
-
/** Job with arguments */
|
|
102
|
-
withArgs: createTestJob({
|
|
103
|
-
id: 'job_with_args',
|
|
104
|
-
name: 'args-job',
|
|
105
|
-
command: 'ls',
|
|
106
|
-
args: ['-la', '/tmp'],
|
|
107
|
-
}),
|
|
108
|
-
/** Scheduled job (daily at midnight) */
|
|
109
|
-
scheduled: createScheduledJob('0 0 * * *', {
|
|
110
|
-
id: 'job_scheduled',
|
|
111
|
-
name: 'daily-job',
|
|
112
|
-
}),
|
|
113
|
-
/** Interval job (every 5 minutes) */
|
|
114
|
-
interval: createIntervalJob(5 * 60 * 1000, {
|
|
115
|
-
id: 'job_interval',
|
|
116
|
-
name: 'frequent-job',
|
|
117
|
-
}),
|
|
118
|
-
/** Long-running job with timeout */
|
|
119
|
-
longRunning: createTestJob({
|
|
120
|
-
id: 'job_long',
|
|
121
|
-
name: 'long-running-job',
|
|
122
|
-
command: 'sleep 300',
|
|
123
|
-
timeout: 60000, // 1 minute timeout
|
|
124
|
-
}),
|
|
125
|
-
/** Job that will fail */
|
|
126
|
-
failing: createTestJob({
|
|
127
|
-
id: 'job_failing',
|
|
128
|
-
name: 'failing-job',
|
|
129
|
-
command: 'exit 1',
|
|
130
|
-
maxRetries: 2,
|
|
131
|
-
}),
|
|
132
|
-
/** Completed job */
|
|
133
|
-
completed: createTestJob({
|
|
134
|
-
id: 'job_completed',
|
|
135
|
-
name: 'completed-job',
|
|
136
|
-
command: 'echo "done"',
|
|
137
|
-
status: 'completed',
|
|
138
|
-
startedAt: new Date(Date.now() - 1000),
|
|
139
|
-
completedAt: new Date(),
|
|
140
|
-
exitCode: 0,
|
|
141
|
-
stdout: 'done\n',
|
|
142
|
-
}),
|
|
143
|
-
/** Failed job */
|
|
144
|
-
failed: createTestJob({
|
|
145
|
-
id: 'job_failed',
|
|
146
|
-
name: 'failed-job',
|
|
147
|
-
command: 'false',
|
|
148
|
-
status: 'failed',
|
|
149
|
-
startedAt: new Date(Date.now() - 1000),
|
|
150
|
-
completedAt: new Date(),
|
|
151
|
-
exitCode: 1,
|
|
152
|
-
stderr: 'Command failed\n',
|
|
153
|
-
retryCount: 3,
|
|
154
|
-
}),
|
|
155
|
-
/** Secrets rotation job (realistic example) */
|
|
156
|
-
secretsRotation: createScheduledJob('0 2 1 * *', {
|
|
157
|
-
id: 'job_secrets_rotation',
|
|
158
|
-
name: 'rotate-api-keys',
|
|
159
|
-
command: './examples/secrets-rotation/rotate-api-keys.sh',
|
|
160
|
-
description: 'Monthly API key rotation',
|
|
161
|
-
tags: ['secrets', 'maintenance', 'security'],
|
|
162
|
-
env: {
|
|
163
|
-
LSH_ENVIRONMENT: 'production',
|
|
164
|
-
LOG_LEVEL: 'info',
|
|
165
|
-
},
|
|
166
|
-
timeout: 300000, // 5 minutes
|
|
167
|
-
}),
|
|
168
|
-
};
|
|
169
|
-
/**
|
|
170
|
-
* Sample execution records for testing history/statistics.
|
|
171
|
-
*/
|
|
172
|
-
export const SAMPLE_EXECUTIONS = {
|
|
173
|
-
success: createTestExecution({
|
|
174
|
-
executionId: 'exec_success',
|
|
175
|
-
jobId: 'job_simple',
|
|
176
|
-
status: 'completed',
|
|
177
|
-
exitCode: 0,
|
|
178
|
-
duration: 150,
|
|
179
|
-
stdout: 'hello\n',
|
|
180
|
-
}),
|
|
181
|
-
failure: createTestExecution({
|
|
182
|
-
executionId: 'exec_failure',
|
|
183
|
-
jobId: 'job_failing',
|
|
184
|
-
status: 'failed',
|
|
185
|
-
exitCode: 1,
|
|
186
|
-
duration: 50,
|
|
187
|
-
stderr: 'Error: Command failed\n',
|
|
188
|
-
errorMessage: 'Process exited with code 1',
|
|
189
|
-
}),
|
|
190
|
-
timeout: createTestExecution({
|
|
191
|
-
executionId: 'exec_timeout',
|
|
192
|
-
jobId: 'job_long',
|
|
193
|
-
status: 'timeout',
|
|
194
|
-
duration: 60000,
|
|
195
|
-
errorMessage: 'Job exceeded timeout of 60000ms',
|
|
196
|
-
}),
|
|
197
|
-
killed: createTestExecution({
|
|
198
|
-
executionId: 'exec_killed',
|
|
199
|
-
jobId: 'job_simple',
|
|
200
|
-
status: 'killed',
|
|
201
|
-
duration: 5000,
|
|
202
|
-
errorMessage: 'Process killed by signal SIGTERM',
|
|
203
|
-
}),
|
|
204
|
-
};
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Supabase Mock Utilities for Testing
|
|
3
|
-
*
|
|
4
|
-
* Provides mock Supabase client and factory functions for creating
|
|
5
|
-
* test data that matches the database schema.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```typescript
|
|
9
|
-
* import { createMockSupabase, mockOrganization } from '../fixtures/supabase-mocks';
|
|
10
|
-
*
|
|
11
|
-
* const supabase = createMockSupabase({
|
|
12
|
-
* organizations: [mockOrganization({ name: 'Test Org' })],
|
|
13
|
-
* });
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
// ============================================================================
|
|
17
|
-
// FACTORY FUNCTIONS
|
|
18
|
-
// ============================================================================
|
|
19
|
-
/**
|
|
20
|
-
* Create a mock organization record with sensible defaults.
|
|
21
|
-
* Override any field by passing it in the partial.
|
|
22
|
-
*/
|
|
23
|
-
export function mockOrganization(overrides = {}) {
|
|
24
|
-
const now = new Date().toISOString();
|
|
25
|
-
return {
|
|
26
|
-
id: `org_${Math.random().toString(36).substr(2, 9)}`,
|
|
27
|
-
name: 'Test Organization',
|
|
28
|
-
slug: 'test-org',
|
|
29
|
-
stripe_customer_id: null,
|
|
30
|
-
subscription_tier: 'free',
|
|
31
|
-
subscription_status: 'active',
|
|
32
|
-
subscription_expires_at: null,
|
|
33
|
-
settings: null,
|
|
34
|
-
created_at: now,
|
|
35
|
-
updated_at: now,
|
|
36
|
-
deleted_at: null,
|
|
37
|
-
...overrides,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Create a mock user record with sensible defaults.
|
|
42
|
-
*/
|
|
43
|
-
export function mockUser(overrides = {}) {
|
|
44
|
-
const now = new Date().toISOString();
|
|
45
|
-
const id = `user_${Math.random().toString(36).substr(2, 9)}`;
|
|
46
|
-
return {
|
|
47
|
-
id,
|
|
48
|
-
email: `${id}@example.com`,
|
|
49
|
-
email_verified: true,
|
|
50
|
-
email_verification_token: null,
|
|
51
|
-
email_verification_expires_at: null,
|
|
52
|
-
password_hash: '$2b$12$mockhashmockhashmockhashmockhash', // Not a real hash
|
|
53
|
-
oauth_provider: null,
|
|
54
|
-
oauth_provider_id: null,
|
|
55
|
-
first_name: 'Test',
|
|
56
|
-
last_name: 'User',
|
|
57
|
-
avatar_url: null,
|
|
58
|
-
last_login_at: now,
|
|
59
|
-
last_login_ip: '127.0.0.1',
|
|
60
|
-
created_at: now,
|
|
61
|
-
updated_at: now,
|
|
62
|
-
deleted_at: null,
|
|
63
|
-
...overrides,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Create a mock team record with sensible defaults.
|
|
68
|
-
*/
|
|
69
|
-
export function mockTeam(overrides = {}) {
|
|
70
|
-
const now = new Date().toISOString();
|
|
71
|
-
return {
|
|
72
|
-
id: `team_${Math.random().toString(36).substr(2, 9)}`,
|
|
73
|
-
organization_id: `org_${Math.random().toString(36).substr(2, 9)}`,
|
|
74
|
-
name: 'Test Team',
|
|
75
|
-
slug: 'test-team',
|
|
76
|
-
description: 'A test team',
|
|
77
|
-
encryption_key_id: null,
|
|
78
|
-
created_at: now,
|
|
79
|
-
updated_at: now,
|
|
80
|
-
deleted_at: null,
|
|
81
|
-
...overrides,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Create a mock secret record with sensible defaults.
|
|
86
|
-
*/
|
|
87
|
-
export function mockSecret(overrides = {}) {
|
|
88
|
-
const now = new Date().toISOString();
|
|
89
|
-
return {
|
|
90
|
-
id: `secret_${Math.random().toString(36).substr(2, 9)}`,
|
|
91
|
-
team_id: `team_${Math.random().toString(36).substr(2, 9)}`,
|
|
92
|
-
environment: 'development',
|
|
93
|
-
key: 'TEST_SECRET',
|
|
94
|
-
encrypted_value: 'encrypted_test_value',
|
|
95
|
-
encryption_key_id: `key_${Math.random().toString(36).substr(2, 9)}`,
|
|
96
|
-
description: 'A test secret',
|
|
97
|
-
tags: '[]',
|
|
98
|
-
last_rotated_at: null,
|
|
99
|
-
rotation_interval_days: null,
|
|
100
|
-
created_at: now,
|
|
101
|
-
created_by: null,
|
|
102
|
-
updated_at: now,
|
|
103
|
-
updated_by: null,
|
|
104
|
-
deleted_at: null,
|
|
105
|
-
deleted_by: null,
|
|
106
|
-
...overrides,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Create a mock organization member record.
|
|
111
|
-
*/
|
|
112
|
-
export function mockOrgMember(overrides = {}) {
|
|
113
|
-
const now = new Date().toISOString();
|
|
114
|
-
return {
|
|
115
|
-
id: `member_${Math.random().toString(36).substr(2, 9)}`,
|
|
116
|
-
organization_id: `org_${Math.random().toString(36).substr(2, 9)}`,
|
|
117
|
-
user_id: `user_${Math.random().toString(36).substr(2, 9)}`,
|
|
118
|
-
role: 'member',
|
|
119
|
-
invited_by: null,
|
|
120
|
-
invited_at: now,
|
|
121
|
-
accepted_at: now,
|
|
122
|
-
created_at: now,
|
|
123
|
-
updated_at: now,
|
|
124
|
-
...overrides,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Create a successful Supabase response.
|
|
129
|
-
*/
|
|
130
|
-
export function mockSuccess(data) {
|
|
131
|
-
return { data, error: null };
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Create an error Supabase response.
|
|
135
|
-
*/
|
|
136
|
-
export function mockError(message, code = 'PGRST000') {
|
|
137
|
-
return { data: null, error: { message, code } };
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Create a mock Supabase client for testing.
|
|
141
|
-
*
|
|
142
|
-
* Returns data from the provided config based on query parameters.
|
|
143
|
-
* Supports basic filtering via .eq() and .single().
|
|
144
|
-
*
|
|
145
|
-
* @example
|
|
146
|
-
* ```typescript
|
|
147
|
-
* const mockClient = createMockSupabase({
|
|
148
|
-
* organizations: [mockOrganization({ id: 'org_123' })],
|
|
149
|
-
* });
|
|
150
|
-
*
|
|
151
|
-
* // In test
|
|
152
|
-
* jest.mock('../../lib/supabase-client', () => ({
|
|
153
|
-
* getSupabaseClient: () => mockClient,
|
|
154
|
-
* }));
|
|
155
|
-
* ```
|
|
156
|
-
*/
|
|
157
|
-
export function createMockSupabase(config = {}) {
|
|
158
|
-
const tables = {
|
|
159
|
-
organizations: config.organizations || [],
|
|
160
|
-
users: config.users || [],
|
|
161
|
-
teams: config.teams || [],
|
|
162
|
-
secrets: config.secrets || [],
|
|
163
|
-
organization_members: config.members || [],
|
|
164
|
-
};
|
|
165
|
-
return {
|
|
166
|
-
from: (tableName) => {
|
|
167
|
-
const tableData = tables[tableName] || [];
|
|
168
|
-
let filteredData = [...tableData];
|
|
169
|
-
let isSingle = false;
|
|
170
|
-
const queryBuilder = {
|
|
171
|
-
select: (_columns) => queryBuilder,
|
|
172
|
-
insert: (data) => {
|
|
173
|
-
if (Array.isArray(data)) {
|
|
174
|
-
tableData.push(...data);
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
tableData.push(data);
|
|
178
|
-
}
|
|
179
|
-
filteredData = Array.isArray(data) ? data : [data];
|
|
180
|
-
return queryBuilder;
|
|
181
|
-
},
|
|
182
|
-
update: (_data) => queryBuilder,
|
|
183
|
-
delete: () => queryBuilder,
|
|
184
|
-
eq: (column, value) => {
|
|
185
|
-
filteredData = filteredData.filter((row) => row[column] === value);
|
|
186
|
-
return queryBuilder;
|
|
187
|
-
},
|
|
188
|
-
neq: (column, value) => {
|
|
189
|
-
filteredData = filteredData.filter((row) => row[column] !== value);
|
|
190
|
-
return queryBuilder;
|
|
191
|
-
},
|
|
192
|
-
is: (column, value) => {
|
|
193
|
-
filteredData = filteredData.filter((row) => row[column] === value);
|
|
194
|
-
return queryBuilder;
|
|
195
|
-
},
|
|
196
|
-
order: (_column, _options) => queryBuilder,
|
|
197
|
-
limit: (count) => {
|
|
198
|
-
filteredData = filteredData.slice(0, count);
|
|
199
|
-
return queryBuilder;
|
|
200
|
-
},
|
|
201
|
-
single: () => {
|
|
202
|
-
isSingle = true;
|
|
203
|
-
return queryBuilder;
|
|
204
|
-
},
|
|
205
|
-
then: (resolve) => {
|
|
206
|
-
if (isSingle) {
|
|
207
|
-
if (filteredData.length === 0) {
|
|
208
|
-
resolve(mockError('No rows found', 'PGRST116'));
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
resolve(mockSuccess(filteredData[0]));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
resolve(mockSuccess(filteredData));
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
};
|
|
219
|
-
return queryBuilder;
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
// ============================================================================
|
|
224
|
-
// SAMPLE DATA
|
|
225
|
-
// ============================================================================
|
|
226
|
-
/**
|
|
227
|
-
* Pre-built sample organization for quick testing.
|
|
228
|
-
*/
|
|
229
|
-
export const SAMPLE_ORG = mockOrganization({
|
|
230
|
-
id: 'org_sample123',
|
|
231
|
-
name: 'Sample Organization',
|
|
232
|
-
slug: 'sample-org',
|
|
233
|
-
subscription_tier: 'pro',
|
|
234
|
-
});
|
|
235
|
-
/**
|
|
236
|
-
* Pre-built sample user for quick testing.
|
|
237
|
-
*/
|
|
238
|
-
export const SAMPLE_USER = mockUser({
|
|
239
|
-
id: 'user_sample123',
|
|
240
|
-
email: 'sample@example.com',
|
|
241
|
-
first_name: 'Sample',
|
|
242
|
-
last_name: 'User',
|
|
243
|
-
});
|
|
244
|
-
/**
|
|
245
|
-
* Pre-built sample team for quick testing.
|
|
246
|
-
*/
|
|
247
|
-
export const SAMPLE_TEAM = mockTeam({
|
|
248
|
-
id: 'team_sample123',
|
|
249
|
-
organization_id: 'org_sample123',
|
|
250
|
-
name: 'Sample Team',
|
|
251
|
-
slug: 'sample-team',
|
|
252
|
-
});
|