Skip to content

Commit 89b903f

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 89b903f

27 files changed

Lines changed: 4484 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: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,34 @@
33
"//2": "stack is imported via a file URL because the wasm-inline subpath isn't on a published version yet — once stack ships with /wasm-inline this can switch to a plain npm: specifier. protect-ffi and auth resolve via npm: against the workspace's installed versions (Deno's nodeModulesDir: auto lets it use what pnpm fetched).",
44
"//3": "No --allow-ffi grant. If protect-ffi ever silently fell back to a native binding under Deno, the test would fail on missing FFI permission — this is the WASM guarantee.",
55
"nodeModulesDir": "auto",
6+
"//4": "Deno 2.9+ enforces a 24h minimumDependencyAge cooldown by default. Mirror pnpm-workspace.yaml's supply-chain policy (minimumReleaseAge: 7d + minimumReleaseAgeExclude) so the two toolchains agree: keep the 7-day cooldown for third-party deps, but exempt the first-party CipherStash packages this test imports — they ship on their own cadence and are bumped in lockstep. Without the exclude, a just-published protect-ffi/auth (e.g. 0.28.0) fails resolution until it ages past the window.",
7+
"//5": "Deno's exclude does not support globs, so the per-platform native binary sub-packages (optional deps published in lockstep at the same version) are enumerated explicitly — mirrors pnpm-workspace.yaml's protect-ffi-* / auth-* wildcards.",
8+
"minimumDependencyAge": {
9+
"age": "P7D",
10+
"exclude": [
11+
"npm:@cipherstash/protect-ffi",
12+
"npm:@cipherstash/protect-ffi-darwin-arm64",
13+
"npm:@cipherstash/protect-ffi-darwin-x64",
14+
"npm:@cipherstash/protect-ffi-linux-arm64-gnu",
15+
"npm:@cipherstash/protect-ffi-linux-x64-gnu",
16+
"npm:@cipherstash/protect-ffi-linux-x64-musl",
17+
"npm:@cipherstash/protect-ffi-win32-x64-msvc",
18+
"npm:@cipherstash/auth",
19+
"npm:@cipherstash/auth-darwin-arm64",
20+
"npm:@cipherstash/auth-darwin-x64",
21+
"npm:@cipherstash/auth-linux-arm64-gnu",
22+
"npm:@cipherstash/auth-linux-x64-gnu",
23+
"npm:@cipherstash/auth-linux-x64-musl",
24+
"npm:@cipherstash/auth-win32-x64-msvc"
25+
]
26+
},
627
"tasks": {
728
"//task": "--no-check because TypeScript can't infer the column-on-table intersection types when stack is loaded via file URL (the dist .d.ts strips brand info). This test exists to verify RUNTIME WASM round-trips — type narrowing is covered by the package's own vitest suite.",
829
"test": "deno test --no-check --allow-env --allow-net --allow-read --allow-sys --no-prompt"
930
},
1031
"imports": {
1132
"@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",
33+
"@cipherstash/protect-ffi/wasm-inline": "npm:@cipherstash/protect-ffi@0.28.0/wasm-inline",
1334
"@cipherstash/auth/wasm-inline": "npm:@cipherstash/auth@0.40.0/wasm-inline"
1435
}
1536
}

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)