@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/README.md +80 -137
- package/THIRD-PARTY-NOTICES.txt +15966 -40
- package/addon-sha256.json +8 -8
- package/backend.d.ts +71 -75
- package/backend.js +124 -249
- package/package.json +9 -13
- package/polyjose.darwin-arm64.node +0 -0
- package/polyjose.darwin-x64.node +0 -0
- package/polyjose.linux-arm64-gnu.node +0 -0
- package/polyjose.linux-arm64-musl.node +0 -0
- package/polyjose.linux-x64-gnu.node +0 -0
- package/polyjose.linux-x64-musl.node +0 -0
- package/polyjose.win32-arm64-msvc.node +0 -0
- package/polyjose.win32-x64-msvc.node +0 -0
- package/binding.cjs +0 -869
- package/binding.d.ts +0 -469
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
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
|
-
* **
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* It
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
|
80
|
-
*
|
|
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
|
|
83
|
-
const
|
|
84
|
-
/** The
|
|
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
|
-
*
|
|
113
|
-
*
|
|
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
|
|
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
|
|
147
|
-
*
|
|
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
|
-
*
|
|
154
|
-
* `
|
|
155
|
-
*
|
|
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
|
|
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
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* refuses instead
|
|
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
|
|
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(
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
250
|
-
function readPins(
|
|
251
|
-
|
|
252
|
-
|
|
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(
|
|
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 (!
|
|
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
|
|
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
|
|
295
|
-
*
|
|
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
|
|
299
|
-
|
|
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
|
|
306
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
|
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
|
-
|
|
374
|
-
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
30
|
-
"
|
|
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
|