Skip to content

Conversation

@tobyhede
Copy link
Contributor

No description provided.

Add new query encryption API for searchable encryption:
- encryptQuery(): Single value query encryption with index type control
- createQuerySearchTerms(): Bulk query encryption with mixed index types
- createJsonSearchTerms(): JSON path and containment query encryption

Features:
- Support for all index types: ore, match, unique, ste_vec
- Lock context support for all query operations
- SEM-only payloads (no ciphertext) optimized for database queries
- Path queries (dot notation and array format)
- Containment queries (contains/contained_by)

Test coverage includes:
- Lock context integration tests
- Boundary conditions (empty strings, Unicode, emoji, large numbers)
- Deep JSON nesting (5+ levels)
- Bulk operation edge cases
- Error handling scenarios
@changeset-bot
Copy link

changeset-bot bot commented Jan 16, 2026

🦋 Changeset detected

Latest commit: e1ea208

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@cipherstash/protect Major
@cipherstash/schema Major
@cipherstash/drizzle Major
@cipherstash/protect-dynamodb Major
@cipherstash/basic-example Patch
@cipherstash/dynamo-example Patch
nest Patch
next-drizzle-mysql Patch
@cipherstash/nextjs-clerk-example Patch
@cipherstash/typeorm-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Remove public API additions that diverged from requirements:
- Requirements specified using existing createSearchTerms function
- Requirements specified NOT changing the existing protectjs public API

Removed:
- encryptQuery(), createQuerySearchTerms(), createJsonSearchTerms() methods
- Public type exports for query-specific types
- Test files for removed public API

Internal operation files remain for potential future use.
- Revert package.json from local link to published 0.19.0
- Define IndexTypeName and QueryOpName locally in types.ts
- These types will be available from FFI once 0.20.0 is released
Add 32 tests covering JsonSearchTermsOperation including:
- Path queries (string/array paths, deep paths, path-only)
- Containment queries (simple/nested objects, multiple keys)
- Bulk operations (mixed queries, multiple columns)
- Lock context integration
- Edge cases (unicode, deep nesting, special chars)
- Error handling (missing ste_vec index)
- Selector generation verification
Add missing public methods to ProtectClient:
- encryptQuery: encrypt single value with explicit index type
- createQuerySearchTerms: bulk query term encryption
- createJsonSearchTerms: JSON path/containment query encryption

Update tests to use public API instead of unsafe internal access.
Export new operation types and search term types.
Updates README.md, schema reference, and searchable encryption guides to include details on the new JSON search capabilities (path and containment queries).
…operations

Covers encryptQuery and createQuerySearchTerms with unique, ORE, and match indexes,
as well as composite-literal return types and lock context integration.
SearchTerm is now a union of SimpleSearchTerm, JsonPathSearchTerm,
and JsonContainmentSearchTerm, enabling createSearchTerms to accept
all search term types in a single call.

- Add SimpleSearchTerm type alias for original behavior
- Update SearchTerm to union type
- Export SimpleSearchTerm from public API
SearchTermsOperation.execute() now handles JSON search terms:
- Partitions terms by type (simple, JSON path, JSON containment)
- Encrypts simple terms with encryptBulk (original behavior)
- Encrypts JSON terms with encryptQueryBulk (ste_vec index)
- Reassembles results in original order
- Supports mixed batches of simple and JSON terms

Also includes:
- Type guards for SearchTerm variants
- Helper functions (pathToSelector, buildNestedObject, flattenJson)
- withLockContext support for JSON terms
- Extracted shared logic into encryptSearchTermsHelper to reduce duplication
Tests for:
- JSON path search term via createSearchTerms
- JSON containment search term via createSearchTerms
- Mixed simple and JSON search terms in single call
Add @deprecated JSDoc tag to guide users toward createSearchTerms.
Implementation unchanged to avoid breaking existing code.
Remove the deprecated createJsonSearchTerms function and supporting code,
consolidating JSON search functionality into the unified createSearchTerms API.

- Remove createJsonSearchTerms method from ProtectClient
- Delete json-search-terms.ts operation file
- Remove JsonSearchTermsOperation export from index
- Migrate comprehensive tests to search-terms.test.ts
- Update documentation examples to use createSearchTerms
Add missing lock context integration tests for JSON search terms and
refactor test file to use shared beforeAll client for efficiency.
@tobyhede tobyhede force-pushed the searchable-json-query-api branch from 66227cd to c4f5d8c Compare January 20, 2026 04:29
Remove __RESOLVE_AT_BUILD__ placeholder in favor of inferring the
ste_vec prefix from table/column context when not explicitly set.

Changes:
- searchableJson() now sets empty ste_vec object
- ProtectTable.build() and buildEncryptConfig() infer prefix when missing
- Simplified error checks in search-terms.ts
- Enabled previously commented test for ste_vec index
Add tests to prevent regressions based on code review feedback:

- Selector prefix resolution test verifying table/column prefix
- encryptQuery(null) null handling verification
- escaped-composite-literal return type for createQuerySearchTerms
- ste_vec index with default queryOp for JSON object encryption
Set temporary column name prefix in searchableJson() to satisfy type
requirements, then always overwrite with full table/column prefix during
build. Update search-terms.ts to always derive prefix from table/column
names rather than relying on column.build() which may have incomplete
prefix.

