Skip to content

Commit 7601291

Browse files
authored
Merge pull request #330 from cipherstash/stack-auth
feat: implement auth into stash cli
2 parents b1b5157 + 07f5c4c commit 7601291

File tree

12 files changed

+253
-54
lines changed

12 files changed

+253
-54
lines changed

.changeset/warm-wasps-joke.md

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+
Implement stack auth into stash cli flow.

packages/stack/README.md

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,29 @@ pnpm add @cipherstash/stack
4444

4545
## Quick Start
4646

47+
### 1. Initialize and authenticate your project
48+
49+
```bash
50+
npx stash init
51+
```
52+
53+
The wizard will authenticate you, walk you through choosing a database connection method, build an encryption schema, and install the required dependencies.
54+
55+
### 2. Encrypt and decrypt
56+
4757
```typescript
4858
import { Encryption } from "@cipherstash/stack"
4959
import { encryptedTable, encryptedColumn } from "@cipherstash/stack/schema"
5060

51-
// 1. Define a schema
61+
// Define a schema
5262
const users = encryptedTable("users", {
5363
email: encryptedColumn("email").equality().freeTextSearch(),
5464
})
5565

56-
// 2. Create a client (reads CS_* env vars automatically)
66+
// Create a client
5767
const client = await Encryption({ schemas: [users] })
5868

59-
// 3. Encrypt a value
69+
// Encrypt a value
6070
const encrypted = await client.encrypt("hello@example.com", {
6171
column: users.email,
6272
table: users,
@@ -68,7 +78,7 @@ if (encrypted.failure) {
6878
console.log("Encrypted payload:", encrypted.data)
6979
}
7080

71-
// 4. Decrypt the value
81+
// Decrypt the value
7282
const decrypted = await client.decrypt(encrypted.data)
7383

7484
if (decrypted.failure) {
@@ -430,6 +440,16 @@ await secrets.delete("DATABASE_URL")
430440

431441
The `stash` CLI is bundled with the package and available after install.
432442

443+
### `stash auth`
444+
445+
Authenticate with CipherStash.
446+
447+
```bash
448+
npx stash auth login
449+
```
450+
451+
This runs the device code flow: it opens your browser, you confirm the code, and a token is saved to `~/.cipherstash/auth.json`. No environment variables or credentials files are needed for local development.
452+
433453
### `stash init`
434454

435455
Initialize CipherStash for your project with an interactive wizard.
@@ -440,11 +460,13 @@ npx stash init --supabase
440460
```
441461

442462
The wizard will:
443-
1. Choose your database connection method (Drizzle ORM, Supabase JS, Prisma, or Raw SQL)
444-
2. Build an encryption schema interactively or use a placeholder, then generate the encryption client file
445-
3. Install `@cipherstash/stack-forge` as a dev dependency for database tooling
463+
1. Authenticate with CipherStash (device code flow)
464+
2. Bind your device to the default Keyset
465+
3. Choose your database connection method (Drizzle ORM, Supabase JS, Prisma, or Raw SQL)
466+
4. Build an encryption schema interactively or use a placeholder, then generate the encryption client file
467+
5. Install `@cipherstash/stack-forge` as a dev dependency for database tooling
446468

