@cipherstash/protect-ffi 0.21.4 → 0.22.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,11 +1,12 @@
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
12
  function isEncrypted(encrypted: Encrypted): boolean;
@@ -16,7 +17,7 @@ declare module './load.cjs' {
16
17
  function encryptQueryBulk(client: Client, opts: EncryptQueryBulkOptions): Promise<Encrypted[]>;
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;
@@ -67,69 +68,99 @@ export type Context = {
67
68
  identityClaim: string[];
68
69
  };
69
70
  /**
70
- * Represents encrypted data in the EQL format.
71
+ * Represents an EQL v2.3 payload returned by the FFI.
71
72
  *
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)
73
+ * Discriminated union keyed on `k`. Narrow on `k` before accessing variant-only
74
+ * fields:
77
75
  *
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.
76
+ * ```ts
77
+ * if (payload.k === 'sv') {
78
+ * payload.sv?.forEach(...)
79
+ * }
80
+ * ```
80
81
  *
81
- * Note: The ciphertext field (c) is serialized in MessagePack Base85 format.
82
+ * - `k: "ct"` scalar payload (storage or query for `unique` / `match` / `ore`
83
+ * indexes). Storage payloads always carry `c`; query payloads omit `c` and
84
+ * carry exactly one of `hm`, `bf`, or `ob`.
85
+ * - `k: "sv"` — STE-vector payload. The FFI emits this for SteVec storage
86
+ * *and* for JSON containment queries (`ste_vec_term`), both of which carry
87
+ * per-selector entries in `sv` with the root document ciphertext at
88
+ * `sv[0].c`. Selector queries (`ste_vec_selector`) instead carry a single
89
+ * tokenized selector `s` and omit `sv`.
82
90
  */
83
- export type Encrypted = {
84
- /** The table and column identifier */
91
+ export type Encrypted = EncryptedScalar | EncryptedSteVec;
92
+ /** Scalar EQL v2.3 payload (`k: "ct"`). */
93
+ export type EncryptedScalar = {
94
+ k: 'ct';
95
+ /** EQL schema version */
96
+ v: number;
97
+ /** Table and column identifier */
85
98
  i: {
86
99
  t: string;
87
100
  c: string;
88
101
  };
89
- /** The encryption version */
90
- v: number;
91
- /** The encrypted ciphertext (mp_base85 encoded, optional for query-mode payloads) */
102
+ /** Encrypted ciphertext (mp_base85). Required on storage payloads; absent on query payloads. */
92
103
  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 */
104
+ /** HMAC-SHA256 hash `unique` index term on storage, or `unique` lookup term on queries. */
100
105
  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[];
106
+ /** Bloom filter (set bit positions) — `match` index term on storage, or `match` lookup term on queries. */
107
+ bf?: number[];
108
+ /** Block ORE u64_8_256 term `ore` index term on storage, or `ore` comparison term on queries. */
109
+ ob?: string[];
111
110
  };
112
111
  /**
113
- * Body of an EQL ciphertext, used recursively in SteVec entries.
112
+ * STE-vector EQL v2.3 payload (`k: "sv"`). The FFI emits two disjoint shapes:
113
+ *
114
+ * - {@link EncryptedSteVecStorage} for storage encryption and JSON containment
115
+ * queries — carries a non-empty `sv` with the root ciphertext at `sv[0].c`.
116
+ * - {@link EncryptedSteVecSelector} for `ste_vec_selector` queries — carries
117
+ * only a tokenized selector `s`.
114
118
  */
115
- export type EqlCiphertextBody = {
116
- /** The encrypted ciphertext (mp_base85 encoded) */
117
- c?: string;
118
- /** Whether this entry is part of an array */
119
+ export type EncryptedSteVec = EncryptedSteVecStorage | EncryptedSteVecSelector;
120
+ /** SteVec storage payload (also used for containment queries). */
121
+ export type EncryptedSteVecStorage = {
122
+ k: 'sv';
123
+ v: number;
124
+ i: {
125
+ t: string;
126
+ c: string;
127
+ };
128
+ /** Per-selector entries; root document ciphertext lives at `sv[0].c`. */
129
+ sv: [SteVecEntry, ...SteVecEntry[]];
130
+ s?: never;
131
+ };
132
+ /** SteVec selector query payload (`ste_vec_selector`). */
133
+ export type EncryptedSteVecSelector = {
134
+ k: 'sv';
135
+ v: number;
136
+ i: {
137
+ t: string;
138
+ c: string;
139
+ };
140
+ /** Tokenized selector for path queries. */
141
+ s: string;
142
+ sv?: never;
143
+ };
144
+ /**
145
+ * One entry inside a SteVec payload (`k: "sv"`).
146
+ *
147
+ * Every element carries `s` (selector), `c` (entry ciphertext), and exactly one
148
+ * per-element equality / ordering term (`hm` or `oc`).
149
+ */
150
+ export type SteVecEntry = {
151
+ /** Hex-encoded tokenized selector — deterministic per (path, key) */
152
+ s: string;
153
+ /** Per-entry encrypted record (mp_base85 encoded) */
154
+ c: string;
155
+ /** Array marker — true when the selector points at a JSON array context */
119
156
  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;
157
+ /** Per-entry HMAC term for non-orderable leaves (objects, arrays, booleans, null) */
158
+ hm?: string;
159
+ /** Per-entry CLLW ORE term for orderable leaves (strings, numbers) — Standard mode */
160
+ oc?: string;
161
+ };
162
+ /** @deprecated Use SteVecEntry instead */
163
+ export type EqlCiphertextBody = SteVecEntry;
133
164
  export type EncryptConfig = {
134
165
  v: number;
135
166
  tables: Record<string, Record<string, Column>>;
@@ -170,10 +201,19 @@ export type ArrayIndexMode = 'all' | 'none' | {
170
201
  wildcard?: boolean;
171
202
  position?: boolean;
172
203
  };
204
+ /**
205
+ * Encoding mode for SteVec indexes.
206
+ *
207
+ * - `standard`: standard encoding (default).
208
+ * - `compat`: backwards-compatible encoding. Set explicitly to preserve the
209
+ * pre-0.34.1-alpha.7 behaviour.
210
+ */
211
+ export type SteVecMode = 'compat' | 'standard';
173
212
  export type SteVecIndexOpts = {
174
213
  prefix: string;
175
214
  term_filters?: TokenFilter[];
176
215
  array_index_mode?: ArrayIndexMode;
216
+ mode?: SteVecMode;
177
217
  };
178
218
  export type Tokenizer = {
179
219
  kind: 'standard';
@@ -188,6 +228,11 @@ export type NewClientOptions = {
188
228
  encryptConfig: EncryptConfig;
189
229
  clientOpts?: ClientOpts;
190
230
  };
231
+ /** Options passed to the native `newClient` after vocabulary normalization. */
232
+ type NativeNewClientOptions = {
233
+ encryptConfig: NativeEncryptConfig;
234
+ clientOpts?: ClientOpts;
235
+ };
191
236
  export type ClientOpts = CredentialOpts & {
192
237
  keyset?: KeysetIdentifier;
193
238
  };
@@ -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.22.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.22.0",
79
+ "@cipherstash/protect-ffi-darwin-arm64": "0.22.0",
80
+ "@cipherstash/protect-ffi-win32-x64-msvc": "0.22.0",
81
+ "@cipherstash/protect-ffi-linux-x64-gnu": "0.22.0",
82
+ "@cipherstash/protect-ffi-linux-arm64-gnu": "0.22.0",
83
+ "@cipherstash/protect-ffi-linux-x64-musl": "0.22.0"
84
84
  }
85
85
  }