@cipherstash/stack 0.1.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/CHANGELOG.md +7 -0
- package/LICENSE.md +21 -0
- package/README.md +670 -0
- package/dist/bin/stash.js +5049 -0
- package/dist/bin/stash.js.map +1 -0
- package/dist/chunk-2GZMIJFO.js +2400 -0
- package/dist/chunk-2GZMIJFO.js.map +1 -0
- package/dist/chunk-5DCT6YU2.js +138 -0
- package/dist/chunk-5DCT6YU2.js.map +1 -0
- package/dist/chunk-7XRPN2KX.js +336 -0
- package/dist/chunk-7XRPN2KX.js.map +1 -0
- package/dist/chunk-SJ7JO4ME.js +28 -0
- package/dist/chunk-SJ7JO4ME.js.map +1 -0
- package/dist/chunk-SUYMGQBY.js +67 -0
- package/dist/chunk-SUYMGQBY.js.map +1 -0
- package/dist/client-BxJG56Ey.d.cts +647 -0
- package/dist/client-DtGq9dJp.d.ts +647 -0
- package/dist/client.cjs +347 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +7 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +11 -0
- package/dist/client.js.map +1 -0
- package/dist/drizzle/index.cjs +1528 -0
- package/dist/drizzle/index.cjs.map +1 -0
- package/dist/drizzle/index.d.cts +350 -0
- package/dist/drizzle/index.d.ts +350 -0
- package/dist/drizzle/index.js +1212 -0
- package/dist/drizzle/index.js.map +1 -0
- package/dist/dynamodb/index.cjs +382 -0
- package/dist/dynamodb/index.cjs.map +1 -0
- package/dist/dynamodb/index.d.cts +125 -0
- package/dist/dynamodb/index.d.ts +125 -0
- package/dist/dynamodb/index.js +355 -0
- package/dist/dynamodb/index.js.map +1 -0
- package/dist/identity/index.cjs +271 -0
- package/dist/identity/index.cjs.map +1 -0
- package/dist/identity/index.d.cts +3 -0
- package/dist/identity/index.d.ts +3 -0
- package/dist/identity/index.js +117 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/index-9-Ya3fDK.d.cts +169 -0
- package/dist/index-9-Ya3fDK.d.ts +169 -0
- package/dist/index.cjs +2915 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/schema/index.cjs +368 -0
- package/dist/schema/index.cjs.map +1 -0
- package/dist/schema/index.d.cts +4 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.js +23 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/secrets/index.cjs +3207 -0
- package/dist/secrets/index.cjs.map +1 -0
- package/dist/secrets/index.d.cts +227 -0
- package/dist/secrets/index.d.ts +227 -0
- package/dist/secrets/index.js +323 -0
- package/dist/secrets/index.js.map +1 -0
- package/dist/supabase/index.cjs +1113 -0
- package/dist/supabase/index.cjs.map +1 -0
- package/dist/supabase/index.d.cts +144 -0
- package/dist/supabase/index.d.ts +144 -0
- package/dist/supabase/index.js +864 -0
- package/dist/supabase/index.js.map +1 -0
- package/dist/types-public-BCj1L4fi.d.cts +1013 -0
- package/dist/types-public-BCj1L4fi.d.ts +1013 -0
- package/dist/types-public.cjs +40 -0
- package/dist/types-public.cjs.map +1 -0
- package/dist/types-public.d.cts +4 -0
- package/dist/types-public.d.ts +4 -0
- package/dist/types-public.js +7 -0
- package/dist/types-public.js.map +1 -0
- package/package.json +202 -0
package/CHANGELOG.md
ADDED
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CipherStash
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
# @cipherstash/stack
|
|
2
|
+
|
|
3
|
+
The all-in-one TypeScript SDK for the CipherStash data security stack.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@cipherstash/stack)
|
|
6
|
+
[](https://github.com/cipherstash/protectjs/blob/main/LICENSE.md)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
9
|
+
--
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Install](#install)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Features](#features)
|
|
16
|
+
- [Schema Definition](#schema-definition)
|
|
17
|
+
- [Encryption and Decryption](#encryption-and-decryption)
|
|
18
|
+
- [Searchable Encryption](#searchable-encryption)
|
|
19
|
+
- [Identity-Aware Encryption](#identity-aware-encryption)
|
|
20
|
+
- [Secrets Management](#secrets-management)
|
|
21
|
+
- [CLI Reference](#cli-reference)
|
|
22
|
+
- [Configuration](#configuration)
|
|
23
|
+
- [Error Handling](#error-handling)
|
|
24
|
+
- [API Reference](#api-reference)
|
|
25
|
+
- [Subpath Exports](#subpath-exports)
|
|
26
|
+
- [Migration from @cipherstash/protect](#migration-from-cipherstashprotect)
|
|
27
|
+
- [Requirements](#requirements)
|
|
28
|
+
- [License](#license)
|
|
29
|
+
|
|
30
|
+
--
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @cipherstash/stack
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or with your preferred package manager:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
yarn add @cipherstash/stack
|
|
42
|
+
pnpm add @cipherstash/stack
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { Encryption } from "@cipherstash/stack"
|
|
49
|
+
import { encryptedTable, encryptedColumn } from "@cipherstash/stack/schema"
|
|
50
|
+
|
|
51
|
+
// 1. Define a schema
|
|
52
|
+
const users = encryptedTable("users", {
|
|
53
|
+
email: encryptedColumn("email").equality().freeTextSearch(),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// 2. Create a client (reads CS_* env vars automatically)
|
|
57
|
+
const client = await Encryption({ schemas: [users] })
|
|
58
|
+
|
|
59
|
+
// 3. Encrypt a value
|
|
60
|
+
const encrypted = await client.encrypt("hello@example.com", {
|
|
61
|
+
column: users.email,
|
|
62
|
+
table: users,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (encrypted.failure) {
|
|
66
|
+
console.error("Encryption failed:", encrypted.failure.message)
|
|
67
|
+
} else {
|
|
68
|
+
console.log("Encrypted payload:", encrypted.data)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. Decrypt the value
|
|
72
|
+
const decrypted = await client.decrypt(encrypted.data)
|
|
73
|
+
|
|
74
|
+
if (decrypted.failure) {
|
|
75
|
+
console.error("Decryption failed:", decrypted.failure.message)
|
|
76
|
+
} else {
|
|
77
|
+
console.log("Plaintext:", decrypted.data) // "hello@example.com"
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Features
|
|
82
|
+
|
|
83
|
+
- **Field-level encryption** - Every value encrypted with its own unique key via [ZeroKMS](https://cipherstash.com/products/zerokms), backed by AWS KMS.
|
|
84
|
+
- **Searchable encryption** - Exact match, free-text search, order/range queries, and encrypted JSONB queries in PostgreSQL.
|
|
85
|
+
- **Bulk operations** - Encrypt or decrypt thousands of values in a single ZeroKMS call (`bulkEncrypt`, `bulkDecrypt`, `bulkEncryptModels`, `bulkDecryptModels`).
|
|
86
|
+
- **Identity-aware encryption** - Tie encryption to a user's JWT via `LockContext`, so only that user can decrypt.
|
|
87
|
+
- **Secrets management** - Store, retrieve, list, and delete encrypted secrets with the `Secrets` class.
|
|
88
|
+
- **CLI (`stash`)** - Manage secrets from the terminal without writing code.
|
|
89
|
+
- **TypeScript-first** - Strongly typed schemas, results, and model operations with full generics support.
|
|
90
|
+
|
|
91
|
+
## Schema Definition
|
|
92
|
+
|
|
93
|
+
Define which tables and columns to encrypt using `encryptedTable` and `encryptedColumn` from `@cipherstash/stack/schema`.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { encryptedTable, encryptedColumn } from "@cipherstash/stack/schema"
|
|
97
|
+
|
|
98
|
+
const users = encryptedTable("users", {
|
|
99
|
+
email: encryptedColumn("email")
|
|
100
|
+
.equality() // exact-match queries
|
|
101
|
+
.freeTextSearch() // full-text search queries
|
|
102
|
+
.orderAndRange(), // sorting and range queries
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const documents = encryptedTable("documents", {
|
|
106
|
+
metadata: encryptedColumn("metadata")
|
|
107
|
+
.searchableJson(), // encrypted JSONB queries (JSONPath + containment)
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Index Types
|
|
112
|
+
|
|
113
|
+
| Method | Purpose | Query Type |
|
|
114
|
+
|----|-----|------|
|
|
115
|
+
| `.equality()` | Exact match lookups | `'equality'` |
|
|
116
|
+
| `.freeTextSearch()` | Full-text / fuzzy search | `'freeTextSearch'` |
|
|
117
|
+
| `.orderAndRange()` | Sorting, comparison, range queries | `'orderAndRange'` |
|
|
118
|
+
| `.searchableJson()` | Encrypted JSONB path and containment queries | `'searchableJson'` |
|
|
119
|
+
| `.dataType(cast)` | Set the plaintext data type (`'string'`, `'number'`, `'boolean'`, `'date'`, `'bigint'`, `'json'`) | N/A |
|
|
120
|
+
|
|
121
|
+
Methods are chainable - call as many as you need on a single column.
|
|
122
|
+
|
|
123
|
+
## Encryption and Decryption
|
|
124
|
+
|
|
125
|
+
### Single Values
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// Encrypt
|
|
129
|
+
const encrypted = await client.encrypt("secret@example.com", {
|
|
130
|
+
column: users.email,
|
|
131
|
+
table: users,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Decrypt
|
|
135
|
+
const decrypted = await client.decrypt(encrypted.data)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Model Operations
|
|
139
|
+
|
|
140
|
+
Encrypt or decrypt an entire object. Only fields matching your schema are encrypted; other fields pass through unchanged.
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const user = {
|
|
144
|
+
id: "user_123",
|
|
145
|
+
email: "alice@example.com", // defined in schema -> encrypted
|
|
146
|
+
createdAt: new Date(), // not in schema -> unchanged
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Encrypt a model
|
|
150
|
+
const encryptedResult = await client.encryptModel(user, users)
|
|
151
|
+
|
|
152
|
+
// Decrypt a model
|
|
153
|
+
const decryptedResult = await client.decryptModel(encryptedResult.data)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Type-safe generics are supported:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
type User = { id: string; email: string; createdAt: Date }
|
|
160
|
+
|
|
161
|
+
const result = await client.encryptModel<User>(user, users)
|
|
162
|
+
const back = await client.decryptModel<User>(result.data)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Bulk Operations
|
|
166
|
+
|
|
167
|
+
All bulk methods make a single call to ZeroKMS regardless of the number of records, while still using a unique key per value.
|
|
168
|
+
|
|
169
|
+
#### Bulk Encrypt / Decrypt (raw values)
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const plaintexts = [
|
|
173
|
+
{ id: "u1", plaintext: "alice@example.com" },
|
|
174
|
+
{ id: "u2", plaintext: "bob@example.com" },
|
|
175
|
+
{ id: "u3", plaintext: null }, // null values are preserved
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
const encrypted = await client.bulkEncrypt(plaintexts, {
|
|
179
|
+
column: users.email,
|
|
180
|
+
table: users,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// encrypted.data = [{ id: "u1", data: EncryptedPayload }, ...]
|
|
184
|
+
|
|
185
|
+
const decrypted = await client.bulkDecrypt(encrypted.data)
|
|
186
|
+
|
|
187
|
+
// Each item has either { data: "plaintext" } or { error: "message" }
|
|
188
|
+
for (const item of decrypted.data) {
|
|
189
|
+
if ("data" in item) {
|
|
190
|
+
console.log(`${item.id}: ${item.data}`)
|
|
191
|
+
} else {
|
|
192
|
+
console.error(`${item.id} failed: ${item.error}`)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Bulk Encrypt / Decrypt Models
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const userModels = [
|
|
201
|
+
{ id: "1", email: "alice@example.com" },
|
|
202
|
+
{ id: "2", email: "bob@example.com" },
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
const encrypted = await client.bulkEncryptModels(userModels, users)
|
|
206
|
+
const decrypted = await client.bulkDecryptModels(encrypted.data)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Searchable Encryption
|
|
210
|
+
|
|
211
|
+
Encrypt a query term so you can search encrypted data in PostgreSQL.
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// Equality query
|
|
215
|
+
const eqQuery = await client.encryptQuery("alice@example.com", {
|
|
216
|
+
column: users.email,
|
|
217
|
+
table: users,
|
|
218
|
+
queryType: "equality",
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// Free-text search
|
|
222
|
+
const matchQuery = await client.encryptQuery("alice", {
|
|
223
|
+
column: users.email,
|
|
224
|
+
table: users,
|
|
225
|
+
queryType: "freeTextSearch",
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// Order and range
|
|
229
|
+
const rangeQuery = await client.encryptQuery("alice@example.com", {
|
|
230
|
+
column: users.email,
|
|
231
|
+
table: users,
|
|
232
|
+
queryType: "orderAndRange",
|
|
233
|
+
})
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Searchable JSON
|
|
237
|
+
|
|
238
|
+
For columns using `.searchableJson()`, the query type is auto-inferred from the plaintext:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
// String -> JSONPath selector query
|
|
242
|
+
const pathQuery = await client.encryptQuery("$.user.email", {
|
|
243
|
+
column: documents.metadata,
|
|
244
|
+
table: documents,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// Object/Array -> containment query
|
|
248
|
+
const containsQuery = await client.encryptQuery({ role: "admin" }, {
|
|
249
|
+
column: documents.metadata,
|
|
250
|
+
table: documents,
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Batch Query Encryption
|
|
255
|
+
|
|
256
|
+
Encrypt multiple query terms in one call:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
const terms = [
|
|
260
|
+
{ value: "alice@example.com", column: users.email, table: users, queryType: "equality" as const },
|
|
261
|
+
{ value: "bob", column: users.email, table: users, queryType: "freeTextSearch" as const },
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
const results = await client.encryptQuery(terms)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Query Result Formatting (`returnType`)
|
|
268
|
+
|
|
269
|
+
By default `encryptQuery` returns an `Encrypted` object (the raw EQL JSON payload). Use `returnType` to change the output format:
|
|
270
|
+
|
|
271
|
+
| `returnType` | Output | Use case |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| `'eql'` (default) | `Encrypted` object | Parameterized queries, ORMs accepting JSON |
|
|
274
|
+
| `'composite-literal'` | `string` | Supabase `.eq()`, string-based APIs |
|
|
275
|
+
| `'escaped-composite-literal'` | `string` | Embedding inside another string or JSON value |
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// Get a composite literal string for use with Supabase
|
|
279
|
+
const term = await client.encryptQuery("alice@example.com", {
|
|
280
|
+
column: users.email,
|
|
281
|
+
table: users,
|
|
282
|
+
queryType: "equality",
|
|
283
|
+
returnType: "composite-literal",
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// term.data is a string — use directly with .eq()
|
|
287
|
+
await supabase.from("users").select().eq("email", term.data)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Each term in a batch can have its own `returnType`.
|
|
291
|
+
|
|
292
|
+
### PostgreSQL / Drizzle Integration Pattern
|
|
293
|
+
|
|
294
|
+
Encrypted data is stored as an [EQL](https://github.com/cipherstash/encrypt-query-language) JSON payload. Install the EQL extension in PostgreSQL to enable searchable queries, then store encrypted data in `eql_v2_encrypted` columns.
|
|
295
|
+
|
|
296
|
+
The `@cipherstash/stack/drizzle` module provides `encryptedType` for defining encrypted columns and `createEncryptionOperators` for querying them:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { pgTable, integer, timestamp } from "drizzle-orm/pg-core"
|
|
300
|
+
import { encryptedType, extractEncryptionSchema, createEncryptionOperators } from "@cipherstash/stack/drizzle"
|
|
301
|
+
import { Encryption } from "@cipherstash/stack"
|
|
302
|
+
|
|
303
|
+
// Define schema with encrypted columns
|
|
304
|
+
const usersTable = pgTable("users", {
|
|
305
|
+
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
|
306
|
+
email: encryptedType<string>("email", {
|
|
307
|
+
equality: true,
|
|
308
|
+
freeTextSearch: true,
|
|
309
|
+
orderAndRange: true,
|
|
310
|
+
}),
|
|
311
|
+
profile: encryptedType<{ name: string; bio: string }>("profile", {
|
|
312
|
+
dataType: "json",
|
|
313
|
+
searchableJson: true,
|
|
314
|
+
}),
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// Initialize
|
|
318
|
+
const usersSchema = extractEncryptionSchema(usersTable)
|
|
319
|
+
const client = await Encryption({ schemas: [usersSchema] })
|
|
320
|
+
const ops = createEncryptionOperators(client)
|
|
321
|
+
|
|
322
|
+
// Query with auto-encrypting operators
|
|
323
|
+
const results = await db.select().from(usersTable)
|
|
324
|
+
.where(await ops.eq(usersTable.email, "alice@example.com"))
|
|
325
|
+
|
|
326
|
+
// JSONB queries on encrypted JSON columns
|
|
327
|
+
const jsonResults = await db.select().from(usersTable)
|
|
328
|
+
.where(await ops.jsonbPathExists(usersTable.profile, "$.bio"))
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### Drizzle `encryptedType` Config Options
|
|
332
|
+
|
|
333
|
+
| Option | Type | Description |
|
|
334
|
+
|---|---|---|
|
|
335
|
+
| `dataType` | `"string"` \| `"number"` \| `"json"` | Plaintext data type (default: `"string"`) |
|
|
336
|
+
| `equality` | `boolean` \| `TokenFilter[]` | Enable equality index |
|
|
337
|
+
| `freeTextSearch` | `boolean` \| `MatchIndexOpts` | Enable free-text search index |
|
|
338
|
+
| `orderAndRange` | `boolean` | Enable ORE index for sorting/range queries |
|
|
339
|
+
| `searchableJson` | `boolean` | Enable JSONB path queries (requires `dataType: "json"`) |
|
|
340
|
+
|
|
341
|
+
#### Drizzle JSONB Operators
|
|
342
|
+
|
|
343
|
+
For columns with `searchableJson: true`, three JSONB operators are available:
|
|
344
|
+
|
|
345
|
+
| Operator | Description |
|
|
346
|
+
|---|---|
|
|
347
|
+
| `jsonbPathExists(col, selector)` | Check if a JSONB path exists (boolean, use in `WHERE`) |
|
|
348
|
+
| `jsonbPathQueryFirst(col, selector)` | Extract first value at a JSONB path |
|
|
349
|
+
| `jsonbGet(col, selector)` | Get value using the JSONB `->` operator |
|
|
350
|
+
|
|
351
|
+
These operators encrypt the JSON path selector using the `steVecSelector` query type and cast it to `eql_v2_encrypted` for use with the EQL PostgreSQL functions.
|
|
352
|
+
|
|
353
|
+
## Identity-Aware Encryption
|
|
354
|
+
|
|
355
|
+
Lock encryption to a specific user by requiring a valid JWT for decryption.
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
import { LockContext } from "@cipherstash/stack/identity"
|
|
359
|
+
|
|
360
|
+
// 1. Create a lock context (defaults to the "sub" claim)
|
|
361
|
+
const lc = new LockContext()
|
|
362
|
+
|
|
363
|
+
// 2. Identify the user with their JWT
|
|
364
|
+
const identifyResult = await lc.identify(userJwt)
|
|
365
|
+
|
|
366
|
+
if (identifyResult.failure) {
|
|
367
|
+
throw new Error(identifyResult.failure.message)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const lockContext = identifyResult.data
|
|
371
|
+
|
|
372
|
+
// 3. Encrypt with lock context
|
|
373
|
+
const encrypted = await client
|
|
374
|
+
.encrypt("sensitive data", { column: users.email, table: users })
|
|
375
|
+
.withLockContext(lockContext)
|
|
376
|
+
|
|
377
|
+
// 4. Decrypt with the same lock context
|
|
378
|
+
const decrypted = await client
|
|
379
|
+
.decrypt(encrypted.data)
|
|
380
|
+
.withLockContext(lockContext)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Lock contexts work with all operations: `encrypt`, `decrypt`, `encryptModel`, `decryptModel`, `bulkEncryptModels`, `bulkDecryptModels`, `bulkEncrypt`, `bulkDecrypt`.
|
|
384
|
+
|
|
385
|
+
## Secrets Management
|
|
386
|
+
|
|
387
|
+
The `Secrets` class provides end-to-end encrypted secret storage. Values are encrypted locally before being sent to the CipherStash API.
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { Secrets } from "@cipherstash/stack/secrets"
|
|
391
|
+
|
|
392
|
+
const secrets = new Secrets({
|
|
393
|
+
workspaceCRN: process.env.CS_WORKSPACE_CRN!,
|
|
394
|
+
clientId: process.env.CS_CLIENT_ID!,
|
|
395
|
+
clientKey: process.env.CS_CLIENT_KEY!,
|
|
396
|
+
apiKey: process.env.CS_CLIENT_ACCESS_KEY!,
|
|
397
|
+
environment: "production",
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// Store a secret
|
|
401
|
+
await secrets.set("DATABASE_URL", "postgres://user:pass@host:5432/db")
|
|
402
|
+
|
|
403
|
+
// Retrieve and decrypt a single secret
|
|
404
|
+
const result = await secrets.get("DATABASE_URL")
|
|
405
|
+
if (!result.failure) {
|
|
406
|
+
console.log(result.data) // "postgres://user:pass@host:5432/db"
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Retrieve multiple secrets in one call
|
|
410
|
+
const many = await secrets.getMany(["DATABASE_URL", "API_KEY"])
|
|
411
|
+
if (!many.failure) {
|
|
412
|
+
console.log(many.data.DATABASE_URL)
|
|
413
|
+
console.log(many.data.API_KEY)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// List secret names (values stay encrypted)
|
|
417
|
+
const list = await secrets.list()
|
|
418
|
+
|
|
419
|
+
// Delete a secret
|
|
420
|
+
await secrets.delete("DATABASE_URL")
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## CLI Reference
|
|
424
|
+
|
|
425
|
+
The `stash` CLI is bundled with the package and available after install.
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
npx stash secrets set -name DATABASE_URL -value "postgres://..." -environment production
|
|
429
|
+
npx stash secrets get -name DATABASE_URL -environment production
|
|
430
|
+
npx stash secrets list -environment production
|
|
431
|
+
npx stash secrets delete -name DATABASE_URL -environment production
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Commands
|
|
435
|
+
|
|
436
|
+
| Command | Flags | Aliases | Description |
|
|
437
|
+
|-----|----|-----|-------|
|
|
438
|
+
| `stash secrets set` | `-name`, `-value`, `-environment` | `-n`, `-V`, `-e` | Encrypt and store a secret |
|
|
439
|
+
| `stash secrets get` | `-name`, `-environment` | `-n`, `-e` | Retrieve and decrypt a secret |
|
|
440
|
+
| `stash secrets list` | `-environment` | `-e` | List all secret names in an environment |
|
|
441
|
+
| `stash secrets delete` | `-name`, `-environment`, `-yes` | `-n`, `-e`, `-y` | Delete a secret (prompts for confirmation unless `-yes`) |
|
|
442
|
+
|
|
443
|
+
The CLI reads credentials from the same `CS_*` environment variables described in [Configuration](#configuration).
|
|
444
|
+
|
|
445
|
+
## Configuration
|
|
446
|
+
|
|
447
|
+
### Environment Variables
|
|
448
|
+
|
|
449
|
+
| Variable | Description |
|
|
450
|
+
|-----|-------|
|
|
451
|
+
| `CS_WORKSPACE_CRN` | The workspace identifier (CRN format) |
|
|
452
|
+
| `CS_CLIENT_ID` | The client identifier |
|
|
453
|
+
| `CS_CLIENT_KEY` | Client key material used with ZeroKMS for encryption |
|
|
454
|
+
| `CS_CLIENT_ACCESS_KEY` | API key for authenticating with the CipherStash API |
|
|
455
|
+
|
|
456
|
+
Store these in a `.env` file or set them in your hosting platform.
|
|
457
|
+
|
|
458
|
+
Sign up at [cipherstash.com/signup](https://cipherstash.com/signup) and follow the onboarding to generate credentials.
|
|
459
|
+
|
|
460
|
+
### TOML Config
|
|
461
|
+
|
|
462
|
+
You can also configure credentials via `cipherstash.toml` and `cipherstash.secret.toml` files in your project root. See the [CipherStash docs](https://cipherstash.com/docs) for format details.
|
|
463
|
+
|
|
464
|
+
### Programmatic Config
|
|
465
|
+
|
|
466
|
+
Pass config directly when initializing the client:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
import { Encryption } from "@cipherstash/stack"
|
|
470
|
+
import { users } from "./schema"
|
|
471
|
+
|
|
472
|
+
const client = await Encryption({
|
|
473
|
+
schemas: [users],
|
|
474
|
+
config: {
|
|
475
|
+
workspaceCrn: "crn:ap-southeast-2.aws:your-workspace-id",
|
|
476
|
+
clientId: "your-client-id",
|
|
477
|
+
clientKey: "your-client-key",
|
|
478
|
+
accessKey: "your-access-key",
|
|
479
|
+
keyset: { name: "my-keyset" }, // or { id: "uuid" }
|
|
480
|
+
},
|
|
481
|
+
})
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Multi-Tenant Encryption (Keysets)
|
|
485
|
+
|
|
486
|
+
Isolate encryption keys per tenant using keysets:
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
const client = await Encryption({
|
|
490
|
+
schemas: [users],
|
|
491
|
+
config: {
|
|
492
|
+
keyset: { id: "123e4567-e89b-12d3-a456-426614174000" },
|
|
493
|
+
},
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
// or by name
|
|
497
|
+
const client2 = await Encryption({
|
|
498
|
+
schemas: [users],
|
|
499
|
+
config: {
|
|
500
|
+
keyset: { name: "Company A" },
|
|
501
|
+
},
|
|
502
|
+
})
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Logging
|
|
506
|
+
|
|
507
|
+
The SDK uses [evlog](https://github.com/HugoRCD/evlog) for structured logging. Each encryption operation emits a single wide event with context such as the operation type, table, column, lock context status, and duration.
|
|
508
|
+
|
|
509
|
+
#### Environment variable
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
STASH_LOG_LEVEL=debug # debug | info | warn | error (default: info)
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
#### Programmatic configuration
|
|
516
|
+
|
|
517
|
+
Pass a `logging` option when initializing the client:
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
const client = await Encryption({
|
|
521
|
+
schemas: [users],
|
|
522
|
+
logging: {
|
|
523
|
+
enabled: true, // toggle logging on/off (default: true)
|
|
524
|
+
pretty: true, // pretty-print in development (default: auto-detected)
|
|
525
|
+
},
|
|
526
|
+
})
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
#### Log draining
|
|
530
|
+
|
|
531
|
+
Send structured logs to an external observability platform by providing a `drain` callback:
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
import { Encryption } from "@cipherstash/stack"
|
|
535
|
+
|
|
536
|
+
const client = await Encryption({
|
|
537
|
+
schemas: [users],
|
|
538
|
+
logging: {
|
|
539
|
+
drain: (ctx) => {
|
|
540
|
+
// Forward to Axiom, Datadog, OTLP, etc.
|
|
541
|
+
fetch("https://your-service.com/logs", {
|
|
542
|
+
method: "POST",
|
|
543
|
+
body: JSON.stringify(ctx.event),
|
|
544
|
+
})
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
})
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
The SDK never logs plaintext data.
|
|
551
|
+
|
|
552
|
+
## Error Handling
|
|
553
|
+
|
|
554
|
+
All async methods return a `Result` object with either a `data` key (success) or a `failure` key (error). This is a discriminated union - you never get both.
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
const result = await client.encrypt("hello", { column: users.email, table: users })
|
|
558
|
+
|
|
559
|
+
if (result.failure) {
|
|
560
|
+
// result.failure.type: string (e.g. "EncryptionError")
|
|
561
|
+
// result.failure.message: string
|
|
562
|
+
console.error(result.failure.type, result.failure.message)
|
|
563
|
+
} else {
|
|
564
|
+
// result.data: Encrypted payload
|
|
565
|
+
console.log(result.data)
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Error Types
|
|
570
|
+
|
|
571
|
+
| Type | When |
|
|
572
|
+
|---|---|
|
|
573
|
+
| `ClientInitError` | Client initialization fails (bad credentials, missing config) |
|
|
574
|
+
| `EncryptionError` | An encrypt operation fails |
|
|
575
|
+
| `DecryptionError` | A decrypt operation fails |
|
|
576
|
+
| `LockContextError` | Lock context creation or usage fails |
|
|
577
|
+
| `CtsTokenError` | Identity token exchange fails |
|
|
578
|
+
|
|
579
|
+
## API Reference
|
|
580
|
+
|
|
581
|
+
### `Encryption(config)` - Initialize the client
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
function Encryption(config: EncryptionClientConfig): Promise<EncryptionClient>
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### `EncryptionClient` Methods
|
|
588
|
+
|
|
589
|
+
| Method | Signature | Returns |
|
|
590
|
+
|----|------|-----|
|
|
591
|
+
| `encrypt` | `(plaintext, { column, table })` | `EncryptOperation` (thenable) |
|
|
592
|
+
| `decrypt` | `(encryptedData)` | `DecryptOperation` (thenable) |
|
|
593
|
+
| `encryptQuery` | `(plaintext, { column, table, queryType?, returnType? })` | `EncryptQueryOperation` (thenable) |
|
|
594
|
+
| `encryptQuery` | `(terms: ScalarQueryTerm[])` | `BatchEncryptQueryOperation` (thenable) |
|
|
595
|
+
| `encryptModel` | `(model, table)` | `EncryptModelOperation<T>` (thenable) |
|
|
596
|
+
| `decryptModel` | `(encryptedModel)` | `DecryptModelOperation<T>` (thenable) |
|
|
597
|
+
| `bulkEncrypt` | `(plaintexts, { column, table })` | `BulkEncryptOperation` (thenable) |
|
|
598
|
+
| `bulkDecrypt` | `(encryptedPayloads)` | `BulkDecryptOperation` (thenable) |
|
|
599
|
+
| `bulkEncryptModels` | `(models, table)` | `BulkEncryptModelsOperation<T>` (thenable) |
|
|
600
|
+
| `bulkDecryptModels` | `(encryptedModels)` | `BulkDecryptModelsOperation<T>` (thenable) |
|
|
601
|
+
|
|
602
|
+
All operations are thenable (awaitable) and support `.withLockContext(lockContext)` for identity-aware encryption.
|
|
603
|
+
|
|
604
|
+
### `LockContext`
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
import { LockContext } from "@cipherstash/stack/identity"
|
|
608
|
+
|
|
609
|
+
const lc = new LockContext(options?)
|
|
610
|
+
const result = await lc.identify(jwtToken)
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### `Secrets`
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
import { Secrets } from "@cipherstash/stack/secrets"
|
|
617
|
+
|
|
618
|
+
const secrets = new Secrets(config)
|
|
619
|
+
await secrets.set(name, value)
|
|
620
|
+
await secrets.get(name)
|
|
621
|
+
await secrets.getMany(names)
|
|
622
|
+
await secrets.list()
|
|
623
|
+
await secrets.delete(name)
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Schema Builders
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
import { encryptedTable, encryptedColumn, csValue } from "@cipherstash/stack/schema"
|
|
630
|
+
|
|
631
|
+
encryptedTable(tableName, columns)
|
|
632
|
+
encryptedColumn(columnName) // returns ProtectColumn
|
|
633
|
+
csValue(valueName) // returns ProtectValue (for nested values)
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## Subpath Exports
|
|
637
|
+
|
|
638
|
+
| Import Path | Provides |
|
|
639
|
+
|-------|-----|
|
|
640
|
+
| `@cipherstash/stack` | `Encryption` function (main entry point) |
|
|
641
|
+
| `@cipherstash/stack/schema` | `encryptedTable`, `encryptedColumn`, `csValue`, schema types |
|
|
642
|
+
| `@cipherstash/stack/identity` | `LockContext` class and identity types |
|
|
643
|
+
| `@cipherstash/stack/secrets` | `Secrets` class and secrets types |
|
|
644
|
+
| `@cipherstash/stack/client` | Client-safe exports (schema builders and types only - no native FFI) |
|
|
645
|
+
| `@cipherstash/stack/types` | All TypeScript types (`Encrypted`, `Decrypted`, `ClientConfig`, `EncryptionClientConfig`, query types, etc.) |
|
|
646
|
+
|
|
647
|
+
## Migration from @cipherstash/protect
|
|
648
|
+
|
|
649
|
+
If you are migrating from `@cipherstash/protect`, the following table maps the old API to the new one:
|
|
650
|
+
|
|
651
|
+
| `@cipherstash/protect` | `@cipherstash/stack` | Import Path |
|
|
652
|
+
|------------|-----------|-------|
|
|
653
|
+
| `protect(config)` | `Encryption(config)` | `@cipherstash/stack` |
|
|
654
|
+
| `csTable(name, cols)` | `encryptedTable(name, cols)` | `@cipherstash/stack/schema` |
|
|
655
|
+
| `csColumn(name)` | `encryptedColumn(name)` | `@cipherstash/stack/schema` |
|
|
656
|
+
| `import { LockContext } from "@cipherstash/protect/identify"` | `import { LockContext } from "@cipherstash/stack/identity"` | `@cipherstash/stack/identity` |
|
|
657
|
+
| N/A | `Secrets` class | `@cipherstash/stack/secrets` |
|
|
658
|
+
| N/A | `stash` CLI | `npx stash` |
|
|
659
|
+
|
|
660
|
+
All method signatures on the encryption client (`encrypt`, `decrypt`, `encryptModel`, etc.) remain the same. The `Result` pattern (`data` / `failure`) is unchanged.
|
|
661
|
+
|
|
662
|
+
## Requirements
|
|
663
|
+
|
|
664
|
+
- **Node.js** >= 18
|
|
665
|
+
- The package includes a native FFI module (`@cipherstash/protect-ffi`) written in Rust and embedded via [Neon](https://github.com/neon-bindings/neon). You must opt out of bundling this package in tools like Webpack, esbuild, or Next.js (`serverExternalPackages`).
|
|
666
|
+
- [Bun](https://bun.sh/) is not currently supported due to incomplete Node-API compatibility.
|
|
667
|
+
|
|
668
|
+
## License
|
|
669
|
+
|
|
670
|
+
MIT - see [LICENSE.md](https://github.com/cipherstash/protectjs/blob/main/LICENSE.md).
|