@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +670 -0
  4. package/dist/bin/stash.js +5049 -0
  5. package/dist/bin/stash.js.map +1 -0
  6. package/dist/chunk-2GZMIJFO.js +2400 -0
  7. package/dist/chunk-2GZMIJFO.js.map +1 -0
  8. package/dist/chunk-5DCT6YU2.js +138 -0
  9. package/dist/chunk-5DCT6YU2.js.map +1 -0
  10. package/dist/chunk-7XRPN2KX.js +336 -0
  11. package/dist/chunk-7XRPN2KX.js.map +1 -0
  12. package/dist/chunk-SJ7JO4ME.js +28 -0
  13. package/dist/chunk-SJ7JO4ME.js.map +1 -0
  14. package/dist/chunk-SUYMGQBY.js +67 -0
  15. package/dist/chunk-SUYMGQBY.js.map +1 -0
  16. package/dist/client-BxJG56Ey.d.cts +647 -0
  17. package/dist/client-DtGq9dJp.d.ts +647 -0
  18. package/dist/client.cjs +347 -0
  19. package/dist/client.cjs.map +1 -0
  20. package/dist/client.d.cts +7 -0
  21. package/dist/client.d.ts +7 -0
  22. package/dist/client.js +11 -0
  23. package/dist/client.js.map +1 -0
  24. package/dist/drizzle/index.cjs +1528 -0
  25. package/dist/drizzle/index.cjs.map +1 -0
  26. package/dist/drizzle/index.d.cts +350 -0
  27. package/dist/drizzle/index.d.ts +350 -0
  28. package/dist/drizzle/index.js +1212 -0
  29. package/dist/drizzle/index.js.map +1 -0
  30. package/dist/dynamodb/index.cjs +382 -0
  31. package/dist/dynamodb/index.cjs.map +1 -0
  32. package/dist/dynamodb/index.d.cts +125 -0
  33. package/dist/dynamodb/index.d.ts +125 -0
  34. package/dist/dynamodb/index.js +355 -0
  35. package/dist/dynamodb/index.js.map +1 -0
  36. package/dist/identity/index.cjs +271 -0
  37. package/dist/identity/index.cjs.map +1 -0
  38. package/dist/identity/index.d.cts +3 -0
  39. package/dist/identity/index.d.ts +3 -0
  40. package/dist/identity/index.js +117 -0
  41. package/dist/identity/index.js.map +1 -0
  42. package/dist/index-9-Ya3fDK.d.cts +169 -0
  43. package/dist/index-9-Ya3fDK.d.ts +169 -0
  44. package/dist/index.cjs +2915 -0
  45. package/dist/index.cjs.map +1 -0
  46. package/dist/index.d.cts +22 -0
  47. package/dist/index.d.ts +22 -0
  48. package/dist/index.js +23 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/schema/index.cjs +368 -0
  51. package/dist/schema/index.cjs.map +1 -0
  52. package/dist/schema/index.d.cts +4 -0
  53. package/dist/schema/index.d.ts +4 -0
  54. package/dist/schema/index.js +23 -0
  55. package/dist/schema/index.js.map +1 -0
  56. package/dist/secrets/index.cjs +3207 -0
  57. package/dist/secrets/index.cjs.map +1 -0
  58. package/dist/secrets/index.d.cts +227 -0
  59. package/dist/secrets/index.d.ts +227 -0
  60. package/dist/secrets/index.js +323 -0
  61. package/dist/secrets/index.js.map +1 -0
  62. package/dist/supabase/index.cjs +1113 -0
  63. package/dist/supabase/index.cjs.map +1 -0
  64. package/dist/supabase/index.d.cts +144 -0
  65. package/dist/supabase/index.d.ts +144 -0
  66. package/dist/supabase/index.js +864 -0
  67. package/dist/supabase/index.js.map +1 -0
  68. package/dist/types-public-BCj1L4fi.d.cts +1013 -0
  69. package/dist/types-public-BCj1L4fi.d.ts +1013 -0
  70. package/dist/types-public.cjs +40 -0
  71. package/dist/types-public.cjs.map +1 -0
  72. package/dist/types-public.d.cts +4 -0
  73. package/dist/types-public.d.ts +4 -0
  74. package/dist/types-public.js +7 -0
  75. package/dist/types-public.js.map +1 -0
  76. package/package.json +202 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @cipherstash/stack
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7ed89a5: Initial release of the CipherStash Stack.
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
+ [![npm version](https://img.shields.io/npm/v/@cipherstash/stack.svg?style=for-the-badge&labelColor=000000)](https://www.npmjs.com/package/@cipherstash/stack)
6
+ [![License: MIT](https://img.shields.io/npm/l/@cipherstash/stack.svg?style=for-the-badge&labelColor=000000)](https://github.com/cipherstash/protectjs/blob/main/LICENSE.md)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-first-blue?style=for-the-badge&labelColor=000000)](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).