@bloodhound-rs/core 0.0.0-stage → 0.1.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @bloodhound-rs/core
2
+
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - e814af8: Distribute native binaries as separate optional packages selected by operating system, CPU architecture, and Linux libc. Keep the core package limited to the generated loader, type declarations, and schemas.
8
+
9
+ ## 0.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 4c754ee: Release the Rust resource scanning SDK under the MIT license with native binaries for Linux, macOS, and Windows. Fix release artifact extraction on Windows.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Justinidlerz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,184 @@
1
- # Temporary Holding Version
1
+ # @bloodhound-rs/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Rust SDK for static analysis of browser resources. Rust exports the public methods directly, and napi-rs generates the npm loader and method declarations.
4
+
5
+ | Component | Implementation |
6
+ | ---------------------------------------------------------------------------- | --------------------------- |
7
+ | JavaScript parsing, scopes, aliases, and bounded evaluation | Rust + Oxc |
8
+ | HTML extraction and CSS parsing | Rust + html5gum / cssparser |
9
+ | Fetching, import maps, recursive queues, budgets, cancellation, and recovery | Rust |
10
+ | Global indexes, report diffs, reports, JSONL, and schema validation | Rust |
11
+ | npm loader and method declarations | Generated by napi-rs |
12
+
13
+ Results describe static accesses and potential issues. They do not prove that code executed or modified runtime state. The scanner never executes the code it analyzes.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pnpm add @bloodhound-rs/core
19
+ ```
20
+
21
+ The core package contains the generated loader, type declarations, and schemas. The package manager selects a native binary through optional dependencies using the operating system, CPU architecture, and Linux libc. Keep optional dependencies enabled.
22
+
23
+ | Platform | Binary package |
24
+ | ------------------- | ------------------------------------- |
25
+ | macOS Apple Silicon | `@bloodhound-rs/core-darwin-arm64` |
26
+ | macOS Intel | `@bloodhound-rs/core-darwin-x64` |
27
+ | Linux glibc ARM64 | `@bloodhound-rs/core-linux-arm64-gnu` |
28
+ | Linux glibc x64 | `@bloodhound-rs/core-linux-x64-gnu` |
29
+ | Windows x64 | `@bloodhound-rs/core-win32-x64-msvc` |
30
+
31
+ The loader uses the architecture of the running Node.js process. Linux musl is not supported. Install dependencies on the target system instead of copying `node_modules` between platforms.
32
+
33
+ ## Build
34
+
35
+ Requires Node.js 22+, pnpm 10.20.0, Rust 1.97+, and the platform's C/C++ build tools.
36
+
37
+ ```sh
38
+ pnpm install --frozen-lockfile
39
+ pnpm build
40
+ pnpm typecheck
41
+ cargo check --locked
42
+ ```
43
+
44
+ `napi build` generates `dist/index.js`, `dist/index.d.ts`, and a platform-specific `.node` file from the exports in `native/src/lib.rs`. `src/types.ts` defines data structures such as reports and emits only `dist/types.d.ts`, with no runtime code. Loading fails if the native binary is missing.
45
+
46
+ ## Local examples
47
+
48
+ Run from the repository root:
49
+
50
+ ```sh
51
+ pnpm build
52
+ pnpm example
53
+ pnpm example:html
54
+ ```
55
+
56
+ | Command | Example | Report |
57
+ | ------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
58
+ | `pnpm example` | Scan JavaScript and query global reads, writes, and potential conflicts | `outputs/example/report.json` |
59
+ | `pnpm example:html` | Discover the HTML base URL, import map, inline and external scripts, and styles, then recursively scan module dependencies | `outputs/example-html/report.json` |
60
+
61
+ The examples import the SDK by package name and read local source files without executing them or making network requests. After building, either example can be run independently.
62
+
63
+ ## SDK
64
+
65
+ ```ts
66
+ import { scanJs, scanHtml, scanResource, GlobalIndex } from '@bloodhound-rs/core';
67
+
68
+ const report = await scanJs('./dist/app.js', {
69
+ scriptMode: 'classic',
70
+ executionEnvironment: 'window',
71
+ dependencyMode: 'recursive',
72
+ scopeKey: 'release-page',
73
+ versions: { scmId: 'artifact-revision' },
74
+ jsonlPath: './scan.jsonl',
75
+ });
76
+
77
+ const page = await scanHtml('https://example.com/page.html');
78
+
79
+ const single = await scanResource(
80
+ {
81
+ kind: 'js',
82
+ source: {
83
+ type: 'content',
84
+ resourceUrl: 'https://example.com/app.js',
85
+ content: 'globalThis.sharedConfig = {};',
86
+ },
87
+ },
88
+ { scriptMode: 'module', executionEnvironment: 'window' },
89
+ );
90
+
91
+ const index = new GlobalIndex(report);
92
+ for (const target of index.findTargets(['sharedConfig'], 'global-object')) {
93
+ console.log(index.sources(target.targetId));
94
+ console.log(index.entryPaths(target.targetId, { limit: 20 }));
95
+ }
96
+ console.log(index.conflicts());
97
+ ```
98
+
99
+ `scan(entries, options)` accepts multiple entry points in one analysis. `scanResource` scans only the specified resource while still recording dependency edges. String inputs represent file paths or HTTP(S) / file URLs. Use `type: 'content'` for source text. Scan CSS with `scanResource({ kind: 'css', source })` or `scan`.
100
+
101
+ `defaults()` returns a plain object containing the default budgets and version information.
102
+
103
+ For JavaScript entry points, provide `scriptMode` and `executionEnvironment` explicitly. Missing values remain `unknown` and produce coverage gaps. HTML derives script modes from element attributes and defaults to the Window environment. Workers and iframes use separate scopes.
104
+
105
+ ## HTML discovery and optional context
106
+
107
+ A typical HTML scan needs only a file path or URL:
108
+
109
+ ```ts
110
+ const report = await scanHtml('./page.html');
111
+ ```
112
+
113
+ The scanner reads `<base href>`, `<script type="importmap">`, script `src` and `type` attributes, stylesheet links, and inline content, then follows module dependencies. Import maps and resource lists already present in the HTML need no additional arguments. The scanner does not execute scripts or discover page state created only at runtime.
114
+
115
+ Use the following options when the input lacks page context or resources need to be loaded from local files:
116
+
117
+ | Option | Purpose |
118
+ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
119
+ | `importMap` / `importMapBaseUrl` | Supply the original page's module mappings and resolution base when scanning JavaScript separately |
120
+ | `resourceUrl` | Supply the original online URL when scanning an offline file |
121
+ | `baseUrl` | Supply a document base when the input lacks one. A valid HTML `<base>` is read automatically |
122
+ | `resourceMappings` | Map resolved resource URLs to local files or source text, such as build artifacts that have not been deployed. This is separate from the HTML import map |
123
+
124
+ ## Offline pages and local resource mappings
125
+
126
+ `resourceUrl` is the logical resource address, and `baseUrl` is the document base. External module dependencies resolve against the module URL, while inline module dependencies resolve against the document base. Browser module resolution does not search Node.js package directories or infer file extensions.
127
+
128
+ ```ts
129
+ const report = await scanHtml('./page.html', {
130
+ resourceUrl: 'https://example.com/page.html',
131
+ resourceMappings: {
132
+ 'https://example.com/assets/app.js': {
133
+ type: 'file',
134
+ path: './artifacts/app.js',
135
+ },
136
+ },
137
+ access: { fileRoots: ['./artifacts', '.'], hosts: ['example.com'] },
138
+ });
139
+ ```
140
+
141
+ HTTP fetching uses Rust's `reqwest`. `requestHeaders` groups headers by destination `host[:port]`. After a redirect, the scanner checks access permissions again and selects headers for the new host. Use `proxy` to configure a proxy. Authentication settings are excluded from reports and checkpoint configuration and must be supplied again when resuming.
142
+
143
+ ## Output and recovery
144
+
145
+ | API or option | Output |
146
+ | ---------------------------------------------------- | --------------------------------------------------------------------------------------------- |
147
+ | `Report` | resources, analysisInstances, references, globalTargets, globalAccesses, findings, unresolved |
148
+ | `jsonlPath` | JSONL written by Rust as resources are processed |
149
+ | `reportPath` | A JSON report written by Rust |
150
+ | `indexArtifact(report)` | A standalone versioned index |
151
+ | `adaptReport(report, 'build' / 'page', metadata)` | A report adapted for build or page integrations |
152
+ | `diffReports(before, after)` | Added and removed items, plus changes to items that can be matched uniquely |
153
+ | `runtimeCandidates(report, evidence)` | Static candidate sources, without treating them as confirmed runtime behavior |
154
+ | `checkpointPath` / `resumeScan(checkpoint, options)` | Save completed records and the pending queue, then fetch and validate inputs again on resume |
155
+
156
+ JSONL records update entities by ID. A reference is emitted again when its target analysis instance completes. `resetAnalysis` indicates an updated cross-module summary: consumers first remove that instance's previous accesses, findings, unresolved items, and outgoing edges, then accept the new records. The final `summary` records scan status and coverage statistics. A file without a `summary` indicates that the scan did not finish normally.
157
+
158
+ Use `AbortSignal` for cancellation. Rust checks the cancellation flag and stops enqueueing resources, reading downloads, and starting further analysis. Blocking network calls are bounded by `resourceTimeoutMs`. Cancellation is checked after an individual Oxc parse returns. Resuming validates all entry points and context again before reusing results.
159
+
160
+ Checkpoints preserve access restrictions, resource mappings, and source map settings. Resume options can explicitly override them. Callers must supply authentication headers and proxy settings again. When resuming an older checkpoint that lacks these settings, provide them in the resume options.
161
+
162
+ ```ts
163
+ const controller = new AbortController();
164
+ const pending = scanJs('./app.js', {
165
+ scriptMode: 'module',
166
+ executionEnvironment: 'window',
167
+ signal: controller.signal,
168
+ checkpointPath: './checkpoint.json',
169
+ });
170
+ // controller.abort();
171
+ const report = await pending;
172
+ ```
173
+
174
+ ## Packaging and releases
175
+
176
+ ```sh
177
+ pnpm pack
178
+ ```
179
+
180
+ `pnpm pack` builds the local SDK and packs core without native binaries. For a complete release, CI builds every target in `.github/native-targets.json`, collects the binaries in `dist/`, then runs `pnpm pack:release` to produce six archives in `artifacts/`.
181
+
182
+ Changesets keeps core and the five binary packages at the same version. The workflow publishes and confirms the binary packages before publishing core with exact optional dependency versions. Run `pnpm changeset` for changes that require a release. See [Releasing](docs/releasing.md) for beta releases, stable releases, and npm authentication.
183
+
184
+ See [Support](docs/support.md) for capabilities and limitations, [Technical design](docs/technical-design.md) for the design, and [Verification](docs/verification.md) for local validation evidence.
@@ -0,0 +1,46 @@
1
+ /* Generated by napi-rs. Do not edit. */
2
+ import type { ScanEntry, ScanOptions, Source, Checkpoint, Report, Target, GlobalAccess, AnalysisInstance, Resource, Budgets } from './types.js';
3
+ export * from './types.js';
4
+ /**
5
+ * Which binding artifact the generated loader actually loaded: `'native'` for
6
+ * a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
7
+ * flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
8
+ * can point the loader at a WASI artifact this package does not build itself.
9
+ */
10
+ export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
11
+
12
+ export declare class GlobalIndex {
13
+ constructor(report: Report)
14
+ get report(): Report
15
+ findTargets(pathSegments: Array<string>, namespace?: string | undefined | null, scopeKey?: string | undefined | null): Target[]
16
+ sources(targetId: string): { access: GlobalAccess; analysis: AnalysisInstance; resource: Resource }[]
17
+ accesses(analysisId: string): GlobalAccess[]
18
+ resourceAccesses(resourceId: string): GlobalAccess[]
19
+ entryPaths(targetId: string, options?: { limit?: number; offset?: number; maxVisits?: number }): { paths: { entryAnalysisId: string; analysisIds: string[]; referenceIds: string[] }[]; truncated: boolean; nextOffset: number }
20
+ conflicts(): { targetId: string; accessIds: string[]; analysisIds: string[]; valueTypes: string[]; certainty: 'static-candidate' }[]
21
+ resolveUnbound(): { targetId: string; candidates: string[]; certainty: 'static-candidate' }[]
22
+ }
23
+
24
+ export declare function adaptReport(report: Report, type: 'build' | 'page', metadata?: Record<string, unknown>): Record<string, unknown>
25
+
26
+ export declare function defaults(): { budgets: Budgets; versions: Record<string, string> }
27
+
28
+ export declare function diffReports(before: Report, after: Report): { added: GlobalAccess[]; removed: GlobalAccess[]; changed: { before: GlobalAccess; after: GlobalAccess }[] }
29
+
30
+ export declare function engineVersion(): string
31
+
32
+ export declare function indexArtifact(report: Report, reportId?: string): Pick<Report, 'indexSchemaVersion' | 'scopeKey' | 'entryAnalysisIds' | 'resources' | 'analysisInstances' | 'references' | 'globalTargets' | 'globalAccesses' | 'findings' | 'unresolved'> & { reportId: string }
33
+
34
+ export declare function resumeScan(checkpoint: Checkpoint, options?: ScanOptions): Promise<Report>
35
+
36
+ export declare function runtimeCandidates(report: Report, input: { resourceUrl?: string; contentHash?: string; line?: number; column?: number; pathSegments?: string[] }): { accessId: string; attribution: 'static_candidate' }[]
37
+
38
+ export declare function scan(entries: ScanEntry[], options?: ScanOptions): Promise<Report>
39
+
40
+ export declare function scanHtml(input: Source | string, options?: ScanOptions): Promise<Report>
41
+
42
+ export declare function scanJs(input: Source | string, options?: ScanOptions): Promise<Report>
43
+
44
+ export declare function scanResource(entry: ScanEntry, options?: ScanOptions): Promise<Report>
45
+
46
+ export declare function validateReport(report: unknown): asserts report is Report