This fixes the DTS build error where prefix was required by the type but
not set until table build time.
Copy link
Contributor

@calvinbrewer calvinbrewer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great @tobyhede good stuff

@calvinbrewer
Copy link
Contributor

Note: you don't have a changeset in here - might be worth bumping the major version just to be safe to account for all the different typescript targets

…ching values

- Rename IndexTypeName to QueryTypeName
- Change values: ore → orderAndRange, match → freeTextSearch,
  unique → equality, ste_vec → searchableJson
- Add queryTypes constant for convenient import
- Update JSDoc examples to use new API
- Add work files to .gitignore
@tobyhede tobyhede force-pushed the searchable-json-query-api branch from b79c96d to 50d5f27 Compare January 22, 2026 00:18
@tobyhede tobyhede requested a review from calvinbrewer January 22, 2026 00:20
Comment on lines 77 to 91
```typescript
const users = [
const usersList = [
{
id: "1",
email: "user1@example.com",
address: "123 Main St",
},
{
id: "2",
email: "user2@example.com",
address: "456 Oak Ave",
},
];

const encryptedResult = await protectClient.bulkEncryptModels(users, users);
const encryptedResult = await protectClient.bulkEncryptModels(usersList, usersSchema);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker for this PR but these reference docs won't end up on the main docs site. They will need to be in the typedoc.

Comment on lines +91 to +92
> [!WARNING]
> `searchableJson()` is mutually exclusive with other index types (`equality()`, `freeTextSearch()`, `orderAndRange()`) on the same column. Combining them will result in runtime errors. This is enforced by the encryption backend, not at the TypeScript type level.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the plan to make this typescript enforceable in the future?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it needs some thought.


/**
* Query operation type for ste_vec index.
* - 'default': Standard JSON query using column's cast_type
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this might be a quirk of how the cipherstash-client is implemented leaking through. The default queryop doesn't mean anything for an ste_vec.

Comment on lines 153 to 156
// Add lock context if provided
if (lockContextData) {
return { ...plaintext, lockContext: lockContextData.context }
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LockContext isn't relevant to generation search terms.

client: Client,
terms: SearchTerm[],
metadata: Record<string, unknown> | undefined,
lockContextData: { context: Context; ctsToken: CtsToken } | undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lockContext isn't used for search.

Comment on lines 28 to 32
public withLockContext(
lockContext: LockContext,
): QuerySearchTermsOperationWithLockContext {
return new QuerySearchTermsOperationWithLockContext(this, lockContext)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed. LockContext isn't applicable to query generation.

Comment on lines 285 to 289
public withLockContext(
lockContext: LockContext,
): SearchTermsOperationWithLockContext {
return new SearchTermsOperationWithLockContext(this, lockContext)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above.

}
}

export class QuerySearchTermsOperationWithLockContext extends ProtectOperation<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this can be removed.

Comment on lines 56 to 60
public withLockContext(
lockContext: LockContext,
): EncryptQueryOperationWithLockContext {
return new EncryptQueryOperationWithLockContext(this, lockContext)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable to queries.

}
}

export class EncryptQueryOperationWithLockContext extends ProtectOperation<Encrypted> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable to queries.

Comment on lines 306 to 310
public withLockContext(
lockContext: LockContext,
): BatchEncryptQueryOperationWithLockContext {
return new BatchEncryptQueryOperationWithLockContext(this, lockContext)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable to queries.

}

const prefix = `${term.table.tableName}/${term.column.getName()}`
const pairs = flattenJson(term.contains, prefix)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something but I don't understand why we are flattening the JSON here. The json should be treated as opaque at this level and all the processing should be done by the rust json indexer in cipherstash-client.

}

const prefix = `${term.table.tableName}/${term.column.getName()}`
const pairs = flattenJson(term.containedBy, prefix)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

: term.path.split('.')
const wrappedValue = buildNestedObject(pathArray, term.value)
jsonItemsWithIndex.push({
selector: pathToSelector(term.path, prefix),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments as above. Just leave the heavy lifting to cs client.

- Encrypt ste_vec selectors using the 'ste_vec_selector' operation.
- Normalize JSON paths to standard '$' prefixed JSONPath strings for FFI compatibility.
- Remove 'withLockContext' from all search-related operations (SearchTermsOperation, QuerySearchTermsOperation, EncryptQueryOperation, BatchEncryptQueryOperation) as it is not applicable to queries.
- Update test suite to expect hex string tokens for selectors and remove now-unsupported LockContext tests.
- Address review feedback regarding naming conventions and result construction.
- Move schema documentation to packages/schema/src/index.ts.
- Move configuration and initialization documentation to packages/protect/src/index.ts.
- Move model operation documentation to packages/protect/src/ffi/index.ts.
- Move searchable encryption and PostgreSQL integration documentation to packages/protect/src/types.ts.
- Move Supabase and composite type helper documentation to packages/protect/src/helpers/index.ts.
- Add integration tips such as the ::jsonb cast requirement for Supabase/PostgreSQL.
- Wired in shared test helpers into batch-encrypt-query.test.ts and search-terms.test.ts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants