@gajae-code/natives 0.18.11 → 0.19.0

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,5 @@
1
+ {
2
+ "schema": "gjc.diagnostic-artifact",
3
+ "version": "",
4
+ "artifacts": {}
5
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schema": "gjc.diagnostic-artifact",
3
- "version": "0.18.11",
3
+ "version": "0.19.0",
4
4
  "artifacts": {
5
- "pi_natives.darwin-arm64.node": "d9cada560d9cfc412caeb1b880ffe535b35de975f4399524b4ddabcc28f0038a",
6
- "pi_natives.darwin-x64-baseline.node": "517f6d12e892f3e98cf8d59715584cf5246047c5355f4533a9be0b3e259e030c",
7
- "pi_natives.linux-arm64.node": "aa60a3e8f99c2e2e0c5345cefbf0dfbd3e0cb2bb7e54c7714b69bc8d2602934c",
8
- "pi_natives.linux-x64-baseline.node": "701d94a804760eb23c7f0f9c7965aa308885e0794a973e8b9bce187fafd2d4db",
9
- "pi_natives.linux-x64-modern.node": "bb4bb9fc065c960f37e32c046b719f78d6b4bd5614e091d1ebc791e8d53a89a8",
10
- "pi_natives.win32-x64-baseline.node": "a2275bdb5de62da0fcd31713e49ef70ae6bfe8ee9bf678ea8308867b5c0216c4"
5
+ "pi_natives.darwin-arm64.node": "bcb20db50a480de66eed5448d63d530ce708ab84d77ab1cb1620132772690af5",
6
+ "pi_natives.darwin-x64-baseline.node": "3de5a046531495e0a0afdff4d6f0bcd4c8639fcd30fd6cff8ea9ec2efd43b63f",
7
+ "pi_natives.linux-arm64.node": "3eb72a1956e59f3134d45690c61c79a24e0eddcb9627b238deb32540ec0433ce",
8
+ "pi_natives.linux-x64-baseline.node": "d6282ea56f7c47bf659fbb1aef7f8df3fb798c9b828abc5dc3b6cf4334f3c651",
9
+ "pi_natives.linux-x64-modern.node": "8d1fd3d3bc98cc479c75cea158b6059dbed4dbd7a614a6a8c9ad60a12b7a4dae",
10
+ "pi_natives.win32-x64-baseline.node": "7d9ed748b9b6eb6e14a2c87afccbe1590498512f2830fb1e813aa3529b715bb2"
11
11
  }
12
12
  }
@@ -19,10 +19,12 @@
19
19
  * - proves the trusted namespace before reading: every component is owned by the
20
20
  * current user or by root, is not a symlink, and grants no group/other write,
21
21
  * so an account that owns none of the path cannot swap the artifact out;
22
- * - proves the bytes on a retained descriptor: SHA-256 equal to the digest this
23
- * package records in `diagnostic-artifact.json`, which is part of the module
24
- * graph so a compiled binary carries it too. An addon's own
25
- * `nativeBuildInfo()` is self-reported by code that has already executed, so
22
+ * - proves the bytes on a retained descriptor: SHA-256 equal to the shipped
23
+ * `diagnostic-artifact.json` digest, a compiled-binary digest captured from the
24
+ * verified embed input, or (in a source checkout only) the gitignored local
25
+ * manifest when adjacent build metadata explicitly identifies a local build.
26
+ * Each manifest belongs to the package and is checked before use. An addon's own
27
+ * `nativeBuildInfo()` is self-reported by code that has already been executed, so
26
28
  * it is only a consistency check afterwards, never the trust anchor;
27
29
  * - activates the verified bytes through that same retained descriptor
28
30
  * (`process.dlopen` on `/dev/fd/<fd>`), so a pathname replaced between
@@ -75,6 +77,8 @@ import * as fs from "node:fs";
75
77
  import { createRequire } from "node:module";
76
78
  import * as os from "node:os";
77
79
  import * as nodePath from "node:path";
80
+ import { fileURLToPath } from "node:url";
81
+ import compiledArtifacts from "./diagnostic-artifact-embedded.json" with { type: "json" };
78
82
  import trustedArtifacts from "./diagnostic-artifact.json" with { type: "json" };
79
83
  import packageManifest from "../package.json" with { type: "json" };
80
84
 
@@ -127,17 +131,126 @@ function cachedArtifactPath() {
127
131
  }
128
132
 
