bun-scan 1.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/client.ts ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Copyright (c) 2025 maloma7. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import type { OSVQuery, OSVVulnerability } from "./schema.js"
7
+ import { OSVResponseSchema, OSVBatchResponseSchema, OSVVulnerabilitySchema } from "./schema.js"
8
+ import { OSV_API, HTTP, PERFORMANCE, getConfig, ENV } from "./constants.js"
9
+ import { withRetry } from "./retry.js"
10
+ import { logger } from "./logger.js"
11
+
12
+ /**
13
+ * OSV API Client
14
+ * Handles all communication with OSV.dev API including batch queries and individual lookups
15
+ */
16
+ export class OSVClient {
17
+ private readonly baseUrl: string
18
+ private readonly timeout: number
19
+ private readonly useBatch: boolean
20
+
21
+ constructor() {
22
+ this.baseUrl = getConfig(ENV.API_BASE_URL, OSV_API.BASE_URL)
23
+ this.timeout = getConfig(ENV.TIMEOUT_MS, OSV_API.TIMEOUT_MS)
24
+ this.useBatch = !getConfig(ENV.DISABLE_BATCH, false)
25
+ }
26
+
27
+ /**
28
+ * Query vulnerabilities for multiple packages
29
+ * Uses batch API when possible for better performance
30
+ */
31
+ async queryVulnerabilities(packages: Bun.Security.Package[]): Promise<OSVVulnerability[]> {
32
+ if (packages.length === 0) {
33
+ return []
34
+ }
35
+
36
+ // Deduplicate packages by name@version
37
+ const uniquePackages = this.deduplicatePackages(packages)
38
+ logger.info(`Scanning ${uniquePackages.length} unique packages (${packages.length} total)`)
39
+
40
+ // Create OSV queries
41
+ const queries = uniquePackages.map((pkg) => ({
42
+ package: {
43
+ name: pkg.name,
44
+ ecosystem: OSV_API.DEFAULT_ECOSYSTEM,
45
+ },
46
+ version: pkg.version,
47
+ }))
48
+
49
+ if (this.useBatch && queries.length > 1) {
50
+ return await this.queryWithBatch(queries)
51
+ } else {
52
+ return await this.queryIndividually(queries)
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Deduplicate packages by name@version to avoid redundant queries
58
+ */
59
+ private deduplicatePackages(packages: Bun.Security.Package[]): Bun.Security.Package[] {
60
+ const packageMap = new Map<string, Bun.Security.Package>()
61
+
62
+ for (const pkg of packages) {
63
+ const key = `${pkg.name}@${pkg.version}`
64
+ if (!packageMap.has(key)) {
65
+ packageMap.set(key, pkg)
66
+ }
67
+ }
68
+
69
+ const uniquePackages = Array.from(packageMap.values())
70
+
71
+ if (uniquePackages.length < packages.length) {
72
+ logger.debug(
73
+ `Deduplicated ${packages.length} packages to ${uniquePackages.length} unique packages`,
74
+ )
75
+ }
76
+
77
+ return uniquePackages
78
+ }
79
+
80
+ /**
81
+ * Use batch API for efficient querying
82
+ * Follows OSV.dev recommended pattern: batch query → individual details
83
+ */
84
+ private async queryWithBatch(queries: OSVQuery[]): Promise<OSVVulnerability[]> {
85
+ const vulnerabilityIds: string[] = []
86
+
87
+ // Process queries in batches
88
+ for (let i = 0; i < queries.length; i += OSV_API.MAX_BATCH_SIZE) {
89
+ const batchQueries = queries.slice(i, i + OSV_API.MAX_BATCH_SIZE)
90
+
91
+ try {
92
+ const batchIds = await this.executeBatchQuery(batchQueries)
93
+ vulnerabilityIds.push(...batchIds)
94
+ } catch (error) {
95
+ logger.error(`Batch query failed for ${batchQueries.length} packages`, {
96
+ error: error instanceof Error ? error.message : String(error),
97
+ startIndex: i,
98
+ })
99
+ // Continue with next batch rather than failing completely
100
+ }
101
+ }
102
+
103
+ // Fetch detailed vulnerability information
104
+ return await this.fetchVulnerabilityDetails(vulnerabilityIds)
105
+ }
106
+
107
+ /**
108
+ * Execute a single batch query
109
+ */
110
+ private async executeBatchQuery(queries: OSVQuery[]): Promise<string[]> {
111
+ const vulnerabilityIds: string[] = []
112
+
113
+ const response = await withRetry(async () => {
114
+ const res = await fetch(`${this.baseUrl}/querybatch`, {
115
+ method: "POST",
116
+ headers: {
117
+ "Content-Type": HTTP.CONTENT_TYPE,
118
+ "User-Agent": HTTP.USER_AGENT,
119
+ },
120
+ body: JSON.stringify({ queries }),
121
+ signal: AbortSignal.timeout(this.timeout),
122
+ })
123
+
124
+ if (!res.ok) {
125
+ throw new Error(`OSV API returned ${res.status}: ${res.statusText}`)
126
+ }
127
+
128
+ return res
129
+ }, `OSV batch query (${queries.length} packages)`)
130
+
131
+ const data = await response.json()
132
+ const parsed = OSVBatchResponseSchema.parse(data)
133
+
134
+ // Extract vulnerability IDs from batch response
135
+ for (const result of parsed.results) {
136
+ if (result.vulns) {
137
+ vulnerabilityIds.push(...result.vulns.map((v) => v.id))
138
+ }
139
+ }
140
+
141
+ const vulnCount = parsed.results.reduce((sum, r) => sum + (r.vulns?.length || 0), 0)
142
+ logger.info(`Batch query found ${vulnCount} vulnerabilities across ${queries.length} packages`)
143
+
144
+ return [...new Set(vulnerabilityIds)] // Deduplicate IDs
145
+ }
146
+
147
+ /**
148
+ * Query packages individually (fallback method)
149
+ */
150
+ private async queryIndividually(queries: OSVQuery[]): Promise<OSVVulnerability[]> {
151
+ const responses = await Promise.allSettled(
152
+ queries.map((query) => this.querySinglePackage(query)),
153
+ )
154
+
155
+ const vulnerabilities: OSVVulnerability[] = []
156
+ let successCount = 0
157
+
158
+ for (const response of responses) {
159
+ if (response.status === "fulfilled") {
160
+ vulnerabilities.push(...response.value)
161
+ successCount++
162
+ }
163
+ }
164
+
165
+ logger.info(`Individual queries completed: ${successCount}/${queries.length} successful`)
166
+ return vulnerabilities
167
+ }
168
+
169
+ /**
170
+ * Query a single package with pagination support
171
+ */
172
+ private async querySinglePackage(query: OSVQuery): Promise<OSVVulnerability[]> {
173
+ const allVulns: OSVVulnerability[] = []
174
+ let currentQuery = { ...query }
175
+
176
+ while (true) {
177
+ try {
178
+ const response = await withRetry(
179
+ async () => {
180
+ const res = await fetch(`${this.baseUrl}/query`, {
181
+ method: "POST",
182
+ headers: {
183
+ "Content-Type": HTTP.CONTENT_TYPE,
184
+ "User-Agent": HTTP.USER_AGENT,
185
+ },
186
+ body: JSON.stringify(currentQuery),
187
+ signal: AbortSignal.timeout(this.timeout),
188
+ })
189
+
190
+ if (!res.ok) {
191
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`)
192
+ }
193
+
194
+ return res
195
+ },
196
+ `OSV query for ${query.package?.name || "unknown"}@${query.version || "unknown"}`,
197
+ )
198
+
199
+ const data = await response.json()
200
+ const parsed = OSVResponseSchema.parse(data)
201
+
202
+ // Add vulnerabilities from this page
203
+ if (parsed.vulns) {
204
+ allVulns.push(...parsed.vulns)
205
+ }
206
+
207
+ // Check for pagination
208
+ if (parsed.next_page_token) {
209
+ currentQuery = { ...query, page_token: parsed.next_page_token }
210
+ } else {
211
+ break // No more pages
212
+ }
213
+ } catch (error) {
214
+ logger.warn(
215
+ `Query failed for ${query.package?.name || "unknown"}@${query.version || "unknown"}`,
216
+ {
217
+ error: error instanceof Error ? error.message : String(error),
218
+ },
219
+ )
220
+ break // Exit pagination loop on error
221
+ }
222
+ }
223
+
224
+ return allVulns
225
+ }
226
+
227
+ /**
228
+ * Fetch detailed vulnerability information by IDs
229
+ */
230
+ private async fetchVulnerabilityDetails(ids: string[]): Promise<OSVVulnerability[]> {
231
+ if (ids.length === 0) return []
232
+
233
+ const uniqueIds = [...new Set(ids)] // Deduplicate requests
234
+ logger.info(`Fetching details for ${uniqueIds.length} vulnerabilities`)
235
+
236
+ // Process in smaller chunks to avoid overwhelming the API
237
+ const chunkSize = PERFORMANCE.MAX_CONCURRENT_DETAILS
238
+ const vulnerabilities: OSVVulnerability[] = []
239
+
240
+ for (let i = 0; i < uniqueIds.length; i += chunkSize) {
241
+ const chunk = uniqueIds.slice(i, i + chunkSize)
242
+ const chunkResults = await Promise.allSettled(
243
+ chunk.map((id) => this.fetchSingleVulnerability(id)),
244
+ )
245
+
246
+ for (const result of chunkResults) {
247
+ if (result.status === "fulfilled" && result.value) {
248
+ vulnerabilities.push(result.value)
249
+ }
250
+ }
251
+ }
252
+
253
+ logger.info(`Retrieved ${vulnerabilities.length}/${uniqueIds.length} vulnerability details`)
254
+ return vulnerabilities
255
+ }
256
+
257
+ /**
258
+ * Fetch a single vulnerability by ID
259
+ */
260
+ private async fetchSingleVulnerability(id: string): Promise<OSVVulnerability | null> {
261
+ try {
262
+ return await withRetry(async () => {
263
+ const response = await fetch(`${this.baseUrl}/vulns/${id}`, {
264
+ headers: {
265
+ "User-Agent": HTTP.USER_AGENT,
266
+ },
267
+ signal: AbortSignal.timeout(this.timeout),
268
+ })
269
+
270
+ if (!response.ok) {
271
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
272
+ }
273
+
274
+ const data = await response.json()
275
+ return OSVVulnerabilitySchema.parse(data)
276
+ }, `Get vulnerability ${id}`)
277
+ } catch (error) {
278
+ logger.warn(`Failed to fetch vulnerability ${id}`, {
279
+ error: error instanceof Error ? error.message : String(error),
280
+ })
281
+ return null
282
+ }
283
+ }
284
+ }
package/src/config.ts ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Copyright (c) 2025 maloma7. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { z } from "zod"
7
+ import { logger } from "./logger.js"
8
+
9
+ /**
10
+ * Schema for package-specific ignore rules
11
+ */
12
+ const IgnorePackageRuleSchema = z.object({
13
+ /** Vulnerability IDs to ignore for this package (CVE-*, GHSA-*) */
14
+ vulnerabilities: z.array(z.string()).optional(),
15
+ /** Ignore until this date (ISO 8601 format) - for temporary ignores */
16
+ until: z.string().optional(),
17
+ /** Reason for ignoring (for documentation) */
18
+ reason: z.string().optional(),
19
+ })
20
+
21
+ /**
22
+ * Schema for the ignore configuration file
23
+ */
24
+ export const IgnoreConfigSchema = z.object({
25
+ /** Vulnerability IDs to ignore globally (CVE-*, GHSA-*) */
26
+ ignore: z.array(z.string()).optional(),
27
+ /** Package-specific ignore rules */
28
+ packages: z.record(z.string(), IgnorePackageRuleSchema).optional(),
29
+ })
30
+
31
+ export type IgnoreConfig = z.infer<typeof IgnoreConfigSchema>
32
+ export type IgnorePackageRule = z.infer<typeof IgnorePackageRuleSchema>
33
+
34
+ /**
35
+ * Default config file names to search for (in order of priority)
36
+ */
37
+ const CONFIG_FILES = [".bun-scan.json", ".bun-scan.config.json"] as const
38
+
39
+ /**
40
+ * Load ignore configuration from the current working directory
41
+ */
42
+ export async function loadIgnoreConfig(): Promise<IgnoreConfig> {
43
+ for (const filename of CONFIG_FILES) {
44
+ const config = await tryLoadConfigFile(filename)
45
+ if (config) {
46
+ return config
47
+ }
48
+ }
49
+
50
+ // No config file found - return empty config
51
+ return {}
52
+ }
53
+
54
+ /**
55
+ * Try to load and parse a config file
56
+ */
57
+ async function tryLoadConfigFile(filename: string): Promise<IgnoreConfig | null> {
58
+ try {
59
+ const file = Bun.file(filename)
60
+ const exists = await file.exists()
61
+
62
+ if (!exists) {
63
+ return null
64
+ }
65
+
66
+ const content = await file.json()
67
+ const parsed = IgnoreConfigSchema.parse(content)
68
+
69
+ logger.info(`Loaded ignore configuration from ${filename}`)
70
+ logIgnoreStats(parsed)
71
+
72
+ return parsed
73
+ } catch (error) {
74
+ if (error instanceof z.ZodError) {
75
+ logger.warn(`Invalid ignore config in ${filename}`, {
76
+ errors: error.issues.map((e) => `${e.path.join(".")}: ${e.message}`),
77
+ })
78
+ } else if (error instanceof SyntaxError) {
79
+ logger.warn(`Failed to parse ${filename} as JSON`, {
80
+ error: error.message,
81
+ })
82
+ }
83
+ // For other errors (file not found, etc.), silently continue
84
+ return null
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Log statistics about the loaded ignore configuration
90
+ */
91
+ function logIgnoreStats(config: IgnoreConfig): void {
92
+ const globalIgnores = config.ignore?.length ?? 0
93
+ const packageRules = Object.keys(config.packages ?? {}).length
94
+
95
+ if (globalIgnores > 0 || packageRules > 0) {
96
+ logger.info(`Ignore rules loaded`, {
97
+ globalIgnores,
98
+ packageRules,
99
+ })
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Check if a vulnerability should be ignored based on the config
105
+ */
106
+ export function shouldIgnoreVulnerability(
107
+ vulnId: string,
108
+ vulnAliases: string[] | undefined,
109
+ packageName: string | undefined,
110
+ config: IgnoreConfig,
111
+ ): { ignored: boolean; reason?: string } {
112
+ // Get all IDs to check (primary ID + aliases)
113
+ const idsToCheck = [vulnId, ...(vulnAliases ?? [])]
114
+
115
+ // Check global ignores
116
+ if (config.ignore) {
117
+ for (const id of idsToCheck) {
118
+ if (config.ignore.includes(id)) {
119
+ return { ignored: true, reason: `globally ignored (${id})` }
120
+ }
121
+ }
122
+ }
123
+
124
+ // Check package-specific ignores
125
+ if (packageName && config.packages?.[packageName]) {
126
+ const rule = config.packages[packageName]
127
+
128
+ // Check if the ignore has expired
129
+ if (rule.until) {
130
+ const untilDate = new Date(rule.until)
131
+ if (untilDate < new Date()) {
132
+ logger.debug(`Ignore rule for ${packageName} expired on ${rule.until}`)
133
+ return { ignored: false }
134
+ }
135
+ }
136
+
137
+ // Check package-specific vulnerability ignores
138
+ if (rule.vulnerabilities) {
139
+ for (const id of idsToCheck) {
140
+ if (rule.vulnerabilities.includes(id)) {
141
+ const reason = rule.reason
142
+ ? `package rule: ${rule.reason}`
143
+ : `ignored for ${packageName} (${id})`
144
+ return { ignored: true, reason }
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ return { ignored: false }
151
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Copyright (c) 2025 maloma7. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ /**
7
+ * Centralized configuration constants for OSV Scanner
8
+ * All magic numbers and configuration values consolidated here
9
+ */
10
+
11
+ import type { FatalSeverity } from "./types.js"
12
+
13
+ /**
14
+ * OSV API Configuration
15
+ */
16
+ export const OSV_API = {
17
+ /** Base URL for OSV API */
18
+ BASE_URL: "https://api.osv.dev/v1",
19
+
20
+ /** Request timeout in milliseconds */
21
+ TIMEOUT_MS: 30_000,
22
+
23
+ /** Maximum packages per batch query */
24
+ MAX_BATCH_SIZE: 1_000,
25
+
26
+ /** Maximum retry attempts for failed requests */
27
+ MAX_RETRY_ATTEMPTS: 2,
28
+
29
+ /** Delay between retry attempts in milliseconds */
30
+ RETRY_DELAY_MS: 1_000,
31
+
32
+ /** Default ecosystem for npm packages */
33
+ DEFAULT_ECOSYSTEM: "npm",
34
+ } as const
35
+
36
+ /**
37
+ * HTTP Configuration
38
+ */
39
+ export const HTTP = {
40
+ /** Content type for API requests */
41
+ CONTENT_TYPE: "application/json",
42
+
43
+ /** User agent for requests */
44
+ USER_AGENT: "bun-osv-scanner/1.0.0",
45
+ } as const
46
+
47
+ /**
48
+ * Security Configuration
49
+ */
50
+ export const SECURITY = {
51
+ /** CVSS score threshold for fatal advisories */
52
+ CVSS_FATAL_THRESHOLD: 7.0,
53
+
54
+ /** Database severities that map to fatal level */
55
+ FATAL_SEVERITIES: ["CRITICAL", "HIGH"] as const satisfies readonly FatalSeverity[],
56
+
57
+ /** Maximum vulnerabilities to process per package */
58
+ MAX_VULNERABILITIES_PER_PACKAGE: 100,
59
+
60
+ /** Maximum length for vulnerability descriptions */
61
+ MAX_DESCRIPTION_LENGTH: 200,
62
+ } as const
63
+
64
+ /**
65
+ * Performance Configuration
66
+ */
67
+ export const PERFORMANCE = {
68
+ /** Enable batch queries for better performance */
69
+ USE_BATCH_QUERIES: true,
70
+
71
+ /** Maximum concurrent vulnerability detail requests */
72
+ MAX_CONCURRENT_DETAILS: 10,
73
+
74
+ /** Maximum response size in bytes (32MB) */
75
+ MAX_RESPONSE_SIZE: 32 * 1024 * 1024,
76
+ } as const
77
+
78
+ /**
79
+ * Environment variable configuration
80
+ */
81
+ export const ENV = {
82
+ /** Log level environment variable */
83
+ LOG_LEVEL: "OSV_LOG_LEVEL",
84
+
85
+ /** Custom API base URL override */
86
+ API_BASE_URL: "OSV_API_BASE_URL",
87
+
88
+ /** Custom timeout override */
89
+ TIMEOUT_MS: "OSV_TIMEOUT_MS",
90
+
91
+ /** Disable batch queries */
92
+ DISABLE_BATCH: "OSV_DISABLE_BATCH",
93
+ } as const
94
+
95
+ /**
96
+ * Get configuration value with environment variable override
97
+ */
98
+ export function getConfig<T>(envVar: string, defaultValue: T, parser?: (value: string) => T): T {
99
+ const envValue = Bun.env[envVar]
100
+ if (!envValue) return defaultValue
101
+
102
+ if (parser) {
103
+ try {
104
+ return parser(envValue)
105
+ } catch {
106
+ return defaultValue
107
+ }
108
+ }
109
+
110
+ // Type-safe parsing for common types
111
+ if (typeof defaultValue === "number") {
112
+ const parsed = Number(envValue)
113
+ return (Number.isNaN(parsed) ? defaultValue : parsed) as T
114
+ }
115
+
116
+ if (typeof defaultValue === "boolean") {
117
+ return (envValue.toLowerCase() === "true") as T
118
+ }
119
+
120
+ return envValue as T
121
+ }
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Copyright (c) 2025 maloma7. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ /// <reference types="bun-types" />
7
+ import "./types.js"
8
+ import { OSVClient } from "./client.js"
9
+ import { VulnerabilityProcessor } from "./processor.js"
10
+ import { logger } from "./logger.js"
11
+ import { loadIgnoreConfig } from "./config.js"
12
+
13
+ /**
14
+ * Bun Security Scanner for OSV.dev vulnerability detection
15
+ * Integrates with Google's OSV database to detect vulnerabilities in npm packages
16
+ */
17
+ export const scanner: Bun.Security.Scanner = {
18
+ version: "1", // This is the version of Bun security scanner implementation. You should keep this set as '1'
19
+
20
+ async scan({ packages }) {
21
+ try {
22
+ logger.info(`Starting OSV scan for ${packages.length} packages`)
23
+
24
+ // Load ignore configuration
25
+ const ignoreConfig = await loadIgnoreConfig()
26
+
27
+ // Initialize components
28
+ const client = new OSVClient()
29
+ const processor = new VulnerabilityProcessor(ignoreConfig)
30
+
31
+ // Fetch vulnerabilities from OSV.dev
32
+ const vulnerabilities = await client.queryVulnerabilities(packages)
33
+
34
+ // Process vulnerabilities into security advisories
35
+ const advisories = processor.processVulnerabilities(vulnerabilities, packages)
36
+
37
+ logger.info(
38
+ `OSV scan completed: ${advisories.length} advisories found for ${packages.length} packages`,
39
+ )
40
+
41
+ return advisories
42
+ } catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error)
44
+ logger.error("OSV scanner encountered an unexpected error", {
45
+ error: message,
46
+ })
47
+
48
+ // Fail-safe: allow installation to proceed on scanner errors
49
+ return []
50
+ }
51
+ },
52
+ }
53
+
54
+ // CLI entry point
55
+ if (import.meta.main) {
56
+ const { runCli } = await import("./cli.js")
57
+ await runCli()
58
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Copyright (c) 2025 maloma7. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ /**
7
+ * Structured logging utilities for OSV Scanner
8
+ * Provides consistent, configurable logging with proper levels and context
9
+ */
10
+
11
+ import { ENV } from "./constants.js"
12
+
13
+ export type LogLevel = "debug" | "info" | "warn" | "error"
14
+ export type LogContext = Record<string, unknown>
15
+
16
+ interface Logger {
17
+ debug(message: string, context?: LogContext): void
18
+ info(message: string, context?: LogContext): void
19
+ warn(message: string, context?: LogContext): void
20
+ error(message: string, context?: LogContext): void
21
+ }
22
+
23
+ class OSVLogger implements Logger {
24
+ private readonly levels = { debug: 0, info: 1, warn: 2, error: 3 }
25
+
26
+ private parseLogLevel(level?: string): LogLevel | null {
27
+ if (!level) return null
28
+ const normalized = level.toLowerCase()
29
+ return ["debug", "info", "warn", "error"].includes(normalized) ? (normalized as LogLevel) : null
30
+ }
31
+
32
+ private get minLevel(): LogLevel {
33
+ return this.parseLogLevel(process.env[ENV.LOG_LEVEL]) || "info"
34
+ }
35
+
36
+ private shouldLog(level: LogLevel): boolean {
37
+ return this.levels[level] >= this.levels[this.minLevel]
38
+ }
39
+
40
+ private safeStringify(obj: unknown): string {
41
+ try {
42
+ return JSON.stringify(obj)
43
+ } catch {
44
+ return "[Circular]"
45
+ }
46
+ }
47
+
48
+ private formatMessage(level: LogLevel, message: string, context?: LogContext): string {
49
+ const timestamp = new Date().toISOString()
50
+ const prefix = `[${timestamp}] OSV-${level.toUpperCase()}:`
51
+ const contextStr = context ? ` ${this.safeStringify(context)}` : ""
52
+ return `${prefix} ${message}${contextStr}`
53
+ }
54
+
55
+ debug(message: string, context?: LogContext): void {
56
+ if (this.shouldLog("debug")) {
57
+ console.debug(this.formatMessage("debug", message, context))
58
+ }
59
+ }
60
+
61
+ info(message: string, context?: LogContext): void {
62
+ if (this.shouldLog("info")) {
63
+ console.info(this.formatMessage("info", message, context))
64
+ }
65
+ }
66
+
67
+ warn(message: string, context?: LogContext): void {
68
+ if (this.shouldLog("warn")) {
69
+ console.warn(this.formatMessage("warn", message, context))
70
+ }
71
+ }
72
+
73
+ error(message: string, context?: LogContext): void {
74
+ if (this.shouldLog("error")) {
75
+ console.error(this.formatMessage("error", message, context))
76
+ }
77
+ }
78
+ }
79
+
80
+ export const logger = new OSVLogger()