@scanoss_test/sdk 2.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.
package/src/binary.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Binary resolution for SCANOSS daemon.
3
+ *
4
+ * The binary is provided by platform-specific packages (@scanoss_test/linux-x64, etc.).
5
+ * npm will automatically install the correct one based on the user's OS and CPU.
6
+ */
7
+
8
+ import { existsSync } from 'fs';
9
+ import { execSync } from 'child_process';
10
+
11
+ const PLATFORMS = [
12
+ '@scanoss_test/linux-x64',
13
+ '@scanoss_test/linux-arm64',
14
+ '@scanoss_test/darwin-x64',
15
+ '@scanoss_test/darwin-arm64',
16
+ '@scanoss_test/win32-x64',
17
+ ] as const;
18
+
19
+ /**
20
+ * Get the path to the SCANOSS binary.
21
+ *
22
+ * @returns Path to the scanoss binary
23
+ * @throws Error if no binary package is installed
24
+ */
25
+ export function getBinaryPath(): string {
26
+ // Try each platform package - npm only installs the matching one
27
+ for (const platform of PLATFORMS) {
28
+ try {
29
+ const binaryPath = require(platform);
30
+ if (existsSync(binaryPath)) {
31
+ return binaryPath;
32
+ }
33
+ } catch {
34
+ // Package not installed for this platform, try next
35
+ }
36
+ }
37
+
38
+ // Fallback: check if binary is in PATH
39
+ try {
40
+ const pathBinary = execSync('which scanoss', { encoding: 'utf-8' }).trim();
41
+ if (pathBinary && existsSync(pathBinary)) {
42
+ return pathBinary;
43
+ }
44
+ } catch {
45
+ // Not in PATH
46
+ }
47
+
48
+ throw new Error(
49
+ 'SCANOSS binary not found. The platform-specific package should be installed automatically.\n' +
50
+ 'If not, install it manually:\n' +
51
+ ' npm install @scanoss_test/linux-x64 # Linux x64\n' +
52
+ ' npm install @scanoss_test/linux-arm64 # Linux ARM64\n' +
53
+ ' npm install @scanoss_test/darwin-x64 # macOS Intel\n' +
54
+ ' npm install @scanoss_test/darwin-arm64 # macOS Apple Silicon\n' +
55
+ ' npm install @scanoss_test/win32-x64 # Windows x64'
56
+ );
57
+ }
package/src/client.ts ADDED
@@ -0,0 +1,327 @@
1
+ /**
2
+ * SCANOSS SDK client using protobuf binary protocol
3
+ */
4
+
5
+ import { spawn, ChildProcess } from 'child_process';
6
+ import { create, toBinary, fromBinary } from '@bufbuild/protobuf';
7
+ import { getBinaryPath } from './binary';
8
+ import { ScanossError } from './errors';
9
+ import {
10
+ RequestSchema,
11
+ ResponseSchema,
12
+ ScanCommandSchema,
13
+ FilterFilesCommandSchema,
14
+ FingerprintCommandSchema,
15
+ GenerateSbomCommandSchema,
16
+ VersionCommandSchema,
17
+ } from './gen/scanoss/v1/commands_pb';
18
+ import type {
19
+ Request,
20
+ Response,
21
+ ScanResult,
22
+ FilterFilesResult,
23
+ FingerprintResult,
24
+ GenerateSbomResult,
25
+ VersionResult,
26
+ } from './gen/scanoss/v1/commands_pb';
27
+ import { OutputFormat, ScanType } from './gen/scanoss/v1/enums_pb';
28
+ import type {
29
+ ScanParams,
30
+ FilterFilesParams,
31
+ FingerprintParams,
32
+ GenerateSbomParams,
33
+ } from './types';
34
+
35
+ /** Options for creating a Scanoss instance */
36
+ export interface ScanossOptions {
37
+ /** Path to the scanoss binary. If not provided, will be auto-detected. */
38
+ binaryPath?: string;
39
+ }
40
+
41
+ interface PendingRequest {
42
+ resolve: (value: Response) => void;
43
+ reject: (reason: Error) => void;
44
+ }
45
+
46
+ /**
47
+ * SCANOSS SDK client.
48
+ *
49
+ * This client communicates with the SCANOSS daemon process via stdin/stdout
50
+ * using length-prefixed protobuf binary protocol.
51
+ *
52
+ * Wire format:
53
+ * ```
54
+ * ┌──────────────────┬─────────────────────────┐
55
+ * │ 4 bytes (BE) │ N bytes │
56
+ * │ message length │ protobuf binary data │
57
+ * └──────────────────┴─────────────────────────┘
58
+ * ```
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const scanoss = new Scanoss();
63
+ * const result = await scanoss.scan({ path: '/path/to/project' });
64
+ * console.log(result);
65
+ * await scanoss.close();
66
+ * ```
67
+ */
68
+ export class Scanoss {
69
+ private process: ChildProcess | null = null;
70
+ private requestId = 0;
71
+ private pending = new Map<number, PendingRequest>();
72
+ private binaryPath: string;
73
+ private closed = false;
74
+ private buffer: Buffer = Buffer.alloc(0);
75
+ private expectedLength: number | null = null;
76
+
77
+ constructor(options: ScanossOptions = {}) {
78
+ this.binaryPath = options.binaryPath ?? getBinaryPath();
79
+ this.start();
80
+ }
81
+
82
+ private start(): void {
83
+ this.process = spawn(this.binaryPath, ['daemon'], {
84
+ stdio: ['pipe', 'pipe', 'pipe'],
85
+ });
86
+
87
+ // Handle binary data from stdout
88
+ this.process.stdout!.on('data', (chunk: Buffer) => {
89
+ this.buffer = Buffer.concat([this.buffer, chunk]);
90
+ this.processBuffer();
91
+ });
92
+
93
+ this.process.on('error', (err) => {
94
+ console.error('Daemon process error:', err);
95
+ this.rejectAllPending(err);
96
+ });
97
+
98
+ this.process.on('exit', (code) => {
99
+ if (!this.closed) {
100
+ const err = new Error(`Daemon process exited unexpectedly with code ${code}`);
101
+ this.rejectAllPending(err);
102
+ }
103
+ });
104
+ }
105
+
106
+ private processBuffer(): void {
107
+ while (true) {
108
+ // Read length prefix if we don't have it
109
+ if (this.expectedLength === null) {
110
+ if (this.buffer.length < 4) {
111
+ return; // Need more data
112
+ }
113
+ this.expectedLength = this.buffer.readUInt32BE(0);
114
+ this.buffer = this.buffer.subarray(4);
115
+ }
116
+
117
+ // Read message if we have enough data
118
+ if (this.buffer.length < this.expectedLength) {
119
+ return; // Need more data
120
+ }
121
+
122
+ const messageData = this.buffer.subarray(0, this.expectedLength);
123
+ this.buffer = this.buffer.subarray(this.expectedLength);
124
+ this.expectedLength = null;
125
+
126
+ // Parse response
127
+ try {
128
+ const response = fromBinary(ResponseSchema, messageData);
129
+ const pending = this.pending.get(response.id);
130
+ if (pending) {
131
+ if (response.result.case === 'error') {
132
+ pending.reject(new ScanossError(
133
+ response.result.value.code,
134
+ response.result.value.message
135
+ ));
136
+ } else {
137
+ pending.resolve(response);
138
+ }
139
+ this.pending.delete(response.id);
140
+ }
141
+ } catch (err) {
142
+ console.error('Failed to parse daemon response:', err);
143
+ }
144
+ }
145
+ }
146
+
147
+ private rejectAllPending(err: Error): void {
148
+ for (const pending of this.pending.values()) {
149
+ pending.reject(err);
150
+ }
151
+ this.pending.clear();
152
+ }
153
+
154
+ private send(request: Request): Promise<Response> {
155
+ return new Promise((resolve, reject) => {
156
+ if (!this.process || this.closed) {
157
+ reject(new Error('Daemon process is not running'));
158
+ return;
159
+ }
160
+
161
+ this.pending.set(request.id, { resolve, reject });
162
+
163
+ // Serialize to binary
164
+ const data = toBinary(RequestSchema, request);
165
+
166
+ // Write length prefix (4 bytes, big-endian)
167
+ const lengthBuf = Buffer.alloc(4);
168
+ lengthBuf.writeUInt32BE(data.length);
169
+
170
+ this.process.stdin!.write(lengthBuf);
171
+ this.process.stdin!.write(data);
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Scan files for open source matches.
177
+ */
178
+ async scan(params: ScanParams): Promise<ScanResult> {
179
+ const id = ++this.requestId;
180
+
181
+ const scanCmd = create(ScanCommandSchema, {
182
+ path: params.path,
183
+ format: formatToEnum(params.format ?? 'json'),
184
+ scanType: scanTypeToEnum(params.scanType ?? 'identify'),
185
+ include: params.include ?? [],
186
+ exclude: params.exclude ?? [],
187
+ apiUrl: params.apiUrl,
188
+ apiKey: params.apiKey,
189
+ });
190
+
191
+ const request = create(RequestSchema, {
192
+ id,
193
+ command: { case: 'scan', value: scanCmd },
194
+ });
195
+
196
+ const response = await this.send(request);
197
+ if (response.result.case !== 'scan') {
198
+ throw new Error('Unexpected response type');
199
+ }
200
+ return response.result.value;
201
+ }
202
+
203
+ /**
204
+ * Filter files in a directory.
205
+ */
206
+ async filterFiles(params: FilterFilesParams): Promise<FilterFilesResult> {
207
+ const id = ++this.requestId;
208
+
209
+ const cmd = create(FilterFilesCommandSchema, {
210
+ path: params.path,
211
+ include: params.include ?? [],
212
+ exclude: params.exclude ?? [],
213
+ skipHidden: params.skipHidden ?? true,
214
+ });
215
+
216
+ const request = create(RequestSchema, {
217
+ id,
218
+ command: { case: 'filterFiles', value: cmd },
219
+ });
220
+
221
+ const response = await this.send(request);
222
+ if (response.result.case !== 'filterFiles') {
223
+ throw new Error('Unexpected response type');
224
+ }
225
+ return response.result.value;
226
+ }
227
+
228
+ /**
229
+ * Generate WFP fingerprints for files.
230
+ */
231
+ async fingerprint(params: FingerprintParams): Promise<FingerprintResult> {
232
+ const id = ++this.requestId;
233
+
234
+ const cmd = create(FingerprintCommandSchema, {
235
+ path: params.path,
236
+ include: params.include ?? [],
237
+ exclude: params.exclude ?? [],
238
+ });
239
+
240
+ const request = create(RequestSchema, {
241
+ id,
242
+ command: { case: 'fingerprint', value: cmd },
243
+ });
244
+
245
+ const response = await this.send(request);
246
+ if (response.result.case !== 'fingerprint') {
247
+ throw new Error('Unexpected response type');
248
+ }
249
+ return response.result.value;
250
+ }
251
+
252
+ /**
253
+ * Generate SBOM from scan results.
254
+ */
255
+ async generateSbom(params: GenerateSbomParams): Promise<GenerateSbomResult> {
256
+ const id = ++this.requestId;
257
+
258
+ const cmd = create(GenerateSbomCommandSchema, {
259
+ inputPath: params.path,
260
+ format: formatToEnum(params.format ?? 'spdx'),
261
+ });
262
+
263
+ const request = create(RequestSchema, {
264
+ id,
265
+ command: { case: 'generateSbom', value: cmd },
266
+ });
267
+
268
+ const response = await this.send(request);
269
+ if (response.result.case !== 'generateSbom') {
270
+ throw new Error('Unexpected response type');
271
+ }
272
+ return response.result.value;
273
+ }
274
+
275
+ /**
276
+ * Get version information.
277
+ */
278
+ async version(): Promise<VersionResult> {
279
+ const id = ++this.requestId;
280
+
281
+ const cmd = create(VersionCommandSchema, {});
282
+
283
+ const request = create(RequestSchema, {
284
+ id,
285
+ command: { case: 'version', value: cmd },
286
+ });
287
+
288
+ const response = await this.send(request);
289
+ if (response.result.case !== 'version') {
290
+ throw new Error('Unexpected response type');
291
+ }
292
+ return response.result.value;
293
+ }
294
+
295
+ /**
296
+ * Close the daemon process.
297
+ */
298
+ async close(): Promise<void> {
299
+ this.closed = true;
300
+
301
+ if (this.process) {
302
+ this.process.kill();
303
+ this.process = null;
304
+ }
305
+
306
+ this.pending.clear();
307
+ }
308
+ }
309
+
310
+ function formatToEnum(format: string): OutputFormat {
311
+ const formats: Record<string, OutputFormat> = {
312
+ json: OutputFormat.JSON,
313
+ spdx: OutputFormat.SPDX,
314
+ cyclonedx: OutputFormat.CYCLONEDX,
315
+ csv: OutputFormat.CSV,
316
+ wfp: OutputFormat.WFP,
317
+ };
318
+ return formats[format.toLowerCase()] ?? OutputFormat.JSON;
319
+ }
320
+
321
+ function scanTypeToEnum(scanType: string): ScanType {
322
+ const types: Record<string, ScanType> = {
323
+ identify: ScanType.IDENTIFY,
324
+ blacklist: ScanType.BLACKLIST,
325
+ };
326
+ return types[scanType.toLowerCase()] ?? ScanType.IDENTIFY;
327
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Error types for SCANOSS SDK
3
+ */
4
+
5
+ /**
6
+ * Error thrown by SCANOSS operations
7
+ */
8
+ export class ScanossError extends Error {
9
+ /** Error code from the daemon */
10
+ readonly code: number;
11
+
12
+ constructor(code: number, message: string) {
13
+ super(`[${code}] ${message}`);
14
+ this.name = 'ScanossError';
15
+ this.code = code;
16
+
17
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
18
+ if (Error.captureStackTrace) {
19
+ Error.captureStackTrace(this, ScanossError);
20
+ }
21
+ }
22
+ }
File without changes