129
133
  /**
130
- * The package-owned expected digest for this artifact, or null when this package
131
- * ships no trusted record for it.
134
+ * The package-owned expected digest, or null when this package has no trusted
135
+ * record. Compiled binaries prefer the digest recorded at their embed boundary;
136
+ * source checkouts may use the gitignored local manifest for an explicitly local
137
+ * build. Installed packages use the shipped record.
132
138
  *
139
+ * @param {string} file
133
140
  * @returns {string | null}
134
141
  */
135
- function trustedDigest() {
142
+ function trustedDigest(file) {
143
+ const compiledDigest = compiledArtifactDigest(file);
144
+ if (compiledDigest !== undefined) return compiledDigest;
145
+ const localDigest = sourceLocalBuildDigest(file);
146
+ if (localDigest !== undefined) return localDigest;
136
147
  if (trustedArtifacts?.version !== packageManifest.version) return null;
137
148
  const digest = trustedArtifacts?.artifacts?.[ARTIFACT_BASENAME];
138
149
  return typeof digest === "string" && /^[0-9a-f]{64}$/.test(digest) ? digest : null;
139
150
  }
140
151
 
152
+ /**
153
+ * A compiled binary carries the digest of the exact addon verified before embedding.
154
+ * An empty stub falls back to the shipped record; a populated but stale or malformed
155
+ * compiled record fails closed.
156
+ *
157
+ * @param {string} file
158
+ * @returns {string | null | undefined} Undefined means no compiled digest is present.
159
+ */
160
+ function compiledArtifactDigest(file) {
161
+ if (!runningFromCompiledBinary() || compiledArtifacts?.version === "") return undefined;
162
+ if (compiledArtifacts?.schema !== "gjc.diagnostic-artifact" || compiledArtifacts?.version !== packageManifest.version) {
163
+ return null;
164
+ }
165
+ const digest = compiledArtifacts?.artifacts?.[nodePath.basename(file)];
166
+ if (digest === undefined) return undefined;
167
+ return typeof digest === "string" && /^[0-9a-f]{64}$/.test(digest) ? digest : null;
168
+ }
169
+
170
+ /**
171
+ * Read one bounded JSON file only after proving its path and retained descriptor are
172
+ * in the same trusted namespace used for the native artifact.
173
+ *
174
+ * @param {string} file
175
+ * @returns {Record<string, unknown> | null}
176
+ */
177
+ function readTrustedLocalJson(file) {
178
+ let fd = -1;
179
+ try {
180
+ if (!trustedNamespace(file)) return null;
181
+ fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
182
+ const stat = fs.fstatSync(fd);
183
+ if (!stat.isFile() || stat.size === 0 || stat.size > 1 << 20) return null;
184
+ if ((stat.mode & 0o022) !== 0) return null;
185
+ if (stat.uid !== process.geteuid?.() && stat.uid !== 0) return null;
186
+ if (inspectAclOnDescriptor(fd) !== "absent") return null;
187
+ const bytes = Buffer.allocUnsafe(stat.size);
188
+ let offset = 0;
189
+ while (offset < bytes.length) {
190
+ const read = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
191
+ if (read === 0) return null;
192
+ offset += read;
193
+ }
194
+ if (fs.fstatSync(fd).size !== stat.size) return null;
195
+ const value = JSON.parse(bytes.toString("utf8"));
196
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
197
+ } catch {
198
+ return null;
199
+ } finally {
200
+ if (fd >= 0) closeDescriptor(fd);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * An unversioned local build is accepted only when the package-owned record proves
206
+ * these exact bytes for the current package version. This is the migration path for
207
+ * builds produced before local sidecars carried their version.
208
+ *
209
+ * @param {unknown} record
210
+ * @param {string} version
211
+ * @param {string} artifact
212
+ * @param {string} digest
213
+ * @returns {boolean}
214
+ */
215
+ function trustedRecordHasDigest(record, version, artifact, digest) {
216
+ if (typeof record !== "object" || record === null || Array.isArray(record)) return false;
217
+ const candidate = /** @type {Record<string, unknown>} */ (record);
218
+ if (candidate.schema !== "gjc.diagnostic-artifact" || candidate.version !== version) return false;
219
+ const artifacts = candidate.artifacts;
220
+ if (typeof artifacts !== "object" || artifacts === null || Array.isArray(artifacts)) return false;
221
+ return /** @type {Record<string, unknown>} */ (artifacts)[artifact] === digest;
222
+ }
223
+
224
+ /**
225
+ * A source-mode local build has an explicit profile marker and a gitignored digest
226
+ * record. Never consult these files for a compiled binary, an installed platform
227
+ * package, or a release/CI addon.
228
+ *
229
+ * @param {string} file
230
+ * @returns {string | null | undefined} Undefined means this is not a local source build.
231
+ */
232
+ function sourceLocalBuildDigest(file) {
233
+ if (runningFromCompiledBinary()) return undefined;
234
+ const sourceAddon = nodePath.join(nodePath.dirname(fileURLToPath(import.meta.url)), ARTIFACT_BASENAME);
235
+ if (nodePath.resolve(file) !== sourceAddon) return undefined;
236
+ const buildMetadata = readTrustedLocalJson(`${sourceAddon}.build.json`);
237
+ if (buildMetadata?.profile !== "local") return undefined;
238
+
239
+ const localManifest = readTrustedLocalJson(
240
+ nodePath.join(nodePath.dirname(sourceAddon), "diagnostic-artifact.local.json"),
241
+ );
242
+ if (localManifest?.schema !== "gjc.diagnostic-artifact") return null;
243
+ if (localManifest.version !== packageManifest.version) return null;
244
+ const artifacts = localManifest.artifacts;
245
+ if (typeof artifacts !== "object" || artifacts === null || Array.isArray(artifacts)) return null;
246
+ const digest = /** @type {Record<string, unknown>} */ (artifacts)[ARTIFACT_BASENAME];
247
+ if (typeof digest !== "string" || !/^[0-9a-f]{64}$/.test(digest)) return null;
248
+ if (buildMetadata.version !== undefined) {
249
+ return buildMetadata.version === packageManifest.version ? digest : null;
250
+ }
251
+ return trustedRecordHasDigest(trustedArtifacts, packageManifest.version, ARTIFACT_BASENAME, digest) ? digest : null;
252
+ }
253
+
141
254
  /**
142
255
  * Select the single applicable layout. Exactly one distribution shape applies per
143
256
  * install, and a failure of the selected layout is never a reason to try another.
@@ -674,7 +787,7 @@ function consumeHandle(handle) {
674
787
  * @returns {{ ok: true, token: string } | { ok: false, reason: "unsupported" }}
675
788
  */
676
789
  export function verifyDiagnosticArtifact(file) {
677
- const expectedDigest = trustedDigest();
790
+ const expectedDigest = trustedDigest(file);
678
791
  if (expectedDigest === null) return unsupported;
679
792
  let fd = -1;
680
793
  try {
package/native/index.d.ts CHANGED
@@ -687,7 +687,7 @@ export declare function __piNativesPublishOutcomeV1(): void
687
687
  * `packages/natives/native/index.js` (which derives the name from
688
688
  * `package.json#version`).
689
689
  */
690
- export declare function __piNativesV0_18_11(): void
690
+ export declare function __piNativesV0_19_0(): void
691
691
 
692
692
  /**
693
693
  * Apply conservative pre-execution rewrites to a bash command.
@@ -795,6 +795,59 @@ export interface AstFindResult {
795
795
  */
796
796
  export declare function astGrep(options: AstFindOptions): Promise<AstFindResult>
797
797
 
798
+ /**
799
+ * Match ast-grep patterns against an in-memory source string; returns a
800
+ * promise resolved on a worker thread.
801
+ *
802
+ * This is the file-free counterpart to [`ast_grep`]: callers that already hold
803
+ * the source (streaming buffers, generated code, editor contents) avoid a
804
+ * temp-file round trip. `lang` is required since there is no path to infer it
805
+ * from.
806
+ */
807
+ export declare function astMatch(options: AstMatchOptions): Promise<AstMatchResult>
808
+
809
+ /**
810
+ * Options for `astMatch`: run ast-grep patterns against an in-memory source
811
+ * string instead of files on disk.
812
+ */
813
+ export interface AstMatchOptions {
814
+ /** Source code to match against (parsed in memory, never read from disk). */
815
+ source: string
816
+ /**
817
+ * Language of `source` (required): an alias ("ts", "rust", "python") or a
818
+ * file extension ("scss"), resolved like a path's extension.
819
+ */
820
+ lang: string
821
+ /** ast-grep patterns to search for (OR across patterns). */
822
+ patterns: Array<string>
823
+ /** Rule selector for multi-rule ast-grep configurations. */
824
+ selector?: string
825
+ /** Pattern strictness; defaults to smart matching when omitted. */
826
+ strictness?: AstMatchStrictness
827
+ /** Maximum matches to return after `offset` (default applies when omitted). */
828
+ limit?: number
829
+ /** Number of leading matches to skip before applying `limit`. */
830
+ offset?: number
831
+ /** When true, include meta-variable bindings per match. */
832
+ includeMeta?: boolean
833
+ /** Optional cancellation handle (library-specific). */
834
+ signal?: unknown
835
+ /** Wall-clock timeout for the worker task in milliseconds. */
836
+ timeoutMs?: number
837
+ }
838
+
839
+ /** Result of an in-memory `astMatch` run. */
840
+ export interface AstMatchResult {
841
+ /** Page of matches after sort, offset, and limit. */
842
+ matches: Array<AstFindMatch>
843
+ /** Total matches found before paging (can exceed `matches.length`). */
844
+ totalMatches: number
845
+ /** True when results were truncated by `limit`. */
846
+ limitReached: boolean
847
+ /** Non-fatal parse or pattern-compile errors collected during the run. */
848
+ parseErrors?: Array<string>
849
+ }
850
+
798
851
  /** ast-grep pattern strictness (controls how patterns match syntax). */
799
852
  export declare enum AstMatchStrictness {
800
853
  /** Match at the concrete syntax tree level. */
@@ -1189,10 +1242,11 @@ export declare function encodeSixel(bytes: Uint8Array, targetWidthPx: number, ta
1189
1242
 
1190
1243
  /**
1191
1244
  * Remove a directory tree only when a fresh descriptor-relative snapshot
1192
- * exactly equals the persisted snapshot. POSIX first no-replace detaches the
1193
- * verified root to its deterministic `.removing` sibling; the reopened
1194
- * detached descriptor remains authoritative throughout payload scrubbing and
1195
- * replay.
1245
+ * exactly equals the persisted snapshot. POSIX removal no-replace detaches the
1246
+ * verified root to its deterministic `.removing` sibling before scrubbing. On
1247
+ * Windows, callers that need retirement semantics must request a detach-only
1248
+ * pass, then replay exact removal at the returned sibling; ordinary Windows
1249
+ * removal never vacates the caller's path before scrubbing completes.
1196
1250
  */
1197
1251
  export declare function exactRemoveDirectoryTree(path: string, snapshot: NativeDirectoryTreeSnapshot, parentIdentity?: NativeDirectoryParentIdentity | undefined | null, detachOnly?: boolean | undefined | null): NativeExactUnlinkResult
1198
1252
 
@@ -1239,9 +1293,10 @@ export declare function exactReplacePath(sourcePath: string, destinationPath: st
1239
1293
  * single-link ownership; hard-linked or symlinked source/destination are
1240
1294
  * rejected) before anything is mutated. On Windows a pre-mutation
1241
1295
  * `STATUS_SHARING_VIOLATION` on the destination open is reported as a
1242
- * distinct `sharing_violation` code with `windows_error_code` set -- exactly
1243
- * as [`exact_replace_path`] already reports it -- and is always surfaced
1244
- * before any rename, so it is never mistaken for a post-effect failure.
1296
+ * distinct `sharing_violation` code, while `STATUS_ACCESS_DENIED` on a
1297
+ * retained open or rename is reported as `access_denied`. Pre-mutation
1298
+ * failures include the original `windows_error_code`; only known-safe rename
1299
+ * operations retry transient access-denied or sharing-violation statuses.
1245
1300
  */
1246
1301
  export declare function exactReplaceRetained(sourcePath: string, destinationPath: string, backupName: string, expectedSource: NativeExactFileIdentity, expectedDestination: NativeExactFileIdentity): NativeExactUnlinkResult
1247
1302
 
@@ -1294,6 +1349,17 @@ export declare function exactUnlinkDirect(path: string, identity: NativeExactFil
1294
1349
  */
1295
1350
  export declare function exactUnlinkDirectDetached(path: string, identity: NativeExactFileIdentity): boolean
1296
1351
 
1352
+ /**
1353
+ * Delete only the empty regular file opened by the caller's retained
1354
+ * descriptor.
1355
+ *
1356
+ * This is used when a newly created staging file cannot be statted through the
1357
+ * host API. Native code derives identity from the open descriptor and verifies
1358
+ * the public entry still names that object before using the platform's exact
1359
+ * unlink protocol.
1360
+ */
1361
+ export declare function exactUnlinkOpenFile(path: string, callerFd: number, quarantineName: string): NativeExactUnlinkResult
1362
+
1297
1363
  /**
1298
1364
  * Execute a brush shell command.
1299
1365
  *
@@ -2105,7 +2171,7 @@ export type NativeCanonicalDirectoryIdentity =
2105
2171
  ok: false;
2106
2172
  platform?: never;
2107
2173
  canonicalPath?: never;
2108
- code: "not_found" | "not_directory" | "not_utf8" | "network_unsupported" | "identity_unavailable" | "io_error";
2174
+ code: "access_denied" | "not_found" | "not_directory" | "not_utf8" | "network_unsupported" | "identity_unavailable" | "io_error";
2109
2175
  }
2110
2176
 
2111
2177
  /** Bytes copied out of one read-only broker publication observation. */
@@ -2321,6 +2387,7 @@ export type NativeOwnerOnlySecurityResult =
2321
2387
  | {
2322
2388
  ok: false;
2323
2389
  code:
2390
+ | "access_denied"
2324
2391
  | "not_found"
2325
2392
  | "not_directory"
2326
2393
  | "network_unsupported"
@@ -2578,16 +2645,22 @@ export interface PtyStartOptions {
2578
2645
  export declare function ptyTimeoutCount(): bigint
2579
2646
 
2580
2647
  /**
2581
- * Rasterize SVG/SVGZ bytes into a bounded PNG without resolving local files.
2648
+ * Rasterize bounded SVG/SVGZ bytes into a PNG without resolving local files.
2649
+ *
2650
+ * DTD/entity declarations and SVG image elements are rejected rather than
2651
+ * allowing unbounded expansion or silently returning an incomplete preview.
2582
2652
  *
2583
2653
  * Conversion runs on the native blocking pool so parsing and rendering do not
2584
2654
  * stall the JavaScript event loop.
2585
2655
  *
2656
+ * When both cell dimensions are provided, the PNG is padded on a transparent
2657
+ * canvas to whole terminal cells without embedding or decoding another image.
2658
+ *
2586
2659
  * # Errors
2587
- * Returns an error for invalid SVG data, zero/oversized limits, allocation
2588
- * failure, or PNG encoding failure.
2660
+ * Returns an error for invalid SVG data, mismatched/zero cell dimensions,
2661
+ * zero/oversized limits, allocation failure, or PNG encoding failure.
2589
2662
  */
2590
- export declare function rasterizeSvg(input: Uint8Array, maxWidthPx: number, maxHeightPx: number): Promise<Uint8Array>
2663
+ export declare function rasterizeSvg(input: Uint8Array, maxWidthPx: number, maxHeightPx: number, cellWidthPx?: number | undefined | null, cellHeightPx?: number | undefined | null): Promise<Uint8Array>
2591
2664
 
2592
2665
  /**
2593
2666
  * Read an image from the system clipboard.
package/native/index.js CHANGED
@@ -35,12 +35,13 @@ export const TtyWriter = nativeBindings.TtyWriter;
35
35
  // functions
36
36
  export const __gjcInstallTokioRuntime = nativeBindings.__gjcInstallTokioRuntime;
37
37
  export const __piNativesPublishOutcomeV1 = nativeBindings.__piNativesPublishOutcomeV1;
38
- export const __piNativesV0_18_11 = nativeBindings.__piNativesV0_18_11;
38
+ export const __piNativesV0_19_0 = nativeBindings.__piNativesV0_19_0;
39
39
  export const applyBashFixups = nativeBindings.applyBashFixups;
40
40
  export const applyOwnerOnlyFdSecurity = nativeBindings.applyOwnerOnlyFdSecurity;
41
41
  export const applyOwnerOnlyPathSecurity = nativeBindings.applyOwnerOnlyPathSecurity;
42
42
  export const astEdit = nativeBindings.astEdit;
43
43
  export const astGrep = nativeBindings.astGrep;
44
+ export const astMatch = nativeBindings.astMatch;
44
45
  export const canonicalExistingDirectoryIdentity = nativeBindings.canonicalExistingDirectoryIdentity;
45
46
  export const computerScreenshot = nativeBindings.computerScreenshot;
46
47
  export const copyToClipboard = nativeBindings.copyToClipboard;
@@ -64,6 +65,7 @@ export const exactSwapManagedLink = nativeBindings.exactSwapManagedLink;
64
65
  export const exactUnlink = nativeBindings.exactUnlink;
65
66
  export const exactUnlinkDirect = nativeBindings.exactUnlinkDirect;
66
67
  export const exactUnlinkDirectDetached = nativeBindings.exactUnlinkDirectDetached;
68
+ export const exactUnlinkOpenFile = nativeBindings.exactUnlinkOpenFile;
67
69
  export const executeShell = nativeBindings.executeShell;
68
70
  export const extractSegments = nativeBindings.extractSegments;
69
71
  export const fuzzyFind = nativeBindings.fuzzyFind;
@@ -88,7 +88,9 @@ export function loadFromCandidates<T>(input: LoadFromCandidatesInput<T>): LoadFr
88
88
  export interface CachedEmbeddedExtractionIsFreshInput {
89
89
  targetPath: string;
90
90
  embeddedPath: string;
91
+ expectedDigest: string | null | undefined;
91
92
  sizeOf: (path: string) => number | null;
93
+ digestOf: (path: string) => string | null;
92
94
  }
93
95
 
94
96
  export function cachedEmbeddedExtractionIsFresh(input: CachedEmbeddedExtractionIsFreshInput): boolean;
@@ -136,6 +138,7 @@ export interface LoadNativeOptions {
136
138
  acquireStagedCandidateLease?: (candidate: string) => () => void;
137
139
  requireCandidate?: (candidate: string) => Record<string, unknown>;
138
140
  validateCandidate?: (bindings: Record<string, unknown>, candidate: string) => void;
141
+ loadNativeLibrary?: (runtime: string) => bigint | number | null;
139
142
  }
140
143
 
141
144
  export function loadNative(options?: LoadNativeOptions): Record<string, unknown>;
@@ -5,6 +5,7 @@ import * as fs from "node:fs";
5
5
  import { createRequire } from "node:module";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
+ import embeddedDiagnosticArtifacts from "./diagnostic-artifact-embedded.json" with { type: "json" };
8
9
  import packageJson from "../package.json" with { type: "json" };
9
10
  import { embeddedAddon } from "./embedded-addon.js";
10
11
 
@@ -32,13 +33,14 @@ import { embeddedAddon } from "./embedded-addon.js";
32
33
  * post-build `--reset` stub) is the authoritative compiled-mode signal.
33
34
  */
34
35
 
35
- const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"];
36
+ const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64", "win32-arm64"];
36
37
  const OPTIONAL_PACKAGE_BY_PLATFORM_TAG = {
37
38
  "darwin-arm64": "@gajae-code/natives-darwin-arm64",
38
39
  "darwin-x64": "@gajae-code/natives-darwin-x64",
39
40
  "linux-arm64": "@gajae-code/natives-linux-arm64",
40
41
  "linux-x64": "@gajae-code/natives-linux-x64",
41
42
  "win32-x64": "@gajae-code/natives-win32-x64",
43
+ "win32-arm64": "@gajae-code/natives-win32-arm64",
42
44
  };
43
45
  const WINDOWS_GENERIC_READ = 0x80000000;
44
46
  const WINDOWS_FILE_SHARE_READ = 0x00000001;
@@ -138,6 +140,32 @@ function validateStagedCandidate(ctx, candidate) {
138
140
  }
139
141
  }
140
142
 
143
+ function preloadSiblingNativeRuntime(ctx, candidate, loadLibrary) {
144
+ if (process.platform !== "win32") return;
145
+ // Staging copies only the .node into the versioned cache and drops the
146
+ // package directory from the candidate list. The DLL stays beside the
147
+ // recorded source addon, not beside that cache copy or bun.exe.
148
+ const sourcePath = ctx.stagedSourceSnapshots?.get(candidate)?.sourcePath;
149
+ const runtimes = [
150
+ typeof sourcePath === "string" ? path.join(path.dirname(sourcePath), "libunwind.dll") : null,
151
+ path.join(path.dirname(candidate), "libunwind.dll"),
152
+ path.join(path.dirname(process.execPath), "libunwind.dll"),
153
+ ];
154
+ const seen = new Set();
155
+ for (const runtime of runtimes) {
156
+ if (!runtime || seen.has(runtime) || !fs.existsSync(runtime)) continue;
157
+ seen.add(runtime);
158
+ const handle = loadLibrary(runtime);
159
+ if (handle !== 0 && handle !== 0n && handle != null) return;
160
+ }
161
+ }
162
+
163
+ function loadSiblingNativeLibrary(runtime) {
164
+ const widePath = new Uint16Array(runtime.length + 1);
165
+ for (let index = 0; index < runtime.length; index++) widePath[index] = runtime.charCodeAt(index);
166
+ return windowsKernel32Bindings().symbols.LoadLibraryW(ptr(widePath));
167
+ }
168
+
141
169
  function windowsKernel32Bindings() {
142
170
  if (windowsKernel32) return windowsKernel32;
143
171
  windowsKernel32 = dlopen("kernel32.dll", {
@@ -147,6 +175,7 @@ function windowsKernel32Bindings() {
147
175
  },
148
176
  CloseHandle: { args: ["i64"], returns: "u32" },
149
177
  GetLastError: { args: [], returns: "u32" },
178
+ LoadLibraryW: { args: ["ptr"], returns: "ptr" },
150
179
  });
151
180
  return windowsKernel32;
152
181
  }
@@ -368,18 +397,60 @@ export function loadFromCandidates({ candidates, requireCandidate, validateCandi
368
397
  /**
369
398
  * Decide whether a previously extracted embedded addon may be reused. A cached
370
399
  * extraction from an earlier build of the same version carries the same version
371
- * sentinel yet can expose a different native surface, so it is fresh only when
372
- * its byte size matches the embedded payload. `sizeOf` returns the byte size of
373
- * a path, or `null` when it cannot be inspected.
374
- * @param {{ targetPath: string; embeddedPath: string; sizeOf: (path: string) => number | null }} input
400
+ * sentinel and may have the same byte size, so freshness also requires its digest
401
+ * to match the package-owned embedded build record. `sizeOf` returns a path's
402
+ * byte size; `digestOf` returns a SHA-256 hex digest. Both return `null` when the
403
+ * path cannot be inspected.
404
+ * @param {{ targetPath: string; embeddedPath: string; expectedDigest: string | null | undefined; sizeOf: (path: string) => number | null; digestOf: (path: string) => string | null }} input
375
405
  * @returns {boolean}
376
406
  */
377
- export function cachedEmbeddedExtractionIsFresh({ targetPath, embeddedPath, sizeOf }) {
407
+ export function cachedEmbeddedExtractionIsFresh({ targetPath, embeddedPath, expectedDigest, sizeOf, digestOf }) {
408
+ if (typeof expectedDigest !== "string" || !/^[0-9a-f]{64}$/.test(expectedDigest)) return false;
378
409
  const cachedSize = sizeOf(targetPath);
379
410
  if (cachedSize === null) return false;
380
411
  const embeddedSize = sizeOf(embeddedPath);
381
412
  if (embeddedSize === null) return false;
382
- return cachedSize === embeddedSize;
413
+ if (cachedSize !== embeddedSize) return false;
414
+ return digestOf(targetPath) === expectedDigest;
415
+ }
416
+
417
+ function sha256File(file) {
418
+ let fd = -1;
419
+ try {
420
+ const noFollow = fs.constants.O_NOFOLLOW ?? 0;
421
+ fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
422
+ const before = fs.fstatSync(fd);
423
+ if (!before.isFile()) return null;
424
+ const hash = createHash("sha256");
425
+ const buffer = Buffer.allocUnsafe(1 << 20);
426
+ let offset = 0;
427
+ while (offset < before.size) {
428
+ const read = fs.readSync(fd, buffer, 0, Math.min(buffer.length, before.size - offset), offset);
429
+ if (read === 0) return null;
430
+ hash.update(buffer.subarray(0, read));
431
+ offset += read;
432
+ }
433
+ const after = fs.fstatSync(fd);
434
+ if (
435
+ after.dev !== before.dev ||
436
+ after.ino !== before.ino ||
437
+ after.size !== before.size ||
438
+ after.mtimeMs !== before.mtimeMs
439
+ ) {
440
+ return null;
441
+ }
442
+ return hash.digest("hex");
443
+ } catch {
444
+ return null;
445
+ } finally {
446
+ if (fd >= 0) {
447
+ try {
448
+ fs.closeSync(fd);
449
+ } catch {
450
+ // A failed close cannot make an unverified digest admissible.
451
+ }
452
+ }
453
+ }
383
454
  }
384
455
 
385
456
  // =========================================================================
@@ -549,12 +620,16 @@ function maybeExtractEmbeddedAddons(ctx, errors) {
549
620
  const extracted = [];
550
621
  for (const embeddedFile of embeddedAddonCandidates(ctx.selectedVariant)) {
551
622
  const targetPath = path.join(ctx.versionedDir, embeddedFile.filename);
623
+ const expectedDigest =
624
+ embeddedDiagnosticArtifacts?.schema === "gjc.diagnostic-artifact" &&
625
+ embeddedDiagnosticArtifacts?.version === embeddedAddon.version
626
+ ? embeddedDiagnosticArtifacts?.artifacts?.[embeddedFile.filename]
627
+ : undefined;
552
628
  if (fs.existsSync(targetPath)) {
553
629
  // Guard against intra-version drift: a cached extraction written by an earlier
554
- // build of the same version carries the same version sentinel but can expose a
555
- // different native surface (e.g. a symbol added mid-cycle). The embedded addon
556
- // is the source of truth, so reuse the cached file only when it matches the
557
- // embedded payload size and re-extract otherwise.
630
+ // build of the same version carries the same version sentinel and may even have
631
+ // the same byte size. The embedded digest is the source of truth, so reuse the
632
+ // cached file only when its bytes match that exact build.
558
633
  const sizeOf = candidate => {
559
634
  try {
560
635
  return fs.statSync(candidate).size;
@@ -562,7 +637,15 @@ function maybeExtractEmbeddedAddons(ctx, errors) {
562
637
  return null;
563
638
  }
564
639
  };
565
- if (cachedEmbeddedExtractionIsFresh({ targetPath, embeddedPath: embeddedFile.filePath, sizeOf })) {
640
+ if (
641
+ cachedEmbeddedExtractionIsFresh({
642
+ targetPath,
643
+ embeddedPath: embeddedFile.filePath,
644
+ expectedDigest,
645
+ sizeOf,
646
+ digestOf: sha256File,
647
+ })
648
+ ) {
566
649
  extracted.push(targetPath);
567
650
  continue;
568
651
  }
@@ -910,6 +993,7 @@ export function loadNative(options = {}) {
910
993
  }
911
994
  try {
912
995
  validateStagedCandidate(ctx, candidate);
996
+ preloadSiblingNativeRuntime(ctx, candidate, options.loadNativeLibrary ?? loadSiblingNativeLibrary);
913
997
  const bindings = options.requireCandidate ? options.requireCandidate(candidate) : require_(candidate);
914
998
  if (typeof bindings.__gjcInstallTokioRuntime === "function") {
915
999
  bindings.__gjcInstallTokioRuntime();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gajae-code/natives",
3
- "version": "0.18.11",
3
+ "version": "0.19.0",
4
4
  "description": "Native Rust bindings for grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API",
5
5
  "type": "module",
6
6
  "homepage": "https://gajae-code.com",
@@ -31,7 +31,7 @@
31
31
  "scripts": {
32
32
  "build": "bun scripts/build-native.ts",
33
33
  "check": "biome check . && bun run check:types",
34
- "check:types": "tsc -p tsconfig.json --noEmit",
34
+ "check:types": "bun ../coding-agent/scripts/generate-docs-index.ts && tsc -p tsconfig.json --noEmit",
35
35
  "lint": "biome lint .",
36
36
  "test": "bun test",
37
37
  "fix": "biome check --write --unsafe .",
@@ -61,15 +61,16 @@
61
61
  "native/diagnostic-loader.d.ts",
62
62
  "native/diagnostic-loader.js",
63
63
  "native/diagnostic-artifact.json",
64
+ "native/diagnostic-artifact-embedded.json",
64
65
  "native/embedded-addon.js",
65
66
  "README.md"
66
67
  ],
67
68
  "optionalDependencies": {
68
- "@gajae-code/natives-darwin-arm64": "0.18.11",
69
- "@gajae-code/natives-darwin-x64": "0.18.11",
70
- "@gajae-code/natives-linux-arm64": "0.18.11",
71
- "@gajae-code/natives-linux-x64": "0.18.11",
72
- "@gajae-code/natives-win32-x64": "0.18.11"
69
+ "@gajae-code/natives-darwin-arm64": "0.19.0",
70
+ "@gajae-code/natives-darwin-x64": "0.19.0",
71
+ "@gajae-code/natives-linux-arm64": "0.19.0",
72
+ "@gajae-code/natives-linux-x64": "0.19.0",
73
+ "@gajae-code/natives-win32-x64": "0.19.0"
73
74
  },
74
75
  "exports": {
75
76
  ".": {