@polyjose/node 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -2
- package/THIRD-PARTY-NOTICES.txt +16006 -0
- package/addon-sha256.json +10 -0
- package/backend.d.ts +86 -0
- package/backend.js +253 -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 +60 -4
- 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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"darwin-arm64": "2fddd1beba8c3bda8c3c6e7ebf84cb19b601ff3cf03db0c2b9ee421b17c5164e",
|
|
3
|
+
"darwin-x64": "b5a0c5b0fa18ffcb372944782f5ebaa01a1353d4734fab372db0d99b673f9866",
|
|
4
|
+
"linux-arm64-gnu": "197e9632aa6737a059789038366b0cbd3fb49ee4e8b09ec1194b7187c6f7d53a",
|
|
5
|
+
"linux-arm64-musl": "1ad5baebf6a0fa75d24ae343b9b5e180876edef836b033e8dbec5dd686df5396",
|
|
6
|
+
"linux-x64-gnu": "69c828632292671c0e3f8004b39de3fc4065cf2b734df85844280ddb35767bad",
|
|
7
|
+
"linux-x64-musl": "cb411795274b60909e022d080431b80494112df6ccf507a50538c12cab60b5d7",
|
|
8
|
+
"win32-arm64-msvc": "157a25122817918748b02eb0715d7f1163254b2575452327b358ac5cb381cd16",
|
|
9
|
+
"win32-x64-msvc": "7c2b07e6d943ea2fd3b6a96d8105e30d10489d719a614c29dba9c4f858a168d3"
|
|
10
|
+
}
|
package/backend.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
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).
|
|
4
|
+
*
|
|
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).
|
|
11
|
+
*
|
|
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.
|
|
19
|
+
*
|
|
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
|
|
33
|
+
* `os.tmpdir()` (mode 0700 on POSIX; on Windows the per-user `%TEMP%` ACL), under a random
|
|
34
|
+
* leaf name (macOS looks a leaf name up in `DYLD_LIBRARY_PATH` before the path), checks the
|
|
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.
|
|
45
|
+
* Every refusal is an `Error` with `code` `ERR_POLYJOSE_ADDON_REFUSED`. This doesn't defend against
|
|
46
|
+
* code already running in the process, which can patch anything. It makes sure the package never
|
|
47
|
+
* selects a substitute.
|
|
48
|
+
*
|
|
49
|
+
* `test/loader.test.ts` pins what this module loads (its built-ins and its one `require`), the
|
|
50
|
+
* platform detection, and each refusal.
|
|
51
|
+
*/
|
|
52
|
+
import type { Backend } from "./facade/backend.js";
|
|
53
|
+
/** The one `fs` call platform detection makes (so tests can fake it). */
|
|
54
|
+
interface TextReader {
|
|
55
|
+
readFileSync(path: string, encoding: "utf8"): string;
|
|
56
|
+
}
|
|
57
|
+
interface ProcessReport {
|
|
58
|
+
excludeNetwork: boolean;
|
|
59
|
+
getReport(): {
|
|
60
|
+
readonly header?: {
|
|
61
|
+
readonly glibcVersionRuntime?: unknown;
|
|
62
|
+
};
|
|
63
|
+
readonly sharedObjects?: unknown;
|
|
64
|
+
} | null;
|
|
65
|
+
}
|
|
66
|
+
/** What `detectPlatform` reads of `process`. */
|
|
67
|
+
interface PlatformHost {
|
|
68
|
+
readonly platform: string;
|
|
69
|
+
readonly arch: string;
|
|
70
|
+
readonly report?: ProcessReport;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The eight platforms whose addon this package ships (ADR 0031 §2's targets): the only keys a map
|
|
74
|
+
* may have. `test/loader.test.ts` checks them against package.json's `napi.targets` and `files`.
|
|
75
|
+
*/
|
|
76
|
+
export declare const PLATFORMS: readonly ["darwin-x64", "darwin-arm64", "linux-x64-gnu", "linux-arm64-gnu", "linux-x64-musl", "linux-arm64-musl", "win32-x64-msvc", "win32-arm64-msvc"];
|
|
77
|
+
/**
|
|
78
|
+
* Step 1: this host's platform name, napi-rs's (`napi build --platform` names the addon after it):
|
|
79
|
+
* `darwin-x64|arm64`, `win32-x64|arm64-msvc`, `linux-x64|arm64-gnu|musl`. Any other platform or
|
|
80
|
+
* architecture is `<platform>-<arch>`, which `PLATFORMS` does not list. On Linux it reads
|
|
81
|
+
* /usr/bin/ldd and may take a process report (`isMusl`); it refuses when it cannot tell the libc.
|
|
82
|
+
*/
|
|
83
|
+
export declare function detectPlatform(host: PlatformHost, fs: TextReader): string;
|
|
84
|
+
/** The native addon, typed as the facade's backend. */
|
|
85
|
+
export declare const backend: Backend;
|
|
86
|
+
export {};
|
package/backend.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
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).
|
|
4
|
+
*
|
|
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).
|
|
11
|
+
*
|
|
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.
|
|
19
|
+
*
|
|
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
|
|
33
|
+
* `os.tmpdir()` (mode 0700 on POSIX; on Windows the per-user `%TEMP%` ACL), under a random
|
|
34
|
+
* leaf name (macOS looks a leaf name up in `DYLD_LIBRARY_PATH` before the path), checks the
|
|
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.
|
|
45
|
+
* Every refusal is an `Error` with `code` `ERR_POLYJOSE_ADDON_REFUSED`. This doesn't defend against
|
|
46
|
+
* code already running in the process, which can patch anything. It makes sure the package never
|
|
47
|
+
* selects a substitute.
|
|
48
|
+
*
|
|
49
|
+
* `test/loader.test.ts` pins what this module loads (its built-ins and its one `require`), the
|
|
50
|
+
* platform detection, and each refusal.
|
|
51
|
+
*/
|
|
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. */
|
|
55
|
+
const REFUSED = "ERR_POLYJOSE_ADDON_REFUSED";
|
|
56
|
+
/**
|
|
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`.
|
|
59
|
+
*/
|
|
60
|
+
export const PLATFORMS = [
|
|
61
|
+
"darwin-x64",
|
|
62
|
+
"darwin-arm64",
|
|
63
|
+
"linux-x64-gnu",
|
|
64
|
+
"linux-arm64-gnu",
|
|
65
|
+
"linux-x64-musl",
|
|
66
|
+
"linux-arm64-musl",
|
|
67
|
+
"win32-x64-msvc",
|
|
68
|
+
"win32-arm64-msvc",
|
|
69
|
+
];
|
|
70
|
+
const SHIPPED = PLATFORMS;
|
|
71
|
+
/**
|
|
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.
|
|
74
|
+
*/
|
|
75
|
+
function refuse(reason) {
|
|
76
|
+
throw Object.assign(new Error(`@polyjose/node: ${reason}`), { code: REFUSED });
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
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.
|
|
83
|
+
*/
|
|
84
|
+
export function detectPlatform(host, fs) {
|
|
85
|
+
const { platform, arch } = host;
|
|
86
|
+
if (arch !== "x64" && arch !== "arm64")
|
|
87
|
+
return `${platform}-${arch}`;
|
|
88
|
+
if (platform === "darwin")
|
|
89
|
+
return `darwin-${arch}`;
|
|
90
|
+
if (platform === "win32")
|
|
91
|
+
return `win32-${arch}-msvc`;
|
|
92
|
+
if (platform === "linux")
|
|
93
|
+
return `linux-${arch}-${isMusl(host, fs) ? "musl" : "gnu"}`;
|
|
94
|
+
return `${platform}-${arch}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
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.
|
|
103
|
+
*/
|
|
104
|
+
function isMusl(host, fs) {
|
|
105
|
+
try {
|
|
106
|
+
return fs.readFileSync("/usr/bin/ldd", "utf8").includes("musl");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Unreadable: ask the process report, as napi-rs does.
|
|
110
|
+
}
|
|
111
|
+
const report = host.report;
|
|
112
|
+
if (report === undefined || typeof report.getReport !== "function") {
|
|
113
|
+
return refuse(`cannot tell glibc from musl on linux-${host.arch} (no /usr/bin/ldd, no process report); refusing to load`);
|
|
114
|
+
}
|
|
115
|
+
const before = report.excludeNetwork;
|
|
116
|
+
report.excludeNetwork = true;
|
|
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
|
+
}
|
|
127
|
+
if (r.header?.glibcVersionRuntime)
|
|
128
|
+
return false;
|
|
129
|
+
const objects = r.sharedObjects;
|
|
130
|
+
const musl = (f) => typeof f === "string" && (f.includes("libc.musl-") || f.includes("ld-musl-"));
|
|
131
|
+
return Array.isArray(objects) && objects.some(musl);
|
|
132
|
+
}
|
|
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 {
|
|
140
|
+
return refuse("addon-sha256.json is missing, so no native addon is trusted; this package was not assembled by polyjose's build");
|
|
141
|
+
}
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(text);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return refuse("addon-sha256.json is not valid JSON; refusing to load");
|
|
148
|
+
}
|
|
149
|
+
const pins = Object.create(null);
|
|
150
|
+
const valid = typeof parsed === "object" &&
|
|
151
|
+
parsed !== null &&
|
|
152
|
+
!Array.isArray(parsed) &&
|
|
153
|
+
Object.entries(parsed).every(([platform, hash]) => {
|
|
154
|
+
if (!SHIPPED.includes(platform) || typeof hash !== "string" || !/^[0-9a-f]{64}$/.test(hash))
|
|
155
|
+
return false;
|
|
156
|
+
pins[platform] = hash;
|
|
157
|
+
return true;
|
|
158
|
+
});
|
|
159
|
+
if (!valid) {
|
|
160
|
+
return refuse("addon-sha256.json is not a map of shipped platforms to lowercase SHA-256 hex; refusing to load");
|
|
161
|
+
}
|
|
162
|
+
return pins;
|
|
163
|
+
}
|
|
164
|
+
/** SHA-256 (FIPS 180-4) of `bytes`, lowercase hex. */
|
|
165
|
+
function sha256(crypto, bytes) {
|
|
166
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
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).
|
|
171
|
+
*/
|
|
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));
|
|
179
|
+
const expected = pins[platform];
|
|
180
|
+
if (expected === undefined) {
|
|
181
|
+
const listed = Object.keys(pins).sort().join(", ");
|
|
182
|
+
return refuse(`this package has no pinned SHA-256 for ${platform} (addon-sha256.json lists ${listed || "none"}); refusing to load`);
|
|
183
|
+
}
|
|
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
|
+
}
|
|
192
|
+
const actual = sha256(b.crypto, bytes);
|
|
193
|
+
if (actual !== expected) {
|
|
194
|
+
refuse(`./${name} for ${platform} has SHA-256 ${actual}, but this package pins ${expected}; refusing to load it`);
|
|
195
|
+
}
|
|
196
|
+
// The real path, as the CommonJS cache keys it (macOS's tmpdir is a symlink).
|
|
197
|
+
const tmp = b.fs.realpathSync(b.fs.mkdtempSync(b.path.join(b.os.tmpdir(), "polyjose-addon-")));
|
|
198
|
+
try {
|
|
199
|
+
const copy = b.path.join(tmp, `polyjose-${b.crypto.randomBytes(16).toString("hex")}.node`);
|
|
200
|
+
b.fs.writeFileSync(copy, bytes, { flag: "wx", mode: 0o700 });
|
|
201
|
+
if (sha256(b.crypto, b.fs.readFileSync(copy)) !== expected) {
|
|
202
|
+
refuse(`the private copy of the addon for ${platform} does not have the pinned SHA-256; refusing to load it`);
|
|
203
|
+
}
|
|
204
|
+
return { loaded: req(copy), copy };
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
try {
|
|
208
|
+
b.fs.rmSync(tmp, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Windows can't delete a DLL while it is loaded; the copy stays in the temporary directory.
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Steps 5-6: the loaded binding is the private copy, at this package's version. */
|
|
216
|
+
function checkLoaded(req, b, dir, loaded, copy) {
|
|
217
|
+
if (typeof loaded !== "object" || loaded === null) {
|
|
218
|
+
return refuse("the native addon returned no binding object");
|
|
219
|
+
}
|
|
220
|
+
const owners = Object.keys(req.cache).filter((file) => req.cache[file]?.exports === loaded);
|
|
221
|
+
if (owners.length !== 1 || owners[0] !== copy) {
|
|
222
|
+
refuse("the loaded binding is not exactly the hash-checked native addon; refusing to load");
|
|
223
|
+
}
|
|
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;
|
|
230
|
+
}
|
|
231
|
+
const actual = typeof loaded.version === "function" ? loaded.version() : undefined;
|
|
232
|
+
if (typeof expected !== "string" || actual !== expected) {
|
|
233
|
+
refuse(`the native addon is version ${String(actual)}, but this package is ${String(expected)}; reinstall it`);
|
|
234
|
+
}
|
|
235
|
+
return loaded;
|
|
236
|
+
}
|
|
237
|
+
/** Loads this package's pinned native addon for this host (the module comment). */
|
|
238
|
+
function loadBinding(host, moduleUrl) {
|
|
239
|
+
const builtins = {
|
|
240
|
+
fs: host.getBuiltinModule("node:fs"),
|
|
241
|
+
crypto: host.getBuiltinModule("node:crypto"),
|
|
242
|
+
os: host.getBuiltinModule("node:os"),
|
|
243
|
+
path: host.getBuiltinModule("node:path"),
|
|
244
|
+
};
|
|
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);
|
|
250
|
+
}
|
|
251
|
+
const binding = loadBinding(globalThis.process, import.meta.url);
|
|
252
|
+
/** The native addon, typed as the facade's backend. */
|
|
253
|
+
export const backend = binding;
|
package/facade/api.d.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The polyjose JS facade (ADR 0030, ADR 0031 §1, ADR 0047), shared unchanged by
|
|
3
|
+
* `@polyjose/node` and `@polyjose/wasm`. Each package's entry binds it to its native layer:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* export const { version, errorCodes, jwsVerifyCompact, ... } = createApi(backend);
|
|
7
|
+
* export { PolyjoseError, ERROR_CODES } from "./api.js";
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* The facade adds idiom only (ADR 0030 §1): Promises, object↔JSON, JS type checks
|
|
11
|
+
* (`TypeError`), `null` → `undefined`, the JS clock for JWT `now`, and `PolyjoseError`.
|
|
12
|
+
* Policy, defaults and every JOSE check stay in the core.
|
|
13
|
+
*/
|
|
14
|
+
import type { Backend } from "./backend.js";
|
|
15
|
+
import { type ErrorCode } from "./error-codes.js";
|
|
16
|
+
import type { EcCurve, JsonInput, JweDecrypted, JweDecryptOptions, JweEncryptOptions, JweJsonForm, Jwk, JwkObject, JwkSelector, JwkSet, JwsSigner, JwsSignOptions, JwsVerified, JwsVerifyOptions, JwtNestedVerified, JwtSignOptions, JwtVerified, JwtVerifyOptions, KeyFormat, OkpCurve, ThumbprintHash } from "./types.js";
|
|
17
|
+
export type * from "./types.js";
|
|
18
|
+
export type { Backend } from "./backend.js";
|
|
19
|
+
export { ERROR_CODES, type ErrorCode } from "./error-codes.js";
|
|
20
|
+
/**
|
|
21
|
+
* A polyjose failure (ADR 0030 §5). `code` is one of {@link ERROR_CODES}, the core's stable
|
|
22
|
+
* `Error::code()` (ADR 0007): match on it, never on `message`, which may be reworded. Neither
|
|
23
|
+
* ever contains key material or input bytes.
|
|
24
|
+
*/
|
|
25
|
+
export declare class PolyjoseError extends Error {
|
|
26
|
+
readonly name = "PolyjoseError";
|
|
27
|
+
/** The stable error code. */
|
|
28
|
+
readonly code: ErrorCode;
|
|
29
|
+
constructor(code: ErrorCode, message: string);
|
|
30
|
+
}
|
|
31
|
+
/** Every operation of ADR 0030 §4, camelCase (§2), each returning a Promise (ADR 0031 §1). */
|
|
32
|
+
export interface PolyjoseApi {
|
|
33
|
+
/** The polyjose crate version. */
|
|
34
|
+
version(): Promise<string>;
|
|
35
|
+
/** The stable error codes, equal to the core's `Error::CODES` (ADR 0030 §5). */
|
|
36
|
+
errorCodes(): Promise<string[]>;
|
|
37
|
+
/** base64url without padding (RFC 4648 §5, RFC 7515 §2). */
|
|
38
|
+
base64urlEncode(bytes: Uint8Array): Promise<string>;
|
|
39
|
+
/** Strict, canonical base64url decoding (RFC 4648 §5, §3.5). */
|
|
40
|
+
base64urlDecode(text: string): Promise<Uint8Array>;
|
|
41
|
+
/** Validates a JWK (RFC 7517 §4, RFC 7518 §6, RFC 8037 §2) and returns the core's serialization. */
|
|
42
|
+
jwkParse(jwk: Jwk): Promise<JwkObject>;
|
|
43
|
+
/** The public half of a JWK (RFC 7517 §4, RFC 7518 §6.2.1, §6.3.1). */
|
|
44
|
+
jwkToPublic(jwk: Jwk): Promise<JwkObject>;
|
|
45
|
+
/**
|
|
46
|
+
* The JWK Thumbprint (RFC 7638 §3, §3.4), base64url: 43, 64 or 86 characters for SHA-256,
|
|
47
|
+
* SHA-384 or SHA-512. An absent `hash` (or `null`) is the core default, SHA-256 (ADR 0054 §9,
|
|
48
|
+
* RFC 9278 §5). Any other name fails with `UnsupportedAlgorithm` from the core, after the key's
|
|
49
|
+
* own import errors (ADR 0057 §2). Never publish the thumbprint of a password or other
|
|
50
|
+
* low-entropy `oct` key, or use it as a `kid`: it is an unsalted fast hash of the secret
|
|
51
|
+
* (ADR 0057 §4, RFC 7638 §7).
|
|
52
|
+
*/
|
|
53
|
+
jwkThumbprint(jwk: Jwk, hash?: ThumbprintHash): Promise<string>;
|
|
54
|
+
/**
|
|
55
|
+
* The JWK Thumbprint URI (RFC 9278 §3), `urn:ietf:params:oauth:jwk-thumbprint:<name>:<thumbprint>`,
|
|
56
|
+
* always with the IANA hash name (`sha-256`, `sha-384`, `sha-512`; RFC 9278 §4). `hash` is as
|
|
57
|
+
* for `jwkThumbprint`.
|
|
58
|
+
*/
|
|
59
|
+
jwkThumbprintUri(jwk: Jwk, hash?: ThumbprintHash): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* The one key of a JWK Set (RFC 7517 §5) that `selector` picks by `kid`, `alg` and `keyUse`,
|
|
62
|
+
* under the core rule (`JwkSet::select`, ADR 0054 §2). An absent selector (or `{}`) matches
|
|
63
|
+
* every member. No match rejects with `NoMatchingKey`, two or more with `MultipleMatchingKeys`
|
|
64
|
+
* (RFC 7517 §5.1 gives a set's order no meaning); an `alg` or `keyUse` the core refuses with
|
|
65
|
+
* `UnsupportedAlgorithm`, after the set's own import errors (ADR 0054 §4).
|
|
66
|
+
*
|
|
67
|
+
* The key may be private (`d`, `k`): call `jwkToPublic` before publishing it. Selection never
|
|
68
|
+
* authorizes an algorithm, so pass the key only to an operation with an allow-list, and pair a
|
|
69
|
+
* `kid` from an untrusted header with `alg` (ADR 0054 §7). A server maps `NoMatchingKey` and
|
|
70
|
+
* `MultipleMatchingKeys` to one opaque client error, such as `invalid_token` (RFC 6750 §3.1).
|
|
71
|
+
*/
|
|
72
|
+
jwksSelect(jwks: JwkSet, selector?: JwkSelector): Promise<JwkObject>;
|
|
73
|
+
/** A random `oct` key of `lenBytes` octets (RFC 7518 §6.4), from the OS CSPRNG (ADR 0005). */
|
|
74
|
+
jwkGenerateOct(lenBytes: number): Promise<JwkObject>;
|
|
75
|
+
/** A random EC private key (RFC 7518 §6.2). */
|
|
76
|
+
jwkGenerateEc(crv: EcCurve): Promise<JwkObject>;
|
|
77
|
+
/** A random OKP private key (RFC 8037 §2). */
|
|
78
|
+
jwkGenerateOkp(crv: OkpCurve): Promise<JwkObject>;
|
|
79
|
+
/** A random key for a JWS `alg`, JWE `alg` or `enc` (RFC 7518 §7.1 registry), with `alg` set. */
|
|
80
|
+
jwkGenerateForAlg(alg: string): Promise<JwkObject>;
|
|
81
|
+
/** Imports one RFC 7468 PEM block (ADR 0018 §3). */
|
|
82
|
+
jwkFromPem(pem: string): Promise<JwkObject>;
|
|
83
|
+
/** Imports DER in the structure `format` names; no sniffing (ADR 0018 §3, ADR 0007). */
|
|
84
|
+
jwkFromDer(der: Uint8Array, format: KeyFormat): Promise<JwkObject>;
|
|
85
|
+
/** Exports a JWK as PEM (RFC 7468, ADR 0018 §3). */
|
|
86
|
+
jwkToPem(jwk: Jwk, format: KeyFormat): Promise<string>;
|
|
87
|
+
/** Exports a JWK as DER (ADR 0018 §3). */
|
|
88
|
+
jwkToDer(jwk: Jwk, format: KeyFormat): Promise<Uint8Array>;
|
|
89
|
+
/** Signs in the JWS Compact Serialization (RFC 7515 §5.1, §7.1). */
|
|
90
|
+
jwsSignCompact(payload: Uint8Array, key: Jwk, options: JwsSignOptions): Promise<string>;
|
|
91
|
+
/** Signs in the flattened JWS JSON Serialization (RFC 7515 §7.2.2); returns JSON text. */
|
|
92
|
+
jwsSignFlattened(payload: Uint8Array, signer: JwsSigner, detached?: boolean): Promise<string>;
|
|
93
|
+
/** Signs in the general JWS JSON Serialization (RFC 7515 §7.2.1); returns JSON text. */
|
|
94
|
+
jwsSignGeneral(payload: Uint8Array, signers: readonly JwsSigner[], detached?: boolean): Promise<string>;
|
|
95
|
+
/** Verifies a compact JWS against an explicit allow-list (RFC 7515 §5.2, RFC 8725 §3.1). */
|
|
96
|
+
jwsVerifyCompact(token: string, key: Jwk, options: JwsVerifyOptions): Promise<JwsVerified>;
|
|
97
|
+
/** Verifies a flattened or general JWS JSON Serialization (RFC 7515 §7.2, §5.2). */
|
|
98
|
+
jwsVerifyJson(jws: JsonInput, key: Jwk, options: JwsVerifyOptions): Promise<JwsVerified>;
|
|
99
|
+
/** Encrypts to one recipient in the JWE Compact Serialization (RFC 7516 §5.1, §7.1). */
|
|
100
|
+
jweEncryptCompact(plaintext: Uint8Array, options: JweEncryptOptions): Promise<string>;
|
|
101
|
+
/** Encrypts in a JWE JSON Serialization (RFC 7516 §7.2); returns JSON text. */
|
|
102
|
+
jweEncryptJson(plaintext: Uint8Array, options: JweEncryptOptions, form: JweJsonForm): Promise<string>;
|
|
103
|
+
/** Decrypts a compact JWE against explicit allow-lists (RFC 7516 §5.2, RFC 8725 §3.1). */
|
|
104
|
+
jweDecryptCompact(token: string, key: Jwk, options: JweDecryptOptions): Promise<JweDecrypted>;
|
|
105
|
+
/** Decrypts a flattened or general JWE JSON Serialization (RFC 7516 §7.2, §5.2). */
|
|
106
|
+
jweDecryptJson(jwe: JsonInput, key: Jwk, options: JweDecryptOptions): Promise<JweDecrypted>;
|
|
107
|
+
/** Issues a signed JWT (RFC 7519 §7.1; ADR 0027 §8). */
|
|
108
|
+
jwtSign(claims: JsonInput, key: Jwk, options: JwtSignOptions): Promise<string>;
|
|
109
|
+
/** Validates a signed JWT with one key (RFC 7519 §7.2; ADR 0027 policy and error order). */
|
|
110
|
+
jwtVerify(token: string, key: Jwk, options: JwtVerifyOptions): Promise<JwtVerified>;
|
|
111
|
+
/** Validates a signed JWT with a JWK Set (RFC 7517 §5; ADR 0029 selection). */
|
|
112
|
+
jwtVerifyWithJwks(token: string, jwks: JwkSet, options: JwtVerifyOptions): Promise<JwtVerified>;
|
|
113
|
+
/** Encrypts a signed JWT as a nested JWT, `cty: "JWT"` (RFC 7519 §5.2, §11.2; ADR 0027 §7). */
|
|
114
|
+
jwtEncryptNested(signedJwt: string, options: JweEncryptOptions): Promise<string>;
|
|
115
|
+
/** Decrypts and validates a nested JWT with one verification key (RFC 7519 §7.2 step 8). */
|
|
116
|
+
jwtDecryptNested(token: string, decryptKey: Jwk, decryptOptions: JweDecryptOptions, key: Jwk, verifyOptions: JwtVerifyOptions): Promise<JwtNestedVerified>;
|
|
117
|
+
/** Decrypts and validates a nested JWT with a JWK Set for the inner signature. */
|
|
118
|
+
jwtDecryptNestedWithJwks(token: string, decryptKey: Jwk, decryptOptions: JweDecryptOptions, jwks: JwkSet, verifyOptions: JwtVerifyOptions): Promise<JwtNestedVerified>;
|
|
119
|
+
/** The error class every operation rejects with for a core failure. */
|
|
120
|
+
readonly PolyjoseError: typeof PolyjoseError;
|
|
121
|
+
}
|
|
122
|
+
/** Binds the facade to one native layer (BACKEND.md). */
|
|
123
|
+
export declare function createApi(backend: Backend): PolyjoseApi;
|