@polyjose/node 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +155 -2
- package/THIRD-PARTY-NOTICES.txt +80 -0
- package/addon-sha256.json +10 -0
- package/backend.d.ts +90 -0
- package/backend.js +378 -0
- package/binding.cjs +869 -0
- package/binding.d.ts +469 -0
- package/facade/api.d.ts +123 -0
- package/facade/api.js +567 -0
- package/facade/backend.d.ts +159 -0
- package/facade/backend.js +19 -0
- package/facade/error-codes.d.ts +4 -0
- package/facade/error-codes.js +22 -0
- package/facade/normalize.d.ts +134 -0
- package/facade/normalize.js +380 -0
- package/facade/types.d.ts +206 -0
- package/facade/types.js +14 -0
- package/index.d.ts +11 -0
- package/index.js +12 -0
- package/package.json +64 -4
package/backend.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The napi-rs backend of `@polyjose/node` (ADR 0031 §1-§2, `bindings/js/BACKEND.md`).
|
|
3
|
+
*
|
|
4
|
+
* The generated loader `binding.cjs` (`napi build --platform --js binding.cjs`) already is the
|
|
5
|
+
* backend contract: one function per ADR 0030 §4 operation, camelCase, in the ADR's argument
|
|
6
|
+
* order, rejecting with an `Error` whose `code` is one of `errorCodes()` (ADR 0030 §5). So this
|
|
7
|
+
* module only loads it, checks what it loaded and gives it the facade's `Backend` type; it adds no
|
|
8
|
+
* JOSE logic and renames nothing (ADR 0030 §1: loading and integrity checks are entry idiom).
|
|
9
|
+
*
|
|
10
|
+
* The loader is CommonJS (it picks the platform's `.node` file at run time), loaded here through
|
|
11
|
+
* `createRequire`, so the package stays ESM-only (ADR 0031 §3). Its types come from the generated
|
|
12
|
+
* `binding.d.ts`.
|
|
13
|
+
*
|
|
14
|
+
* **Fail closed (feature 0007 F02; review-code.md SF-4, review-security.md M1).** The generated
|
|
15
|
+
* loader returns the first module that loads, of any kind:
|
|
16
|
+
* - `NAPI_RS_NATIVE_LIBRARY_PATH`, if set;
|
|
17
|
+
* - then `./polyjose.<platform>.node` next to it;
|
|
18
|
+
* - then `require('@polyjose/node-<platform>')`, which walks every ancestor `node_modules` and
|
|
19
|
+
* `NODE_PATH`;
|
|
20
|
+
* - then a WASI chain: `./polyjose.wasi.cjs`, then `@polyjose/node-wasm32-wasi`. The napi-rs CLI
|
|
21
|
+
* 3.10.12 template always emits it; no `napi build` option omits it, and
|
|
22
|
+
* `NAPI_RS_FORCE_WASI` / `NAPI_RS_WASI_FLAVOR` can select it.
|
|
23
|
+
* It stamps a JavaScript module found that way `"native"` too. So a JS file planted in an
|
|
24
|
+
* ancestor directory or on `NODE_PATH` would become the crypto backend and could "verify" any
|
|
25
|
+
* token. polyjose publishes only native addons (ADR 0031 §2: eight targets, no WASI build;
|
|
26
|
+
* `@polyjose/wasm` is a separate package). The generated file is never edited; this module wraps
|
|
27
|
+
* it and accepts nothing else.
|
|
28
|
+
*
|
|
29
|
+
* **Before loading,** nothing runs unless every module the loader could pick is a native addon:
|
|
30
|
+
* 1. Neither WASI variable is set, whatever its value.
|
|
31
|
+
* 2. No WASI candidate resolves.
|
|
32
|
+
* 3. Every native candidate that resolves is a `.node` file: the override path, and for every
|
|
33
|
+
* platform the loader probes, `./polyjose.<platform>.node` and `@polyjose/node-<platform>`.
|
|
34
|
+
* `@polyjose/node-<platform>/package.json`, which the loader also reads, must be a
|
|
35
|
+
* `package.json`.
|
|
36
|
+
* Resolution is the loader's own: `backend.js` sits next to `binding.cjs`, and `require.resolve`
|
|
37
|
+
* uses the same directory, `node_modules` walk, `NODE_PATH` and symlink handling as the loader's
|
|
38
|
+
* `require`, and runs no code. Node loads a `.node` file only as a native addon, never as
|
|
39
|
+
* JavaScript.
|
|
40
|
+
*
|
|
41
|
+
* **The hash pin (feature 0007 F07; bindings security audit N-M1, the JVM's 0008 F05 bar).** Steps
|
|
42
|
+
* 1-6 make sure the package loads a native addon at this version, but `version()` is what the
|
|
43
|
+
* addon itself says, and a native addon is as easy to plant as a JavaScript file. So before the
|
|
44
|
+
* loader runs, this module also *authenticates* the addon:
|
|
45
|
+
* a. `addon-sha256.json`, next to this file, maps each platform polyjose built this package for
|
|
46
|
+
* to its addon's SHA-256 (FIPS 180-4). It is generated, never hand-edited:
|
|
47
|
+
* `scripts/addon-hashes.sh`, run by `scripts/build.sh` for a local build and by the release
|
|
48
|
+
* assembly of the main package for every published platform. No map, a malformed one, or no
|
|
49
|
+
* entry for this host refuses the load: nothing is trusted by default.
|
|
50
|
+
* b. It picks the one file the loader would load on this host (the platform name of
|
|
51
|
+
* `hostPlatform`, the loader's own): `NAPI_RS_NATIVE_LIBRARY_PATH` when set, else
|
|
52
|
+
* `./polyjose.<platform>.node`, else `@polyjose/node-<platform>`, resolved as above. A
|
|
53
|
+
* caller's override is not refused; it is held to the same pin.
|
|
54
|
+
* c. It reads that file once, hashes those bytes with `node:crypto` and refuses on a mismatch,
|
|
55
|
+
* naming the platform, the source and both hashes. There is no fallback to another candidate.
|
|
56
|
+
* d. It writes the same bytes, with `O_EXCL`, into a new directory from `fs.mkdtempSync` under
|
|
57
|
+
* `os.tmpdir()` (mode 0700 on POSIX; on Windows the per-user `%TEMP%` ACL), under a random
|
|
58
|
+
* leaf name (macOS looks a leaf name up in `DYLD_LIBRARY_PATH` before the path), checks the
|
|
59
|
+
* copy's SHA-256 again, and points `NAPI_RS_NATIVE_LIBRARY_PATH` at it for the one
|
|
60
|
+
* synchronous `require("./binding.cjs")`. The loader's override branch loads exactly that
|
|
61
|
+
* path and probes nothing else, so between the check and `dlopen` only this user (who can
|
|
62
|
+
* already change anything they run) could swap the file; whoever can write the installed
|
|
63
|
+
* file or an ancestor `node_modules` can't. The caller's environment is restored right
|
|
64
|
+
* after, and the copy is deleted (a loaded library stays mapped; Windows can't delete a
|
|
65
|
+
* loaded DLL, so there the copy stays in the temporary directory).
|
|
66
|
+
*
|
|
67
|
+
* **After loading,** what the loader returned must be:
|
|
68
|
+
* 4. the exports of exactly one `.node` module in the CommonJS cache, the private copy of step
|
|
69
|
+
* d, shared only with a loader (`binding.cjs`), so no JavaScript module wraps or re-exports
|
|
70
|
+
* it, and a `binding.cjs` that other code required first (with its own addon) is refused;
|
|
71
|
+
* 5. stamped `__napiBindingTarget === "native"`;
|
|
72
|
+
* 6. at this package's version: `version()` must equal `package.json`'s version. That is the
|
|
73
|
+
* workspace version, which every release keeps in step (feature 0007 T04). So an addon from
|
|
74
|
+
* another release is refused, whatever `NAPI_RS_ENFORCE_VERSION_CHECK` says (SF-4 item 3).
|
|
75
|
+
* Every refusal is an `Error` with `code` `ERR_POLYJOSE_ADDON_REFUSED`. This doesn't defend against
|
|
76
|
+
* code already running in the process, which can patch anything. It makes sure the package never
|
|
77
|
+
* selects a substitute.
|
|
78
|
+
*
|
|
79
|
+
* `test/loader.test.ts` pins the loader's load targets and checks `LOADER_PLATFORMS` against them.
|
|
80
|
+
* So a CLI bump that adds a candidate fails there before this module can miss it.
|
|
81
|
+
*/
|
|
82
|
+
/** The loader's override variable: a path it `require`s instead of probing (binding.cjs `requireNative`). */
|
|
83
|
+
const OVERRIDE = "NAPI_RS_NATIVE_LIBRARY_PATH";
|
|
84
|
+
/** The addon hash map next to this file (step a): `{ "<platform>": "<SHA-256, 64 lowercase hex>" }`. */
|
|
85
|
+
const PINS = "./addon-sha256.json";
|
|
86
|
+
/** The `code` of every load refusal (step c's mismatch included). */
|
|
87
|
+
const REFUSED = "ERR_POLYJOSE_ADDON_REFUSED";
|
|
88
|
+
/** The loader's WASI switches: `NAPI_RS_FORCE_WASI` (`true`, `error`) and `NAPI_RS_WASI_FLAVOR`. */
|
|
89
|
+
const WASI_VARIABLES = ["NAPI_RS_FORCE_WASI", "NAPI_RS_WASI_FLAVOR"];
|
|
90
|
+
/**
|
|
91
|
+
* Everything the loader's WASI chain resolves before it loads (binding.cjs,
|
|
92
|
+
* `__napiWasiResolveCandidate`): the local loader and the package, whose `package.json` alone
|
|
93
|
+
* already proves it is installed.
|
|
94
|
+
*/
|
|
95
|
+
const WASI_CANDIDATES = [
|
|
96
|
+
"./polyjose.wasi.cjs",
|
|
97
|
+
"@polyjose/node-wasm32-wasi",
|
|
98
|
+
"@polyjose/node-wasm32-wasi/package.json",
|
|
99
|
+
];
|
|
100
|
+
/** The eight platforms polyjose publishes an addon for (ADR 0031 §2): the only keys a map may have. */
|
|
101
|
+
const PUBLISHED_PLATFORMS = [
|
|
102
|
+
"darwin-x64",
|
|
103
|
+
"darwin-arm64",
|
|
104
|
+
"linux-x64-gnu",
|
|
105
|
+
"linux-arm64-gnu",
|
|
106
|
+
"linux-x64-musl",
|
|
107
|
+
"linux-arm64-musl",
|
|
108
|
+
"win32-x64-msvc",
|
|
109
|
+
"win32-arm64-msvc",
|
|
110
|
+
];
|
|
111
|
+
/**
|
|
112
|
+
* Every platform name the generated loader probes: the eight polyjose publishes (ADR 0031 §2) and
|
|
113
|
+
* the napi-rs template's others, which are never published. `test/loader.test.ts` checks that this
|
|
114
|
+
* is exactly the loader's set.
|
|
115
|
+
*/
|
|
116
|
+
export const LOADER_PLATFORMS = [
|
|
117
|
+
"darwin-x64",
|
|
118
|
+
"darwin-arm64",
|
|
119
|
+
"linux-x64-gnu",
|
|
120
|
+
"linux-arm64-gnu",
|
|
121
|
+
"linux-x64-musl",
|
|
122
|
+
"linux-arm64-musl",
|
|
123
|
+
"win32-x64-msvc",
|
|
124
|
+
"win32-arm64-msvc",
|
|
125
|
+
"android-arm64",
|
|
126
|
+
"android-arm-eabi",
|
|
127
|
+
"win32-x64-gnu",
|
|
128
|
+
"win32-ia32-msvc",
|
|
129
|
+
"darwin-universal",
|
|
130
|
+
"freebsd-x64",
|
|
131
|
+
"freebsd-arm64",
|
|
132
|
+
"freebsd-ppc64",
|
|
133
|
+
"linux-arm-musleabihf",
|
|
134
|
+
"linux-arm-gnueabihf",
|
|
135
|
+
"linux-loong64-musl",
|
|
136
|
+
"linux-loong64-gnu",
|
|
137
|
+
"linux-riscv64-musl",
|
|
138
|
+
"linux-riscv64-gnu",
|
|
139
|
+
"linux-ppc64-gnu",
|
|
140
|
+
"linux-s390x-gnu",
|
|
141
|
+
"openharmony-arm64",
|
|
142
|
+
"openharmony-x64",
|
|
143
|
+
"openharmony-arm",
|
|
144
|
+
];
|
|
145
|
+
/**
|
|
146
|
+
* A load refusal, with `code` `ERR_POLYJOSE_ADDON_REFUSED`. It names a variable, a specifier, a
|
|
147
|
+
* platform, a version or a SHA-256; never a path's contents.
|
|
148
|
+
*/
|
|
149
|
+
function refuse(reason) {
|
|
150
|
+
throw Object.assign(new Error(`@polyjose/node: ${reason}`), { code: REFUSED });
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Where `id` resolves from the loader's directory: the file, or `null` when it is absent. Only
|
|
154
|
+
* `MODULE_NOT_FOUND` means absent. Any other failure, such as an `exports` restriction or an
|
|
155
|
+
* unreadable directory, is a refusal.
|
|
156
|
+
*/
|
|
157
|
+
function resolved(req, id) {
|
|
158
|
+
try {
|
|
159
|
+
return req.resolve(id);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (err?.code === "MODULE_NOT_FOUND")
|
|
163
|
+
return null;
|
|
164
|
+
return refuse(`${id} cannot be resolved cleanly, so it cannot be checked; refusing to load`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Steps 1-3 of the module comment: every module the loader could pick is a native addon. */
|
|
168
|
+
function checkCandidates(host, req) {
|
|
169
|
+
for (const name of WASI_VARIABLES) {
|
|
170
|
+
if (host.env[name] !== undefined) {
|
|
171
|
+
refuse(`${name} is set, but polyjose publishes no WASI build; unset it to load the native addon`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const id of WASI_CANDIDATES) {
|
|
175
|
+
if (resolved(req, id) !== null) {
|
|
176
|
+
refuse(`${id} resolves next to the loader, but polyjose publishes no WASI build; remove it`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// The loader tests the override for truthiness, so an empty value means "not set".
|
|
180
|
+
const override = host.env[OVERRIDE];
|
|
181
|
+
if (override !== undefined && override !== "") {
|
|
182
|
+
const file = resolved(req, override);
|
|
183
|
+
if (file === null || !file.endsWith(".node")) {
|
|
184
|
+
refuse("NAPI_RS_NATIVE_LIBRARY_PATH does not name a native .node addon; refusing to load");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const platform of LOADER_PLATFORMS) {
|
|
188
|
+
for (const id of [`./polyjose.${platform}.node`, `@polyjose/node-${platform}`]) {
|
|
189
|
+
const file = resolved(req, id);
|
|
190
|
+
if (file !== null && !file.endsWith(".node")) {
|
|
191
|
+
refuse(`${id} resolves to a file that is not a native .node addon; refusing to load`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const manifest = resolved(req, `@polyjose/node-${platform}/package.json`);
|
|
195
|
+
if (manifest !== null && !/[\\/]package\.json$/.test(manifest)) {
|
|
196
|
+
refuse(`@polyjose/node-${platform}/package.json resolves to another file; refusing to load`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The loader's platform name for this host, as binding.cjs `requireNative` computes it for the
|
|
202
|
+
* eight published platforms; any other host gets `<platform>-<arch>`, which no map lists.
|
|
203
|
+
*/
|
|
204
|
+
function hostPlatform(host, fs) {
|
|
205
|
+
const { platform, arch } = host;
|
|
206
|
+
if (arch !== "x64" && arch !== "arm64")
|
|
207
|
+
return `${platform}-${arch}`;
|
|
208
|
+
if (platform === "darwin")
|
|
209
|
+
return `darwin-${arch}`;
|
|
210
|
+
if (platform === "linux")
|
|
211
|
+
return `linux-${arch}-${isMusl(host, fs) ? "musl" : "gnu"}`;
|
|
212
|
+
if (platform === "win32") {
|
|
213
|
+
// The loader sends these Windows x64 hosts to win32-x64-gnu, which polyjose doesn't publish.
|
|
214
|
+
const v = host.config?.variables;
|
|
215
|
+
if (arch === "x64" && (v?.shlib_suffix === "dll.a" || v?.node_target_type === "shared_library"))
|
|
216
|
+
return "win32-x64-gnu";
|
|
217
|
+
return `win32-${arch}-msvc`;
|
|
218
|
+
}
|
|
219
|
+
return `${platform}-${arch}`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* musl or glibc, decided as the loader's `isMusl` decides it, in its order: `/usr/bin/ldd` names
|
|
223
|
+
* musl or not; else the process report (setting `excludeNetwork` first, as the loader does): a
|
|
224
|
+
* glibc runtime version means glibc, a `libc.musl-`/`ld-musl-` shared object means musl. The
|
|
225
|
+
* loader's last resort, `ldd --version` in a child process, is not run: without a report this
|
|
226
|
+
* refuses instead of guessing.
|
|
227
|
+
*/
|
|
228
|
+
function isMusl(host, fs) {
|
|
229
|
+
try {
|
|
230
|
+
return fs.readFileSync("/usr/bin/ldd", "utf8").includes("musl");
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Unreadable: ask the process report, as the loader does.
|
|
234
|
+
}
|
|
235
|
+
const report = host.report;
|
|
236
|
+
if (report === undefined || typeof report.getReport !== "function") {
|
|
237
|
+
return refuse("cannot tell glibc from musl (no /usr/bin/ldd, no process report); refusing to load");
|
|
238
|
+
}
|
|
239
|
+
report.excludeNetwork = true;
|
|
240
|
+
const r = report.getReport();
|
|
241
|
+
if (r === null || typeof r !== "object")
|
|
242
|
+
return refuse("cannot tell glibc from musl (empty process report)");
|
|
243
|
+
if (r.header?.glibcVersionRuntime)
|
|
244
|
+
return false;
|
|
245
|
+
const objects = r.sharedObjects;
|
|
246
|
+
const musl = (f) => typeof f === "string" && (f.includes("libc.musl-") || f.includes("ld-musl-"));
|
|
247
|
+
return Array.isArray(objects) && objects.some(musl);
|
|
248
|
+
}
|
|
249
|
+
/** Step a: the package's addon hash map, validated, as a null-prototype record. */
|
|
250
|
+
function readPins(req, fs) {
|
|
251
|
+
const file = resolved(req, PINS);
|
|
252
|
+
if (file === null) {
|
|
253
|
+
return refuse("addon-sha256.json is missing, so no native addon is trusted; this package was not assembled by polyjose's build");
|
|
254
|
+
}
|
|
255
|
+
let parsed;
|
|
256
|
+
try {
|
|
257
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return refuse("addon-sha256.json is not valid JSON; refusing to load");
|
|
261
|
+
}
|
|
262
|
+
const pins = Object.create(null);
|
|
263
|
+
const valid = typeof parsed === "object" &&
|
|
264
|
+
parsed !== null &&
|
|
265
|
+
!Array.isArray(parsed) &&
|
|
266
|
+
Object.entries(parsed).every(([platform, hash]) => {
|
|
267
|
+
if (!PUBLISHED_PLATFORMS.includes(platform) || typeof hash !== "string" || !/^[0-9a-f]{64}$/.test(hash))
|
|
268
|
+
return false;
|
|
269
|
+
pins[platform] = hash;
|
|
270
|
+
return true;
|
|
271
|
+
});
|
|
272
|
+
if (!valid) {
|
|
273
|
+
return refuse("addon-sha256.json is not a map of published platforms to lowercase SHA-256 hex; refusing to load");
|
|
274
|
+
}
|
|
275
|
+
return pins;
|
|
276
|
+
}
|
|
277
|
+
/** SHA-256 (FIPS 180-4) of `bytes`, lowercase hex. */
|
|
278
|
+
function sha256(crypto, bytes) {
|
|
279
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
280
|
+
}
|
|
281
|
+
/** Step b: the one file the loader would load on this host, and how it was named. */
|
|
282
|
+
function pickSource(host, req, platform) {
|
|
283
|
+
const override = host.env[OVERRIDE];
|
|
284
|
+
const overridden = override !== undefined && override !== "";
|
|
285
|
+
const ids = overridden ? [override] : [`./polyjose.${platform}.node`, `@polyjose/node-${platform}`];
|
|
286
|
+
for (const id of ids) {
|
|
287
|
+
const file = resolved(req, id);
|
|
288
|
+
if (file !== null)
|
|
289
|
+
return { file, name: overridden ? OVERRIDE : id };
|
|
290
|
+
}
|
|
291
|
+
return refuse(`no native addon for ${platform} is installed (@polyjose/node-${platform}); reinstall without --omit=optional`);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Steps a-d: loads `binding.cjs` with the override pointing at a private, hash-checked copy of
|
|
295
|
+
* the addon. Returns what it loaded and the copy's path (the CommonJS cache key, step 4).
|
|
296
|
+
*/
|
|
297
|
+
function loadPinned(host, req, b) {
|
|
298
|
+
const pins = readPins(req, b.fs);
|
|
299
|
+
const platform = hostPlatform(host, b.fs);
|
|
300
|
+
const expected = pins[platform];
|
|
301
|
+
if (expected === undefined) {
|
|
302
|
+
const listed = Object.keys(pins).sort().join(", ");
|
|
303
|
+
return refuse(`this package has no pinned SHA-256 for ${platform} (addon-sha256.json lists ${listed || "none"}); refusing to load`);
|
|
304
|
+
}
|
|
305
|
+
const source = pickSource(host, req, platform);
|
|
306
|
+
const bytes = b.fs.readFileSync(source.file);
|
|
307
|
+
const actual = sha256(b.crypto, bytes);
|
|
308
|
+
if (actual !== expected) {
|
|
309
|
+
refuse(`${source.name} for ${platform} has SHA-256 ${actual}, but this package pins ${expected}; refusing to load it`);
|
|
310
|
+
}
|
|
311
|
+
// The real path, as the CommonJS cache keys it (macOS's tmpdir is a symlink).
|
|
312
|
+
const dir = b.fs.realpathSync(b.fs.mkdtempSync(b.path.join(b.os.tmpdir(), "polyjose-addon-")));
|
|
313
|
+
try {
|
|
314
|
+
const copy = b.path.join(dir, `polyjose-${b.crypto.randomBytes(16).toString("hex")}.node`);
|
|
315
|
+
b.fs.writeFileSync(copy, bytes, { flag: "wx", mode: 0o700 });
|
|
316
|
+
if (sha256(b.crypto, b.fs.readFileSync(copy)) !== expected) {
|
|
317
|
+
refuse(`the private copy of the addon for ${platform} does not have the pinned SHA-256; refusing to load it`);
|
|
318
|
+
}
|
|
319
|
+
const previous = host.env[OVERRIDE];
|
|
320
|
+
host.env[OVERRIDE] = copy;
|
|
321
|
+
try {
|
|
322
|
+
return { loaded: req("./binding.cjs"), copy };
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
if (previous === undefined)
|
|
326
|
+
delete host.env[OVERRIDE];
|
|
327
|
+
else
|
|
328
|
+
host.env[OVERRIDE] = previous;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
try {
|
|
333
|
+
b.fs.rmSync(dir, { recursive: true, force: true });
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Windows can't delete a DLL while it is loaded; the copy stays in the temporary directory.
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/** Steps 4-6 of the module comment: the loaded binding is this release's native addon. */
|
|
341
|
+
function checkLoaded(req, loaded, copy) {
|
|
342
|
+
if (typeof loaded !== "object" || loaded === null) {
|
|
343
|
+
return refuse("the loader returned no binding object");
|
|
344
|
+
}
|
|
345
|
+
const owners = Object.keys(req.cache).filter((file) => req.cache[file]?.exports === loaded);
|
|
346
|
+
const addons = owners.filter((file) => file.endsWith(".node"));
|
|
347
|
+
// A second copy of this package (another path, same platform package) has its own binding.cjs.
|
|
348
|
+
const others = owners.filter((file) => !file.endsWith(".node") && !/[\\/]binding\.cjs$/.test(file));
|
|
349
|
+
if (addons.length !== 1 || addons[0] !== copy || others.length !== 0) {
|
|
350
|
+
refuse("the loaded binding is not exactly the hash-checked native addon; refusing to load");
|
|
351
|
+
}
|
|
352
|
+
if (loaded.__napiBindingTarget !== "native") {
|
|
353
|
+
refuse('the loaded binding is not the native addon (__napiBindingTarget is not "native")');
|
|
354
|
+
}
|
|
355
|
+
const expected = req("./package.json").version;
|
|
356
|
+
const actual = typeof loaded.version === "function" ? loaded.version() : undefined;
|
|
357
|
+
if (typeof expected !== "string" || actual !== expected) {
|
|
358
|
+
refuse(`the native addon is version ${String(actual)}, but this package is ${String(expected)}; reinstall it`);
|
|
359
|
+
}
|
|
360
|
+
return loaded;
|
|
361
|
+
}
|
|
362
|
+
/** Loads `binding.cjs` only when it can load nothing but the pinned native addon (the module comment). */
|
|
363
|
+
function loadBinding(host, moduleUrl) {
|
|
364
|
+
// backend.js and binding.cjs are siblings, so this resolves exactly as the loader does.
|
|
365
|
+
const req = host.getBuiltinModule("node:module").createRequire(moduleUrl);
|
|
366
|
+
checkCandidates(host, req);
|
|
367
|
+
const builtins = {
|
|
368
|
+
fs: host.getBuiltinModule("node:fs"),
|
|
369
|
+
crypto: host.getBuiltinModule("node:crypto"),
|
|
370
|
+
os: host.getBuiltinModule("node:os"),
|
|
371
|
+
path: host.getBuiltinModule("node:path"),
|
|
372
|
+
};
|
|
373
|
+
const { loaded, copy } = loadPinned(host, req, builtins);
|
|
374
|
+
return checkLoaded(req, loaded, copy);
|
|
375
|
+
}
|
|
376
|
+
const binding = loadBinding(globalThis.process, import.meta.url);
|
|
377
|
+
/** The napi loader, typed as the facade's backend. */
|
|
378
|
+
export const backend = binding;
|