Skip to content

Commit fcbe7da

Browse files
committed
chore(stack): EQL v3 concrete types via protect-ffi 0.28
Squashes the concrete-type domain work and the protect-ffi 0.24 -> 0.28 bump into a single commit, removing the intermediate 0.27 -> 0.28 rename and re-bump churn (package.json, pnpm-lock.yaml, changeset).
1 parent 4d5bf63 commit fcbe7da

27 files changed

Lines changed: 4463 additions & 4392 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cipherstash/stack': minor
3+
---
4+
5+
Upgrade `@cipherstash/protect-ffi` to 0.28.0 and update EQL v3 concrete Postgres domain names to match the SQL fixture (`integer*`, `smallint*`, `bool`, `real*`, and `double*`). The public factories remain semantic (`Integer`, `Smallint`, `Boolean`, `Real`, `Double`) while their concrete domains change, so this is a minor release.

.changeset/eql-v3-typed-client.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,19 @@ Every method derives its types from the concrete `table` / `column` builder
1111
arguments:
1212

1313
- `encrypt` / `encryptQuery` pin the plaintext to the column's domain type
14-
(`text → string`, `int8 → bigint`, `timestamp → Date`, …).
14+
(`text → string`, `timestamp → Date`, …).
1515
- `encryptQuery` constrains `queryType` to the column's capabilities and rejects
1616
storage-only columns at compile time.
1717
- `encryptModel` / `bulkEncryptModels` validate schema-column fields against their
1818
inferred plaintext type (passthrough fields are untouched) and return a precise
1919
encrypted model.
2020
- `decryptModel` / `bulkDecryptModels` return the precise plaintext model,
21-
reconstructing `Date` / `bigint` values from the encrypt-config `cast_as`.
21+
reconstructing `Date` values from the encrypt-config `cast_as`.
2222

2323
Because the typed methods bind to the concrete branded v3 classes, a hand-rolled
2424
structural table/column is rejected — closing the soundness gap where a non-branded
2525
table could be encrypted at runtime while typed as plaintext.
2626

2727
Runtime behaviour is unchanged: the encrypt/query paths return the same operations
28-
as the base client; only the model-decrypt paths add a per-column `Date` / `bigint`
28+
as the base client; only the model-decrypt paths add a per-column `Date`
2929
reconstruction step. The v2 client surface (`Encryption`) is untouched.

.changeset/eql-v3-typed-schema.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
'@cipherstash/stack': minor
33
---
44

5-
Add EQL v3 schema builders for all generated SQL domains under `@cipherstash/stack/eql/v3`, exposed as the `types` namespace (one member per EQL v3 domain, e.g. `types.TextEq` / `types.Int4Ord` / `types.Timestamp`), including explicit query capability metadata (`getQueryCapabilities()` / `isQueryable()`) and v3 table support in model encryption helpers (`encryptModel` / `bulkEncryptModels`).
5+
Add EQL v3 schema builders for supported generated SQL domains under `@cipherstash/stack/eql/v3`, exposed as the `types` namespace (one member per supported EQL v3 domain, e.g. `types.TextEq` / `types.IntegerOrd` / `types.Timestamp`), including explicit query capability metadata (`getQueryCapabilities()` / `isQueryable()`) and v3 table support in model encryption helpers (`encryptModel` / `bulkEncryptModels`).
66

7-
Also widen the accepted plaintext input type for `encrypt` / `encryptQuery` to include `Date` and `bigint` (via the new `Plaintext` type), so v3 `date` / `timestamp` / `int8` domains can be encrypted and queried with their natural JavaScript values.
7+
Also widen the accepted plaintext input type for `encrypt` / `encryptQuery` to include `Date` (via the new `Plaintext` type), so v3 `date` / `timestamp` domains can be encrypted and queried with their natural JavaScript values.

.changeset/skip-v3-bigint.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cipherstash/stack': patch
3+
---
4+
5+
Omit EQL v3 bigint factories from the public DSL and test matrix until native bigint round-tripping is supported.