447-
After `stash init`, create a CipherStash account at [dashboard.cipherstash.com/sign-up](https://dashboard.cipherstash.com/sign-up) to get your credentials, then run `npx stash-forge setup` to configure your database connection.
469+
After `stash init`, run `npx stash-forge setup` to configure your database.
448470

449471
| Flag | Description |
450472
|------|-------------|
@@ -468,11 +490,15 @@ npx stash secrets delete -name DATABASE_URL -environment production
468490
| `stash secrets list` | `-environment` | `-e` | List all secret names in an environment |
469491
| `stash secrets delete` | `-name`, `-environment`, `-yes` | `-n`, `-e`, `-y` | Delete a secret (prompts for confirmation unless `-yes`) |
470492

471-
The CLI reads credentials from the same `CS_*` environment variables described in [Configuration](#configuration).
472-
473493
## Configuration
474494

475-
### Environment Variables
495+
### Local Development
496+
497+
No environment variables or credentials are needed for local development. Run `npx @cipherstash/stack auth login` to authenticate via the device code flow, and the SDK and CLI will use the token saved to `~/.cipherstash/auth.json`.
498+
499+
### Going to Production
500+
501+
For production, CI/CD, and deployed environments, you'll need to set up machine credentials via environment variables:
476502

477503
| Variable | Description |
478504
|-----|-------|
@@ -481,13 +507,7 @@ The CLI reads credentials from the same `CS_*` environment variables described i
481507
| `CS_CLIENT_KEY` | Client key material used with ZeroKMS for encryption |
482508
| `CS_CLIENT_ACCESS_KEY` | API key for authenticating with the CipherStash API |
483509

484-
Store these in a `.env` file or set them in your hosting platform.
485-
486-
Sign up at [cipherstash.com/signup](https://cipherstash.com/signup) and follow the onboarding to generate credentials.
487-
488-
### TOML Config
489-
490-
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.
510+
See the [Going to Production](https://cipherstash.com/docs/stack/going-to-production) guide for full details on creating machine clients, setting up access keys, and configuring CI/CD pipelines.
491511

492512
### Programmatic Config
493513

packages/stack/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,11 @@
206206
"access": "public"
207207
},
208208
"dependencies": {
209-
"@byteslice/result": "^0.2.0",
209+
"@byteslice/result": "0.2.0",
210+
"@cipherstash/auth": "0.34.2",
210211
"@cipherstash/protect-ffi": "0.21.0",
211-
"evlog": "^1.9.0",
212-
"uuid": "^13.0.0",
212+
"evlog": "1.9.0",
213+
"uuid": "13.0.0",
213214
"zod": "3.24.2"
214215
},
215216
"peerDependencies": {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { bindDevice, login, selectRegion } from './login.js'
2+
3+
const HELP = `
4+
Usage: stash auth <command>
5+
6+
Commands:
7+
login Authenticate with CipherStash
8+
9+
Examples:
10+
stash auth login
11+
`.trim()
12+
13+
export async function authCommand(args: string[]) {
14+
const subcommand = args[0]
15+
16+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
17+
console.log(HELP)
18+
return
19+
}
20+
21+
switch (subcommand) {
22+
case 'login': {
23+
const region = await selectRegion()
24+
await login(region)
25+
await bindDevice()
26+
}
27+
break
28+
default:
29+
console.error(`Unknown auth command: ${subcommand}\n`)
30+
console.log(HELP)
31+
process.exit(1)
32+
}
33+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as p from '@clack/prompts'
2+
import auth from '@cipherstash/auth'
3+
const { beginDeviceCodeFlow, bindClientDevice } = auth
4+
5+
// TODO: pull from the CTS API
6+
export const regions = [
7+
{ value: 'ap-southeast-2.aws', label: 'Asia Pacific (Sydney)' },
8+
{ value: 'eu-central-1.aws', label: 'Europe (Frankfurt)' },
9+
{ value: 'eu-west-1.aws', label: 'Europe (Ireland)' },
10+
{ value: 'us-east-1.aws', label: 'US East (N. Virginia)' },
11+
{ value: 'us-east-2.aws', label: 'US East (Ohio)' },
12+
{ value: 'us-west-1.aws', label: 'US West (N. California)' },
13+
{ value: 'us-west-2.aws', label: 'US West (Oregon)' },
14+
]
15+
16+
export async function selectRegion(): Promise<string> {
17+
const region = await p.select({
18+
message: 'Select a region',
19+
options: regions,
20+
})
21+
22+
if (p.isCancel(region)) {
23+
p.cancel('Cancelled.')
24+
process.exit(0)
25+
}
26+
27+
return region
28+
}
29+
30+
export async function login(region: string) {
31+
const s = p.spinner()
32+
33+
const pending = await beginDeviceCodeFlow(region, 'cli')
34+
35+
p.log.info(`Your code is: ${pending.userCode}`)
36+
p.log.info(`Visit: ${pending.verificationUriComplete}`)
37+
p.log.info(`Code expires in: ${pending.expiresIn}s`)
38+
39+
const opened = pending.openInBrowser()
40+
if (!opened) {
41+
p.log.warn('Could not open browser — please visit the URL above manually.')
42+
}
43+
44+
s.start('Waiting for authorization...')
45+
const auth = await pending.pollForToken()
46+
s.stop('Authenticated! Token saved to ~/.cipherstash/auth.json')
47+
48+
p.log.info(
49+
`Token expires at: ${new Date(auth.expiresAt * 1000).toISOString()}`,
50+
)
51+
}
52+
53+
export async function bindDevice() {
54+
const s = p.spinner()
55+
s.start('Binding device to the default Keyset...')
56+
57+
try {
58+
await bindClientDevice()
59+
s.stop('Your device has been bound to the default Keyset!')
60+
} catch (error) {
61+
s.stop('Failed to bind your device to the default Keyset!')
62+
p.log.error(error instanceof Error ? error.message : 'Unknown error')
63+
process.exit(1)
64+
}
65+
}

packages/stack/src/bin/commands/init/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as p from '@clack/prompts'
22
import { createBaseProvider } from './providers/base.js'
33
import { createSupabaseProvider } from './providers/supabase.js'
4+
import { authenticateStep } from './steps/authenticate.js'
45
import { buildSchemaStep } from './steps/build-schema.js'
56
import { installForgeStep } from './steps/install-forge.js'
67
import { nextStepsStep } from './steps/next-steps.js'
@@ -13,6 +14,7 @@ const PROVIDER_MAP: Record<string, () => InitProvider> = {
1314
}
1415

1516
const STEPS = [
17+
authenticateStep,
1618
selectConnectionStep,
1719
buildSchemaStep,
1820
installForgeStep,

packages/stack/src/bin/commands/init/providers/base.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ export function createBaseProvider(): InitProvider {
1111
{ value: 'raw-sql', label: 'Raw SQL / pg' },
1212
],
1313
getNextSteps(state: InitState): string[] {
14-
const steps = [
15-
'Create a CipherStash account and get your credentials:\n https://dashboard.cipherstash.com/sign-up\n Then set: CS_WORKSPACE_CRN, CS_CLIENT_ID, CS_CLIENT_KEY, CS_CLIENT_ACCESS_KEY',
16-
'Set up your database: npx stash-forge setup',
17-
]
14+
const steps = ['Set up your database: npx stash-forge setup']
1815

1916
if (state.clientFilePath) {
2017
steps.push(`Edit your encryption schema: ${state.clientFilePath}`)

packages/stack/src/bin/commands/init/providers/supabase.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ export function createSupabaseProvider(): InitProvider {
1515
{ value: 'raw-sql', label: 'Raw SQL / pg' },
1616
],
1717
getNextSteps(state: InitState): string[] {
18-
const steps = [
19-
'Create a CipherStash account and get your credentials:\n https://dashboard.cipherstash.com/sign-up\n Then set: CS_WORKSPACE_CRN, CS_CLIENT_ID, CS_CLIENT_KEY, CS_CLIENT_ACCESS_KEY',
20-
'Set up your database: npx stash-forge setup',
21-
]
18+
const steps = ['Set up your database: npx stash-forge setup']
2219

2320
if (state.clientFilePath) {
2421
steps.push(`Edit your encryption schema: ${state.clientFilePath}`)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { bindDevice, login, selectRegion } from '../../auth/login.js'
2+
import type { InitProvider, InitState, InitStep } from '../types.js'
3+
4+
export const authenticateStep: InitStep = {
5+
id: 'authenticate',
6+
name: 'Authenticate with CipherStash',
7+
async run(state: InitState, _provider: InitProvider): Promise<InitState> {
8+
const region = await selectRegion()
9+
await login(region)
10+
await bindDevice()
11+
return { ...state, authenticated: true }
12+
},
13+
}

packages/stack/src/bin/commands/init/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface SchemaDef {
1818
}
1919

2020
export interface InitState {
21+
authenticated?: boolean
2122
connectionMethod?: ConnectionMethod
2223
clientFilePath?: string
2324
schemaGenerated?: boolean

0 commit comments

Comments
 (0)