@zergai/cyberdeck 0.1.0-beta.1
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/COMMERCIAL-LICENSING.md +27 -0
- package/LICENSE.md +375 -0
- package/NOTICE +17 -0
- package/README.md +284 -0
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/dist/automation.js +161 -0
- package/dist/client.js +493 -0
- package/dist/commands/auth.js +193 -0
- package/dist/commands/automation.js +164 -0
- package/dist/commands/decks.js +310 -0
- package/dist/commands/fleet.js +393 -0
- package/dist/commands/oracle.js +143 -0
- package/dist/commands/portfolio.js +19 -0
- package/dist/commands/scenarios.js +207 -0
- package/dist/commands/sdk.js +229 -0
- package/dist/commands/workbench.js +225 -0
- package/dist/config.js +48 -0
- package/dist/fleet.js +88 -0
- package/dist/index.js +43 -0
- package/dist/portfolio.js +148 -0
- package/dist/runtime.js +165 -0
- package/dist/vendors.js +65 -0
- package/package.json +51 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI config — persisted to ~/.config/zergcyberdeck/config.json (0600).
|
|
3
|
+
* Env overrides win: ZERGCYBERDECK_BASE_URL, ZERGCYBERDECK_TOKEN.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
const DEFAULT_BASE = 'https://zergcyberdeck.com';
|
|
9
|
+
function configDir() {
|
|
10
|
+
return process.env.ZERGCYBERDECK_CONFIG_DIR || path.join(os.homedir(), '.config', 'zergcyberdeck');
|
|
11
|
+
}
|
|
12
|
+
export function configFilePath() {
|
|
13
|
+
return path.join(configDir(), 'config.json');
|
|
14
|
+
}
|
|
15
|
+
export function loadConfig() {
|
|
16
|
+
let file = {};
|
|
17
|
+
try {
|
|
18
|
+
file = JSON.parse(fs.readFileSync(configFilePath(), 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// no config yet — fall back to defaults + env
|
|
22
|
+
}
|
|
23
|
+
const baseUrl = (process.env.ZERGCYBERDECK_BASE_URL || file.baseUrl || DEFAULT_BASE).replace(/\/+$/, '');
|
|
24
|
+
return {
|
|
25
|
+
baseUrl,
|
|
26
|
+
token: process.env.ZERGCYBERDECK_TOKEN || file.token,
|
|
27
|
+
expiresAt: file.expiresAt,
|
|
28
|
+
workspace: file.workspace,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function saveConfig(cfg) {
|
|
32
|
+
const dir = configDir();
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
34
|
+
try {
|
|
35
|
+
fs.chmodSync(dir, 0o700);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// best-effort on platforms without chmod
|
|
39
|
+
}
|
|
40
|
+
fs.writeFileSync(configFilePath(), JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
41
|
+
try {
|
|
42
|
+
fs.chmodSync(configFilePath(), 0o600);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// best-effort on platforms without chmod
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=config.js.map
|
package/dist/fleet.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2
|
+
const LOG_CREDENTIAL_KEY = /(authorization|credential|password|passwd|secret|token|api[_-]?key|access[_-]?key|client[_-]?secret)/i;
|
|
3
|
+
const LOG_CREDENTIAL_PAIR = /\b((?:[A-Za-z0-9]+[_-])*(?:authorization|credential|password|passwd|secret|token|api[_-]?key|access[_-]?key|client[_-]?secret)(?:[_-][A-Za-z0-9]+)*)\b["']?\s*([=:])\s*(?:"(?:\\.|[^"\\\r\n])*"|'(?:\\.|[^'\\\r\n])*'|[^\s,;&"']+)/gi;
|
|
4
|
+
const LOG_AUTHORIZATION_HEADER = /\b(?:proxy-)?authorization\s*[:=]\s*[^\r\n,;]+/gi;
|
|
5
|
+
const LOG_AUTHORIZATION = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
6
|
+
const LOG_JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g;
|
|
7
|
+
function redactLogText(value) {
|
|
8
|
+
return value
|
|
9
|
+
.replace(LOG_AUTHORIZATION_HEADER, 'Authorization: [REDACTED]')
|
|
10
|
+
.replace(LOG_AUTHORIZATION, '$1 [REDACTED]')
|
|
11
|
+
.replace(LOG_CREDENTIAL_PAIR, '$1$2[REDACTED]')
|
|
12
|
+
.replace(LOG_JWT, '[REDACTED]');
|
|
13
|
+
}
|
|
14
|
+
export function redactLogValue(value, depth = 0) {
|
|
15
|
+
if (depth > 20)
|
|
16
|
+
return '[TRUNCATED]';
|
|
17
|
+
if (typeof value === 'string')
|
|
18
|
+
return redactLogText(value);
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return value.map(item => redactLogValue(item, depth + 1));
|
|
21
|
+
}
|
|
22
|
+
if (typeof value !== 'object' || value === null)
|
|
23
|
+
return value;
|
|
24
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
25
|
+
key,
|
|
26
|
+
LOG_CREDENTIAL_KEY.test(key)
|
|
27
|
+
? '[REDACTED]'
|
|
28
|
+
: redactLogValue(item, depth + 1),
|
|
29
|
+
]));
|
|
30
|
+
}
|
|
31
|
+
export function uniqueStrings(values) {
|
|
32
|
+
return [...new Set(values)];
|
|
33
|
+
}
|
|
34
|
+
export function isTerminalRun(status) {
|
|
35
|
+
return status === 'completed' || status === 'failed' || status === 'partial';
|
|
36
|
+
}
|
|
37
|
+
export function runHasFailures(envelope) {
|
|
38
|
+
return (envelope.run.status === 'failed'
|
|
39
|
+
|| envelope.run.status === 'partial'
|
|
40
|
+
|| envelope.run.failedCount > 0
|
|
41
|
+
|| envelope.clones.some(clone => clone.status === 'failed'));
|
|
42
|
+
}
|
|
43
|
+
export function diagnosticHasFailure(clone) {
|
|
44
|
+
return (clone.status === 'error'
|
|
45
|
+
|| clone.error !== null
|
|
46
|
+
|| clone.latestFailure !== null
|
|
47
|
+
|| clone.operation?.status === 'failed');
|
|
48
|
+
}
|
|
49
|
+
export function diagnosticsHaveFailures(diagnostics) {
|
|
50
|
+
return diagnostics.clones.some(diagnosticHasFailure);
|
|
51
|
+
}
|
|
52
|
+
export function selectDiagnostic(diagnostics, selector) {
|
|
53
|
+
const value = selector.trim();
|
|
54
|
+
if (!value) {
|
|
55
|
+
throw new Error('--clone must name a vendor or clone UUID.');
|
|
56
|
+
}
|
|
57
|
+
if (UUID.test(value)) {
|
|
58
|
+
const match = diagnostics.clones.find(clone => clone.cloneId.toLowerCase() === value.toLowerCase());
|
|
59
|
+
if (match)
|
|
60
|
+
return match;
|
|
61
|
+
throw new Error(`Clone UUID '${value}' was not found in deck ${diagnostics.deck.id}.`);
|
|
62
|
+
}
|
|
63
|
+
const vendor = value.toLowerCase();
|
|
64
|
+
const matches = diagnostics.clones.filter(clone => clone.vendor.toLowerCase() === vendor);
|
|
65
|
+
if (matches.length === 0) {
|
|
66
|
+
throw new Error(`Clone selector '${value}' did not match a clone in deck `
|
|
67
|
+
+ `${diagnostics.deck.id}.`);
|
|
68
|
+
}
|
|
69
|
+
if (matches.length > 1) {
|
|
70
|
+
throw new Error(`Clone vendor '${vendor}' is ambiguous in deck ${diagnostics.deck.id}. `
|
|
71
|
+
+ `Matching clone IDs: ${matches.map(clone => clone.cloneId).join(', ')}.`);
|
|
72
|
+
}
|
|
73
|
+
return matches[0];
|
|
74
|
+
}
|
|
75
|
+
export async function mapLimit(values, concurrency, operation) {
|
|
76
|
+
const results = new Array(values.length);
|
|
77
|
+
let next = 0;
|
|
78
|
+
const workers = Array.from({ length: Math.min(Math.max(1, concurrency), values.length) }, async () => {
|
|
79
|
+
while (next < values.length) {
|
|
80
|
+
const index = next;
|
|
81
|
+
next += 1;
|
|
82
|
+
results[index] = await operation(values[index]);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
await Promise.all(workers);
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=fleet.js.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* zcd — ZergCyberDeck CLI.
|
|
4
|
+
*
|
|
5
|
+
* Drives the deck API (against the canonical zergcyberdeck.com origin by default) to
|
|
6
|
+
* spin up a deck of zerg-clones, seed prebaked synthetic data, and check that
|
|
7
|
+
* each clone's vendor API + its zerg are healthy — the fast path for test/QA/dev
|
|
8
|
+
* of the platform.
|
|
9
|
+
*/
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { registerAuthCommands } from './commands/auth.js';
|
|
13
|
+
import { registerAutomationCommands } from './commands/automation.js';
|
|
14
|
+
import { registerDeckCommands } from './commands/decks.js';
|
|
15
|
+
import { registerFleetCommands } from './commands/fleet.js';
|
|
16
|
+
import { registerSdkCommand } from './commands/sdk.js';
|
|
17
|
+
import { registerWorkbenchCommands } from './commands/workbench.js';
|
|
18
|
+
import { registerOracleCommands } from './commands/oracle.js';
|
|
19
|
+
import { registerPortfolioCommand } from './commands/portfolio.js';
|
|
20
|
+
import { registerScenarioCommands } from './commands/scenarios.js';
|
|
21
|
+
const program = new Command();
|
|
22
|
+
const packageMetadata = createRequire(import.meta.url)('../package.json');
|
|
23
|
+
program
|
|
24
|
+
.name('zcd')
|
|
25
|
+
.description('ZergCyberDeck CLI — spin up decks of zerg-clones, seed data, check everything')
|
|
26
|
+
.version(packageMetadata.version);
|
|
27
|
+
registerAuthCommands(program);
|
|
28
|
+
registerDeckCommands(program);
|
|
29
|
+
registerFleetCommands(program);
|
|
30
|
+
registerAutomationCommands(program);
|
|
31
|
+
registerSdkCommand(program);
|
|
32
|
+
registerWorkbenchCommands(program);
|
|
33
|
+
registerOracleCommands(program);
|
|
34
|
+
registerPortfolioCommand(program);
|
|
35
|
+
registerScenarioCommands(program);
|
|
36
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
37
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
38
|
+
if (process.env.ZCD_DEBUG && err instanceof Error && err.stack) {
|
|
39
|
+
console.error(err.stack);
|
|
40
|
+
}
|
|
41
|
+
process.exit(1);
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export const PORTFOLIO_DIMENSIONS = [
|
|
2
|
+
'genericRuntime',
|
|
3
|
+
'vendorApi',
|
|
4
|
+
'officialSdkOrNativeProtocol',
|
|
5
|
+
'connectorWorkflow',
|
|
6
|
+
'deployedQualification',
|
|
7
|
+
];
|
|
8
|
+
const EVIDENCE_STATUSES = new Set([
|
|
9
|
+
'passing', 'partial', 'failing', 'not_run', 'not_applicable',
|
|
10
|
+
]);
|
|
11
|
+
const CERTIFICATIONS = new Set([
|
|
12
|
+
'certified', 'blocked', 'stale', 'unqualified', 'not_applicable',
|
|
13
|
+
]);
|
|
14
|
+
export function parseDeckPortfolio(value, label) {
|
|
15
|
+
if (!isRecord(value)
|
|
16
|
+
|| value.schemaVersion !== 2
|
|
17
|
+
|| value.catalogTargetCount !== 69
|
|
18
|
+
|| !canonicalTimestamp(value.generatedAt)
|
|
19
|
+
|| !Array.isArray(value.targets)
|
|
20
|
+
|| value.targets.length !== 69) {
|
|
21
|
+
throw new Error(`${label} returned an invalid portfolio denominator`);
|
|
22
|
+
}
|
|
23
|
+
const targets = value.targets.map((target, index) => parseTarget(target, `${label} targets[${index}]`));
|
|
24
|
+
const ids = targets.map(target => target.targetId);
|
|
25
|
+
if (new Set(ids).size !== ids.length || targets.filter(target => target.kind === 'platform').length !== 1) {
|
|
26
|
+
throw new Error(`${label} returned an invalid portfolio target catalog`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export function renderDeckPortfolio(portfolio) {
|
|
31
|
+
const connectorCount = portfolio.targets.filter(target => target.kind === 'connector').length;
|
|
32
|
+
const certified = portfolio.targets.filter(target => target.certification === 'certified').length;
|
|
33
|
+
const lines = [
|
|
34
|
+
`Portfolio v2 · ${connectorCount} connector targets · ${certified} certified`,
|
|
35
|
+
`Evidence as of ${portfolio.generatedAt}`,
|
|
36
|
+
'',
|
|
37
|
+
];
|
|
38
|
+
for (const target of portfolio.targets) {
|
|
39
|
+
if (target.kind === 'platform') {
|
|
40
|
+
lines.push(`${target.targetId} platform-exempt — ${target.exemption.reason}`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const dimensions = [
|
|
44
|
+
['runtime', target.dimensions.genericRuntime],
|
|
45
|
+
['vendor-api', target.dimensions.vendorApi],
|
|
46
|
+
['sdk-protocol', target.dimensions.officialSdkOrNativeProtocol],
|
|
47
|
+
['workflow', target.dimensions.connectorWorkflow],
|
|
48
|
+
['qualification', target.dimensions.deployedQualification],
|
|
49
|
+
];
|
|
50
|
+
lines.push(`${target.targetId} ${target.certification} · ${dimensions
|
|
51
|
+
.map(([name, dimension]) => `${name}=${renderDimension(dimension)}`)
|
|
52
|
+
.join(' ')}`);
|
|
53
|
+
}
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
function parseTarget(value, label) {
|
|
57
|
+
if (!isRecord(value)
|
|
58
|
+
|| typeof value.targetId !== 'string'
|
|
59
|
+
|| !value.targetId.trim()
|
|
60
|
+
|| (value.kind !== 'connector' && value.kind !== 'platform')
|
|
61
|
+
|| !['dedicated', 'shared', 'platform'].includes(String(value.implementation))
|
|
62
|
+
|| !CERTIFICATIONS.has(value.certification)
|
|
63
|
+
|| !['ready', 'failed', 'not_built', 'not_observed'].includes(String(value.build))
|
|
64
|
+
|| (value.exactArtifact !== null && !validArtifact(value.exactArtifact))
|
|
65
|
+
|| !isRecord(value.suite)
|
|
66
|
+
|| !Number.isSafeInteger(value.suite.caseCount)
|
|
67
|
+
|| Number(value.suite.caseCount) < 0
|
|
68
|
+
|| (value.suite.version !== null && typeof value.suite.version !== 'string')
|
|
69
|
+
|| !isRecord(value.dimensions)
|
|
70
|
+
|| !stringArray(value.parityGaps)
|
|
71
|
+
|| !stringArray(value.deploymentConflicts)
|
|
72
|
+
|| !stringArray(value.knownBlockers)) {
|
|
73
|
+
throw new Error(`${label} is invalid`);
|
|
74
|
+
}
|
|
75
|
+
const dimensionRecord = value.dimensions;
|
|
76
|
+
const dimensions = Object.fromEntries(PORTFOLIO_DIMENSIONS.map(name => [
|
|
77
|
+
name,
|
|
78
|
+
parseDimension(dimensionRecord[name], `${label} ${name}`),
|
|
79
|
+
]));
|
|
80
|
+
const exemption = value.exemption === null
|
|
81
|
+
? null
|
|
82
|
+
: isRecord(value.exemption)
|
|
83
|
+
&& value.exemption.status === 'platform_exempt'
|
|
84
|
+
&& typeof value.exemption.reason === 'string'
|
|
85
|
+
&& value.exemption.reason.trim()
|
|
86
|
+
? { status: 'platform_exempt', reason: value.exemption.reason }
|
|
87
|
+
: undefined;
|
|
88
|
+
if (exemption === undefined
|
|
89
|
+
|| (value.kind === 'platform') !== (exemption !== null)
|
|
90
|
+
|| (value.kind === 'platform') !== (value.certification === 'not_applicable')) {
|
|
91
|
+
throw new Error(`${label} has an invalid platform exemption`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
targetId: value.targetId,
|
|
95
|
+
kind: value.kind,
|
|
96
|
+
implementation: value.implementation,
|
|
97
|
+
exemption,
|
|
98
|
+
suite: { version: value.suite.version, caseCount: Number(value.suite.caseCount) },
|
|
99
|
+
build: value.build,
|
|
100
|
+
exactArtifact: value.exactArtifact,
|
|
101
|
+
certification: value.certification,
|
|
102
|
+
dimensions,
|
|
103
|
+
parityGaps: value.parityGaps,
|
|
104
|
+
deploymentConflicts: value.deploymentConflicts,
|
|
105
|
+
knownBlockers: value.knownBlockers,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function validArtifact(value) {
|
|
109
|
+
return isRecord(value)
|
|
110
|
+
&& typeof value.imageDigest === 'string'
|
|
111
|
+
&& /^sha256:[a-f0-9]{64}$/.test(value.imageDigest)
|
|
112
|
+
&& typeof value.sourceCommit === 'string'
|
|
113
|
+
&& /^[a-f0-9]{40}$/.test(value.sourceCommit)
|
|
114
|
+
&& typeof value.runtimeProfileHash === 'string'
|
|
115
|
+
&& /^[a-f0-9]{64}$/.test(value.runtimeProfileHash);
|
|
116
|
+
}
|
|
117
|
+
function parseDimension(value, label) {
|
|
118
|
+
if (!isRecord(value)
|
|
119
|
+
|| !EVIDENCE_STATUSES.has(value.status)
|
|
120
|
+
|| ![value.passed, value.failed, value.total].every(item => Number.isSafeInteger(item) && Number(item) >= 0)
|
|
121
|
+
|| Number(value.passed) + Number(value.failed) !== Number(value.total)
|
|
122
|
+
|| !stringArray(value.caseIds)
|
|
123
|
+
|| !stringArray(value.evidenceRefs)
|
|
124
|
+
|| (value.score !== null && (typeof value.score !== 'number' || value.score < 0 || value.score > 1))) {
|
|
125
|
+
throw new Error(`${label} evidence is invalid`);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
function renderDimension(dimension) {
|
|
130
|
+
if (dimension.status === 'not_run')
|
|
131
|
+
return 'not-run';
|
|
132
|
+
if (dimension.status === 'not_applicable')
|
|
133
|
+
return 'n/a';
|
|
134
|
+
return `${dimension.passed}/${dimension.total}`;
|
|
135
|
+
}
|
|
136
|
+
function canonicalTimestamp(value) {
|
|
137
|
+
if (typeof value !== 'string')
|
|
138
|
+
return false;
|
|
139
|
+
const parsed = new Date(value);
|
|
140
|
+
return Number.isFinite(parsed.getTime()) && parsed.toISOString() === value;
|
|
141
|
+
}
|
|
142
|
+
function stringArray(value) {
|
|
143
|
+
return Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
144
|
+
}
|
|
145
|
+
function isRecord(value) {
|
|
146
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=portfolio.js.map
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const FIELD_KINDS = new Set([
|
|
2
|
+
'text',
|
|
3
|
+
'url',
|
|
4
|
+
'code',
|
|
5
|
+
'number',
|
|
6
|
+
'boolean',
|
|
7
|
+
'json',
|
|
8
|
+
]);
|
|
9
|
+
const QUALIFICATION_STATUSES = new Set([
|
|
10
|
+
'certified',
|
|
11
|
+
'blocked',
|
|
12
|
+
'stale',
|
|
13
|
+
'unqualified',
|
|
14
|
+
]);
|
|
15
|
+
const IMAGE_DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function isNonemptySingleLine(value) {
|
|
20
|
+
return (typeof value === 'string'
|
|
21
|
+
&& value.trim().length > 0
|
|
22
|
+
&& !/[\u0000-\u001f\u007f]/u.test(value));
|
|
23
|
+
}
|
|
24
|
+
function isOptionalString(value, key) {
|
|
25
|
+
return value[key] === undefined || typeof value[key] === 'string';
|
|
26
|
+
}
|
|
27
|
+
function isOptionalNullableString(value, key) {
|
|
28
|
+
return (value[key] === undefined
|
|
29
|
+
|| value[key] === null
|
|
30
|
+
|| typeof value[key] === 'string');
|
|
31
|
+
}
|
|
32
|
+
function isPublishedField(value) {
|
|
33
|
+
if (!isRecord(value))
|
|
34
|
+
return false;
|
|
35
|
+
if (!isNonemptySingleLine(value.moduleName)
|
|
36
|
+
|| !isNonemptySingleLine(value.name)
|
|
37
|
+
|| !isNonemptySingleLine(value.label)
|
|
38
|
+
|| typeof value.description !== 'string'
|
|
39
|
+
|| typeof value.group !== 'string'
|
|
40
|
+
|| !FIELD_KINDS.has(value.kind)
|
|
41
|
+
|| (value.language !== null
|
|
42
|
+
&& typeof value.language !== 'string')
|
|
43
|
+
|| typeof value.available !== 'boolean') {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return !value.available || Object.hasOwn(value, 'value');
|
|
47
|
+
}
|
|
48
|
+
function isPublishedModule(value) {
|
|
49
|
+
return (isRecord(value)
|
|
50
|
+
&& isNonemptySingleLine(value.moduleName)
|
|
51
|
+
&& Array.isArray(value.fields)
|
|
52
|
+
&& value.fields.every(isPublishedField));
|
|
53
|
+
}
|
|
54
|
+
function isQualification(value) {
|
|
55
|
+
if (!isRecord(value)
|
|
56
|
+
|| value.schemaVersion !== 1
|
|
57
|
+
|| !isNonemptySingleLine(value.id)
|
|
58
|
+
|| !isNonemptySingleLine(value.cloneId)
|
|
59
|
+
|| typeof value.score !== 'number'
|
|
60
|
+
|| !Number.isFinite(value.score)
|
|
61
|
+
|| value.score < 0
|
|
62
|
+
|| value.score > 100
|
|
63
|
+
|| !QUALIFICATION_STATUSES.has(value.status)
|
|
64
|
+
|| typeof value.certified !== 'boolean'
|
|
65
|
+
|| typeof value.imageDigest !== 'string'
|
|
66
|
+
|| !IMAGE_DIGEST.test(value.imageDigest)
|
|
67
|
+
|| !isNonemptySingleLine(value.verifiedAt)
|
|
68
|
+
|| !Number.isFinite(new Date(value.verifiedAt).getTime())
|
|
69
|
+
|| !isNonemptySingleLine(value.staleAt)
|
|
70
|
+
|| !Number.isFinite(new Date(value.staleAt).getTime())
|
|
71
|
+
|| !isRecord(value.categoryScores)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return Object.values(value.categoryScores).every(score => (typeof score === 'number'
|
|
75
|
+
&& Number.isFinite(score)
|
|
76
|
+
&& score >= 0
|
|
77
|
+
&& score <= 100));
|
|
78
|
+
}
|
|
79
|
+
function isCloneRuntime(value) {
|
|
80
|
+
if (!isRecord(value))
|
|
81
|
+
return false;
|
|
82
|
+
const hasRuntimeState = (typeof value.provisioned === 'boolean'
|
|
83
|
+
|| typeof value.status === 'string'
|
|
84
|
+
|| typeof value.cloneStatus === 'string');
|
|
85
|
+
if (!hasRuntimeState
|
|
86
|
+
|| (value.provisioned !== undefined
|
|
87
|
+
&& typeof value.provisioned !== 'boolean')
|
|
88
|
+
|| !isOptionalNullableString(value, 'zergId')
|
|
89
|
+
|| !isOptionalString(value, 'status')
|
|
90
|
+
|| !isOptionalString(value, 'cloneStatus')
|
|
91
|
+
|| !isOptionalString(value, 'deployPhase')
|
|
92
|
+
|| !isOptionalNullableString(value, 'serviceUrl')
|
|
93
|
+
|| !isOptionalNullableString(value, 'socketUrl')
|
|
94
|
+
|| !isOptionalNullableString(value, 'mirrorUrl')
|
|
95
|
+
|| !isOptionalNullableString(value, 'errorMessage')
|
|
96
|
+
|| !isOptionalNullableString(value, 'zergCloudUrl')
|
|
97
|
+
|| !isOptionalNullableString(value, 'zstackRunUrl')
|
|
98
|
+
|| !isOptionalNullableString(value, 'flyAppName')
|
|
99
|
+
|| !isOptionalNullableString(value, 'flyAppUrl')
|
|
100
|
+
|| !isOptionalNullableString(value, 'updatedAt')) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const publicationKeys = [
|
|
104
|
+
'publicationStatus',
|
|
105
|
+
'publishedModules',
|
|
106
|
+
'publicationError',
|
|
107
|
+
];
|
|
108
|
+
const publicationFieldCount = publicationKeys.filter(key => value[key] !== undefined).length;
|
|
109
|
+
if (publicationFieldCount !== 0 && publicationFieldCount !== publicationKeys.length) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (publicationFieldCount !== 0
|
|
113
|
+
&& (!['ready', 'none', 'unavailable'].includes(String(value.publicationStatus))
|
|
114
|
+
|| !Array.isArray(value.publishedModules)
|
|
115
|
+
|| !value.publishedModules.every(isPublishedModule)
|
|
116
|
+
|| (value.publicationError !== null
|
|
117
|
+
&& typeof value.publicationError !== 'string'))) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return (value.qualification === undefined
|
|
121
|
+
|| value.qualification === null
|
|
122
|
+
|| isQualification(value.qualification));
|
|
123
|
+
}
|
|
124
|
+
function isCloneCredential(value) {
|
|
125
|
+
if (!isRecord(value)
|
|
126
|
+
|| value.schemaVersion !== 1
|
|
127
|
+
|| !isNonemptySingleLine(value.vendor)
|
|
128
|
+
|| !isRecord(value.authentication)
|
|
129
|
+
|| value.authentication.type !== 'token'
|
|
130
|
+
|| !isNonemptySingleLine(value.authentication.header)
|
|
131
|
+
|| !isNonemptySingleLine(value.authentication.token)
|
|
132
|
+
|| !isCredentialScheme(value.authentication.scheme, value.authentication.header)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (value.login === undefined)
|
|
136
|
+
return true;
|
|
137
|
+
return (isRecord(value.login)
|
|
138
|
+
&& isNonemptySingleLine(value.login.username)
|
|
139
|
+
&& isNonemptySingleLine(value.login.path));
|
|
140
|
+
}
|
|
141
|
+
function isCredentialScheme(value, header) {
|
|
142
|
+
if (typeof value !== 'string'
|
|
143
|
+
|| value !== value.trim()
|
|
144
|
+
|| value.length > 64
|
|
145
|
+
|| /[\u0000-\u001f\u007f]/u.test(value))
|
|
146
|
+
return false;
|
|
147
|
+
if (value === '') {
|
|
148
|
+
return (typeof header === 'string'
|
|
149
|
+
&& header.toLowerCase() !== 'authorization');
|
|
150
|
+
}
|
|
151
|
+
return /^[A-Za-z][A-Za-z0-9._~-]*$/.test(value);
|
|
152
|
+
}
|
|
153
|
+
export function parseCloneRuntime(value, source) {
|
|
154
|
+
if (!isCloneRuntime(value)) {
|
|
155
|
+
throw new Error(`${source} returned an invalid runtime schema.`);
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
export function parseCloneCredential(value, source) {
|
|
160
|
+
if (!isCloneCredential(value)) {
|
|
161
|
+
throw new Error(`${source} returned an invalid credential schema.`);
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=runtime.js.map
|
package/dist/vendors.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-vendor smoke checks — hit each clone the way its real SOC API/SDK would,
|
|
3
|
+
* to prove the vendor surface responds. zlunk + zokta are fully implemented;
|
|
4
|
+
* every other vendor falls back to /health + /metrics.
|
|
5
|
+
*/
|
|
6
|
+
import { hit } from './client.js';
|
|
7
|
+
async function health(h) {
|
|
8
|
+
const r = await h('GET', '/health');
|
|
9
|
+
const vendor = r.json ?? {};
|
|
10
|
+
return { name: 'GET /health', ok: r.status === 200 && vendor.status === 'up', detail: `${r.status} ${vendor.vendor ?? ''}`.trim() };
|
|
11
|
+
}
|
|
12
|
+
const arr = (v) => (Array.isArray(v) ? v : null);
|
|
13
|
+
export const VENDOR_SMOKE = {
|
|
14
|
+
zlunk: {
|
|
15
|
+
evokes: 'Splunk',
|
|
16
|
+
smoke: async (h) => {
|
|
17
|
+
const out = [await health(h)];
|
|
18
|
+
const s = await h('POST', '/services/search/jobs/export', {
|
|
19
|
+
headers: { Authorization: 'Splunk simtoken', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
20
|
+
// index=main holds the demo seed's HEC events; the mock doesn't expand index=*.
|
|
21
|
+
body: `search=${encodeURIComponent('search index=main')}&output_mode=json`,
|
|
22
|
+
});
|
|
23
|
+
const body = s.json;
|
|
24
|
+
const results = arr(body?.results) ?? arr(s.json);
|
|
25
|
+
out.push({
|
|
26
|
+
name: 'POST /services/search/jobs/export (SPL search)',
|
|
27
|
+
ok: s.status === 200 && Array.isArray(results),
|
|
28
|
+
detail: `${s.status}, ${results ? `${results.length} events` : 'no results'}`,
|
|
29
|
+
});
|
|
30
|
+
return out;
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
zokta: {
|
|
34
|
+
evokes: 'Okta',
|
|
35
|
+
smoke: async (h) => {
|
|
36
|
+
const out = [await health(h)];
|
|
37
|
+
const u = await h('GET', '/api/v1/users', { headers: { Authorization: 'SSWS ssws-simtoken' } });
|
|
38
|
+
out.push({ name: 'GET /api/v1/users', ok: u.status === 200 && !!arr(u.json), detail: `${u.status}, ${arr(u.json)?.length ?? '?'} users` });
|
|
39
|
+
const l = await h('GET', '/api/v1/logs?limit=100', { headers: { Authorization: 'SSWS ssws-simtoken' } });
|
|
40
|
+
out.push({ name: 'GET /api/v1/logs', ok: l.status === 200 && !!arr(l.json), detail: `${l.status}, ${arr(l.json)?.length ?? '?'} system-log events` });
|
|
41
|
+
return out;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
function genericSpec(evokes) {
|
|
46
|
+
return {
|
|
47
|
+
evokes,
|
|
48
|
+
smoke: async (h) => {
|
|
49
|
+
const out = [await health(h)];
|
|
50
|
+
const m = await h('GET', '/metrics');
|
|
51
|
+
out.push({ name: 'GET /metrics', ok: m.status === 200, detail: `${m.status}` });
|
|
52
|
+
return out;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function smokeFor(vendor, vendorEvokes = vendor) {
|
|
57
|
+
const spec = VENDOR_SMOKE[vendor] ?? genericSpec(vendorEvokes);
|
|
58
|
+
return spec.evokes === vendorEvokes ? spec : { ...spec, evokes: vendorEvokes };
|
|
59
|
+
}
|
|
60
|
+
/** Build a base-bound hit for a clone's public service URL. */
|
|
61
|
+
export function boundHit(cloneBaseUrl) {
|
|
62
|
+
const base = cloneBaseUrl.replace(/\/+$/, '');
|
|
63
|
+
return (method, path, opts) => hit(method, `${base}${path}`, opts);
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=vendors.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zergai/cyberdeck",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "Command-line client for Zerg CyberDeck clone environments and server-side SDK conformance runs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zcd": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/**/*.js",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE.md",
|
|
13
|
+
"COMMERCIAL-LICENSING.md",
|
|
14
|
+
"NOTICE",
|
|
15
|
+
"THIRD_PARTY_NOTICES.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/Epoch-ML/zerg.git",
|
|
24
|
+
"directory": "cybersim/packages/deck-cli"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/Epoch-ML/zerg/tree/development/cybersim/packages/deck-cli",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/Epoch-ML/zerg/issues"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"zerg",
|
|
32
|
+
"cyberdeck",
|
|
33
|
+
"cybersecurity",
|
|
34
|
+
"sdk-conformance",
|
|
35
|
+
"cli"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"tag": "next"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc --build --force",
|
|
43
|
+
"clean": "tsc --build --clean",
|
|
44
|
+
"prepack": "npm run clean && npm run build",
|
|
45
|
+
"test:public-tarball": "node scripts/smoke-packed-public-api.mjs"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"commander": "^12.1.0",
|
|
49
|
+
"socket.io-client": "^4.8.1"
|
|
50
|
+
}
|
|
51
|
+
}
|