@cipherstash/stack 1.0.0-rc.1 → 1.0.0-rc.3
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/CHANGELOG.md +33 -0
- package/dist/adapter-kit.cjs +2 -2
- package/dist/adapter-kit.cjs.map +1 -1
- package/dist/adapter-kit.js +1 -1
- package/dist/{chunk-HQANMV7R.js → chunk-BCY6WB24.js} +3 -3
- package/dist/{chunk-HQANMV7R.js.map → chunk-BCY6WB24.js.map} +1 -1
- package/dist/{chunk-IDKP6ABU.js → chunk-Q4WTOYVI.js} +2 -2
- package/dist/{chunk-L7ISHSG7.js → chunk-UI7V2QCK.js} +5 -5
- package/dist/{chunk-L7ISHSG7.js.map → chunk-UI7V2QCK.js.map} +1 -1
- package/dist/dynamodb/index.cjs +2 -2
- package/dist/dynamodb/index.cjs.map +1 -1
- package/dist/dynamodb/index.js +1 -1
- package/dist/encryption/index.cjs +4 -4
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.js +3 -3
- package/dist/encryption/v3.cjs +4 -4
- package/dist/encryption/v3.cjs.map +1 -1
- package/dist/encryption/v3.js +3 -3
- package/dist/identity/index.cjs +2 -2
- package/dist/identity/index.cjs.map +1 -1
- package/dist/identity/index.js +2 -2
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/wasm-inline.d.ts +241 -56
- package/dist/wasm-inline.js +268 -0
- package/dist/wasm-inline.js.map +1 -1
- package/package.json +17 -17
- /package/dist/{chunk-IDKP6ABU.js.map → chunk-Q4WTOYVI.js.map} +0 -0
package/dist/wasm-inline.js
CHANGED
|
@@ -11,10 +11,134 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
decrypt as wasmDecrypt,
|
|
13
13
|
encrypt as wasmEncrypt,
|
|
14
|
+
encryptQuery as wasmEncryptQuery,
|
|
15
|
+
encryptQueryBulk as wasmEncryptQueryBulk,
|
|
14
16
|
isEncrypted as wasmIsEncrypted,
|
|
15
17
|
newClient as wasmNewClient
|
|
16
18
|
} from "@cipherstash/protect-ffi/wasm-inline";
|
|
17
19
|
|
|
20
|
+
// src/types.ts
|
|
21
|
+
var queryTypeToFfi = {
|
|
22
|
+
// v3 `_ord` domains carry `ope` instead — `resolveIndexType` swaps this
|
|
23
|
+
// static default for the ordering index the column actually configures.
|
|
24
|
+
orderAndRange: "ore",
|
|
25
|
+
freeTextSearch: "match",
|
|
26
|
+
equality: "unique",
|
|
27
|
+
steVecSelector: "ste_vec",
|
|
28
|
+
steVecTerm: "ste_vec",
|
|
29
|
+
searchableJson: "ste_vec"
|
|
30
|
+
};
|
|
31
|
+
var queryTypeToQueryOp = {
|
|
32
|
+
steVecSelector: "ste_vec_selector",
|
|
33
|
+
steVecTerm: "ste_vec_term"
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/encryption/helpers/infer-index-type.ts
|
|
37
|
+
function inferIndexType(column) {
|
|
38
|
+
const config = column.build();
|
|
39
|
+
const indexes = config.indexes;
|
|
40
|
+
if (!indexes || Object.keys(indexes).length === 0) {
|
|
41
|
+
throw new Error(`Column "${column.getName()}" has no indexes configured`);
|
|
42
|
+
}
|
|
43
|
+
if (indexes.unique) return "unique";
|
|
44
|
+
if (indexes.match) return "match";
|
|
45
|
+
if (indexes.ore) return "ore";
|
|
46
|
+
if (indexes.ope) return "ope";
|
|
47
|
+
if (indexes.ste_vec) return "ste_vec";
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Column "${column.getName()}" has no suitable index for queries`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
function inferQueryOpFromPlaintext(plaintext) {
|
|
53
|
+
if (typeof plaintext === "string") {
|
|
54
|
+
return "ste_vec_selector";
|
|
55
|
+
}
|
|
56
|
+
if (typeof plaintext === "object" || typeof plaintext === "number" || typeof plaintext === "boolean") {
|
|
57
|
+
return "ste_vec_term";
|
|
58
|
+
}
|
|
59
|
+
return "ste_vec_term";
|
|
60
|
+
}
|
|
61
|
+
function validateIndexType(column, indexType) {
|
|
62
|
+
const config = column.build();
|
|
63
|
+
const indexes = config.indexes ?? {};
|
|
64
|
+
const indexMap = {
|
|
65
|
+
unique: !!indexes.unique,
|
|
66
|
+
match: !!indexes.match,
|
|
67
|
+
ore: !!indexes.ore,
|
|
68
|
+
ope: !!indexes.ope,
|
|
69
|
+
ste_vec: !!indexes.ste_vec
|
|
70
|
+
};
|
|
71
|
+
if (!indexMap[indexType]) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Index type "${indexType}" is not configured on column "${column.getName()}"`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function equalityOrderingIndex(column) {
|
|
78
|
+
if (!("getQueryCapabilities" in column)) return null;
|
|
79
|
+
if (!column.getQueryCapabilities().equality) return null;
|
|
80
|
+
const indexes = column.build().indexes ?? {};
|
|
81
|
+
if (indexes.unique) return null;
|
|
82
|
+
if (indexes.ore) return "ore";
|
|
83
|
+
if (indexes.ope) return "ope";
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function resolveIndexType(column, queryType, plaintext) {
|
|
87
|
+
let indexType = queryType ? queryTypeToFfi[queryType] : inferIndexType(column);
|
|
88
|
+
if (queryType) {
|
|
89
|
+
if (queryType === "equality") {
|
|
90
|
+
const ordering = equalityOrderingIndex(column);
|
|
91
|
+
if (ordering) return { indexType: ordering };
|
|
92
|
+
}
|
|
93
|
+
if (queryType === "orderAndRange" && indexType === "ore") {
|
|
94
|
+
const indexes = column.build().indexes ?? {};
|
|
95
|
+
if (!indexes.ore && indexes.ope) indexType = "ope";
|
|
96
|
+
}
|
|
97
|
+
validateIndexType(column, indexType);
|
|
98
|
+
if (queryType === "searchableJson") {
|
|
99
|
+
if (plaintext === void 0 || plaintext === null) {
|
|
100
|
+
return { indexType };
|
|
101
|
+
}
|
|
102
|
+
return { indexType, queryOp: inferQueryOpFromPlaintext(plaintext) };
|
|
103
|
+
}
|
|
104
|
+
return { indexType, queryOp: queryTypeToQueryOp[queryType] };
|
|
105
|
+
}
|
|
106
|
+
if (indexType === "ste_vec") {
|
|
107
|
+
if (plaintext === void 0 || plaintext === null) {
|
|
108
|
+
return { indexType };
|
|
109
|
+
}
|
|
110
|
+
return { indexType, queryOp: inferQueryOpFromPlaintext(plaintext) };
|
|
111
|
+
}
|
|
112
|
+
return { indexType };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/encryption/helpers/validation.ts
|
|
116
|
+
var INT64_MIN = -9223372036854775808n;
|
|
117
|
+
var INT64_MAX = 9223372036854775807n;
|
|
118
|
+
function isBigintOutOfInt64Range(value) {
|
|
119
|
+
return typeof value === "bigint" && (value < INT64_MIN || value > INT64_MAX);
|
|
120
|
+
}
|
|
121
|
+
function assertValidNumericValue(value) {
|
|
122
|
+
if (typeof value === "number" && Number.isNaN(value)) {
|
|
123
|
+
throw new Error("[encryption]: Cannot encrypt NaN value");
|
|
124
|
+
}
|
|
125
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
126
|
+
throw new Error("[encryption]: Cannot encrypt Infinity value");
|
|
127
|
+
}
|
|
128
|
+
if (isBigintOutOfInt64Range(value)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"[encryption]: Cannot encrypt bigint value out of int64 range"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function assertValueIndexCompatibility(value, indexType, columnName) {
|
|
135
|
+
if ((typeof value === "number" || typeof value === "bigint") && indexType === "match") {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`[encryption]: Cannot use 'match' index with numeric value on column "${columnName}". The 'freeTextSearch' index only supports string values. Configure the column with 'orderAndRange()' or 'equality()' for numeric queries.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
18
142
|
// src/schema/match-defaults.ts
|
|
19
143
|
function defaultMatchOpts() {
|
|
20
144
|
return {
|
|
@@ -4748,6 +4872,134 @@ var WasmEncryptionClient = class {
|
|
|
4748
4872
|
isEncrypted(value) {
|
|
4749
4873
|
return wasmIsEncrypted(value);
|
|
4750
4874
|
}
|
|
4875
|
+
/**
|
|
4876
|
+
* Encrypt a QUERY TERM (search needle) for a queryable v3 column —
|
|
4877
|
+
* equality, free-text match, ORE range, or JSON containment/selector.
|
|
4878
|
+
*
|
|
4879
|
+
* The returned term is **ciphertext-free**: it is matched against stored
|
|
4880
|
+
* envelopes, never decrypted, so it is safe to log-scrub less aggressively
|
|
4881
|
+
* than storage payloads (though it still derives from the plaintext).
|
|
4882
|
+
* Interpolate it as a parameter and cast to the column's
|
|
4883
|
+
* `eql_v3.query_<domain>` type to reach the indexed operators — the domain
|
|
4884
|
+
* suffix mirrors the storage domain (`eql_v3_text_eq` →
|
|
4885
|
+
* `eql_v3.query_text_eq`; irregular: `eql_v3_json` → `eql_v3.query_jsonb`).
|
|
4886
|
+
*
|
|
4887
|
+
* @example Equality (unique index)
|
|
4888
|
+
* ```ts
|
|
4889
|
+
* const term = await client.encryptQuery("alice@example.com", {
|
|
4890
|
+
* table: users, column: users.email, queryType: "equality",
|
|
4891
|
+
* })
|
|
4892
|
+
* // postgres-js:
|
|
4893
|
+
* sql`SELECT * FROM users
|
|
4894
|
+
* WHERE email = ${term}::jsonb::eql_v3.query_text_eq`
|
|
4895
|
+
* ```
|
|
4896
|
+
*
|
|
4897
|
+
* @example Free-text match (bloom index — one-sided, fuzzy)
|
|
4898
|
+
* ```ts
|
|
4899
|
+
* const term = await client.encryptQuery("needle", {
|
|
4900
|
+
* table: users, column: users.bio, queryType: "freeTextSearch",
|
|
4901
|
+
* })
|
|
4902
|
+
* sql`SELECT * FROM users
|
|
4903
|
+
* WHERE eql_v3.contains(bio, ${term}::jsonb::eql_v3.query_text_search)`
|
|
4904
|
+
* ```
|
|
4905
|
+
*
|
|
4906
|
+
* @example Range / ORDER BY (ORE index)
|
|
4907
|
+
* ```ts
|
|
4908
|
+
* const term = await client.encryptQuery(42, {
|
|
4909
|
+
* table: users, column: users.age, queryType: "orderAndRange",
|
|
4910
|
+
* })
|
|
4911
|
+
* sql`SELECT * FROM users
|
|
4912
|
+
* WHERE eql_v3.gte(age, ${term}::jsonb::eql_v3.query_integer_ord)`
|
|
4913
|
+
* ```
|
|
4914
|
+
*
|
|
4915
|
+
* @example Encrypted JSON — containment and JSONPath selector
|
|
4916
|
+
* ```ts
|
|
4917
|
+
* // Object value → containment needle (a v3 envelope):
|
|
4918
|
+
* const contains = await client.encryptQuery({ role: "admin" }, {
|
|
4919
|
+
* table: users, column: users.prefs, queryType: "searchableJson",
|
|
4920
|
+
* })
|
|
4921
|
+
* sql`SELECT * FROM users
|
|
4922
|
+
* WHERE prefs @> ${contains}::jsonb::eql_v3.query_jsonb`
|
|
4923
|
+
*
|
|
4924
|
+
* // String value → JSONPath selector. NOTE: v3 has no encrypted-selector
|
|
4925
|
+
* // envelope — this returns the BARE selector-hash string, bound as the
|
|
4926
|
+
* // text argument of -> / ->>:
|
|
4927
|
+
* const selector = await client.encryptQuery("$.role", {
|
|
4928
|
+
* table: users, column: users.prefs, queryType: "searchableJson",
|
|
4929
|
+
* })
|
|
4930
|
+
* sql`SELECT prefs -> ${selector} FROM users`
|
|
4931
|
+
* ```
|
|
4932
|
+
*
|
|
4933
|
+
* @param plaintext - The search needle. `null`/`undefined` returns `null`
|
|
4934
|
+
* without contacting ZeroKMS (nothing to search for), mirroring the
|
|
4935
|
+
* native client.
|
|
4936
|
+
* @param opts - Table, column, and (optionally) which index to target —
|
|
4937
|
+
* see {@link WasmEncryptQueryOptions.queryType} for the inference rules.
|
|
4938
|
+
* @returns The v3 query term, or `null` for null plaintext.
|
|
4939
|
+
* @throws When the requested `queryType` isn't configured on the column,
|
|
4940
|
+
* the column has no indexes at all, the value fails the same pre-flight
|
|
4941
|
+
* validation the native client runs (NaN / Infinity / out-of-int64
|
|
4942
|
+
* bigint, or a numeric value against a `freeTextSearch` index), or
|
|
4943
|
+
* encryption fails. Errors THROW, consistent with this surface's
|
|
4944
|
+
* `encrypt`/`decrypt` (the native entry's `{ data } | { failure }`
|
|
4945
|
+
* envelope lives on the Node client only).
|
|
4946
|
+
*/
|
|
4947
|
+
async encryptQuery(plaintext, opts) {
|
|
4948
|
+
if (plaintext === null || plaintext === void 0) return null;
|
|
4949
|
+
return await wasmEncryptQuery(
|
|
4950
|
+
// biome-ignore lint/plugin: the FFI handle is an opaque wasm-bindgen pointer with no JS-side type
|
|
4951
|
+
this.client,
|
|
4952
|
+
// biome-ignore lint/plugin: the term crosses the serde boundary, whose shape protect-ffi types as `any`
|
|
4953
|
+
toFfiQueryTerm(plaintext, opts)
|
|
4954
|
+
);
|
|
4955
|
+
}
|
|
4956
|
+
/**
|
|
4957
|
+
* Batch form of {@link encryptQuery} — one ZeroKMS round trip for many
|
|
4958
|
+
* terms, which is the shape query builders want (encrypt every needle in
|
|
4959
|
+
* a WHERE clause together).
|
|
4960
|
+
*
|
|
4961
|
+
* Position-stable: the result array is index-aligned with `terms`, and a
|
|
4962
|
+
* `null`/`undefined` value yields `null` at the same index (an all-null
|
|
4963
|
+
* batch short-circuits without calling ZeroKMS). Terms may mix query
|
|
4964
|
+
* types and columns freely.
|
|
4965
|
+
*
|
|
4966
|
+
* @example
|
|
4967
|
+
* ```ts
|
|
4968
|
+
* const [emailEq, bioMatch] = await client.encryptQueryBulk([
|
|
4969
|
+
* { value: "alice@example.com", table: users, column: users.email,
|
|
4970
|
+
* queryType: "equality" },
|
|
4971
|
+
* { value: "needle", table: users, column: users.bio,
|
|
4972
|
+
* queryType: "freeTextSearch" },
|
|
4973
|
+
* ])
|
|
4974
|
+
* ```
|
|
4975
|
+
*
|
|
4976
|
+
* @param terms - The needles to encrypt; see {@link WasmQueryTerm}.
|
|
4977
|
+
* @returns Index-aligned array of v3 query terms (or `null` per null value).
|
|
4978
|
+
* @throws As {@link encryptQuery} — the first invalid term aborts the batch.
|
|
4979
|
+
*/
|
|
4980
|
+
async encryptQueryBulk(terms) {
|
|
4981
|
+
const live = [];
|
|
4982
|
+
terms.forEach((term, at) => {
|
|
4983
|
+
if (term.value !== null && term.value !== void 0)
|
|
4984
|
+
live.push({ term, at });
|
|
4985
|
+
});
|
|
4986
|
+
const out = terms.map(() => null);
|
|
4987
|
+
if (live.length === 0) return out;
|
|
4988
|
+
const ffiTerms = live.map(({ term }) => toFfiQueryTerm(term.value, term));
|
|
4989
|
+
const encrypted = await wasmEncryptQueryBulk(
|
|
4990
|
+
// biome-ignore lint/plugin: the FFI handle is an opaque wasm-bindgen pointer with no JS-side type
|
|
4991
|
+
this.client,
|
|
4992
|
+
// biome-ignore lint/plugin: the batch crosses the serde boundary, whose shape protect-ffi types as `any`
|
|
4993
|
+
{
|
|
4994
|
+
queries: ffiTerms
|
|
4995
|
+
}
|
|
4996
|
+
);
|
|
4997
|
+
encrypted.forEach((value, i) => {
|
|
4998
|
+
const slot = live[i];
|
|
4999
|
+
if (slot) out[slot.at] = value;
|
|
5000
|
+
});
|
|
5001
|
+
return out;
|
|
5002
|
+
}
|
|
4751
5003
|
};
|
|
4752
5004
|
async function Encryption(config) {
|
|
4753
5005
|
const { schemas, config: clientConfig } = config;
|
|
@@ -4806,6 +5058,22 @@ function getColumnName(col) {
|
|
|
4806
5058
|
"[encryption]: opts.column must be a column builder exposing getName()"
|
|
4807
5059
|
);
|
|
4808
5060
|
}
|
|
5061
|
+
function toFfiQueryTerm(value, opts) {
|
|
5062
|
+
assertValidNumericValue(value);
|
|
5063
|
+
const { indexType, queryOp } = resolveIndexType(
|
|
5064
|
+
opts.column,
|
|
5065
|
+
opts.queryType,
|
|
5066
|
+
value
|
|
5067
|
+
);
|
|
5068
|
+
assertValueIndexCompatibility(value, indexType, getColumnName(opts.column));
|
|
5069
|
+
return {
|
|
5070
|
+
plaintext: value,
|
|
5071
|
+
table: opts.table.tableName,
|
|
5072
|
+
column: getColumnName(opts.column),
|
|
5073
|
+
indexType,
|
|
5074
|
+
...queryOp ? { queryOp } : {}
|
|
5075
|
+
};
|
|
5076
|
+
}
|
|
4809
5077
|
var warnedStrategyDeprecated = false;
|
|
4810
5078
|
function warnStrategyDeprecated() {
|
|
4811
5079
|
if (warnedStrategyDeprecated) return;
|