@c7-digital/scan 0.0.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.
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ /**
4
+ * Build script for @c7-digital/scan
5
+ *
6
+ * Steps:
7
+ * 1. Parse --splice-version (default: 0.5.10)
8
+ * 2. If bundled spec doesn't exist, run download-spec.ts
9
+ * 3. Run openapi-typescript to generate types
10
+ * 4. Generate sdk-version.ts constant
11
+ * 5. Compile TypeScript (tsc + tsc-alias)
12
+ */
13
+
14
+ import { join, dirname } from "path";
15
+ import { fileURLToPath } from "url";
16
+ import { exec } from "child_process";
17
+ import { promisify } from "util";
18
+ import { writeFile, mkdir, readdir } from "fs/promises";
19
+ import { existsSync } from "fs";
20
+
21
+ const execAsync = promisify(exec);
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+ const projectRoot = join(__dirname, "..");
25
+
26
+ function getSpliceVersion(): string {
27
+ const args = process.argv.slice(2);
28
+ const versionArg = args.find(arg => arg.startsWith("--splice-version="));
29
+ if (versionArg) {
30
+ return versionArg.split("=")[1]!;
31
+ }
32
+ return "0.5.10";
33
+ }
34
+
35
+ async function discoverSpecFile(version?: string): Promise<{ specPath: string; version: string }> {
36
+ const specsDir = join(projectRoot, "specs");
37
+
38
+ if (version) {
39
+ const specFile = `scan_bundled_${version}.yaml`;
40
+ const specPath = join(specsDir, specFile);
41
+
42
+ if (existsSync(specPath)) {
43
+ console.log(`Found bundled spec: ${specFile}`);
44
+ return { specPath, version };
45
+ }
46
+
47
+ // Spec doesn't exist yet — run download script
48
+ console.log(`Bundled spec not found for ${version}, downloading...`);
49
+ try {
50
+ const { stdout, stderr } = await execAsync(
51
+ `pnpm exec tsx "${join(projectRoot, "scripts", "download-spec.ts")}" --splice-version=${version}`,
52
+ { cwd: projectRoot }
53
+ );
54
+ if (stdout) console.log(stdout);
55
+ if (stderr) console.warn(stderr);
56
+ } catch (error: any) {
57
+ console.error("Failed to download spec:", error.stderr || error.message);
58
+ throw error;
59
+ }
60
+
61
+ if (!existsSync(specPath)) {
62
+ throw new Error(`Spec file still not found after download: ${specFile}`);
63
+ }
64
+
65
+ return { specPath, version };
66
+ }
67
+
68
+ // Auto-discover latest
69
+ const files = await readdir(specsDir);
70
+ const bundledSpecs = files
71
+ .filter(f => f.startsWith("scan_bundled_") && f.endsWith(".yaml"))
72
+ .sort()
73
+ .reverse();
74
+
75
+ if (bundledSpecs.length === 0) {
76
+ throw new Error("No bundled spec files found. Run download-spec.ts first or specify --splice-version.");
77
+ }
78
+
79
+ const latestSpec = bundledSpecs[0]!;
80
+ const match = latestSpec.match(/scan_bundled_(.+)\.yaml/);
81
+ const detectedVersion = match ? match[1]! : "unknown";
82
+
83
+ console.log(`Auto-discovered spec: ${latestSpec} (version: ${detectedVersion})`);
84
+ return { specPath: join(specsDir, latestSpec), version: detectedVersion };
85
+ }
86
+
87
+ async function ensureGeneratedDirectory(): Promise<void> {
88
+ const outputDir = join(projectRoot, "src", "generated");
89
+ await mkdir(outputDir, { recursive: true });
90
+ }
91
+
92
+ async function generateTypes(specPath: string): Promise<void> {
93
+ console.log("Generating OpenAPI types...");
94
+ const outputPath = join(projectRoot, "src", "generated", "api.ts");
95
+
96
+ try {
97
+ const { stdout, stderr } = await execAsync(
98
+ `pnpm exec openapi-typescript "${specPath}" --output "${outputPath}"`,
99
+ { cwd: projectRoot }
100
+ );
101
+
102
+ if (stderr && !stderr.includes("Warning")) {
103
+ console.warn("openapi-typescript warnings:", stderr);
104
+ }
105
+ if (stdout) {
106
+ console.log(stdout);
107
+ }
108
+
109
+ console.log("Generated OpenAPI types successfully.");
110
+ } catch (error) {
111
+ console.error("Error generating OpenAPI types:", (error as Error).message);
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ async function generateSpliceVersionFile(version: string): Promise<void> {
117
+ console.log("Generating Splice version constant...");
118
+ const outputDir = join(projectRoot, "src", "generated");
119
+ const outputPath = join(outputDir, "sdk-version.ts");
120
+
121
+ const content = `// Auto-generated file - do not edit manually
122
+ // Generated from Splice version: ${version}
123
+
124
+ export const SPLICE_VERSION = "${version}";
125
+ `;
126
+
127
+ await writeFile(outputPath, content, "utf-8");
128
+ console.log(`Generated Splice version constant: ${version}`);
129
+ }
130
+
131
+ async function compileTypeScript(): Promise<void> {
132
+ console.log("Compiling TypeScript...");
133
+
134
+ try {
135
+ const tscCommand = `pnpm exec tsc -p ${join(projectRoot, "tsconfig.json")}`;
136
+ const aliasCommand = `pnpm exec tsc-alias -p ${join(projectRoot, "tsconfig.json")}`;
137
+
138
+ const { stdout: tscStdout, stderr: tscStderr } = await execAsync(tscCommand);
139
+ if (tscStderr) {
140
+ console.error("TypeScript compiler errors/warnings:");
141
+ console.error(tscStderr);
142
+ }
143
+ if (tscStdout) {
144
+ console.log(tscStdout);
145
+ }
146
+
147
+ const { stdout: aliasStdout, stderr: aliasStderr } = await execAsync(aliasCommand);
148
+ if (aliasStderr) {
149
+ console.warn("tsc-alias warnings:", aliasStderr);
150
+ }
151
+ if (aliasStdout) {
152
+ console.log(aliasStdout);
153
+ }
154
+
155
+ console.log("TypeScript compilation complete.");
156
+ } catch (error: any) {
157
+ console.error("Error compiling TypeScript:");
158
+ if (error.stdout) console.error(error.stdout);
159
+ if (error.stderr) console.error(error.stderr);
160
+ throw error;
161
+ }
162
+ }
163
+
164
+ async function build(): Promise<void> {
165
+ console.log("Starting @c7-digital/scan build...\n");
166
+
167
+ try {
168
+ const spliceVersion = getSpliceVersion();
169
+ const { specPath, version } = await discoverSpecFile(spliceVersion);
170
+
171
+ await ensureGeneratedDirectory();
172
+
173
+ // Generate types and version constant in parallel
174
+ await Promise.all([
175
+ generateTypes(specPath),
176
+ generateSpliceVersionFile(version),
177
+ ]);
178
+
179
+ // Compile TypeScript (depends on generated types)
180
+ await compileTypeScript();
181
+
182
+ console.log(`\nBuild completed successfully for Splice version: ${version}`);
183
+ } catch (error) {
184
+ console.error("Build failed:", (error as Error).message);
185
+ process.exit(1);
186
+ }
187
+ }
188
+
189
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
190
+ build();
191
+ }
192
+
193
+ export { build };
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ /**
4
+ * Downloads Scan API OpenAPI specs from the Splice repo and bundles them
5
+ * into a single resolved YAML file.
6
+ *
7
+ * The raw spec has $ref references to sibling files:
8
+ * - common-external.yaml (health endpoints, error responses)
9
+ * - common-internal.yaml (DSO schemas, validator license schemas)
10
+ *
11
+ * We download all three, then resolve all external $ref references into a
12
+ * single bundled YAML file. Schemas referenced via local #/components/schemas
13
+ * refs in external files are collected into the root document.
14
+ */
15
+
16
+ import { join, dirname, resolve } from "path";
17
+ import { fileURLToPath } from "url";
18
+ import { writeFile, mkdir, rm, readFile } from "fs/promises";
19
+ import { existsSync } from "fs";
20
+ import fetch from "cross-fetch";
21
+ import YAML from "yaml";
22
+
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = dirname(__filename);
25
+ const projectRoot = join(__dirname, "..");
26
+
27
+ const GITHUB_RAW_BASE = "https://raw.githubusercontent.com/hyperledger-labs/splice";
28
+
29
+ interface SpecSource {
30
+ repoPath: string;
31
+ filename: string;
32
+ }
33
+
34
+ const SCAN_SPEC: SpecSource = {
35
+ repoPath: "apps/scan/src/main/openapi",
36
+ filename: "scan.yaml",
37
+ };
38
+
39
+ const COMMON_SPECS: SpecSource[] = [
40
+ {
41
+ repoPath: "apps/common/src/main/openapi",
42
+ filename: "common-external.yaml",
43
+ },
44
+ {
45
+ repoPath: "apps/common/src/main/openapi",
46
+ filename: "common-internal.yaml",
47
+ },
48
+ ];
49
+
50
+ function getSpliceVersion(): string {
51
+ const args = process.argv.slice(2);
52
+ const versionArg = args.find(arg => arg.startsWith("--splice-version="));
53
+ if (versionArg) {
54
+ return versionArg.split("=")[1]!;
55
+ }
56
+ return "0.5.10";
57
+ }
58
+
59
+ async function downloadFile(url: string, destPath: string): Promise<void> {
60
+ console.log(` Downloading: ${url}`);
61
+ const response = await fetch(url);
62
+
63
+ if (!response.ok) {
64
+ throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
65
+ }
66
+
67
+ const content = await response.text();
68
+ await mkdir(dirname(destPath), { recursive: true });
69
+ await writeFile(destPath, content, "utf-8");
70
+ }
71
+
72
+ async function downloadSpecs(version: string, tempDir: string): Promise<string> {
73
+ console.log(`\nDownloading Scan API specs for Splice ${version}...\n`);
74
+
75
+ const scanUrl = `${GITHUB_RAW_BASE}/${version}/${SCAN_SPEC.repoPath}/${SCAN_SPEC.filename}`;
76
+ const scanDest = join(tempDir, SCAN_SPEC.repoPath, SCAN_SPEC.filename);
77
+ await downloadFile(scanUrl, scanDest);
78
+
79
+ for (const spec of COMMON_SPECS) {
80
+ const url = `${GITHUB_RAW_BASE}/${version}/${spec.repoPath}/${spec.filename}`;
81
+ const dest = join(tempDir, spec.repoPath, spec.filename);
82
+ await downloadFile(url, dest);
83
+ }
84
+
85
+ return scanDest;
86
+ }
87
+
88
+ async function parseYamlFile(filePath: string): Promise<any> {
89
+ const content = await readFile(filePath, "utf-8");
90
+ return YAML.parse(content);
91
+ }
92
+
93
+ function parseRef(ref: string, currentFilePath: string): { filePath: string; pointer: string } {
94
+ const hashIndex = ref.indexOf("#");
95
+ if (hashIndex === -1) {
96
+ return { filePath: resolve(dirname(currentFilePath), ref), pointer: "" };
97
+ }
98
+ const fileRef = ref.substring(0, hashIndex);
99
+ const pointer = ref.substring(hashIndex + 1);
100
+ if (!fileRef) {
101
+ return { filePath: currentFilePath, pointer };
102
+ }
103
+ return { filePath: resolve(dirname(currentFilePath), fileRef), pointer };
104
+ }
105
+
106
+ function resolvePointer(obj: any, pointer: string): any {
107
+ if (!pointer || pointer === "/") return obj;
108
+ const parts = pointer.split("/").filter(Boolean);
109
+ let current = obj;
110
+ for (const part of parts) {
111
+ const decoded = part.replace(/~1/g, "/").replace(/~0/g, "~");
112
+ if (current === undefined || current === null) {
113
+ throw new Error(`Cannot resolve pointer "${pointer}" — reached undefined at "${decoded}"`);
114
+ }
115
+ current = current[decoded];
116
+ }
117
+ return current;
118
+ }
119
+
120
+ const fileCache = new Map<string, any>();
121
+
122
+ async function getFileContent(filePath: string): Promise<any> {
123
+ if (fileCache.has(filePath)) {
124
+ return fileCache.get(filePath);
125
+ }
126
+ const content = await parseYamlFile(filePath);
127
+ fileCache.set(filePath, content);
128
+ return content;
129
+ }
130
+
131
+ /**
132
+ * Track schemas to collect from external files.
133
+ * Maps schema name to its fully-resolved schema object.
134
+ */
135
+ const collectedSchemas = new Map<string, any>();
136
+
137
+ /**
138
+ * Scan an object for local $ref patterns like "#/components/schemas/Foo"
139
+ */
140
+ function findLocalSchemaRefs(obj: any): Set<string> {
141
+ const refs = new Set<string>();
142
+ const seen = new Set<any>();
143
+
144
+ function walk(node: any): void {
145
+ if (node === null || node === undefined || typeof node !== "object") return;
146
+ if (seen.has(node)) return;
147
+ seen.add(node);
148
+
149
+ if (Array.isArray(node)) {
150
+ for (const item of node) walk(item);
151
+ return;
152
+ }
153
+
154
+ if ("$ref" in node && typeof node["$ref"] === "string") {
155
+ const ref: string = node["$ref"];
156
+ const match = ref.match(/^#\/components\/schemas\/(.+)$/);
157
+ if (match) {
158
+ refs.add(match[1]!);
159
+ }
160
+ return;
161
+ }
162
+
163
+ for (const value of Object.values(node)) {
164
+ walk(value);
165
+ }
166
+ }
167
+
168
+ walk(obj);
169
+ return refs;
170
+ }
171
+
172
+ /**
173
+ * Fully resolve a schema from an external file, including all its external $refs.
174
+ * The resulting object only contains local #/ refs (pointing to our root schemas).
175
+ */
176
+ async function resolveSchemaDeep(
177
+ schema: any,
178
+ sourceFilePath: string,
179
+ rootSchemas: Record<string, any>,
180
+ processingSet: Set<string>,
181
+ ): Promise<any> {
182
+ if (schema === null || schema === undefined) return schema;
183
+ if (typeof schema !== "object") return schema;
184
+
185
+ if (Array.isArray(schema)) {
186
+ const resolved = [];
187
+ for (const item of schema) {
188
+ resolved.push(await resolveSchemaDeep(item, sourceFilePath, rootSchemas, processingSet));
189
+ }
190
+ return resolved;
191
+ }
192
+
193
+ if ("$ref" in schema && typeof schema["$ref"] === "string") {
194
+ const refString: string = schema["$ref"];
195
+
196
+ // Local ref to #/components/schemas/X — check if we need to collect it
197
+ if (refString.startsWith("#/components/schemas/")) {
198
+ const schemaName = refString.substring("#/components/schemas/".length);
199
+
200
+ // If not in root schemas and not already collected, collect it
201
+ if (!rootSchemas[schemaName] && !collectedSchemas.has(schemaName) && !processingSet.has(schemaName)) {
202
+ const fileContent = await getFileContent(sourceFilePath);
203
+ const externalSchema = fileContent?.components?.schemas?.[schemaName];
204
+ if (externalSchema) {
205
+ processingSet.add(schemaName);
206
+ const resolved = await resolveSchemaDeep(externalSchema, sourceFilePath, rootSchemas, processingSet);
207
+ collectedSchemas.set(schemaName, resolved);
208
+ console.log(` Collected schema: ${schemaName}`);
209
+ }
210
+ }
211
+ // Keep as local ref
212
+ return schema;
213
+ }
214
+
215
+ // Other local refs — keep as-is
216
+ if (refString.startsWith("#")) {
217
+ return schema;
218
+ }
219
+
220
+ // External ref — resolve it
221
+ const { filePath, pointer } = parseRef(refString, sourceFilePath);
222
+ const fileContent = await getFileContent(filePath);
223
+ const resolved = resolvePointer(fileContent, pointer);
224
+
225
+ // Recursively resolve
226
+ return resolveSchemaDeep(resolved, filePath, rootSchemas, processingSet);
227
+ }
228
+
229
+ const result: any = {};
230
+ for (const [key, value] of Object.entries(schema)) {
231
+ result[key] = await resolveSchemaDeep(value, sourceFilePath, rootSchemas, processingSet);
232
+ }
233
+ return result;
234
+ }
235
+
236
+ /**
237
+ * Recursively resolve all external $ref references in the root document.
238
+ * When an external ref is inlined, any local schema refs it contains
239
+ * are collected into collectedSchemas.
240
+ */
241
+ async function resolveRefs(
242
+ obj: any,
243
+ currentFilePath: string,
244
+ rootFilePath: string,
245
+ rootSchemas: Record<string, any>,
246
+ ): Promise<any> {
247
+ if (obj === null || obj === undefined) return obj;
248
+ if (typeof obj !== "object") return obj;
249
+
250
+ if (Array.isArray(obj)) {
251
+ const resolved = [];
252
+ for (const item of obj) {
253
+ resolved.push(await resolveRefs(item, currentFilePath, rootFilePath, rootSchemas));
254
+ }
255
+ return resolved;
256
+ }
257
+
258
+ if ("$ref" in obj && typeof obj["$ref"] === "string") {
259
+ const refString: string = obj["$ref"];
260
+
261
+ // Local refs — keep as-is
262
+ if (refString.startsWith("#")) {
263
+ return obj;
264
+ }
265
+
266
+ // External ref — fully resolve it (including nested external refs)
267
+ const { filePath, pointer } = parseRef(refString, currentFilePath);
268
+ const fileContent = await getFileContent(filePath);
269
+ const resolved = resolvePointer(fileContent, pointer);
270
+
271
+ // Deep-resolve: resolve all external refs and collect schemas
272
+ return resolveSchemaDeep(resolved, filePath, rootSchemas, new Set());
273
+ }
274
+
275
+ const result: any = {};
276
+ for (const [key, value] of Object.entries(obj)) {
277
+ result[key] = await resolveRefs(value, currentFilePath, rootFilePath, rootSchemas);
278
+ }
279
+ return result;
280
+ }
281
+
282
+ async function bundleSpec(scanSpecPath: string, outputPath: string): Promise<void> {
283
+ console.log(`\nBundling spec...`);
284
+
285
+ const scanDoc = await parseYamlFile(scanSpecPath);
286
+
287
+ // Get root schemas so we know what's already defined
288
+ const rootSchemas = scanDoc?.components?.schemas || {};
289
+
290
+ // Resolve all external $refs (this also collects external schemas)
291
+ const bundled = await resolveRefs(scanDoc, scanSpecPath, scanSpecPath, rootSchemas);
292
+
293
+ // Merge collected external schemas into the bundled document
294
+ if (collectedSchemas.size > 0) {
295
+ if (!bundled.components) bundled.components = {};
296
+ if (!bundled.components.schemas) bundled.components.schemas = {};
297
+
298
+ for (const [name, schema] of collectedSchemas) {
299
+ if (!bundled.components.schemas[name]) {
300
+ bundled.components.schemas[name] = schema;
301
+ }
302
+ }
303
+ }
304
+
305
+ const yamlOutput = YAML.stringify(bundled, { lineWidth: 0 });
306
+ await writeFile(outputPath, yamlOutput, "utf-8");
307
+
308
+ console.log(`\nBundled spec written to ${outputPath}`);
309
+ }
310
+
311
+ async function main(): Promise<void> {
312
+ const version = getSpliceVersion();
313
+ const outputPath = join(projectRoot, "specs", `scan_bundled_${version}.yaml`);
314
+
315
+ if (existsSync(outputPath)) {
316
+ console.log(`Bundled spec already exists: ${outputPath}`);
317
+ console.log("Delete it first if you want to re-download.");
318
+ return;
319
+ }
320
+
321
+ const tempDir = join(projectRoot, ".tmp-splice-specs");
322
+
323
+ try {
324
+ if (existsSync(tempDir)) {
325
+ await rm(tempDir, { recursive: true });
326
+ }
327
+ await mkdir(tempDir, { recursive: true });
328
+
329
+ const scanSpecPath = await downloadSpecs(version, tempDir);
330
+ await mkdir(dirname(outputPath), { recursive: true });
331
+ await bundleSpec(scanSpecPath, outputPath);
332
+
333
+ console.log(`\nDone! Bundled spec: ${outputPath}`);
334
+ } finally {
335
+ if (existsSync(tempDir)) {
336
+ await rm(tempDir, { recursive: true });
337
+ }
338
+ }
339
+ }
340
+
341
+ main().catch(error => {
342
+ console.error("Error:", error.message);
343
+ process.exit(1);
344
+ });