@polyjose/node 0.1.1 → 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/backend.js CHANGED
@@ -1,119 +1,63 @@
1
1
  /**
2
- * The napi-rs backend of `@polyjose/node` (ADR 0031 §1-§2, `bindings/js/BACKEND.md`).
2
+ * The napi-rs backend of `@polyjose/node` (ADR 0031 §1-§2, `bindings/js/BACKEND.md`; packaging as
3
+ * superseded by the feature 0017 ADR: one package that ships the addon of all eight targets).
3
4
  *
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).
5
+ * The native addon `polyjose.<platform>.node` already is the backend contract: one function per
6
+ * ADR 0030 §4 operation, camelCase, in the ADR's argument order, rejecting with an `Error` whose
7
+ * `code` is one of `errorCodes()` (ADR 0030 §5). Its types are the `binding.d.ts` that
8
+ * `napi build --dts` generates. So this module only picks the addon for this host, authenticates
9
+ * it, loads it and gives it the facade's `Backend` type; it adds no JOSE logic and renames nothing
10
+ * (ADR 0030 §1: loading and integrity checks are entry idiom).
9
11
  *
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`.
12
+ * **No resolving loader (feature 0017; review-security.md M1, feature 0007 F02).** napi-rs's
13
+ * generated loader picks the first module that loads from an override variable, the package
14
+ * directory, every ancestor `node_modules`, `NODE_PATH` and a WASI chain, so a JavaScript or native
15
+ * file planted in any of them could become the crypto backend and "verify" any token. polyjose
16
+ * builds no such loader (`napi build --no-js`) and resolves no module name at all: this file reads
17
+ * only files of its own package directory, `dirname(import.meta.url)`, by absolute path. It never
18
+ * reads `process.env`, so neither `NODE_PATH` nor any `NAPI_RS_*` variable changes what loads.
13
19
  *
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
20
+ * **The steps (the F07 hash-pin model, bindings security audit N-M1, the JVM's 0008 F05 bar):**
21
+ * 1. The platform name (`detectPlatform`): `darwin-<arch>`, `win32-<arch>-msvc`, or
22
+ * `linux-<arch>-gnu|musl` for x64 and arm64, with napi-rs's names. Any other host is refused
23
+ * by name, pointing at `@polyjose/wasm`, which runs everywhere.
24
+ * 2. `addon-sha256.json`, in the package directory, maps each shipped platform to its addon's
25
+ * SHA-256 (FIPS 180-4). It is generated, never hand-edited: `scripts/addon-hashes.sh`, run by
26
+ * `scripts/build.sh` for a local build and by `scripts/pack-main.sh` over the eight addons the
27
+ * package ships. No map, a malformed one, or no entry for this host refuses the load: nothing
28
+ * is trusted by default.
29
+ * 3. It reads `polyjose.<platform>.node` from the package directory once, hashes those bytes with
30
+ * `node:crypto` and refuses on a mismatch, naming the platform, the file and both hashes.
31
+ * There is no fallback to another file.
32
+ * 4. It writes the same bytes, with `O_EXCL`, into a new directory from `fs.mkdtempSync` under
57
33
  * `os.tmpdir()` (mode 0700 on POSIX; on Windows the per-user `%TEMP%` ACL), under a random
58
34
  * 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).
35
+ * copy's SHA-256 again, and `require`s that absolute path, which Node's `.node` handler hands
36
+ * to `dlopen`. So between the check and `dlopen` only this user (who can already change
37
+ * anything they run) could swap the file; whoever can write the installed package can't. The
38
+ * copy is deleted right after (a loaded library stays mapped; Windows can't delete a loaded
39
+ * DLL, so there the copy stays in the temporary directory).
40
+ * 5. What loaded must be the exports of exactly that one module in the CommonJS cache, the
41
+ * private copy, so no JavaScript module wraps or re-exports it.
42
+ * 6. It must be this package's version: `version()` equals `package.json`'s version, the
43
+ * workspace version every release keeps in step (feature 0007 T04), so an addon of another
44
+ * release is refused.
75
45
  * Every refusal is an `Error` with `code` `ERR_POLYJOSE_ADDON_REFUSED`. This doesn't defend against
76
46
  * code already running in the process, which can patch anything. It makes sure the package never
77
47
  * selects a substitute.
78
48
  *
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.
49
+ * `test/loader.test.ts` pins what this module loads (its built-ins and its one `require`), the
50
+ * platform detection, and each refusal.
81
51
  */
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). */
52
+ /** The addon hash map in the package directory (step 2): `{ "<platform>": "<SHA-256, 64 lowercase hex>" }`. */
53
+ const PINS = "addon-sha256.json";
54
+ /** The `code` of every load refusal. */
87
55
  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
