@cipherstash/protect-ffi 0.21.4 → 0.23.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/lib/index.cjs CHANGED
@@ -47,6 +47,7 @@ exports.encryptQueryBulk = encryptQueryBulk;
47
47
  exports.ensureKeyset = ensureKeyset;
48
48
  const credentials_js_1 = require("./credentials.js");
49
49
  const native = __importStar(require("./load.cjs"));
50
+ const normalizeEncryptConfig_js_1 = require("./normalizeEncryptConfig.js");
50
51
  var credentials_js_2 = require("./credentials.js");
51
52
  Object.defineProperty(exports, "withEnvCredentials", { enumerable: true, get: function () { return credentials_js_2.withEnvCredentials; } });
52
53
  class ProtectError extends Error {
@@ -81,9 +82,15 @@ function inferErrorCode(message) {
81
82
  if (message.includes(' index configured')) {
82
83
  return 'MISSING_INDEX';
83
84
  }
84
- if (message.includes("ste_vec index requires cast_as: 'json'")) {
85
+ if (message.includes('requires plaintext_type: json')) {
85
86
  return 'STE_VEC_REQUIRES_JSON_CAST_AS';
86
87
  }
88
+ if (message.includes('requires plaintext_type: text')) {
89
+ return 'MATCH_REQUIRES_TEXT';
90
+ }
91
+ if (message.includes('unsupported config version')) {
92
+ return 'UNSUPPORTED_CONFIG_VERSION';
93
+ }
87
94
  return 'UNKNOWN';
88
95
  }
89
96
  function normalizeError(err) {
@@ -117,7 +124,7 @@ function wrapSync(fn) {
117
124
  }
118
125
  function newClient(opts) {
119
126
  return wrapAsync(() => native.newClient({
120
- ...opts,
127
+ encryptConfig: (0, normalizeEncryptConfig_js_1.normalizeEncryptConfig)(opts.encryptConfig),
121
128
  clientOpts: (0, credentials_js_1.withEnvCredentials)(opts.clientOpts),
122
129
  }));
123
130
  }
package/lib/index.d.cts CHANGED
@@ -1,22 +1,23 @@
1
1
  import { type CredentialOpts } from './credentials.js';
2
+ import { type NativeEncryptConfig } from './normalizeEncryptConfig.js';
2
3
  export { withEnvCredentials, type EnvReader, type CredentialOpts, } from './credentials.js';
3
4
  declare const sym: unique symbol;
4
5
  export type Client = {
5
6
  readonly [sym]: unknown;
6
7
  };
7
8
  declare module './load.cjs' {
8
- function newClient(opts: NewClientOptions): Promise<Client>;
9
+ function newClient(opts: NativeNewClientOptions): Promise<Client>;
9
10
  function encrypt(client: Client, opts: EncryptOptions): Promise<Encrypted>;
10
11
  function decrypt(client: Client, opts: DecryptOptions): Promise<JsPlaintext>;
11
- function isEncrypted(encrypted: Encrypted): boolean;
12
+ function isEncrypted(encrypted: unknown): boolean;
12
13
  function encryptBulk(client: Client, opts: EncryptBulkOptions): Promise<Encrypted[]>;
13
14
  function decryptBulk(client: Client, opts: DecryptBulkOptions): Promise<JsPlaintext[]>;
14
15
  function decryptBulkFallible(client: Client, opts: DecryptBulkOptions): Promise<DecryptResult[]>;
15
- function encryptQuery(client: Client, opts: EncryptQueryOptions): Promise<Encrypted>;
16
- function encryptQueryBulk(client: Client, opts: EncryptQueryBulkOptions): Promise<Encrypted[]>;
16
+ function encryptQuery(client: Client, opts: EncryptQueryOptions): Promise<Encrypted | EncryptedQuery>;
17
+ function encryptQueryBulk(client: Client, opts: EncryptQueryBulkOptions): Promise<(Encrypted | EncryptedQuery)[]>;
17
18
  function ensureKeyset(opts: EnsureKeysetOpts): Promise<EnsureKeysetResult>;
18
19
  }
19
- export type ProtectErrorCode = 'INVARIANT_VIOLATION' | 'UNKNOWN_QUERY_OP' | 'UNKNOWN_COLUMN' | 'MISSING_INDEX' | 'INVALID_QUERY_INPUT' | 'INVALID_JSON_PATH' | 'STE_VEC_REQUIRES_JSON_CAST_AS' | 'UNKNOWN';
20
+ export type ProtectErrorCode = 'INVARIANT_VIOLATION' | 'UNKNOWN_QUERY_OP' | 'UNKNOWN_COLUMN' | 'MISSING_INDEX' | 'INVALID_QUERY_INPUT' | 'INVALID_JSON_PATH' | 'STE_VEC_REQUIRES_JSON_CAST_AS' | 'MATCH_REQUIRES_TEXT' | 'UNSUPPORTED_CONFIG_VERSION' | 'UNKNOWN';
20
21
  export declare class ProtectError extends Error {
21
22
  code: ProtectErrorCode;
22
23
  details?: unknown;
@@ -37,12 +38,12 @@ export type DecryptResult = {
37
38
  export declare function newClient(opts: NewClientOptions): Promise<Client>;
38
39
  export declare function encrypt(client: Client, opts: EncryptOptions): Promise<Encrypted>;
39
40
  export declare function decrypt(client: Client, opts: DecryptOptions): Promise<JsPlaintext>;
40
- export declare function isEncrypted(encrypted: Encrypted): boolean;
41
+ export declare function isEncrypted(encrypted: unknown): boolean;
41
42
  export declare function encryptBulk(client: Client, opts: EncryptBulkOptions): Promise<Encrypted[]>;
42
43
  export declare function decryptBulk(client: Client, opts: DecryptBulkOptions): Promise<JsPlaintext[]>;
43
44
  export declare function decryptBulkFallible(client: Client, opts: DecryptBulkOptions): Promise<DecryptResult[]>;
44
- export declare function encryptQuery(client: Client, opts: EncryptQueryOptions): Promise<Encrypted>;
45
- export declare function encryptQueryBulk(client: Client, opts: EncryptQueryBulkOptions): Promise<Encrypted[]>;
45
+ export declare function encryptQuery(client: Client, opts: EncryptQueryOptions): Promise<Encrypted | EncryptedQuery>;
46
+ export declare function encryptQueryBulk(client: Client, opts: EncryptQueryBulkOptions): Promise<(Encrypted | EncryptedQuery)[]>;
46
47
  /**
47
48
  * Test-only helper: ensures a keyset with the given name exists, creating it if necessary,
48
49
  * and grants the current client access. Not safe for concurrent use — intended for
@@ -67,69 +68,126 @@ export type Context = {
67
68
  identityClaim: string[];
68
69
  };
69
70
  /**
70
- * Represents encrypted data in the EQL format.
71
+ * EQL v2.3 **storage** payload the shape persisted in an `eql_v2_encrypted`
72
+ * column. Returned by {@link encrypt} / {@link encryptBulk}; the only shape
73
+ * {@link decrypt} accepts.
71
74
  *
72
- * This TypeScript type mirrors the Rust `EqlCiphertext` structure from `cipherstash-client`.
73
- * The Rust type hierarchy is:
74
- * - `EqlCiphertext` (identifier + version + body)
75
- * - `EqlCiphertextBody` (ciphertext + SEM fields + array flag)
76
- * - `EqlSEM` (all searchable encrypted metadata fields)
75
+ * Discriminated on `k`. A storage payload always carries the ciphertext — `c`
76
+ * on the scalar variant, or `sv[0].c` on the STE-vector variant. Query payloads
77
+ * carry no ciphertext and are a separate type — see {@link EncryptedQuery}.
77
78
  *
78
- * In the serialized JSON format, `#[serde(flatten)]` is used in Rust to produce a flat
79
- * structure where all fields appear at the top level rather than nested.
79
+ * ```ts
80
+ * if (payload.k === 'sv') {
81
+ * payload.sv.forEach(...)
82
+ * }
83
+ * ```
84
+ */
85
+ export type Encrypted = EncryptedScalar | EncryptedSteVec;
86
+ /** Scalar EQL v2.3 storage payload (`k: "ct"`). */
87
+ export type EncryptedScalar = {
88
+ k: 'ct';
89
+ /** EQL schema version */
90
+ v: number;
91
+ /** Table and column identifier */
92
+ i: {
93
+ t: string;
94
+ c: string;
95
+ };
96
+ /** Encrypted ciphertext (mp_base85). Always present on a storage payload. */
97
+ c: string;
98
+ /** HMAC-SHA256 term — present when a `unique` index is configured. */
99
+ hm?: string;
100
+ /** Bloom filter (set bit positions) — present when a `match` index is configured. */
101
+ bf?: number[];
102
+ /** Block ORE u64_8_256 term — present when an `ore` index is configured. */
103
+ ob?: string[];
104
+ };
105
+ /**
106
+ * STE-vector EQL v2.3 storage payload (`k: "sv"`). Carries the per-selector
107
+ * entries in `sv`; the root document ciphertext lives at `sv[0].c`.
108
+ */
109
+ export type EncryptedSteVec = {
110
+ k: 'sv';
111
+ v: number;
112
+ i: {
113
+ t: string;
114
+ c: string;
115
+ };
116
+ /** Per-selector entries; root document ciphertext lives at `sv[0].c`. */
117
+ sv: [SteVecEntry, ...SteVecEntry[]];
118
+ s?: never;
119
+ };
120
+ /**
121
+ * EQL v2.3 **query** payload — an encrypted search term. Returned, alongside
122
+ * {@link Encrypted}, by {@link encryptQuery} / {@link encryptQueryBulk}.
123
+ *
124
+ * Unlike a storage payload, a query payload carries no ciphertext (`c`): it is
125
+ * matched against stored values, never decrypted. It must not be passed to
126
+ * {@link decrypt}.
80
127
  *
81
- * Note: The ciphertext field (c) is serialized in MessagePack Base85 format.
128
+ * This covers the query shapes protect-ffi currently emits. cipherstash-client
129
+ * additionally defines `k: "sv"` hmac / ore / containment query terms; the FFI
130
+ * does not emit those today — JSON containment queries come back as an
131
+ * {@link EncryptedSteVec} storage payload.
132
+ */
133
+ export type EncryptedQuery = EncryptedScalarQuery | EncryptedSteVecSelector;
134
+ /**
135
+ * Scalar query term (`k: "ct"`, no ciphertext) — a `unique` / `match` / `ore`
136
+ * lookup term carrying exactly one of `hm`, `bf`, or `ob`.
82
137
  */
83
- export type Encrypted = {
84
- /** The table and column identifier */
138
+ export type EncryptedScalarQuery = {
139
+ k: 'ct';
140
+ /** EQL schema version */
141
+ v: number;
142
+ /** Table and column identifier */
85
143
  i: {
86
144
  t: string;
87
145
  c: string;
88
146
  };
89
- /** The encryption version */
147
+ /** Query payloads carry no ciphertext — discriminates against {@link EncryptedScalar}. */
148
+ c?: never;
149
+ } & ({
150
+ hm: string;
151
+ } | {
152
+ bf: number[];
153
+ } | {
154
+ ob: string[];
155
+ });
156
+ /**
157
+ * STE-vector selector query payload (`ste_vec_selector`) — a tokenized JSON
158
+ * path selector, no ciphertext.
159
+ */
160
+ export type EncryptedSteVecSelector = {
161
+ k: 'sv';
90
162
  v: number;
91
- /** The encrypted ciphertext (mp_base85 encoded, optional for query-mode payloads) */
92
- c?: string;
93
- /** Whether this encrypted value is part of an array */
94
- a?: boolean;
95
- /** ORE block index for 64-bit integers */
96
- ob?: string[];
97
- /** Bloom filter for approximate match queries */
98
- bf?: number[];
99
- /** HMAC-SHA256 hash for exact matches */
100
- hm?: string;
101
- /** Selector value for field selection (SteVec) */
102
- s?: string;
103
- /** Blake3 hash for exact matches (SteVec) */
104
- b3?: string;
105
- /** ORE CLLW fixed-width index for 64-bit values (SteVec) */
106
- ocf?: string;
107
- /** ORE CLLW variable-width index for strings (SteVec) */
108
- ocv?: string;
109
- /** Structured encryption vector entries (recursive) */
110
- sv?: EqlCiphertextBody[];
163
+ i: {
164
+ t: string;
165
+ c: string;
166
+ };
167
+ /** Tokenized selector for path queries. */
168
+ s: string;
169
+ sv?: never;
111
170
  };
112
171
  /**
113
- * Body of an EQL ciphertext, used recursively in SteVec entries.
172
+ * One entry inside a SteVec payload (`k: "sv"`).
173
+ *
174
+ * Every element carries `s` (selector), `c` (entry ciphertext), and exactly one
175
+ * per-element equality / ordering term (`hm` or `oc`).
114
176
  */
115
- export type EqlCiphertextBody = {
116
- /** The encrypted ciphertext (mp_base85 encoded) */
117
- c?: string;
118
- /** Whether this entry is part of an array */
177
+ export type SteVecEntry = {
178
+ /** Hex-encoded tokenized selector — deterministic per (path, key) */
179
+ s: string;
180
+ /** Per-entry encrypted record (mp_base85 encoded) */
181
+ c: string;
182
+ /** Array marker — true when the selector points at a JSON array context */
119
183
  a?: boolean;
120
- /** Selector value for field selection */
121
- s?: string;
122
- /** Blake3 hash for exact matches */
123
- b3?: string;
124
- /** ORE CLLW fixed-width index */
125
- ocf?: string;
126
- /** ORE CLLW variable-width index */
127
- ocv?: string;
128
- /** Nested SteVec entries (for deeply nested JSON) */
129
- sv?: EqlCiphertextBody[];
130
- };
131
- /** @deprecated Use EqlCiphertextBody instead */
132
- export type SteVecEntry = EqlCiphertextBody;
184
+ /** Per-entry HMAC term for non-orderable leaves (objects, arrays, booleans, null) */
185
+ hm?: string;
186
+ /** Per-entry CLLW ORE term for orderable leaves (strings, numbers) — Standard mode */
187
+ oc?: string;
188
+ };
189
+ /** @deprecated Use SteVecEntry instead */
190
+ export type EqlCiphertextBody = SteVecEntry;
133
191
  export type EncryptConfig = {
134
192
  v: number;
135
193
  tables: Record<string, Record<string, Column>>;
@@ -170,10 +228,19 @@ export type ArrayIndexMode = 'all' | 'none' | {
170
228
  wildcard?: boolean;
171
229
  position?: boolean;
172
230
  };
231
+ /**
232
+ * Encoding mode for SteVec indexes.
233
+ *
234
+ * - `standard`: standard encoding (default).
235
+ * - `compat`: backwards-compatible encoding. Set explicitly to preserve the
236
+ * pre-0.34.1-alpha.7 behaviour.
237
+ */
238
+ export type SteVecMode = 'compat' | 'standard';
173
239
  export type SteVecIndexOpts = {
174
240
  prefix: string;
175
241
  term_filters?: TokenFilter[];
176
242
  array_index_mode?: ArrayIndexMode;
243
+ mode?: SteVecMode;
177
244
  };
178
245
  export type Tokenizer = {
179
246
  kind: 'standard';
@@ -188,6 +255,11 @@ export type NewClientOptions = {
188
255
  encryptConfig: EncryptConfig;
189
256
  clientOpts?: ClientOpts;
190
257
  };
258
+ /** Options passed to the native `newClient` after vocabulary normalization. */
259
+ type NativeNewClientOptions = {
260
+ encryptConfig: NativeEncryptConfig;
261
+ clientOpts?: ClientOpts;
262
+ };
191
263
  export type ClientOpts = CredentialOpts & {
192
264
  keyset?: KeysetIdentifier;
193
265
  };
@@ -0,0 +1,29 @@
1
+ import type { Column, EncryptConfig } from './index.cjs';
2
+ /**
3
+ * The `cast_as` vocabulary the native addon (cipherstash-config's
4
+ * `CanonicalEncryptionConfig`) accepts. The public `CastAs` union contains
5
+ * three JS-only members (`string`, `number`, `bigint`) that are remapped to
6
+ * their canonical equivalents (`text`, `float`, `big_int`) before being
7
+ * handed to the native side.
8
+ */
9
+ export type NativeCastAs = 'text' | 'float' | 'big_int' | 'boolean' | 'date' | 'json' | 'timestamp';
10
+ /** A column after normalization — `cast_as` is in the canonical vocabulary. */
11
+ export type NativeColumn = Omit<Column, 'cast_as'> & {
12
+ cast_as?: NativeCastAs;
13
+ };
14
+ /** An encrypt config in the vocabulary the native addon expects. */
15
+ export type NativeEncryptConfig = Omit<EncryptConfig, 'tables'> & {
16
+ tables: Record<string, Record<string, NativeColumn>>;
17
+ };
18
+ /**
19
+ * Translate a public `EncryptConfig` into the vocabulary the native addon
20
+ * expects:
21
+ *
22
+ * - `cast_as` values `string`/`number`/`bigint` become `text`/`float`/`big_int`.
23
+ * - `ste_vec` indexes without an explicit `array_index_mode` default to
24
+ * `'none'` — the library would otherwise default to `'all'`.
25
+ *
26
+ * `mode` is intentionally left untouched: an omitted `mode` follows the
27
+ * library default (`standard`). The input config is never mutated.
28
+ */
29
+ export declare function normalizeEncryptConfig(config: EncryptConfig): NativeEncryptConfig;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeEncryptConfig = normalizeEncryptConfig;
4
+ /**
5
+ * The native addon uses a different `cast_as` vocabulary than the public JS
6
+ * API. These three JS values have no direct equivalent and are remapped to
7
+ * their canonical names.
8
+ */
9
+ const CAST_AS_REMAP = {
10
+ string: 'text',
11
+ number: 'float',
12
+ bigint: 'big_int',
13
+ };
14
+ /**
15
+ * Translate a public `EncryptConfig` into the vocabulary the native addon
16
+ * expects:
17
+ *
18
+ * - `cast_as` values `string`/`number`/`bigint` become `text`/`float`/`big_int`.
19
+ * - `ste_vec` indexes without an explicit `array_index_mode` default to
20
+ * `'none'` — the library would otherwise default to `'all'`.
21
+ *
22
+ * `mode` is intentionally left untouched: an omitted `mode` follows the
23
+ * library default (`standard`). The input config is never mutated.
24
+ */
25
+ function normalizeEncryptConfig(config) {
26
+ const tables = {};
27
+ for (const [tableName, columns] of Object.entries(config.tables)) {
28
+ const normalizedColumns = {};
29
+ for (const [columnName, column] of Object.entries(columns)) {
30
+ normalizedColumns[columnName] = normalizeColumn(column);
31
+ }
32
+ tables[tableName] = normalizedColumns;
33
+ }
34
+ return { ...config, tables };
35
+ }
36
+ function normalizeColumn(column) {
37
+ const { cast_as, indexes, ...rest } = column;
38
+ const normalized = { ...rest };
39
+ if (cast_as !== undefined) {
40
+ normalized.cast_as = remapCastAs(cast_as);
41
+ }
42
+ const steVec = indexes?.ste_vec;
43
+ if (indexes !== undefined) {
44
+ normalized.indexes = indexes;
45
+ }
46
+ if (steVec !== undefined && steVec.array_index_mode === undefined) {
47
+ normalized.indexes = {
48
+ ...indexes,
49
+ ste_vec: { ...steVec, array_index_mode: 'none' },
50
+ };
51
+ }
52
+ return normalized;
53
+ }
54
+ function remapCastAs(value) {
55
+ if (value in CAST_AS_REMAP) {
56
+ return CAST_AS_REMAP[value];
57
+ }
58
+ // The remaining `CastAs` members are already canonical `NativeCastAs` values.
59
+ return value;
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cipherstash/protect-ffi",
3
- "version": "0.21.4",
3
+ "version": "0.23.0",
4
4
  "description": "",
5
5
  "main": "./lib/index.cjs",
6
6
  "scripts": {
@@ -75,11 +75,11 @@
75
75
  "vite": "^8.0.5"
76
76
  },
77
77
  "optionalDependencies": {
78
- "@cipherstash/protect-ffi-darwin-x64": "0.21.4",
79
- "@cipherstash/protect-ffi-darwin-arm64": "0.21.4",
80
- "@cipherstash/protect-ffi-win32-x64-msvc": "0.21.4",
81
- "@cipherstash/protect-ffi-linux-x64-gnu": "0.21.4",
82
- "@cipherstash/protect-ffi-linux-arm64-gnu": "0.21.4",
83
- "@cipherstash/protect-ffi-linux-x64-musl": "0.21.4"
78
+ "@cipherstash/protect-ffi-darwin-x64": "0.23.0",
79
+ "@cipherstash/protect-ffi-darwin-arm64": "0.23.0",
80
+ "@cipherstash/protect-ffi-win32-x64-msvc": "0.23.0",
81
+ "@cipherstash/protect-ffi-linux-x64-gnu": "0.23.0",
82
+ "@cipherstash/protect-ffi-linux-arm64-gnu": "0.23.0",
83
+ "@cipherstash/protect-ffi-linux-x64-musl": "0.23.0"
84
84
  }
85
85
  }