e2e/wasm/deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
},
1010
"imports": {
1111
"@cipherstash/stack/wasm-inline": "../../packages/stack/dist/wasm-inline.js",
12-
"@cipherstash/protect-ffi/wasm-inline": "npm:@cipherstash/protect-ffi@0.26.0/wasm-inline",
12+
"@cipherstash/protect-ffi/wasm-inline": "npm:@cipherstash/protect-ffi@0.28.0/wasm-inline",
1313
"@cipherstash/auth/wasm-inline": "npm:@cipherstash/auth@0.40.0/wasm-inline"
1414
}
1515
}

packages/stack/__tests__/cjs-require.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe('CJS consumers can require the built bundles', () => {
9393
`if (typeof v3.encryptedTable !== 'function') { throw new Error('missing v3 CJS export: encryptedTable') }`,
9494
`if (typeof v3.buildEncryptConfig !== 'function') { throw new Error('missing v3 CJS export: buildEncryptConfig') }`,
9595
`if (typeof v3.types !== 'object' || v3.types === null) { throw new Error('missing v3 CJS export: types namespace') }`,
96-
`const requiredTypes = ['TextSearch', 'TextEq', 'Int4Ord', 'Bool', 'Timestamp']`,
96+
`const requiredTypes = ['TextSearch', 'TextEq', 'IntegerOrd', 'Boolean', 'Timestamp']`,
9797
`const missing = requiredTypes.filter((k) => typeof v3.types[k] !== 'function')`,
9898
`if (missing.length > 0) { throw new Error('missing v3 types.* CJS members: ' + missing.join(', ')) }`,
9999
].join('\n')

packages/stack/__tests__/encrypt-lock-context-guards.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const users = encryptedTable('users', {
4040
})
4141

4242
const usersV3 = encryptedTableV3('users_v3', {
43-
score: types.Int4Ord('score'),
43+
score: types.IntegerOrd('score'),
4444
})
4545

4646
// biome-ignore lint/suspicious/noExplicitAny: test helper reads the Result union

packages/stack/__tests__/error-codes.test.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { EncryptionClient } from '@/encryption'
55
import { EncryptionErrorTypes } from '@/errors'
66
import { Encryption } from '@/index'
77
import { encryptedColumn, encryptedTable } from '@/schema'
8+
import type { BulkDecryptPayload } from '@/types'
89

910
/** FFI tests require longer timeout due to client initialization */
1011
const FFI_TEST_TIMEOUT = 30_000
@@ -246,26 +247,30 @@ describe('FFI Error Code Preservation', () => {
246247

247248
describe('bulkDecrypt error codes', () => {
248249
it(
249-
'returns undefined code for malformed ciphertexts (non-FFI validation)',
250+
'returns per-item errors for malformed ciphertexts',
250251
async () => {
251-
// bulkDecrypt uses the "fallible" FFI API (decryptBulkFallible) which normally:
252-
// - Succeeds at the operation level
253-
// - Returns per-item results with either { data } or { error }
254-
//
255-
// However, malformed ciphertexts cause parsing errors BEFORE the fallible API,
256-
// which throws and triggers a top-level failure (not per-item errors).
257-
// These pre-FFI errors don't have structured FFI error codes.
252+
// bulkDecrypt uses the fallible FFI API, so malformed items are
253+
// reported per item instead of failing the whole operation.
258254
const invalidCiphertexts = [
259255
{ data: { i: { t: 'test_table', c: 'email' }, v: 2, c: 'invalid1' } },
260256
{ data: { i: { t: 'test_table', c: 'email' }, v: 2, c: 'invalid2' } },
261-
]
257+
] as unknown as BulkDecryptPayload
262258

263259
const result = await protectClient.bulkDecrypt(invalidCiphertexts)
264260

265-
expect(result.failure).toBeDefined()
266-
expect(result.failure?.type).toBe(EncryptionErrorTypes.DecryptionError)
267-
// FFI parsing errors don't have structured error codes
268-
expect(result.failure?.code).toBeUndefined()
261+
if (result.failure) {
262+
throw new Error(
263+
`Expected per-item errors, got ${result.failure.type}`,
264+
)
265+
}
266+
267+
expect(result.data).toHaveLength(2)
268+
expect(result.data.every((item) => 'error' in item)).toBe(true)
269+
270+
for (const item of result.data) {
271+
expect(item.error).toBeDefined()
272+
expect('code' in item).toBe(false)
273+
}
269274
},
270275
FFI_TEST_TIMEOUT,
271276
)

0 commit comments

Comments
 (0)