56
  /**
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.
57
+ * The eight platforms whose addon this package ships (ADR 0031 §2's targets): the only keys a map
58
+ * may have. `test/loader.test.ts` checks them against package.json's `napi.targets` and `files`.
115
59
  */
116
- export const LOADER_PLATFORMS = [
60
+ export const PLATFORMS = [
117
61
  "darwin-x64",
118
62
  "darwin-arm64",
119
63
  "linux-x64-gnu",
@@ -122,139 +66,82 @@ export const LOADER_PLATFORMS = [
122
66
  "linux-arm64-musl",
123
67
  "win32-x64-msvc",
124
68
  "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
69
  ];
70
+ const SHIPPED = PLATFORMS;
145
71
  /**
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.
72
+ * A load refusal, with `code` `ERR_POLYJOSE_ADDON_REFUSED`. It names a platform, a file of the
73
+ * package, a version or a SHA-256; never a file's contents.
148
74
  */
149
75
  function refuse(reason) {
150
76
  throw Object.assign(new Error(`@polyjose/node: ${reason}`), { code: REFUSED });
151
77
  }
152
78
  /**
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.
79
+ * Step 1: this host's platform name, napi-rs's (`napi build --platform` names the addon after it):
80
+ * `darwin-x64|arm64`, `win32-x64|arm64-msvc`, `linux-x64|arm64-gnu|musl`. Any other platform or
81
+ * architecture is `<platform>-<arch>`, which `PLATFORMS` does not list. On Linux it reads
82
+ * /usr/bin/ldd and may take a process report (`isMusl`); it refuses when it cannot tell the libc.
156
83
  */
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) {
84
+ export function detectPlatform(host, fs) {
205
85
  const { platform, arch } = host;
206
86
  if (arch !== "x64" && arch !== "arm64")
207
87
  return `${platform}-${arch}`;
208
88
  if (platform === "darwin")
209
89
  return `darwin-${arch}`;
90
+ if (platform === "win32")
91
+ return `win32-${arch}-msvc`;
210
92
  if (platform === "linux")
211
93
  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
94
  return `${platform}-${arch}`;
220
95
  }
221
96
  /**
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.
97
+ * musl or glibc, decided in napi-rs's order (its generated `isMusl`): `/usr/bin/ldd` names musl
98
+ * or not (glibc ships ldd as a script of its own, musl's runs `ld-musl-*`); if it cannot be read,
99
+ * the process report, taken with `excludeNetwork` set (restored after): a glibc runtime version
100
+ * means glibc, else a `libc.musl-`/`ld-musl-` shared object means musl. napi-rs's last resort,
101
+ * `ldd --version` in a child process, is not run: without a usable report this refuses instead
102
+ * of guessing.
227
103
  */
228
104
  function isMusl(host, fs) {
229
105
  try {
230
106
  return fs.readFileSync("/usr/bin/ldd", "utf8").includes("musl");
231
107
  }
232
108
  catch {
233
- // Unreadable: ask the process report, as the loader does.
109
+ // Unreadable: ask the process report, as napi-rs does.
234
110
  }
235
111
  const report = host.report;
236
112
  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");
113
+ return refuse(`cannot tell glibc from musl on linux-${host.arch} (no /usr/bin/ldd, no process report); refusing to load`);
238
114
  }
115
+ const before = report.excludeNetwork;
239
116
  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)");
117
+ let r;
118
+ try {
119
+ r = report.getReport();
120
+ }
121
+ finally {
122
+ report.excludeNetwork = before;
123
+ }
124
+ if (r === null || typeof r !== "object") {
125
+ return refuse(`cannot tell glibc from musl on linux-${host.arch} (empty process report); refusing to load`);
126
+ }
243
127
  if (r.header?.glibcVersionRuntime)
244
128
  return false;
245
129
  const objects = r.sharedObjects;
246
130
  const musl = (f) => typeof f === "string" && (f.includes("libc.musl-") || f.includes("ld-musl-"));
247
131
  return Array.isArray(objects) && objects.some(musl);
248
132
  }
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) {
133
+ /** Step 2: the package's addon hash map, validated, as a null-prototype record. */
134
+ function readPins(fs, file) {
135
+ let text;
136
+ try {
137
+ text = fs.readFileSync(file, "utf8");
138
+ }
139
+ catch {
253
140
  return refuse("addon-sha256.json is missing, so no native addon is trusted; this package was not assembled by polyjose's build");
254
141
  }
255
142
  let parsed;
256
143
  try {
257
- parsed = JSON.parse(fs.readFileSync(file, "utf8"));
144
+ parsed = JSON.parse(text);
258
145
  }
259
146
  catch {
260
147
  return refuse("addon-sha256.json is not valid JSON; refusing to load");
@@ -264,13 +151,13 @@ function readPins(req, fs) {
264
151
  parsed !== null &&
265
152
  !Array.isArray(parsed) &&
266
153
  Object.entries(parsed).every(([platform, hash]) => {
267
- if (!PUBLISHED_PLATFORMS.includes(platform) || typeof hash !== "string" || !/^[0-9a-f]{64}$/.test(hash))
154
+ if (!SHIPPED.includes(platform) || typeof hash !== "string" || !/^[0-9a-f]{64}$/.test(hash))
268
155
  return false;
269
156
  pins[platform] = hash;
270
157
  return true;
271
158
  });
272
159
  if (!valid) {
273
- return refuse("addon-sha256.json is not a map of published platforms to lowercase SHA-256 hex; refusing to load");
160
+ return refuse("addon-sha256.json is not a map of shipped platforms to lowercase SHA-256 hex; refusing to load");
274
161
  }
275
162
  return pins;
276
163
  }
@@ -278,101 +165,89 @@ function readPins(req, fs) {
278
165
  function sha256(crypto, bytes) {
279
166
  return crypto.createHash("sha256").update(bytes).digest("hex");
280
167
  }
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
168
  /**
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).
169
+ * Steps 1-4: requires a private, hash-checked copy of this package's addon for this host. Returns
170
+ * what it loaded and the copy's path (the CommonJS cache key, step 5).
296
171
  */
297
- function loadPinned(host, req, b) {
298
- const pins = readPins(req, b.fs);
299
- const platform = hostPlatform(host, b.fs);
172
+ function loadPinned(host, req, b, dir) {
173
+ const platform = detectPlatform(host, b.fs);
174
+ if (!SHIPPED.includes(platform)) {
175
+ return refuse(`there is no native addon for ${platform} (this package ships ${SHIPPED.join(", ")}); ` +
176
+ "use @polyjose/wasm, which has the same API and runs on every platform");
177
+ }
178
+ const pins = readPins(b.fs, b.path.join(dir, PINS));
300
179
  const expected = pins[platform];
301
180
  if (expected === undefined) {
302
181
  const listed = Object.keys(pins).sort().join(", ");
303
182
  return refuse(`this package has no pinned SHA-256 for ${platform} (addon-sha256.json lists ${listed || "none"}); refusing to load`);
304
183
  }
305
- const source = pickSource(host, req, platform);
306
- const bytes = b.fs.readFileSync(source.file);
184
+ const name = `polyjose.${platform}.node`;
185
+ let bytes;
186
+ try {
187
+ bytes = b.fs.readFileSync(b.path.join(dir, name));
188
+ }
189
+ catch {
190
+ return refuse(`./${name} is missing from this package or cannot be read; reinstall @polyjose/node`);
191
+ }
307
192
  const actual = sha256(b.crypto, bytes);
308
193
  if (actual !== expected) {
309
- refuse(`${source.name} for ${platform} has SHA-256 ${actual}, but this package pins ${expected}; refusing to load it`);
194
+ refuse(`./${name} for ${platform} has SHA-256 ${actual}, but this package pins ${expected}; refusing to load it`);
310
195
  }
311
196
  // 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-")));
197
+ const tmp = b.fs.realpathSync(b.fs.mkdtempSync(b.path.join(b.os.tmpdir(), "polyjose-addon-")));
313
198
  try {
314
- const copy = b.path.join(dir, `polyjose-${b.crypto.randomBytes(16).toString("hex")}.node`);
199
+ const copy = b.path.join(tmp, `polyjose-${b.crypto.randomBytes(16).toString("hex")}.node`);
315
200
  b.fs.writeFileSync(copy, bytes, { flag: "wx", mode: 0o700 });
316
201
  if (sha256(b.crypto, b.fs.readFileSync(copy)) !== expected) {
317
202
  refuse(`the private copy of the addon for ${platform} does not have the pinned SHA-256; refusing to load it`);
318
203
  }
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
- }
204
+ return { loaded: req(copy), copy };
330
205
  }
331
206
  finally {
332
207
  try {
333
- b.fs.rmSync(dir, { recursive: true, force: true });
208
+ b.fs.rmSync(tmp, { recursive: true, force: true });
334
209
  }
335
210
  catch {
336
211
  // Windows can't delete a DLL while it is loaded; the copy stays in the temporary directory.
337
212
  }
338
213
  }
339
214
  }
340
- /** Steps 4-6 of the module comment: the loaded binding is this release's native addon. */
341
- function checkLoaded(req, loaded, copy) {
215
+ /** Steps 5-6: the loaded binding is the private copy, at this package's version. */
216
+ function checkLoaded(req, b, dir, loaded, copy) {
342
217
  if (typeof loaded !== "object" || loaded === null) {
343
- return refuse("the loader returned no binding object");
218
+ return refuse("the native addon returned no binding object");
344
219
  }
345
220
  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) {
221
+ if (owners.length !== 1 || owners[0] !== copy) {
350
222
  refuse("the loaded binding is not exactly the hash-checked native addon; refusing to load");
351
223
  }
352
- if (loaded.__napiBindingTarget !== "native") {
353
- refuse('the loaded binding is not the native addon (__napiBindingTarget is not "native")');
224
+ let expected;
225
+ try {
226
+ expected = JSON.parse(b.fs.readFileSync(b.path.join(dir, "package.json"), "utf8")).version;
227
+ }
228
+ catch {
229
+ expected = undefined;
354
230
  }
355
- const expected = req("./package.json").version;
356
231
  const actual = typeof loaded.version === "function" ? loaded.version() : undefined;
357
232
  if (typeof expected !== "string" || actual !== expected) {
358
233
  refuse(`the native addon is version ${String(actual)}, but this package is ${String(expected)}; reinstall it`);
359
234
  }
360
235
  return loaded;
361
236
  }
362
- /** Loads `binding.cjs` only when it can load nothing but the pinned native addon (the module comment). */
237
+ /** Loads this package's pinned native addon for this host (the module comment). */
363
238
  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
239
  const builtins = {
368
240
  fs: host.getBuiltinModule("node:fs"),
369
241
  crypto: host.getBuiltinModule("node:crypto"),
370
242
  os: host.getBuiltinModule("node:os"),
371
243
  path: host.getBuiltinModule("node:path"),
372
244
  };
373
- const { loaded, copy } = loadPinned(host, req, builtins);
374
- return checkLoaded(req, loaded, copy);
245
+ // The package directory: backend.js sits at its root, next to the addons and the pins.
246
+ const dir = builtins.path.dirname(host.getBuiltinModule("node:url").fileURLToPath(moduleUrl));
247
+ const req = host.getBuiltinModule("node:module").createRequire(moduleUrl);
248
+ const { loaded, copy } = loadPinned(host, req, builtins, dir);
249
+ return checkLoaded(req, builtins, dir, loaded, copy);
375
250
  }
376
251
  const binding = loadBinding(globalThis.process, import.meta.url);
377
- /** The napi loader, typed as the facade's backend. */
252
+ /** The native addon, typed as the facade's backend. */
378
253
  export const backend = binding;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polyjose/node",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "polyjose for Node.js: JOSE (JWS, JWE, JWK, JWT) in pure Rust through Node-API, with the shared TypeScript facade (ADR 0031, ADR 0047).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,8 +26,14 @@
26
26
  "backend.d.ts",
27
27
  "facade/*.js",
28
28
  "facade/*.d.ts",
29
- "binding.cjs",
30
- "binding.d.ts",
29
+ "polyjose.darwin-x64.node",
30
+ "polyjose.darwin-arm64.node",
31
+ "polyjose.linux-x64-gnu.node",
32
+ "polyjose.linux-arm64-gnu.node",
33
+ "polyjose.linux-x64-musl.node",
34
+ "polyjose.linux-arm64-musl.node",
35
+ "polyjose.win32-x64-msvc.node",
36
+ "polyjose.win32-arm64-msvc.node",
31
37
  "addon-sha256.json",
32
38
  "LICENSE",
33
39
  "README.md",
@@ -52,15 +58,5 @@
52
58
  },
53
59
  "devDependencies": {
54
60
  "@napi-rs/cli": "3.10.12"
55
- },
56
- "optionalDependencies": {
57
- "@polyjose/node-darwin-arm64": "0.1.1",
58
- "@polyjose/node-darwin-x64": "0.1.1",
59
- "@polyjose/node-linux-arm64-gnu": "0.1.1",
60
- "@polyjose/node-linux-arm64-musl": "0.1.1",
61
- "@polyjose/node-linux-x64-gnu": "0.1.1",
62
- "@polyjose/node-linux-x64-musl": "0.1.1",
63
- "@polyjose/node-win32-arm64-msvc": "0.1.1",
64
- "@polyjose/node-win32-x64-msvc": "0.1.1"
65
61
  }
66
62
  }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file