openlinear 0.1.3
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/README.md +97 -0
- package/bin/openlinear.js +21 -0
- package/dist/chunk-5E4TN2YB.mjs +87 -0
- package/dist/chunk-HARBMOVK.mjs +107 -0
- package/dist/chunk-P3FFNK6N.mjs +103 -0
- package/dist/config/index.d.mts +33 -0
- package/dist/config/index.d.ts +33 -0
- package/dist/config/index.js +119 -0
- package/dist/config/index.mjs +18 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +304 -0
- package/dist/index.mjs +42 -0
- package/dist/types/index.d.mts +70 -0
- package/dist/types/index.d.ts +70 -0
- package/dist/types/index.js +140 -0
- package/dist/types/index.mjs +20 -0
- package/dist/validation/index.d.mts +74 -0
- package/dist/validation/index.d.ts +74 -0
- package/dist/validation/index.js +137 -0
- package/dist/validation/index.mjs +22 -0
- package/package.json +61 -0
- package/scripts/postinstall.js +65 -0
- package/src/config/feature-flags.ts +121 -0
- package/src/config/index.ts +1 -0
- package/src/index.ts +6 -0
- package/src/types/execution-metadata.ts +116 -0
- package/src/types/index.ts +1 -0
- package/src/validation/index.ts +1 -0
- package/src/validation/security.ts +113 -0
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# OpenLinear
|
|
2
|
+
|
|
3
|
+
OpenLinear launcher, installer, and validation utilities.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g openlinear
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### CLI Launcher
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
openlinear
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Programmatic API
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import {
|
|
23
|
+
validateExecutionMetadataSync,
|
|
24
|
+
safeValidateExecutionMetadataSync,
|
|
25
|
+
FORBIDDEN_SYNC_FIELDS,
|
|
26
|
+
isLocalExecutionEnabled,
|
|
27
|
+
parseFeatureFlags
|
|
28
|
+
} from 'openlinear';
|
|
29
|
+
|
|
30
|
+
// Validate execution metadata
|
|
31
|
+
const result = safeValidateExecutionMetadataSync({
|
|
32
|
+
taskId: 'tsk_123',
|
|
33
|
+
runId: 'run_456',
|
|
34
|
+
status: 'completed',
|
|
35
|
+
durationMs: 45000
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (result.success) {
|
|
39
|
+
console.log('Valid metadata:', result.data);
|
|
40
|
+
} else {
|
|
41
|
+
console.error('Validation failed:', result.error);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check feature flags
|
|
45
|
+
const flags = parseFeatureFlags();
|
|
46
|
+
const localEnabled = isLocalExecutionEnabled('user-123', flags);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Exports
|
|
50
|
+
|
|
51
|
+
### Main (`openlinear`)
|
|
52
|
+
|
|
53
|
+
- `validateExecutionMetadataSync` - Validate metadata payload
|
|
54
|
+
- `safeValidateExecutionMetadataSync` - Safe validation that returns result
|
|
55
|
+
- `FORBIDDEN_SYNC_FIELDS` - List of fields that cannot be synced
|
|
56
|
+
- `isForbiddenField` - Check if a field is forbidden
|
|
57
|
+
- `sanitizePayload` - Remove forbidden fields from payload
|
|
58
|
+
- `parseFeatureFlags` - Parse feature flags from environment
|
|
59
|
+
- `isLocalExecutionEnabled` - Check if local execution is enabled
|
|
60
|
+
- `getMigrationPhase` - Get current migration phase
|
|
61
|
+
|
|
62
|
+
### Types (`openlinear/types`)
|
|
63
|
+
|
|
64
|
+
- `ExecutionMetadataSync` - Type for execution metadata
|
|
65
|
+
- `ExecutionMetadataSyncSchema` - Zod schema for validation
|
|
66
|
+
- `ExecutionStatus` - Execution status enum
|
|
67
|
+
- `ErrorCategory` - Error category enum
|
|
68
|
+
|
|
69
|
+
### Validation (`openlinear/validation`)
|
|
70
|
+
|
|
71
|
+
- `checkExecutionMetadataSync` - Check if payload is valid
|
|
72
|
+
- `isForbiddenField` - Check if field is forbidden
|
|
73
|
+
- `sanitizePayload` - Sanitize payload by removing forbidden fields
|
|
74
|
+
- `FORBIDDEN_SYNC_FIELDS` - Array of forbidden field names
|
|
75
|
+
|
|
76
|
+
### Config (`openlinear/config`)
|
|
77
|
+
|
|
78
|
+
- `parseFeatureFlags` - Parse feature flags
|
|
79
|
+
- `getFeatureFlags` - Get feature flags from environment
|
|
80
|
+
- `isLocalExecutionEnabled` - Check local execution for user
|
|
81
|
+
- `isServerExecutionEnabled` - Check server execution
|
|
82
|
+
- `validateFlagConfiguration` - Validate flag config
|
|
83
|
+
- `getMigrationPhase` - Get migration phase
|
|
84
|
+
|
|
85
|
+
## Security
|
|
86
|
+
|
|
87
|
+
This package enforces the trust-boundary policy:
|
|
88
|
+
|
|
89
|
+
- **Cloud-Allowed**: taskId, status, durationMs, progress, branch, prUrl, etc.
|
|
90
|
+
- **Local-Only**: accessToken, apiKey, repoPath, etc.
|
|
91
|
+
- **Forbidden**: prompt, logs, toolLogs, executionLogs, etc.
|
|
92
|
+
|
|
93
|
+
See [docs/security/trust-boundary.md](docs/security/trust-boundary.md) for full policy.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawn } = require('node:child_process');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const appImagePath = path.join(os.homedir(), '.openlinear', 'openlinear.AppImage');
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(appImagePath)) {
|
|
10
|
+
console.error('OpenLinear AppImage not found.');
|
|
11
|
+
console.error('Run: npm install -g @kaizen403/openlinear to download it.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const child = spawn(appImagePath, process.argv.slice(2), {
|
|
16
|
+
stdio: 'inherit'
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
child.on('exit', (code) => {
|
|
20
|
+
process.exit(code ?? 0);
|
|
21
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/config/feature-flags.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var FeatureFlagsSchema = z.object({
|
|
4
|
+
LOCAL_EXECUTION_ENABLED: z.enum(["true", "false"]).default("false").transform((v) => v === "true"),
|
|
5
|
+
SERVER_EXECUTION_ENABLED: z.enum(["true", "false"]).default("true").transform((v) => v === "true"),
|
|
6
|
+
CANARY_PERCENTAGE: z.string().default("0").transform((v) => parseInt(v, 10)).refine((v) => v >= 0 && v <= 100, {
|
|
7
|
+
message: "CANARY_PERCENTAGE must be between 0 and 100"
|
|
8
|
+
}),
|
|
9
|
+
FORCE_LOCAL_EXECUTION: z.enum(["true", "false"]).default("false").transform((v) => v === "true"),
|
|
10
|
+
KILL_SWITCH_LOCAL_EXECUTION: z.enum(["true", "false"]).default("false").transform((v) => v === "true")
|
|
11
|
+
});
|
|
12
|
+
function parseFeatureFlags(env = process.env) {
|
|
13
|
+
return FeatureFlagsSchema.parse(env);
|
|
14
|
+
}
|
|
15
|
+
function getFeatureFlags() {
|
|
16
|
+
return parseFeatureFlags();
|
|
17
|
+
}
|
|
18
|
+
function isLocalExecutionEnabled(userId, flags = getFeatureFlags()) {
|
|
19
|
+
if (flags.KILL_SWITCH_LOCAL_EXECUTION) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (flags.FORCE_LOCAL_EXECUTION) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (!flags.LOCAL_EXECUTION_ENABLED) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (flags.CANARY_PERCENTAGE >= 100) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (flags.CANARY_PERCENTAGE <= 0) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const hash = hashString(userId);
|
|
35
|
+
const userPercentage = hash % 100 + 1;
|
|
36
|
+
return userPercentage <= flags.CANARY_PERCENTAGE;
|
|
37
|
+
}
|
|
38
|
+
function isServerExecutionEnabled(flags = getFeatureFlags()) {
|
|
39
|
+
return flags.SERVER_EXECUTION_ENABLED;
|
|
40
|
+
}
|
|
41
|
+
function validateFlagConfiguration(flags) {
|
|
42
|
+
const errors = [];
|
|
43
|
+
if (flags.FORCE_LOCAL_EXECUTION && flags.KILL_SWITCH_LOCAL_EXECUTION) {
|
|
44
|
+
errors.push("Cannot enable both FORCE_LOCAL_EXECUTION and KILL_SWITCH_LOCAL_EXECUTION");
|
|
45
|
+
}
|
|
46
|
+
if (!flags.LOCAL_EXECUTION_ENABLED && !flags.SERVER_EXECUTION_ENABLED) {
|
|
47
|
+
errors.push("At least one execution mode must be enabled");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
valid: errors.length === 0,
|
|
51
|
+
errors
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function hashString(str) {
|
|
55
|
+
let hash = 0;
|
|
56
|
+
for (let i = 0; i < str.length; i++) {
|
|
57
|
+
const char = str.charCodeAt(i);
|
|
58
|
+
hash = (hash << 5) - hash + char;
|
|
59
|
+
hash = hash & hash;
|
|
60
|
+
}
|
|
61
|
+
return Math.abs(hash);
|
|
62
|
+
}
|
|
63
|
+
function getMigrationPhase(flags = getFeatureFlags()) {
|
|
64
|
+
if (flags.KILL_SWITCH_LOCAL_EXECUTION) {
|
|
65
|
+
return "rollback";
|
|
66
|
+
}
|
|
67
|
+
if (!flags.SERVER_EXECUTION_ENABLED) {
|
|
68
|
+
return "cutover";
|
|
69
|
+
}
|
|
70
|
+
if (flags.LOCAL_EXECUTION_ENABLED && flags.CANARY_PERCENTAGE > 0) {
|
|
71
|
+
return "canary";
|
|
72
|
+
}
|
|
73
|
+
if (flags.LOCAL_EXECUTION_ENABLED && flags.CANARY_PERCENTAGE === 0) {
|
|
74
|
+
return "shadow";
|
|
75
|
+
}
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
FeatureFlagsSchema,
|
|
81
|
+
parseFeatureFlags,
|
|
82
|
+
getFeatureFlags,
|
|
83
|
+
isLocalExecutionEnabled,
|
|
84
|
+
isServerExecutionEnabled,
|
|
85
|
+
validateFlagConfiguration,
|
|
86
|
+
getMigrationPhase
|
|
87
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// src/types/execution-metadata.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var ErrorCategory = z.enum([
|
|
4
|
+
"AUTH",
|
|
5
|
+
"RATE_LIMIT",
|
|
6
|
+
"MERGE_CONFLICT",
|
|
7
|
+
"TIMEOUT",
|
|
8
|
+
"UNKNOWN"
|
|
9
|
+
]);
|
|
10
|
+
var ExecutionStatus = z.enum([
|
|
11
|
+
"pending",
|
|
12
|
+
"running",
|
|
13
|
+
"completed",
|
|
14
|
+
"failed",
|
|
15
|
+
"cancelled"
|
|
16
|
+
]);
|
|
17
|
+
var ExecutionMetadataSyncSchema = z.object({
|
|
18
|
+
taskId: z.string(),
|
|
19
|
+
runId: z.string(),
|
|
20
|
+
status: ExecutionStatus,
|
|
21
|
+
startedAt: z.string().datetime().optional(),
|
|
22
|
+
completedAt: z.string().datetime().optional(),
|
|
23
|
+
durationMs: z.number().int().min(0).optional(),
|
|
24
|
+
progress: z.number().int().min(0).max(100).optional(),
|
|
25
|
+
branch: z.string().optional(),
|
|
26
|
+
commitSha: z.string().optional(),
|
|
27
|
+
prUrl: z.string().url().optional(),
|
|
28
|
+
prNumber: z.number().int().positive().optional(),
|
|
29
|
+
outcome: z.string().max(500).optional(),
|
|
30
|
+
errorCategory: ErrorCategory.optional(),
|
|
31
|
+
filesChanged: z.number().int().min(0).optional(),
|
|
32
|
+
toolsExecuted: z.number().int().min(0).optional()
|
|
33
|
+
}).strict();
|
|
34
|
+
function validateExecutionMetadataSync(payload) {
|
|
35
|
+
return ExecutionMetadataSyncSchema.parse(payload);
|
|
36
|
+
}
|
|
37
|
+
function safeValidateExecutionMetadataSync(payload) {
|
|
38
|
+
const result = ExecutionMetadataSyncSchema.safeParse(payload);
|
|
39
|
+
if (result.success) {
|
|
40
|
+
return { success: true, data: result.data };
|
|
41
|
+
} else {
|
|
42
|
+
return { success: false, error: result.error };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function checkExecutionMetadataSync(payload) {
|
|
46
|
+
const result = ExecutionMetadataSyncSchema.safeParse(payload);
|
|
47
|
+
if (result.success) {
|
|
48
|
+
return { valid: true };
|
|
49
|
+
} else {
|
|
50
|
+
return {
|
|
51
|
+
valid: false,
|
|
52
|
+
issues: result.error.errors.map(
|
|
53
|
+
(e) => `${e.path.join(".")}: ${e.message}`
|
|
54
|
+
)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function validateExecutionMetadataMiddleware() {
|
|
59
|
+
return (req, res, next) => {
|
|
60
|
+
const result = safeValidateExecutionMetadataSync(req.body);
|
|
61
|
+
if (!result.success) {
|
|
62
|
+
const hasUnknownKeys = result.error.errors.some(
|
|
63
|
+
(e) => e.message.includes("Unrecognized key") || e.code === "unrecognized_keys"
|
|
64
|
+
);
|
|
65
|
+
return res.status(400).json({
|
|
66
|
+
error: "Invalid sync payload",
|
|
67
|
+
code: hasUnknownKeys ? "FORBIDDEN_FIELDS" : "VALIDATION_ERROR",
|
|
68
|
+
details: result.error.errors.map((e) => ({
|
|
69
|
+
field: e.path.join("."),
|
|
70
|
+
message: e.message
|
|
71
|
+
}))
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
req.validatedMetadata = result.data;
|
|
75
|
+
next();
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
var FORBIDDEN_SYNC_FIELDS = [
|
|
79
|
+
"prompt",
|
|
80
|
+
"logs",
|
|
81
|
+
"toolLogs",
|
|
82
|
+
"executionLogs",
|
|
83
|
+
"repoPath",
|
|
84
|
+
"accessToken",
|
|
85
|
+
"apiKey",
|
|
86
|
+
"passwordHash",
|
|
87
|
+
"jwt",
|
|
88
|
+
"client",
|
|
89
|
+
"timeoutId",
|
|
90
|
+
"rawOutput",
|
|
91
|
+
"diff",
|
|
92
|
+
"fileContents",
|
|
93
|
+
"env",
|
|
94
|
+
"environment",
|
|
95
|
+
"processEnv"
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
export {
|
|
99
|
+
ErrorCategory,
|
|
100
|
+
ExecutionStatus,
|
|
101
|
+
ExecutionMetadataSyncSchema,
|
|
102
|
+
validateExecutionMetadataSync,
|
|
103
|
+
safeValidateExecutionMetadataSync,
|
|
104
|
+
checkExecutionMetadataSync,
|
|
105
|
+
validateExecutionMetadataMiddleware,
|
|
106
|
+
FORBIDDEN_SYNC_FIELDS
|
|
107
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/validation/security.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var ErrorCategory = z.enum([
|
|
4
|
+
"AUTH",
|
|
5
|
+
"RATE_LIMIT",
|
|
6
|
+
"MERGE_CONFLICT",
|
|
7
|
+
"TIMEOUT",
|
|
8
|
+
"UNKNOWN"
|
|
9
|
+
]);
|
|
10
|
+
var ExecutionStatus = z.enum([
|
|
11
|
+
"pending",
|
|
12
|
+
"running",
|
|
13
|
+
"completed",
|
|
14
|
+
"failed",
|
|
15
|
+
"cancelled"
|
|
16
|
+
]);
|
|
17
|
+
var ExecutionMetadataSyncSchema = z.object({
|
|
18
|
+
taskId: z.string(),
|
|
19
|
+
runId: z.string(),
|
|
20
|
+
status: ExecutionStatus,
|
|
21
|
+
startedAt: z.string().datetime().optional(),
|
|
22
|
+
completedAt: z.string().datetime().optional(),
|
|
23
|
+
durationMs: z.number().int().min(0).optional(),
|
|
24
|
+
progress: z.number().int().min(0).max(100).optional(),
|
|
25
|
+
branch: z.string().optional(),
|
|
26
|
+
commitSha: z.string().optional(),
|
|
27
|
+
prUrl: z.string().url().optional(),
|
|
28
|
+
prNumber: z.number().int().positive().optional(),
|
|
29
|
+
outcome: z.string().max(500).optional(),
|
|
30
|
+
errorCategory: ErrorCategory.optional(),
|
|
31
|
+
filesChanged: z.number().int().min(0).optional(),
|
|
32
|
+
toolsExecuted: z.number().int().min(0).optional()
|
|
33
|
+
}).strict();
|
|
34
|
+
function validateExecutionMetadataSync(payload) {
|
|
35
|
+
return ExecutionMetadataSyncSchema.parse(payload);
|
|
36
|
+
}
|
|
37
|
+
function safeValidateExecutionMetadataSync(payload) {
|
|
38
|
+
const result = ExecutionMetadataSyncSchema.safeParse(payload);
|
|
39
|
+
if (result.success) {
|
|
40
|
+
return { success: true, data: result.data };
|
|
41
|
+
} else {
|
|
42
|
+
return { success: false, error: result.error };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function checkExecutionMetadataSync(payload) {
|
|
46
|
+
const result = ExecutionMetadataSyncSchema.safeParse(payload);
|
|
47
|
+
if (result.success) {
|
|
48
|
+
return { valid: true };
|
|
49
|
+
} else {
|
|
50
|
+
return {
|
|
51
|
+
valid: false,
|
|
52
|
+
issues: result.error.errors.map(
|
|
53
|
+
(e) => `${e.path.join(".")}: ${e.message}`
|
|
54
|
+
)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
var FORBIDDEN_SYNC_FIELDS = [
|
|
59
|
+
"prompt",
|
|
60
|
+
"logs",
|
|
61
|
+
"toolLogs",
|
|
62
|
+
"executionLogs",
|
|
63
|
+
"repoPath",
|
|
64
|
+
"accessToken",
|
|
65
|
+
"apiKey",
|
|
66
|
+
"passwordHash",
|
|
67
|
+
"jwt",
|
|
68
|
+
"client",
|
|
69
|
+
"timeoutId",
|
|
70
|
+
"rawOutput",
|
|
71
|
+
"diff",
|
|
72
|
+
"fileContents",
|
|
73
|
+
"env",
|
|
74
|
+
"environment",
|
|
75
|
+
"processEnv"
|
|
76
|
+
];
|
|
77
|
+
function isForbiddenField(field) {
|
|
78
|
+
return FORBIDDEN_SYNC_FIELDS.includes(field);
|
|
79
|
+
}
|
|
80
|
+
function sanitizePayload(payload) {
|
|
81
|
+
const sanitized = {};
|
|
82
|
+
const removed = [];
|
|
83
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
84
|
+
if (isForbiddenField(key)) {
|
|
85
|
+
removed.push(key);
|
|
86
|
+
} else {
|
|
87
|
+
sanitized[key] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { sanitized, removed };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
ErrorCategory,
|
|
95
|
+
ExecutionStatus,
|
|
96
|
+
ExecutionMetadataSyncSchema,
|
|
97
|
+
validateExecutionMetadataSync,
|
|
98
|
+
safeValidateExecutionMetadataSync,
|
|
99
|
+
checkExecutionMetadataSync,
|
|
100
|
+
FORBIDDEN_SYNC_FIELDS,
|
|
101
|
+
isForbiddenField,
|
|
102
|
+
sanitizePayload
|
|
103
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
declare const FeatureFlagsSchema: z.ZodObject<{
|
|
4
|
+
LOCAL_EXECUTION_ENABLED: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "true" | "false" | undefined>;
|
|
5
|
+
SERVER_EXECUTION_ENABLED: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "true" | "false" | undefined>;
|
|
6
|
+
CANARY_PERCENTAGE: z.ZodEffects<z.ZodEffects<z.ZodDefault<z.ZodString>, number, string | undefined>, number, string | undefined>;
|
|
7
|
+
FORCE_LOCAL_EXECUTION: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "true" | "false" | undefined>;
|
|
8
|
+
KILL_SWITCH_LOCAL_EXECUTION: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "true" | "false" | undefined>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
LOCAL_EXECUTION_ENABLED: boolean;
|
|
11
|
+
SERVER_EXECUTION_ENABLED: boolean;
|
|
12
|
+
CANARY_PERCENTAGE: number;
|
|
13
|
+
FORCE_LOCAL_EXECUTION: boolean;
|
|
14
|
+
KILL_SWITCH_LOCAL_EXECUTION: boolean;
|
|
15
|
+
}, {
|
|
16
|
+
LOCAL_EXECUTION_ENABLED?: "true" | "false" | undefined;
|
|
17
|
+
SERVER_EXECUTION_ENABLED?: "true" | "false" | undefined;
|
|
18
|
+
CANARY_PERCENTAGE?: string | undefined;
|
|
19
|
+
FORCE_LOCAL_EXECUTION?: "true" | "false" | undefined;
|
|
20
|
+
KILL_SWITCH_LOCAL_EXECUTION?: "true" | "false" | undefined;
|
|
21
|
+
}>;
|
|
22
|
+
type FeatureFlags = z.infer<typeof FeatureFlagsSchema>;
|
|
23
|
+
declare function parseFeatureFlags(env?: Record<string, string | undefined>): FeatureFlags;
|
|
24
|
+
declare function getFeatureFlags(): FeatureFlags;
|
|
25
|
+
declare function isLocalExecutionEnabled(userId: string, flags?: FeatureFlags): boolean;
|
|
26
|
+
declare function isServerExecutionEnabled(flags?: FeatureFlags): boolean;
|
|
27
|
+
declare function validateFlagConfiguration(flags: FeatureFlags): {
|
|
28
|
+
valid: boolean;
|
|
29
|
+
errors: string[];
|
|
30
|
+
};
|
|
31
|
+
declare function getMigrationPhase(flags?: FeatureFlags): 'shadow' | 'canary' | 'cutover' | 'rollback' | 'unknown';
|
|
32
|
+
|
|
33
|
+
export { type FeatureFlags, FeatureFlagsSchema, getFeatureFlags, getMigrationPhase, isLocalExecutionEnabled, isServerExecutionEnabled, parseFeatureFlags, validateFlagConfiguration };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
declare const FeatureFlagsSchema: z.ZodObject<{
|
|
4
|
+
LOCAL_EXECUTION_ENABLED: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "true" | "false" | undefined>;
|
|
5
|
+
SERVER_EXECUTION_ENABLED: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "true" | "false" | undefined>;
|
|
6
|
+
CANARY_PERCENTAGE: z.ZodEffects<z.ZodEffects<z.ZodDefault<z.ZodString>, number, string | undefined>, number, string | undefined>;
|
|
7
|
+
FORCE_LOCAL_EXECUTION: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "true" | "false" | undefined>;
|
|
8
|
+
KILL_SWITCH_LOCAL_EXECUTION: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "true" | "false" | undefined>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
LOCAL_EXECUTION_ENABLED: boolean;
|
|
11
|
+
SERVER_EXECUTION_ENABLED: boolean;
|
|
12
|
+
CANARY_PERCENTAGE: number;
|
|
13
|
+
FORCE_LOCAL_EXECUTION: boolean;
|
|
14
|
+
KILL_SWITCH_LOCAL_EXECUTION: boolean;
|
|
15
|
+
}, {
|
|
16
|
+
LOCAL_EXECUTION_ENABLED?: "true" | "false" | undefined;
|
|
17
|
+
SERVER_EXECUTION_ENABLED?: "true" | "false" | undefined;
|
|
18
|
+
CANARY_PERCENTAGE?: string | undefined;
|
|
19
|
+
FORCE_LOCAL_EXECUTION?: "true" | "false" | undefined;
|
|
20
|
+
KILL_SWITCH_LOCAL_EXECUTION?: "true" | "false" | undefined;
|
|
21
|
+
}>;
|
|
22
|
+
type FeatureFlags = z.infer<typeof FeatureFlagsSchema>;
|
|
23
|
+
declare function parseFeatureFlags(env?: Record<string, string | undefined>): FeatureFlags;
|
|
24
|
+
declare function getFeatureFlags(): FeatureFlags;
|
|
25
|
+
declare function isLocalExecutionEnabled(userId: string, flags?: FeatureFlags): boolean;
|
|
26
|
+
declare function isServerExecutionEnabled(flags?: FeatureFlags): boolean;
|
|
27
|
+
declare function validateFlagConfiguration(flags: FeatureFlags): {
|
|
28
|
+
valid: boolean;
|
|
29
|
+
errors: string[];
|
|
30
|
+
};
|
|
31
|
+
declare function getMigrationPhase(flags?: FeatureFlags): 'shadow' | 'canary' | 'cutover' | 'rollback' | 'unknown';
|
|
32
|
+
|
|
33
|
+
export { type FeatureFlags, FeatureFlagsSchema, getFeatureFlags, getMigrationPhase, isLocalExecutionEnabled, isServerExecutionEnabled, parseFeatureFlags, validateFlagConfiguration };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/config/index.ts
|
|
21
|
+
var config_exports = {};
|
|
22
|
+
__export(config_exports, {
|
|
23
|
+
FeatureFlagsSchema: () => FeatureFlagsSchema,
|
|
24
|
+
getFeatureFlags: () => getFeatureFlags,
|
|
25
|
+
getMigrationPhase: () => getMigrationPhase,
|
|
26
|
+
isLocalExecutionEnabled: () => isLocalExecutionEnabled,
|
|
27
|
+
isServerExecutionEnabled: () => isServerExecutionEnabled,
|
|
28
|
+
parseFeatureFlags: () => parseFeatureFlags,
|
|
29
|
+
validateFlagConfiguration: () => validateFlagConfiguration
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(config_exports);
|
|
32
|
+
|
|
33
|
+
// src/config/feature-flags.ts
|
|
34
|
+
var import_zod = require("zod");
|
|
35
|
+
var FeatureFlagsSchema = import_zod.z.object({
|
|
36
|
+
LOCAL_EXECUTION_ENABLED: import_zod.z.enum(["true", "false"]).default("false").transform((v) => v === "true"),
|
|
37
|
+
SERVER_EXECUTION_ENABLED: import_zod.z.enum(["true", "false"]).default("true").transform((v) => v === "true"),
|
|
38
|
+
CANARY_PERCENTAGE: import_zod.z.string().default("0").transform((v) => parseInt(v, 10)).refine((v) => v >= 0 && v <= 100, {
|
|
39
|
+
message: "CANARY_PERCENTAGE must be between 0 and 100"
|
|
40
|
+
}),
|
|
41
|
+
FORCE_LOCAL_EXECUTION: import_zod.z.enum(["true", "false"]).default("false").transform((v) => v === "true"),
|
|
42
|
+
KILL_SWITCH_LOCAL_EXECUTION: import_zod.z.enum(["true", "false"]).default("false").transform((v) => v === "true")
|
|
43
|
+
});
|
|
44
|
+
function parseFeatureFlags(env = process.env) {
|
|
45
|
+
return FeatureFlagsSchema.parse(env);
|
|
46
|
+
}
|
|
47
|
+
function getFeatureFlags() {
|
|
48
|
+
return parseFeatureFlags();
|
|
49
|
+
}
|
|
50
|
+
function isLocalExecutionEnabled(userId, flags = getFeatureFlags()) {
|
|
51
|
+
if (flags.KILL_SWITCH_LOCAL_EXECUTION) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (flags.FORCE_LOCAL_EXECUTION) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (!flags.LOCAL_EXECUTION_ENABLED) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (flags.CANARY_PERCENTAGE >= 100) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (flags.CANARY_PERCENTAGE <= 0) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const hash = hashString(userId);
|
|
67
|
+
const userPercentage = hash % 100 + 1;
|
|
68
|
+
return userPercentage <= flags.CANARY_PERCENTAGE;
|
|
69
|
+
}
|
|
70
|
+
function isServerExecutionEnabled(flags = getFeatureFlags()) {
|
|
71
|
+
return flags.SERVER_EXECUTION_ENABLED;
|
|
72
|
+
}
|
|
73
|
+
function validateFlagConfiguration(flags) {
|
|
74
|
+
const errors = [];
|
|
75
|
+
if (flags.FORCE_LOCAL_EXECUTION && flags.KILL_SWITCH_LOCAL_EXECUTION) {
|
|
76
|
+
errors.push("Cannot enable both FORCE_LOCAL_EXECUTION and KILL_SWITCH_LOCAL_EXECUTION");
|
|
77
|
+
}
|
|
78
|
+
if (!flags.LOCAL_EXECUTION_ENABLED && !flags.SERVER_EXECUTION_ENABLED) {
|
|
79
|
+
errors.push("At least one execution mode must be enabled");
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
valid: errors.length === 0,
|
|
83
|
+
errors
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function hashString(str) {
|
|
87
|
+
let hash = 0;
|
|
88
|
+
for (let i = 0; i < str.length; i++) {
|
|
89
|
+
const char = str.charCodeAt(i);
|
|
90
|
+
hash = (hash << 5) - hash + char;
|
|
91
|
+
hash = hash & hash;
|
|
92
|
+
}
|
|
93
|
+
return Math.abs(hash);
|
|
94
|
+
}
|
|
95
|
+
function getMigrationPhase(flags = getFeatureFlags()) {
|
|
96
|
+
if (flags.KILL_SWITCH_LOCAL_EXECUTION) {
|
|
97
|
+
return "rollback";
|
|
98
|
+
}
|
|
99
|
+
if (!flags.SERVER_EXECUTION_ENABLED) {
|
|
100
|
+
return "cutover";
|
|
101
|
+
}
|
|
102
|
+
if (flags.LOCAL_EXECUTION_ENABLED && flags.CANARY_PERCENTAGE > 0) {
|
|
103
|
+
return "canary";
|
|
104
|
+
}
|
|
105
|
+
if (flags.LOCAL_EXECUTION_ENABLED && flags.CANARY_PERCENTAGE === 0) {
|
|
106
|
+
return "shadow";
|
|
107
|
+
}
|
|
108
|
+
return "unknown";
|
|
109
|
+
}
|
|
110
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
111
|
+
0 && (module.exports = {
|
|
112
|
+
FeatureFlagsSchema,
|
|
113
|
+
getFeatureFlags,
|
|
114
|
+
getMigrationPhase,
|
|
115
|
+
isLocalExecutionEnabled,
|
|
116
|
+
isServerExecutionEnabled,
|
|
117
|
+
parseFeatureFlags,
|
|
118
|
+
validateFlagConfiguration
|
|
119
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FeatureFlagsSchema,
|
|
3
|
+
getFeatureFlags,
|
|
4
|
+
getMigrationPhase,
|
|
5
|
+
isLocalExecutionEnabled,
|
|
6
|
+
isServerExecutionEnabled,
|
|
7
|
+
parseFeatureFlags,
|
|
8
|
+
validateFlagConfiguration
|
|
9
|
+
} from "../chunk-5E4TN2YB.mjs";
|
|
10
|
+
export {
|
|
11
|
+
FeatureFlagsSchema,
|
|
12
|
+
getFeatureFlags,
|
|
13
|
+
getMigrationPhase,
|
|
14
|
+
isLocalExecutionEnabled,
|
|
15
|
+
isServerExecutionEnabled,
|
|
16
|
+
parseFeatureFlags,
|
|
17
|
+
validateFlagConfiguration
|
|
18
|
+
};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { FeatureFlags, FeatureFlagsSchema, getFeatureFlags, getMigrationPhase, isLocalExecutionEnabled, isServerExecutionEnabled, parseFeatureFlags, validateFlagConfiguration } from './config/index.mjs';
|
|
2
|
+
export { isForbiddenField, sanitizePayload } from './validation/index.mjs';
|
|
3
|
+
export { ErrorCategory, ExecutionMetadataSync, ExecutionMetadataSyncSchema, ExecutionStatus, FORBIDDEN_SYNC_FIELDS, checkExecutionMetadataSync, safeValidateExecutionMetadataSync, validateExecutionMetadataMiddleware, validateExecutionMetadataSync } from './types/index.mjs';
|
|
4
|
+
import 'zod';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { FeatureFlags, FeatureFlagsSchema, getFeatureFlags, getMigrationPhase, isLocalExecutionEnabled, isServerExecutionEnabled, parseFeatureFlags, validateFlagConfiguration } from './config/index.js';
|
|
2
|
+
export { isForbiddenField, sanitizePayload } from './validation/index.js';
|
|
3
|
+
export { ErrorCategory, ExecutionMetadataSync, ExecutionMetadataSyncSchema, ExecutionStatus, FORBIDDEN_SYNC_FIELDS, checkExecutionMetadataSync, safeValidateExecutionMetadataSync, validateExecutionMetadataMiddleware, validateExecutionMetadataSync } from './types/index.js';
|
|
4
|
+
import 'zod';
|