@zivis/cli 0.1.0-alpha.40 → 0.1.0-alpha.41
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/dist/commands/app/index.js +4 -2
- package/dist/commands/assurance/index.d.ts +33 -0
- package/dist/commands/assurance/index.js +149 -0
- package/dist/commands/auth/index.js +1 -0
- package/dist/commands/auth/init.d.ts +1 -0
- package/dist/commands/auth/init.js +12 -8
- package/dist/commands/gate/index.d.ts +2 -0
- package/dist/commands/gate/index.js +171 -0
- package/dist/commands/mcp/index.js +33 -1
- package/dist/commands/run/index.d.ts +2 -0
- package/dist/commands/run/index.js +111 -0
- package/dist/commands/sync/index.d.ts +2 -0
- package/dist/commands/sync/index.js +187 -0
- package/dist/commands/test/index.d.ts +2 -0
- package/dist/commands/test/index.js +115 -0
- package/dist/commands/tm/index.js +4 -0
- package/dist/index.js +11 -0
- package/dist/internal/application-binding.d.ts +8 -0
- package/dist/internal/application-binding.js +167 -0
- package/dist/internal/cli-output.d.ts +16 -0
- package/dist/internal/cli-output.js +39 -0
- package/dist/internal/devx-run.d.ts +47 -0
- package/dist/internal/devx-run.js +36 -0
- package/dist/internal/gate-evaluate.d.ts +50 -0
- package/dist/internal/gate-evaluate.js +111 -0
- package/dist/internal/gate-policy.d.ts +38 -0
- package/dist/internal/gate-policy.js +167 -0
- package/dist/internal/git-metadata.d.ts +8 -0
- package/dist/internal/git-metadata.js +38 -0
- package/dist/internal/git.d.ts +8 -0
- package/dist/internal/git.js +30 -0
- package/dist/internal/ide-setup.d.ts +1 -0
- package/dist/internal/ide-setup.js +76 -19
- package/dist/internal/inventory-sync.d.ts +74 -0
- package/dist/internal/inventory-sync.js +189 -0
- package/dist/internal/packs/cache.d.ts +8 -0
- package/dist/internal/packs/cache.js +60 -0
- package/dist/internal/packs/index.d.ts +10 -0
- package/dist/internal/packs/index.js +7 -0
- package/dist/internal/packs/integrity.d.ts +9 -0
- package/dist/internal/packs/integrity.js +24 -0
- package/dist/internal/packs/jcs.d.ts +2 -0
- package/dist/internal/packs/jcs.js +57 -0
- package/dist/internal/packs/local-paths.d.ts +4 -0
- package/dist/internal/packs/local-paths.js +26 -0
- package/dist/internal/packs/registry-client.d.ts +20 -0
- package/dist/internal/packs/registry-client.js +40 -0
- package/dist/internal/packs/resolve.d.ts +8 -0
- package/dist/internal/packs/resolve.js +89 -0
- package/dist/internal/packs/semver.d.ts +9 -0
- package/dist/internal/packs/semver.js +57 -0
- package/dist/internal/packs/signing.d.ts +5 -0
- package/dist/internal/packs/signing.js +49 -0
- package/dist/internal/packs/types.d.ts +70 -0
- package/dist/internal/packs/types.js +20 -0
- package/dist/internal/run-workdir.d.ts +1 -0
- package/dist/internal/run-workdir.js +11 -0
- package/dist/internal/security-context.d.ts +62 -0
- package/dist/internal/security-context.js +50 -0
- package/dist/internal/sync-outbox.d.ts +40 -0
- package/dist/internal/sync-outbox.js +66 -0
- package/dist/internal/test-scope.d.ts +68 -0
- package/dist/internal/test-scope.js +69 -0
- package/dist/internal/zivis-local-state.d.ts +1 -0
- package/dist/internal/zivis-local-state.js +22 -0
- package/package.json +3 -2
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
export function validateInventorySyncInput(raw) {
|
|
2
|
+
const errors = [];
|
|
3
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4
|
+
return { ok: false, errors: ["input must be a JSON object"] };
|
|
5
|
+
}
|
|
6
|
+
const obj = raw;
|
|
7
|
+
const allowedKeys = new Set(["endpoints", "features", "technologyStack"]);
|
|
8
|
+
for (const key of Object.keys(obj)) {
|
|
9
|
+
if (!allowedKeys.has(key))
|
|
10
|
+
errors.push(`unknown field "${key}"`);
|
|
11
|
+
}
|
|
12
|
+
let endpoints;
|
|
13
|
+
if (obj.endpoints !== undefined) {
|
|
14
|
+
if (!Array.isArray(obj.endpoints)) {
|
|
15
|
+
errors.push("endpoints must be an array");
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
endpoints = [];
|
|
19
|
+
obj.endpoints.forEach((ep, i) => {
|
|
20
|
+
if (typeof ep !== "object" || ep === null) {
|
|
21
|
+
errors.push(`endpoints[${i}] must be an object`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const e = ep;
|
|
25
|
+
if (typeof e.path !== "string" || !e.path) {
|
|
26
|
+
errors.push(`endpoints[${i}].path must be a non-empty string`);
|
|
27
|
+
}
|
|
28
|
+
if (typeof e.method !== "string" || !e.method) {
|
|
29
|
+
errors.push(`endpoints[${i}].method must be a non-empty string`);
|
|
30
|
+
}
|
|
31
|
+
if (e.tags !== undefined && (!Array.isArray(e.tags) || !e.tags.every((t) => typeof t === "string"))) {
|
|
32
|
+
errors.push(`endpoints[${i}].tags must be an array of strings`);
|
|
33
|
+
}
|
|
34
|
+
if (errors.length === 0 || (typeof e.path === "string" && typeof e.method === "string")) {
|
|
35
|
+
endpoints.push({
|
|
36
|
+
path: e.path,
|
|
37
|
+
method: e.method,
|
|
38
|
+
summary: typeof e.summary === "string" ? e.summary : undefined,
|
|
39
|
+
description: typeof e.description === "string" ? e.description : undefined,
|
|
40
|
+
authRequired: typeof e.authRequired === "boolean" ? e.authRequired : undefined,
|
|
41
|
+
tags: Array.isArray(e.tags) ? e.tags : undefined,
|
|
42
|
+
deprecated: typeof e.deprecated === "boolean" ? e.deprecated : undefined,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const validCriticalities = new Set(["critical", "high", "medium", "low"]);
|
|
49
|
+
let features;
|
|
50
|
+
if (obj.features !== undefined) {
|
|
51
|
+
if (!Array.isArray(obj.features)) {
|
|
52
|
+
errors.push("features must be an array");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
features = [];
|
|
56
|
+
obj.features.forEach((f, i) => {
|
|
57
|
+
if (typeof f !== "object" || f === null) {
|
|
58
|
+
errors.push(`features[${i}] must be an object`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const feat = f;
|
|
62
|
+
if (typeof feat.name !== "string" || !feat.name) {
|
|
63
|
+
errors.push(`features[${i}].name must be a non-empty string`);
|
|
64
|
+
}
|
|
65
|
+
if (feat.criticality !== undefined && !validCriticalities.has(feat.criticality)) {
|
|
66
|
+
errors.push(`features[${i}].criticality must be one of: ${[...validCriticalities].join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
if (feat.endpoints !== undefined && (!Array.isArray(feat.endpoints) || !feat.endpoints.every((p) => typeof p === "string"))) {
|
|
69
|
+
errors.push(`features[${i}].endpoints must be an array of strings`);
|
|
70
|
+
}
|
|
71
|
+
if (typeof feat.name === "string" && feat.name) {
|
|
72
|
+
features.push({
|
|
73
|
+
name: feat.name,
|
|
74
|
+
description: typeof feat.description === "string" ? feat.description : undefined,
|
|
75
|
+
criticality: validCriticalities.has(feat.criticality)
|
|
76
|
+
? feat.criticality
|
|
77
|
+
: undefined,
|
|
78
|
+
endpoints: Array.isArray(feat.endpoints) ? feat.endpoints : undefined,
|
|
79
|
+
tags: Array.isArray(feat.tags) ? feat.tags : undefined,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let technologyStack;
|
|
86
|
+
if (obj.technologyStack !== undefined) {
|
|
87
|
+
if (!Array.isArray(obj.technologyStack) || !obj.technologyStack.every((t) => typeof t === "string")) {
|
|
88
|
+
errors.push("technologyStack must be an array of strings");
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
technologyStack = obj.technologyStack;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (errors.length > 0)
|
|
95
|
+
return { ok: false, errors };
|
|
96
|
+
if (endpoints === undefined && features === undefined && technologyStack === undefined) {
|
|
97
|
+
return { ok: false, errors: ["input must include at least one of: endpoints, features, technologyStack"] };
|
|
98
|
+
}
|
|
99
|
+
const secretHits = scanForSecrets({ endpoints, features, technologyStack });
|
|
100
|
+
if (secretHits.length > 0) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
errors: secretHits.map((hit) => `${hit} looks like it contains a credential/token — refusing to sync`),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, value: { endpoints, features, technologyStack } };
|
|
107
|
+
}
|
|
108
|
+
const SECRET_PATTERNS = [
|
|
109
|
+
/sk-[a-zA-Z0-9]{20,}/,
|
|
110
|
+
/ghp_[a-zA-Z0-9]{30,}/,
|
|
111
|
+
/gh[oprs]_[a-zA-Z0-9]{30,}/,
|
|
112
|
+
/AKIA[0-9A-Z]{16}/,
|
|
113
|
+
/-----BEGIN[ A-Z]*PRIVATE KEY-----/,
|
|
114
|
+
/\bBearer\s+[a-zA-Z0-9._-]{20,}/i,
|
|
115
|
+
/\bAuthorization\s*:\s*\S+/i,
|
|
116
|
+
];
|
|
117
|
+
function looksLikeSecret(value) {
|
|
118
|
+
return SECRET_PATTERNS.some((pattern) => pattern.test(value));
|
|
119
|
+
}
|
|
120
|
+
function scanForSecrets(input) {
|
|
121
|
+
const hits = [];
|
|
122
|
+
const check = (label, value) => {
|
|
123
|
+
if (value && looksLikeSecret(value))
|
|
124
|
+
hits.push(label);
|
|
125
|
+
};
|
|
126
|
+
(input.endpoints ?? []).forEach((ep, i) => {
|
|
127
|
+
check(`endpoints[${i}].summary`, ep.summary);
|
|
128
|
+
check(`endpoints[${i}].description`, ep.description);
|
|
129
|
+
(ep.tags ?? []).forEach((t, j) => check(`endpoints[${i}].tags[${j}]`, t));
|
|
130
|
+
});
|
|
131
|
+
(input.features ?? []).forEach((f, i) => {
|
|
132
|
+
check(`features[${i}].name`, f.name);
|
|
133
|
+
check(`features[${i}].description`, f.description);
|
|
134
|
+
(f.tags ?? []).forEach((t, j) => check(`features[${i}].tags[${j}]`, t));
|
|
135
|
+
});
|
|
136
|
+
(input.technologyStack ?? []).forEach((t, i) => check(`technologyStack[${i}]`, t));
|
|
137
|
+
return hits;
|
|
138
|
+
}
|
|
139
|
+
function toEndpointReconcilePayload(input, provenance) {
|
|
140
|
+
return {
|
|
141
|
+
endpoints: input.endpoints,
|
|
142
|
+
gitCommitSha: provenance.gitCommitSha ?? undefined,
|
|
143
|
+
source: provenance.source,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function toFeatureReconcilePayload(input, provenance) {
|
|
147
|
+
return {
|
|
148
|
+
features: (input.features ?? []).map((f) => ({
|
|
149
|
+
name: f.name,
|
|
150
|
+
description: f.description,
|
|
151
|
+
criticality: f.criticality ?? "medium",
|
|
152
|
+
endpoint_count: f.endpoints?.length ?? 0,
|
|
153
|
+
endpoints: f.endpoints ?? [],
|
|
154
|
+
tags: f.tags ?? [],
|
|
155
|
+
})),
|
|
156
|
+
endpoints: [],
|
|
157
|
+
integrations: [],
|
|
158
|
+
frameworks: [],
|
|
159
|
+
repos_scanned: 1,
|
|
160
|
+
files_scanned: 0,
|
|
161
|
+
gitCommitSha: provenance.gitCommitSha ?? undefined,
|
|
162
|
+
source: provenance.source,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function toTechStackPayload(input) {
|
|
166
|
+
return { technologies: input.technologyStack };
|
|
167
|
+
}
|
|
168
|
+
export async function syncApplicationInventory(params) {
|
|
169
|
+
const { client, applicationId, input, provenance } = params;
|
|
170
|
+
const summary = {};
|
|
171
|
+
if (input.technologyStack !== undefined) {
|
|
172
|
+
const result = await client.put(`/api/rt/applications/${encodeURIComponent(applicationId)}/technology-stack`, toTechStackPayload(input));
|
|
173
|
+
summary.technologyStack = { count: result.techStack.length };
|
|
174
|
+
}
|
|
175
|
+
if (input.endpoints !== undefined) {
|
|
176
|
+
summary.endpoints = await client.post(`/api/rt/applications/${encodeURIComponent(applicationId)}/endpoints/reconcile`, toEndpointReconcilePayload(input, provenance));
|
|
177
|
+
}
|
|
178
|
+
if (input.features !== undefined) {
|
|
179
|
+
summary.features = await client.post(`/api/features/applications/${encodeURIComponent(applicationId)}/reconcile`, toFeatureReconcilePayload(input, provenance));
|
|
180
|
+
}
|
|
181
|
+
if (provenance.repoFullName && provenance.gitCommitSha) {
|
|
182
|
+
summary.repo = await client.put(`/api/rt/applications/${encodeURIComponent(applicationId)}/repos/sync`, {
|
|
183
|
+
repoFullName: provenance.repoFullName,
|
|
184
|
+
latestCommitSha: provenance.gitCommitSha,
|
|
185
|
+
defaultBranch: provenance.defaultBranch ?? undefined,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return summary;
|
|
189
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PackManifestV1, PackType } from "./types.js";
|
|
2
|
+
export declare function writeVerifiedPackToCache(packType: PackType, version: string, manifest: PackManifestV1, contentByPath: Map<string, string>): Promise<string>;
|
|
3
|
+
export declare function loadVerifiedFromCache(packType: PackType, version: string): Promise<{
|
|
4
|
+
manifest: PackManifestV1;
|
|
5
|
+
contentByPath: Map<string, string>;
|
|
6
|
+
cacheDir: string;
|
|
7
|
+
} | null>;
|
|
8
|
+
export declare function listCachedVersions(packType: PackType): Promise<string[]>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { packTypeDir, packVersionDir } from "./local-paths.js";
|
|
4
|
+
import { verifyPack } from "./integrity.js";
|
|
5
|
+
function contentPaths(manifest) {
|
|
6
|
+
return [manifest.base.path, ...(manifest.scopes ?? []).map((s) => s.path)];
|
|
7
|
+
}
|
|
8
|
+
export async function writeVerifiedPackToCache(packType, version, manifest, contentByPath) {
|
|
9
|
+
const dir = packVersionDir(packType, version);
|
|
10
|
+
await fs.mkdir(dir, { recursive: true });
|
|
11
|
+
await atomicWrite(path.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
12
|
+
for (const relPath of contentPaths(manifest)) {
|
|
13
|
+
const content = contentByPath.get(relPath);
|
|
14
|
+
if (content === undefined)
|
|
15
|
+
continue;
|
|
16
|
+
const target = path.join(dir, relPath);
|
|
17
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
18
|
+
await atomicWrite(target, content);
|
|
19
|
+
}
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
async function atomicWrite(target, content) {
|
|
23
|
+
const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
24
|
+
await fs.writeFile(tmp, content, "utf-8");
|
|
25
|
+
await fs.rename(tmp, target);
|
|
26
|
+
}
|
|
27
|
+
export async function loadVerifiedFromCache(packType, version) {
|
|
28
|
+
const dir = packVersionDir(packType, version);
|
|
29
|
+
let manifest;
|
|
30
|
+
try {
|
|
31
|
+
const raw = await fs.readFile(path.join(dir, "manifest.json"), "utf-8");
|
|
32
|
+
manifest = JSON.parse(raw);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const contentByPath = new Map();
|
|
38
|
+
for (const relPath of contentPaths(manifest)) {
|
|
39
|
+
try {
|
|
40
|
+
contentByPath.set(relPath, await fs.readFile(path.join(dir, relPath), "utf-8"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const result = verifyPack(manifest, contentByPath);
|
|
47
|
+
if (!result.valid) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return { manifest, contentByPath, cacheDir: dir };
|
|
51
|
+
}
|
|
52
|
+
export async function listCachedVersions(packType) {
|
|
53
|
+
try {
|
|
54
|
+
const entries = await fs.readdir(packTypeDir(packType), { withFileTypes: true });
|
|
55
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { PackType, PackContentEntry, PackScopeEntry, PackSignature, PackManifestV1, PackVersionSummary, PackVersionListing, PackSource, ResolvedScope, ResolvedPack, PackRegistryClient, } from "./types.js";
|
|
2
|
+
export { PackIntegrityError, PackUnavailableError, UnknownScopeError } from "./types.js";
|
|
3
|
+
export { resolveCompatiblePack, resolveScope, listAvailableScopes } from "./resolve.js";
|
|
4
|
+
export type { ResolveOptions } from "./resolve.js";
|
|
5
|
+
export { HttpPackRegistryClient, LocalPackRegistryClient, registryDirOverride, } from "./registry-client.js";
|
|
6
|
+
export type { MinimalApiClient } from "./registry-client.js";
|
|
7
|
+
export { userCacheRoot, packsRoot, packTypeDir, packVersionDir } from "./local-paths.js";
|
|
8
|
+
export { verifyManifestSignature, manifestSigningPayload, DEV_PACK_SIGNING_KEY_ID } from "./signing.js";
|
|
9
|
+
export { sha256Hex, verifyContentHash, verifyPack } from "./integrity.js";
|
|
10
|
+
export { compareVersions, satisfiesMinVersion, parseVersion } from "./semver.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { PackIntegrityError, PackUnavailableError, UnknownScopeError } from "./types.js";
|
|
2
|
+
export { resolveCompatiblePack, resolveScope, listAvailableScopes } from "./resolve.js";
|
|
3
|
+
export { HttpPackRegistryClient, LocalPackRegistryClient, registryDirOverride, } from "./registry-client.js";
|
|
4
|
+
export { userCacheRoot, packsRoot, packTypeDir, packVersionDir } from "./local-paths.js";
|
|
5
|
+
export { verifyManifestSignature, manifestSigningPayload, DEV_PACK_SIGNING_KEY_ID } from "./signing.js";
|
|
6
|
+
export { sha256Hex, verifyContentHash, verifyPack } from "./integrity.js";
|
|
7
|
+
export { compareVersions, satisfiesMinVersion, parseVersion } from "./semver.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PackContentEntry, PackManifestV1 } from "./types.js";
|
|
2
|
+
export declare function sha256Hex(content: string): string;
|
|
3
|
+
export declare function verifyContentHash(content: string, entry: PackContentEntry): boolean;
|
|
4
|
+
export declare function verifyPack(manifest: PackManifestV1, contentByPath: Map<string, string>): {
|
|
5
|
+
valid: true;
|
|
6
|
+
} | {
|
|
7
|
+
valid: false;
|
|
8
|
+
reason: string;
|
|
9
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import { verifyManifestSignature } from "./signing.js";
|
|
3
|
+
export function sha256Hex(content) {
|
|
4
|
+
return crypto.createHash("sha256").update(content, "utf8").digest("hex");
|
|
5
|
+
}
|
|
6
|
+
export function verifyContentHash(content, entry) {
|
|
7
|
+
return sha256Hex(content) === entry.sha256;
|
|
8
|
+
}
|
|
9
|
+
export function verifyPack(manifest, contentByPath) {
|
|
10
|
+
if (!verifyManifestSignature(manifest)) {
|
|
11
|
+
return { valid: false, reason: "manifest signature verification failed" };
|
|
12
|
+
}
|
|
13
|
+
const entries = [manifest.base, ...(manifest.scopes ?? [])];
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const content = contentByPath.get(entry.path);
|
|
16
|
+
if (content === undefined) {
|
|
17
|
+
return { valid: false, reason: `missing content for "${entry.path}"` };
|
|
18
|
+
}
|
|
19
|
+
if (!verifyContentHash(content, entry)) {
|
|
20
|
+
return { valid: false, reason: `content hash mismatch for "${entry.path}"` };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { valid: true };
|
|
24
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
function compareUtf16(a, b) {
|
|
3
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
4
|
+
}
|
|
5
|
+
function serialize(value) {
|
|
6
|
+
if (value !== null &&
|
|
7
|
+
typeof value === "object" &&
|
|
8
|
+
typeof value.toJSON === "function") {
|
|
9
|
+
value = value.toJSON();
|
|
10
|
+
}
|
|
11
|
+
if (value === null)
|
|
12
|
+
return "null";
|
|
13
|
+
switch (typeof value) {
|
|
14
|
+
case "boolean":
|
|
15
|
+
return value ? "true" : "false";
|
|
16
|
+
case "number":
|
|
17
|
+
if (!Number.isFinite(value)) {
|
|
18
|
+
throw new TypeError(`JCS: non-finite number is not representable in JSON: ${value}`);
|
|
19
|
+
}
|
|
20
|
+
return JSON.stringify(value);
|
|
21
|
+
case "string":
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
case "bigint":
|
|
24
|
+
throw new TypeError("JCS: BigInt values are not representable in JSON");
|
|
25
|
+
case "undefined":
|
|
26
|
+
case "function":
|
|
27
|
+
case "symbol":
|
|
28
|
+
return undefined;
|
|
29
|
+
case "object": {
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
const items = value.map((item) => serialize(item) ?? "null");
|
|
32
|
+
return `[${items.join(",")}]`;
|
|
33
|
+
}
|
|
34
|
+
const keys = Object.keys(value).sort(compareUtf16);
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
const serialized = serialize(value[key]);
|
|
38
|
+
if (serialized !== undefined) {
|
|
39
|
+
parts.push(`${JSON.stringify(key)}:${serialized}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return `{${parts.join(",")}}`;
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
throw new TypeError(`JCS: cannot canonicalize value of type ${typeof value}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function canonicalize(value) {
|
|
49
|
+
const result = serialize(value);
|
|
50
|
+
if (result === undefined) {
|
|
51
|
+
throw new TypeError("JCS: top-level value has no JSON representation");
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
export function jcsSha256Hex(value) {
|
|
56
|
+
return crypto.createHash("sha256").update(canonicalize(value), "utf8").digest("hex");
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
export function userCacheRoot() {
|
|
4
|
+
if (process.env.ZIVIS_CACHE_ROOT) {
|
|
5
|
+
return process.env.ZIVIS_CACHE_ROOT;
|
|
6
|
+
}
|
|
7
|
+
const home = os.homedir();
|
|
8
|
+
if (process.platform === "darwin") {
|
|
9
|
+
return path.join(home, "Library", "Caches", "zivis");
|
|
10
|
+
}
|
|
11
|
+
if (process.platform === "win32") {
|
|
12
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local");
|
|
13
|
+
return path.join(localAppData, "zivis", "Cache");
|
|
14
|
+
}
|
|
15
|
+
const xdgCacheHome = process.env.XDG_CACHE_HOME ?? path.join(home, ".cache");
|
|
16
|
+
return path.join(xdgCacheHome, "zivis");
|
|
17
|
+
}
|
|
18
|
+
export function packsRoot() {
|
|
19
|
+
return path.join(userCacheRoot(), "packs");
|
|
20
|
+
}
|
|
21
|
+
export function packVersionDir(packType, version) {
|
|
22
|
+
return path.join(packsRoot(), packType, version);
|
|
23
|
+
}
|
|
24
|
+
export function packTypeDir(packType) {
|
|
25
|
+
return path.join(packsRoot(), packType);
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PackManifestV1, PackRegistryClient, PackType, PackVersionListing } from "./types.js";
|
|
2
|
+
export interface MinimalApiClient {
|
|
3
|
+
get<T>(path: string): Promise<T>;
|
|
4
|
+
getText(path: string): Promise<string>;
|
|
5
|
+
}
|
|
6
|
+
export declare class HttpPackRegistryClient implements PackRegistryClient {
|
|
7
|
+
private readonly apiClient;
|
|
8
|
+
constructor(apiClient: MinimalApiClient);
|
|
9
|
+
listVersions(packType: PackType): Promise<PackVersionListing>;
|
|
10
|
+
getManifest(packType: PackType, version: string): Promise<PackManifestV1>;
|
|
11
|
+
getContent(packType: PackType, version: string, relativePath: string): Promise<string>;
|
|
12
|
+
}
|
|
13
|
+
export declare class LocalPackRegistryClient implements PackRegistryClient {
|
|
14
|
+
private readonly rootDir;
|
|
15
|
+
constructor(rootDir: string);
|
|
16
|
+
listVersions(packType: PackType): Promise<PackVersionListing>;
|
|
17
|
+
getManifest(packType: PackType, version: string): Promise<PackManifestV1>;
|
|
18
|
+
getContent(packType: PackType, version: string, relativePath: string): Promise<string>;
|
|
19
|
+
}
|
|
20
|
+
export declare function registryDirOverride(): string | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
export class HttpPackRegistryClient {
|
|
4
|
+
apiClient;
|
|
5
|
+
constructor(apiClient) {
|
|
6
|
+
this.apiClient = apiClient;
|
|
7
|
+
}
|
|
8
|
+
listVersions(packType) {
|
|
9
|
+
return this.apiClient.get(`/api/zivis-packs/${packType}`);
|
|
10
|
+
}
|
|
11
|
+
getManifest(packType, version) {
|
|
12
|
+
return this.apiClient.get(`/api/zivis-packs/${packType}/${version}/manifest.json`);
|
|
13
|
+
}
|
|
14
|
+
getContent(packType, version, relativePath) {
|
|
15
|
+
return this.apiClient.getText(`/api/zivis-packs/${packType}/${version}/content/${relativePath}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class LocalPackRegistryClient {
|
|
19
|
+
rootDir;
|
|
20
|
+
constructor(rootDir) {
|
|
21
|
+
this.rootDir = rootDir;
|
|
22
|
+
}
|
|
23
|
+
async listVersions(packType) {
|
|
24
|
+
const listingPath = path.join(this.rootDir, packType, "versions.json");
|
|
25
|
+
const raw = await fs.readFile(listingPath, "utf-8");
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
async getManifest(packType, version) {
|
|
29
|
+
const manifestPath = path.join(this.rootDir, packType, version, "manifest.json");
|
|
30
|
+
const raw = await fs.readFile(manifestPath, "utf-8");
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
async getContent(packType, version, relativePath) {
|
|
34
|
+
const contentPath = path.join(this.rootDir, packType, version, relativePath);
|
|
35
|
+
return fs.readFile(contentPath, "utf-8");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function registryDirOverride() {
|
|
39
|
+
return process.env.ZIVIS_PACK_REGISTRY_DIR || null;
|
|
40
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PackRegistryClient, PackScopeEntry, PackType, ResolvedPack, ResolvedScope } from "./types.js";
|
|
2
|
+
export interface ResolveOptions {
|
|
3
|
+
registryClient: PackRegistryClient;
|
|
4
|
+
cliVersion: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveCompatiblePack(packType: PackType, opts: ResolveOptions): Promise<ResolvedPack>;
|
|
7
|
+
export declare function resolveScope(resolved: ResolvedPack, scopeId: string): Promise<ResolvedScope>;
|
|
8
|
+
export declare function listAvailableScopes(resolved: ResolvedPack): Pick<PackScopeEntry, "id" | "label" | "description">[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { PackIntegrityError, PackUnavailableError, UnknownScopeError } from "./types.js";
|
|
2
|
+
import { verifyPack } from "./integrity.js";
|
|
3
|
+
import { compareVersions, satisfiesMinVersion } from "./semver.js";
|
|
4
|
+
import { listCachedVersions, loadVerifiedFromCache, writeVerifiedPackToCache } from "./cache.js";
|
|
5
|
+
import { packVersionDir } from "./local-paths.js";
|
|
6
|
+
function contentPaths(manifest) {
|
|
7
|
+
return [manifest.base.path, ...(manifest.scopes ?? []).map((s) => s.path)];
|
|
8
|
+
}
|
|
9
|
+
async function fetchAndVerify(packType, version, client) {
|
|
10
|
+
const manifest = await client.getManifest(packType, version);
|
|
11
|
+
const contentByPath = new Map();
|
|
12
|
+
for (const relPath of contentPaths(manifest)) {
|
|
13
|
+
contentByPath.set(relPath, await client.getContent(packType, version, relPath));
|
|
14
|
+
}
|
|
15
|
+
const result = verifyPack(manifest, contentByPath);
|
|
16
|
+
if (!result.valid) {
|
|
17
|
+
throw new PackIntegrityError(`Downloaded ${packType} pack ${version} failed verification: ${result.reason}`);
|
|
18
|
+
}
|
|
19
|
+
return { manifest, contentByPath };
|
|
20
|
+
}
|
|
21
|
+
function toResolvedPack(manifest, contentByPath, source, cacheDir) {
|
|
22
|
+
return {
|
|
23
|
+
packId: manifest.pack_id,
|
|
24
|
+
packType: manifest.pack_type,
|
|
25
|
+
version: manifest.version,
|
|
26
|
+
minCliVersion: manifest.min_cli_version,
|
|
27
|
+
source,
|
|
28
|
+
basePath: `${cacheDir}/${manifest.base.path}`,
|
|
29
|
+
baseContent: contentByPath.get(manifest.base.path) ?? "",
|
|
30
|
+
scopes: manifest.scopes ?? [],
|
|
31
|
+
cacheDir,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function resolveCompatiblePack(packType, opts) {
|
|
35
|
+
try {
|
|
36
|
+
const listing = await opts.registryClient.listVersions(packType);
|
|
37
|
+
const compatible = listing.versions
|
|
38
|
+
.filter((v) => satisfiesMinVersion(opts.cliVersion, v.min_cli_version))
|
|
39
|
+
.sort((a, b) => compareVersions(b.version, a.version));
|
|
40
|
+
if (compatible.length === 0) {
|
|
41
|
+
throw new PackUnavailableError(`No ${packType} pack is compatible with @zivis/cli ${opts.cliVersion}. ` +
|
|
42
|
+
`Available versions: ${listing.versions.map((v) => v.version).join(", ") || "(none)"}`);
|
|
43
|
+
}
|
|
44
|
+
const target = compatible[0];
|
|
45
|
+
const cached = await loadVerifiedFromCache(packType, target.version);
|
|
46
|
+
if (cached) {
|
|
47
|
+
return toResolvedPack(cached.manifest, cached.contentByPath, "cache-verified", cached.cacheDir);
|
|
48
|
+
}
|
|
49
|
+
const { manifest, contentByPath } = await fetchAndVerify(packType, target.version, opts.registryClient);
|
|
50
|
+
const cacheDir = await writeVerifiedPackToCache(packType, target.version, manifest, contentByPath);
|
|
51
|
+
return toResolvedPack(manifest, contentByPath, "network", cacheDir);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
if (err instanceof PackIntegrityError || err instanceof PackUnavailableError) {
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
return resolveFromOfflineCache(packType, opts.cliVersion, err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function resolveFromOfflineCache(packType, cliVersion, networkError) {
|
|
61
|
+
const cachedVersions = await listCachedVersions(packType);
|
|
62
|
+
const verified = [];
|
|
63
|
+
for (const version of cachedVersions) {
|
|
64
|
+
const entry = await loadVerifiedFromCache(packType, version);
|
|
65
|
+
if (entry && satisfiesMinVersion(cliVersion, entry.manifest.min_cli_version)) {
|
|
66
|
+
verified.push(entry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (verified.length === 0) {
|
|
70
|
+
const reason = networkError instanceof Error ? networkError.message : String(networkError);
|
|
71
|
+
throw new PackUnavailableError(`Could not reach the ZIVIS pack registry (${reason}), and no previously verified ${packType} ` +
|
|
72
|
+
`pack compatible with @zivis/cli ${cliVersion} is cached at ${packVersionDir(packType, "*")}.`);
|
|
73
|
+
}
|
|
74
|
+
verified.sort((a, b) => compareVersions(b.manifest.version, a.manifest.version));
|
|
75
|
+
const best = verified[0];
|
|
76
|
+
return toResolvedPack(best.manifest, best.contentByPath, "cache-offline", best.cacheDir);
|
|
77
|
+
}
|
|
78
|
+
export async function resolveScope(resolved, scopeId) {
|
|
79
|
+
const scope = resolved.scopes.find((s) => s.id === scopeId);
|
|
80
|
+
if (!scope) {
|
|
81
|
+
throw new UnknownScopeError(scopeId, resolved.scopes);
|
|
82
|
+
}
|
|
83
|
+
const fs = await import("node:fs/promises");
|
|
84
|
+
const content = await fs.readFile(`${resolved.cacheDir}/${scope.path}`, "utf-8");
|
|
85
|
+
return { id: scope.id, label: scope.label, description: scope.description, content };
|
|
86
|
+
}
|
|
87
|
+
export function listAvailableScopes(resolved) {
|
|
88
|
+
return resolved.scopes.map(({ id, label, description }) => ({ id, label, description }));
|
|
89
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ParsedVersion {
|
|
2
|
+
major: number;
|
|
3
|
+
minor: number;
|
|
4
|
+
patch: number;
|
|
5
|
+
prerelease: (string | number)[];
|
|
6
|
+
}
|
|
7
|
+
export declare function parseVersion(version: string): ParsedVersion;
|
|
8
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
9
|
+
export declare function satisfiesMinVersion(cliVersion: string, minCliVersion: string): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
|
|
2
|
+
export function parseVersion(version) {
|
|
3
|
+
const m = SEMVER_RE.exec(version.trim());
|
|
4
|
+
if (!m) {
|
|
5
|
+
throw new Error(`Not a valid semver-like version: "${version}"`);
|
|
6
|
+
}
|
|
7
|
+
const [, major, minor, patch, prerelease] = m;
|
|
8
|
+
return {
|
|
9
|
+
major: Number(major),
|
|
10
|
+
minor: Number(minor),
|
|
11
|
+
patch: Number(patch),
|
|
12
|
+
prerelease: prerelease
|
|
13
|
+
? prerelease.split(".").map((part) => (/^\d+$/.test(part) ? Number(part) : part))
|
|
14
|
+
: [],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function compareIdentifier(a, b) {
|
|
18
|
+
if (typeof a === "number" && typeof b === "number")
|
|
19
|
+
return a - b;
|
|
20
|
+
const as = String(a);
|
|
21
|
+
const bs = String(b);
|
|
22
|
+
if (typeof a === "number")
|
|
23
|
+
return -1;
|
|
24
|
+
if (typeof b === "number")
|
|
25
|
+
return 1;
|
|
26
|
+
return as < bs ? -1 : as > bs ? 1 : 0;
|
|
27
|
+
}
|
|
28
|
+
export function compareVersions(a, b) {
|
|
29
|
+
const pa = parseVersion(a);
|
|
30
|
+
const pb = parseVersion(b);
|
|
31
|
+
if (pa.major !== pb.major)
|
|
32
|
+
return pa.major - pb.major;
|
|
33
|
+
if (pa.minor !== pb.minor)
|
|
34
|
+
return pa.minor - pb.minor;
|
|
35
|
+
if (pa.patch !== pb.patch)
|
|
36
|
+
return pa.patch - pb.patch;
|
|
37
|
+
if (pa.prerelease.length === 0 && pb.prerelease.length === 0)
|
|
38
|
+
return 0;
|
|
39
|
+
if (pa.prerelease.length === 0)
|
|
40
|
+
return 1;
|
|
41
|
+
if (pb.prerelease.length === 0)
|
|
42
|
+
return -1;
|
|
43
|
+
const len = Math.max(pa.prerelease.length, pb.prerelease.length);
|
|
44
|
+
for (let i = 0; i < len; i++) {
|
|
45
|
+
if (i >= pa.prerelease.length)
|
|
46
|
+
return -1;
|
|
47
|
+
if (i >= pb.prerelease.length)
|
|
48
|
+
return 1;
|
|
49
|
+
const cmp = compareIdentifier(pa.prerelease[i], pb.prerelease[i]);
|
|
50
|
+
if (cmp !== 0)
|
|
51
|
+
return cmp;
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
export function satisfiesMinVersion(cliVersion, minCliVersion) {
|
|
56
|
+
return compareVersions(cliVersion, minCliVersion) >= 0;
|
|
57
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PackManifestV1, PackSignature } from "./types.js";
|
|
2
|
+
export declare const DEV_PACK_SIGNING_KEY_ID = "zivis-pack-dev-2026-08";
|
|
3
|
+
export declare function manifestSigningPayload(manifest: Omit<PackManifestV1, "signature">): string;
|
|
4
|
+
export declare function verifyManifestSignature(manifest: PackManifestV1): boolean;
|
|
5
|
+
export declare function signManifestDev(manifest: Omit<PackManifestV1, "signature">, privateKeySpkiB64: string): PackSignature;
|