@cipherstash/protect-ffi 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/README.md +72 -16
- package/dist/wasm/protect_ffi_bg.js +44 -2
- package/dist/wasm/protect_ffi_bg.wasm +0 -0
- package/dist/wasm/protect_ffi_bg.wasm.d.ts +2 -2
- package/dist/wasm/protect_ffi_inline.js +1 -1
- package/lib/bigintWire.d.ts +56 -0
- package/lib/bigintWire.js +83 -0
- package/lib/index.cjs +11 -4
- package/lib/index.d.cts +23 -1
- package/package.json +11 -11
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal wire encoding for JS `bigint` plaintexts crossing the Neon
|
|
3
|
+
* boundary.
|
|
4
|
+
*
|
|
5
|
+
* The native addon extracts every options object with neon's `Json`
|
|
6
|
+
* extractor, which runs `JSON.stringify` on the JS side — and
|
|
7
|
+
* `JSON.stringify` throws a `TypeError` on any `bigint`. So `bigint`
|
|
8
|
+
* plaintexts are bounds-checked against i64 here (with a clear
|
|
9
|
+
* `RangeError`) and encoded into the tagged single-key map
|
|
10
|
+
* `{"__protect_ffi_bigint__": "<decimal string>"}`, which the Rust side
|
|
11
|
+
* deserializes into `JsPlaintext::BigInt`. The value is a decimal string,
|
|
12
|
+
* not a number: an i64-magnitude number literal would already have lost
|
|
13
|
+
* precision beyond 2^53.
|
|
14
|
+
*
|
|
15
|
+
* The wasm build performs the equivalent encoding in Rust
|
|
16
|
+
* (`encode_plaintext` in `crates/protect-ffi/src/wasm.rs`) because
|
|
17
|
+
* wasm consumers call the wasm-bindgen exports directly, without this
|
|
18
|
+
* wrapper.
|
|
19
|
+
*
|
|
20
|
+
* The decrypt direction needs no decoding: the native addon constructs a
|
|
21
|
+
* real JS `bigint` (`JsBigInt::from_i64` on Neon, `js_sys::BigInt` on
|
|
22
|
+
* wasm).
|
|
23
|
+
*/
|
|
24
|
+
/** Must match `BIGINT_WIRE_KEY` in `crates/protect-ffi/src/js_plaintext.rs`. */
|
|
25
|
+
export declare const BIGINT_WIRE_KEY = "__protect_ffi_bigint__";
|
|
26
|
+
/** Smallest value an encrypted bigint column can store (i64::MIN). */
|
|
27
|
+
export declare const BIGINT_MIN: bigint;
|
|
28
|
+
/** Largest value an encrypted bigint column can store (i64::MAX). */
|
|
29
|
+
export declare const BIGINT_MAX: bigint;
|
|
30
|
+
/** The tagged wire form a `bigint` plaintext crosses the boundary in. */
|
|
31
|
+
export type BigIntWire = {
|
|
32
|
+
[K in typeof BIGINT_WIRE_KEY]: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Encode a `bigint` plaintext into its tagged wire form, throwing a
|
|
36
|
+
* `RangeError` (naming the i64 bounds and the offending direction) when
|
|
37
|
+
* the value does not fit a signed 64-bit integer. Non-`bigint` values pass
|
|
38
|
+
* through untouched. The error deliberately does not echo the value: it is
|
|
39
|
+
* plaintext being encrypted.
|
|
40
|
+
*/
|
|
41
|
+
export declare function encodeBigIntPlaintext<T>(plaintext: T | bigint): T | BigIntWire;
|
|
42
|
+
/**
|
|
43
|
+
* Return `opts` with a `bigint` `plaintext` replaced by its tagged wire
|
|
44
|
+
* form. When the plaintext is not a `bigint`, the original object is
|
|
45
|
+
* returned unchanged (no clone).
|
|
46
|
+
*/
|
|
47
|
+
export declare function withEncodedPlaintext<T extends {
|
|
48
|
+
plaintext: unknown;
|
|
49
|
+
}>(opts: T): T;
|
|
50
|
+
/**
|
|
51
|
+
* Bulk variant of {@link withEncodedPlaintext}: returns the original array
|
|
52
|
+
* (no clone) when no payload carries a `bigint` plaintext.
|
|
53
|
+
*/
|
|
54
|
+
export declare function withEncodedPlaintexts<T extends {
|
|
55
|
+
plaintext: unknown;
|
|
56
|
+
}>(payloads: T[]): T[];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Internal wire encoding for JS `bigint` plaintexts crossing the Neon
|
|
4
|
+
* boundary.
|
|
5
|
+
*
|
|
6
|
+
* The native addon extracts every options object with neon's `Json`
|
|
7
|
+
* extractor, which runs `JSON.stringify` on the JS side — and
|
|
8
|
+
* `JSON.stringify` throws a `TypeError` on any `bigint`. So `bigint`
|
|
9
|
+
* plaintexts are bounds-checked against i64 here (with a clear
|
|
10
|
+
* `RangeError`) and encoded into the tagged single-key map
|
|
11
|
+
* `{"__protect_ffi_bigint__": "<decimal string>"}`, which the Rust side
|
|
12
|
+
* deserializes into `JsPlaintext::BigInt`. The value is a decimal string,
|
|
13
|
+
* not a number: an i64-magnitude number literal would already have lost
|
|
14
|
+
* precision beyond 2^53.
|
|
15
|
+
*
|
|
16
|
+
* The wasm build performs the equivalent encoding in Rust
|
|
17
|
+
* (`encode_plaintext` in `crates/protect-ffi/src/wasm.rs`) because
|
|
18
|
+
* wasm consumers call the wasm-bindgen exports directly, without this
|
|
19
|
+
* wrapper.
|
|
20
|
+
*
|
|
21
|
+
* The decrypt direction needs no decoding: the native addon constructs a
|
|
22
|
+
* real JS `bigint` (`JsBigInt::from_i64` on Neon, `js_sys::BigInt` on
|
|
23
|
+
* wasm).
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.BIGINT_MAX = exports.BIGINT_MIN = exports.BIGINT_WIRE_KEY = void 0;
|
|
27
|
+
exports.encodeBigIntPlaintext = encodeBigIntPlaintext;
|
|
28
|
+
exports.withEncodedPlaintext = withEncodedPlaintext;
|
|
29
|
+
exports.withEncodedPlaintexts = withEncodedPlaintexts;
|
|
30
|
+
/** Must match `BIGINT_WIRE_KEY` in `crates/protect-ffi/src/js_plaintext.rs`. */
|
|
31
|
+
exports.BIGINT_WIRE_KEY = '__protect_ffi_bigint__';
|
|
32
|
+
/** Smallest value an encrypted bigint column can store (i64::MIN). */
|
|
33
|
+
exports.BIGINT_MIN = -(2n ** 63n);
|
|
34
|
+
/** Largest value an encrypted bigint column can store (i64::MAX). */
|
|
35
|
+
exports.BIGINT_MAX = 2n ** 63n - 1n;
|
|
36
|
+
const I64_BOUNDS_SUFFIX = 'encrypted bigint values must fit in a signed 64-bit integer ' +
|
|
37
|
+
'(-9223372036854775808 to 9223372036854775807)';
|
|
38
|
+
/**
|
|
39
|
+
* Encode a `bigint` plaintext into its tagged wire form, throwing a
|
|
40
|
+
* `RangeError` (naming the i64 bounds and the offending direction) when
|
|
41
|
+
* the value does not fit a signed 64-bit integer. Non-`bigint` values pass
|
|
42
|
+
* through untouched. The error deliberately does not echo the value: it is
|
|
43
|
+
* plaintext being encrypted.
|
|
44
|
+
*/
|
|
45
|
+
function encodeBigIntPlaintext(plaintext) {
|
|
46
|
+
if (typeof plaintext !== 'bigint') {
|
|
47
|
+
return plaintext;
|
|
48
|
+
}
|
|
49
|
+
if (plaintext > exports.BIGINT_MAX) {
|
|
50
|
+
throw new RangeError(`BigInt plaintext is above the maximum supported value: ${I64_BOUNDS_SUFFIX}`);
|
|
51
|
+
}
|
|
52
|
+
if (plaintext < exports.BIGINT_MIN) {
|
|
53
|
+
throw new RangeError(`BigInt plaintext is below the minimum supported value: ${I64_BOUNDS_SUFFIX}`);
|
|
54
|
+
}
|
|
55
|
+
return { [exports.BIGINT_WIRE_KEY]: plaintext.toString() };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Return `opts` with a `bigint` `plaintext` replaced by its tagged wire
|
|
59
|
+
* form. When the plaintext is not a `bigint`, the original object is
|
|
60
|
+
* returned unchanged (no clone).
|
|
61
|
+
*/
|
|
62
|
+
function withEncodedPlaintext(opts) {
|
|
63
|
+
// `opts?.` (matching the detection pass in withEncodedPlaintexts): a
|
|
64
|
+
// null/undefined element must fall through to native's own validation
|
|
65
|
+
// error rather than throw a raw property-access TypeError here — the
|
|
66
|
+
// error a bad element produces must not depend on whether a *sibling*
|
|
67
|
+
// element happens to carry a bigint.
|
|
68
|
+
if (typeof opts?.plaintext !== 'bigint') {
|
|
69
|
+
return opts;
|
|
70
|
+
}
|
|
71
|
+
return { ...opts, plaintext: encodeBigIntPlaintext(opts.plaintext) };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Bulk variant of {@link withEncodedPlaintext}: returns the original array
|
|
75
|
+
* (no clone) when no payload carries a `bigint` plaintext.
|
|
76
|
+
*/
|
|
77
|
+
function withEncodedPlaintexts(payloads) {
|
|
78
|
+
if (!Array.isArray(payloads) ||
|
|
79
|
+
!payloads.some((p) => typeof p?.plaintext === 'bigint')) {
|
|
80
|
+
return payloads;
|
|
81
|
+
}
|
|
82
|
+
return payloads.map(withEncodedPlaintext);
|
|
83
|
+
}
|
package/lib/index.cjs
CHANGED
|
@@ -48,6 +48,7 @@ exports.decryptBulkFallible = decryptBulkFallible;
|
|
|
48
48
|
exports.encryptQuery = encryptQuery;
|
|
49
49
|
exports.encryptQueryBulk = encryptQueryBulk;
|
|
50
50
|
exports.ensureKeyset = ensureKeyset;
|
|
51
|
+
const bigintWire_js_1 = require("./bigintWire.js");
|
|
51
52
|
const credentials_js_1 = require("./credentials.js");
|
|
52
53
|
const native = __importStar(require("./load.cjs"));
|
|
53
54
|
const normalizeEncryptConfig_js_1 = require("./normalizeEncryptConfig.js");
|
|
@@ -81,7 +82,7 @@ function newClient(opts) {
|
|
|
81
82
|
}, opts.strategy));
|
|
82
83
|
}
|
|
83
84
|
function encrypt(client, opts) {
|
|
84
|
-
return wrapAsync(() => native.encrypt(client, opts));
|
|
85
|
+
return wrapAsync(() => native.encrypt(client, (0, bigintWire_js_1.withEncodedPlaintext)(opts)));
|
|
85
86
|
}
|
|
86
87
|
function decrypt(client, opts) {
|
|
87
88
|
return wrapAsync(() => native.decrypt(client, opts));
|
|
@@ -97,7 +98,10 @@ function isEncrypted(encrypted) {
|
|
|
97
98
|
return wrapSync(() => native.isEncrypted(encrypted));
|
|
98
99
|
}
|
|
99
100
|
function encryptBulk(client, opts) {
|
|
100
|
-
return wrapAsync(() =>
|
|
101
|
+
return wrapAsync(() => {
|
|
102
|
+
const plaintexts = (0, bigintWire_js_1.withEncodedPlaintexts)(opts.plaintexts);
|
|
103
|
+
return native.encryptBulk(client, plaintexts === opts.plaintexts ? opts : { ...opts, plaintexts });
|
|
104
|
+
});
|
|
101
105
|
}
|
|
102
106
|
function decryptBulk(client, opts) {
|
|
103
107
|
return wrapAsync(() => native.decryptBulk(client, opts));
|
|
@@ -125,11 +129,14 @@ async function decryptBulkFallible(client, opts) {
|
|
|
125
129
|
* shape exists yet, so those queries require an `eqlVersion: 2` client.
|
|
126
130
|
*/
|
|
127
131
|
function encryptQuery(client, opts) {
|
|
128
|
-
return wrapAsync(() => native.encryptQuery(client, opts));
|
|
132
|
+
return wrapAsync(() => native.encryptQuery(client, (0, bigintWire_js_1.withEncodedPlaintext)(opts)));
|
|
129
133
|
}
|
|
130
134
|
/** Bulk variant of {@link encryptQuery} — same EQL v3 restrictions apply. */
|
|
131
135
|
function encryptQueryBulk(client, opts) {
|
|
132
|
-
return wrapAsync(() =>
|
|
136
|
+
return wrapAsync(() => {
|
|
137
|
+
const queries = (0, bigintWire_js_1.withEncodedPlaintexts)(opts.queries);
|
|
138
|
+
return native.encryptQueryBulk(client, queries === opts.queries ? opts : { ...opts, queries });
|
|
139
|
+
});
|
|
133
140
|
}
|
|
134
141
|
/**
|
|
135
142
|
* Test-only helper: ensures a keyset with the given name exists, creating it if necessary,
|
package/lib/index.d.cts
CHANGED
|
@@ -325,7 +325,29 @@ export type EnsureKeysetResult = {
|
|
|
325
325
|
id: string;
|
|
326
326
|
name: string;
|
|
327
327
|
};
|
|
328
|
-
|
|
328
|
+
/**
|
|
329
|
+
* A plaintext value accepted by {@link encrypt} / {@link encryptBulk} /
|
|
330
|
+
* {@link encryptQuery} and returned by {@link decrypt} / {@link decryptBulk} /
|
|
331
|
+
* {@link decryptBulkFallible} (in the `data` arm of each result).
|
|
332
|
+
*
|
|
333
|
+
* `bigint` support (encrypted `cast_as: 'bigint'` columns store signed
|
|
334
|
+
* 64-bit integers):
|
|
335
|
+
*
|
|
336
|
+
* - **Input**: a top-level `bigint` plaintext is accepted alongside
|
|
337
|
+
* `number`. Values outside the i64 range (-2^63 to 2^63 - 1) throw a
|
|
338
|
+
* `RangeError` at the boundary — this covers index-term generation too,
|
|
339
|
+
* since terms derive from the same value. `number` inputs keep the
|
|
340
|
+
* existing exact-integer guard (fractional, non-finite, or beyond-2^53
|
|
341
|
+
* inexact values are rejected). `bigint` values nested inside JSON
|
|
342
|
+
* objects/arrays are NOT supported (JSON has no bigint) and throw a
|
|
343
|
+
* `TypeError` on both Neon and wasm — plaintexts follow
|
|
344
|
+
* `JSON.stringify` semantics on both platforms.
|
|
345
|
+
* - **Output** (BREAKING since the introduction of bigint support):
|
|
346
|
+
* decrypting a `cast_as: 'bigint'` column ALWAYS returns a `bigint`,
|
|
347
|
+
* even for values that fit in a JS number. Previous releases returned a
|
|
348
|
+
* `number`, silently losing precision beyond `Number.MAX_SAFE_INTEGER`.
|
|
349
|
+
*/
|
|
350
|
+
export type JsPlaintext = string | number | boolean | bigint | Record<string, unknown> | JsPlaintext[];
|
|
329
351
|
export type EncryptOptions = {
|
|
330
352
|
plaintext: JsPlaintext;
|
|
331
353
|
column: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cipherstash/protect-ffi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/cipherstash/protectjs-ffi.git"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"url": "https://github.com/cipherstash/protectjs-ffi/issues"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://github.com/cipherstash/protectjs-ffi#readme",
|
|
12
|
-
"description": "",
|
|
12
|
+
"description": "Native FFI bindings to the CipherStash Client SDK — powers @cipherstash/stack",
|
|
13
13
|
"main": "./lib/index.cjs",
|
|
14
14
|
"scripts": {
|
|
15
15
|
"test": "npm run test:typecheck && npm run test:unit && npm run test:lint && npm run test:format && npm run test:rust",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"cross": "npm run cross-build -- --release",
|
|
33
33
|
"zigbuild": "npm run zig-build -- --release",
|
|
34
34
|
"prepack": "tsc &&neon update",
|
|
35
|
-
"version": "neon bump --binaries platforms && git add .",
|
|
35
|
+
"version": "neon bump --binaries platforms && node scripts/changelog-release.mjs && git add .",
|
|
36
36
|
"release": "gh workflow run release.yml -f dryrun=false -f version=patch",
|
|
37
37
|
"dryrun": "gh workflow run release.yml -f dryrun=true -f version=patch",
|
|
38
38
|
"build:wasm": "wasm-pack build crates/protect-ffi --target bundler --out-dir ../../dist/wasm --no-pack",
|
|
39
39
|
"postbuild:wasm": "node scripts/inline-wasm.mjs"
|
|
40
40
|
},
|
|
41
|
-
"author": "",
|
|
42
|
-
"license": "
|
|
41
|
+
"author": "CipherStash <humans@cipherstash.com>",
|
|
42
|
+
"license": "MIT",
|
|
43
43
|
"exports": {
|
|
44
44
|
".": {
|
|
45
45
|
"node": {
|
|
@@ -103,11 +103,11 @@
|
|
|
103
103
|
"vite": "^8.0.5"
|
|
104
104
|
},
|
|
105
105
|
"optionalDependencies": {
|
|
106
|
-
"@cipherstash/protect-ffi-darwin-x64": "0.
|
|
107
|
-
"@cipherstash/protect-ffi-darwin-arm64": "0.
|
|
108
|
-
"@cipherstash/protect-ffi-win32-x64-msvc": "0.
|
|
109
|
-
"@cipherstash/protect-ffi-linux-x64-gnu": "0.
|
|
110
|
-
"@cipherstash/protect-ffi-linux-arm64-gnu": "0.
|
|
111
|
-
"@cipherstash/protect-ffi-linux-x64-musl": "0.
|
|
106
|
+
"@cipherstash/protect-ffi-darwin-x64": "0.28.0",
|
|
107
|
+
"@cipherstash/protect-ffi-darwin-arm64": "0.28.0",
|
|
108
|
+
"@cipherstash/protect-ffi-win32-x64-msvc": "0.28.0",
|
|
109
|
+
"@cipherstash/protect-ffi-linux-x64-gnu": "0.28.0",
|
|
110
|
+
"@cipherstash/protect-ffi-linux-arm64-gnu": "0.28.0",
|
|
111
|
+
"@cipherstash/protect-ffi-linux-x64-musl": "0.28.0"
|
|
112
112
|
}
|
|
113
113
|
}
|