@promptcellar/pc 0.5.4 → 0.5.5

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.
@@ -1,776 +0,0 @@
1
- # wa-auth Integration Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Migrate pc-cli authentication to wa-auth device flow, add client-side encryption for zero-knowledge prompt capture.
6
-
7
- **Architecture:** Three repos change: wa-auth gets `client_id` on device flow + JWT issuance, prompt-registry gets a JWT-to-API-key exchange endpoint, pc-cli rewrites login and adds encryption. Changes are independent per-repo and can be implemented in sequence: wa-auth first, prompt-registry second, pc-cli third.
8
-
9
- **Tech Stack:** Python/Flask (wa-auth, prompt-registry), Node.js/ESM (pc-cli), AES-256-GCM (encryption), JWT/HS256 (tokens), Node.js built-in `crypto` module.
10
-
11
- **Design doc:** `docs/plans/2026-01-30-wa-auth-integration-design.md`
12
-
13
- ---
14
-
15
- ## Task 1: wa-auth -- Add `client_id` to DeviceAuthRequest model
16
-
17
- **Files:**
18
- - Modify: `/home/ddnewell/weldedanvil/wa-auth/models.py:45-52`
19
-
20
- **Step 1: Add `client_id` column to DeviceAuthRequest**
21
-
22
- ```python
23
- class DeviceAuthRequest(db.Model):
24
- __tablename__ = 'device_auth_requests'
25
- id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
26
- device_code = db.Column(db.String(64), unique=True, nullable=False)
27
- user_code = db.Column(db.String(16), unique=True, nullable=False)
28
- user_id = db.Column(db.String(36), db.ForeignKey('users.id'), nullable=True)
29
- client_id = db.Column(db.String(64), nullable=True)
30
- status = db.Column(db.String(16), default='pending')
31
- expires_at = db.Column(db.DateTime, nullable=False)
32
- ```
33
-
34
- **Step 2: Commit**
35
-
36
- ```bash
37
- cd /home/ddnewell/weldedanvil/wa-auth
38
- git add models.py
39
- git commit -m "feat: add client_id column to DeviceAuthRequest model"
40
- ```
41
-
42
- ---
43
-
44
- ## Task 2: wa-auth -- Accept `client_id` in `POST /device/code`
45
-
46
- **Files:**
47
- - Modify: `/home/ddnewell/weldedanvil/wa-auth/app.py:113-128`
48
-
49
- **Step 1: Update device_code route to validate and store client_id**
50
-
51
- ```python
52
- @app.route('/device/code', methods=['POST'])
53
- def device_code():
54
- payload = request.get_json(silent=True) or {}
55
- client_id = payload.get('client_id')
56
-
57
- # Validate client_id if provided
58
- if client_id:
59
- oidc_clients = app.config.get('OIDC_CLIENTS') or {}
60
- if client_id not in oidc_clients:
61
- return {'error': 'invalid_client'}, 400
62
-
63
- code = secrets.token_urlsafe(32)
64
- user_code = secrets.token_hex(4)
65
- req = DeviceAuthRequest(
66
- device_code=code,
67
- user_code=user_code,
68
- client_id=client_id,
69
- expires_at=datetime.utcnow() + timedelta(minutes=10),
70
- )
71
- db.session.add(req)
72
- db.session.commit()
73
- return {
74
- 'device_code': code,
75
- 'user_code': user_code,
76
- 'verification_url': 'https://account.weldedanvil.com/device',
77
- }
78
- ```
79
-
80
- **Step 2: Commit**
81
-
82
- ```bash
83
- git add app.py
84
- git commit -m "feat: accept client_id in POST /device/code"
85
- ```
86
-
87
- ---
88
-
89
- ## Task 3: wa-auth -- Return JWT from `POST /device/poll` on approval
90
-
91
- **Files:**
92
- - Modify: `/home/ddnewell/weldedanvil/wa-auth/app.py:168-192`
93
-
94
- **Context:** wa-auth already has JWT generation in the `/token` endpoint. Check how it's done there and reuse the same pattern. The app config has `JWT_SECRET`, `JWT_ISSUER`, and `ACCESS_TOKEN_TTL_SECONDS`.
95
-
96
- **Step 1: Find existing JWT generation code**
97
-
98
- Read `/home/ddnewell/weldedanvil/wa-auth/app.py` to find the `/token` endpoint and its JWT creation logic. Reuse the same `jwt.encode()` call pattern.
99
-
100
- **Step 2: Update device_poll to return JWT on approval**
101
-
102
- When `status == 'approved'`, generate a JWT with:
103
- - `sub`: `req.user_id`
104
- - `aud`: `req.client_id` (from the stored device request)
105
- - `iss`: `app.config['JWT_ISSUER']`
106
- - `iat`: current timestamp
107
- - `exp`: current timestamp + `app.config.get('ACCESS_TOKEN_TTL_SECONDS', 900)`
108
-
109
- Return:
110
- ```json
111
- {
112
- "status": "approved",
113
- "user_id": "...",
114
- "access_token": "<jwt>"
115
- }
116
- ```
117
-
118
- If `req.client_id` is `None` (legacy requests without client_id), omit `aud` from JWT and don't return `access_token` -- just return the existing `user_id`-only response for backward compatibility.
119
-
120
- **Step 3: Commit**
121
-
122
- ```bash
123
- git add app.py
124
- git commit -m "feat: return JWT from device poll on approval"
125
- ```
126
-
127
- ---
128
-
129
- ## Task 4: prompt-registry -- Add `POST /api/v1/auth/device-token` endpoint
130
-
131
- **Files:**
132
- - Modify: `/home/ddnewell/weldedanvil/prompt-registry/app.py`
133
-
134
- **Context:** This endpoint exchanges a wa-auth JWT for a `pk_*` API key. It uses existing functions: `verify_account_token()` from `auth.py`, `create_or_get_user()` from `auth.py`, `generate_api_key()` and `hash_api_key()` from `app.py`.
135
-
136
- **Step 1: Add the endpoint inside `register_routes(app)`**
137
-
138
- Place it after the existing device_auth_verify route (around line 193). The endpoint:
139
-
140
- 1. Reads JWT from `Authorization: Bearer <jwt>` header
141
- 2. Validates with `verify_account_token(jwt_str)` -- checks signature, audience, issuer, expiry
142
- 3. Extracts `sub` (user ID) and `email` from JWT payload
143
- 4. Calls `create_or_get_user(account_sub=sub, email=email)` to resolve user
144
- 5. Reads `device_name` from request JSON body
145
- 6. Deletes any existing `UserApiKey` where `user_id == user.id` and `name == device_name`
146
- 7. Creates new `UserApiKey` with `generate_api_key()`, stores `hash_api_key(key)` and `key_prefix`
147
- 8. Checks if vault exists by fetching from wa-auth `GET /vault` using the JWT (or a server-to-server call)
148
- 9. Returns JSON response
149
-
150
- ```python
151
- @app.route('/api/v1/auth/device-token', methods=['POST'])
152
- def device_token_exchange():
153
- """Exchange a wa-auth JWT for a pk_* API key."""
154
- auth_header = request.headers.get('Authorization', '')
155
- if not auth_header.startswith('Bearer '):
156
- return {'error': 'missing_token'}, 401
157
-
158
- jwt_str = auth_header[7:]
159
- payload, err = verify_account_token(jwt_str)
160
- if err:
161
- return {'error': err}, 401
162
-
163
- account_sub = payload.get('sub')
164
- email = payload.get('email')
165
- if not account_sub:
166
- return {'error': 'invalid_token'}, 401
167
-
168
- user = create_or_get_user(account_sub, email)
169
-
170
- data = request.get_json(silent=True) or {}
171
- device_name = data.get('device_name', 'CLI')
172
-
173
- # Auto-revoke previous key with same device name
174
- existing = UserApiKey.query.filter_by(user_id=user.id, name=device_name).all()
175
- for key in existing:
176
- db.session.delete(key)
177
-
178
- # Create new API key
179
- raw_key = generate_api_key()
180
- new_key = UserApiKey(
181
- id=str(uuid.uuid4()),
182
- user_id=user.id,
183
- name=device_name,
184
- key_hash=hash_api_key(raw_key),
185
- key_prefix=raw_key[:12],
186
- )
187
- db.session.add(new_key)
188
- db.session.commit()
189
-
190
- # Check vault availability
191
- vault_response = {'available': False}
192
- try:
193
- account_url = current_app.config['ACCOUNT_BASE_URL'].rstrip('/')
194
- vault_resp = requests.get(
195
- f"{account_url}/vault",
196
- headers={'Authorization': f'Bearer {jwt_str}'},
197
- timeout=5,
198
- )
199
- if vault_resp.status_code == 200:
200
- vault_data = vault_resp.json()
201
- vault_response = {
202
- 'available': True,
203
- 'encrypted_metadata': vault_data.get('encrypted_metadata'),
204
- }
205
- except Exception:
206
- pass
207
-
208
- return {
209
- 'api_key': raw_key,
210
- 'email': user.email,
211
- 'vault': vault_response,
212
- }, 201
213
- ```
214
-
215
- **Important notes:**
216
- - `uuid` import already exists at top of file
217
- - `requests` import already exists (used by auth.py, check if app.py needs it too)
218
- - `verify_account_token` and `create_or_get_user` need to be imported from `auth.py`
219
- - The vault check calls wa-auth's `/vault` endpoint using the JWT as bearer token. wa-auth's `/vault` currently uses session auth -- this will need wa-auth to also accept JWT bearer auth on `/vault`. See Task 5.
220
-
221
- **Step 2: Commit**
222
-
223
- ```bash
224
- cd /home/ddnewell/weldedanvil/prompt-registry
225
- git add app.py
226
- git commit -m "feat: add device-token exchange endpoint"
227
- ```
228
-
229
- ---
230
-
231
- ## Task 5: wa-auth -- Accept JWT bearer auth on `GET /vault`
232
-
233
- **Files:**
234
- - Modify: `/home/ddnewell/weldedanvil/wa-auth/app.py:232-242`
235
-
236
- **Context:** The vault endpoint currently only accepts session-based auth. prompt-registry needs to fetch vault metadata using the JWT it received. Add bearer token support.
237
-
238
- **Step 1: Update get_vault to accept JWT bearer token**
239
-
240
- ```python
241
- @app.route('/vault')
242
- def get_vault():
243
- user_id = session.get('user_id')
244
-
245
- # Also accept JWT bearer token
246
- if not user_id:
247
- auth_header = request.headers.get('Authorization', '')
248
- if auth_header.startswith('Bearer '):
249
- try:
250
- payload = jwt.decode(
251
- auth_header[7:],
252
- app.config['JWT_SECRET'],
253
- algorithms=['HS256'],
254
- options={'require': ['exp', 'sub']},
255
- )
256
- user_id = payload.get('sub')
257
- except Exception:
258
- pass
259
-
260
- if not user_id:
261
- return {'error': 'unauthorized'}, 401
262
-
263
- vault = Vault.query.filter_by(user_id=user_id).first()
264
- if not vault:
265
- return {'error': 'not_found'}, 404
266
-
267
- return {'vault_id': vault.id, 'encrypted_metadata': vault.encrypted_metadata}
268
- ```
269
-
270
- **Step 2: Commit**
271
-
272
- ```bash
273
- cd /home/ddnewell/weldedanvil/wa-auth
274
- git add app.py
275
- git commit -m "feat: accept JWT bearer auth on GET /vault"
276
- ```
277
-
278
- ---
279
-
280
- ## Task 6: pc-cli -- Add new config fields
281
-
282
- **Files:**
283
- - Modify: `/home/ddnewell/weldedanvil/pc-cli/.worktrees/wa-auth-integration/src/lib/config.js`
284
-
285
- **Step 1: Add accountUrl, vaultMetadata, and vaultAvailable to schema and exports**
286
-
287
- Add three new fields to the `Conf` schema:
288
-
289
- ```javascript
290
- accountUrl: {
291
- type: 'string',
292
- default: 'https://account.weldedanvil.com'
293
- },
294
- vaultMetadata: {
295
- type: 'string',
296
- default: ''
297
- },
298
- vaultAvailable: {
299
- type: 'boolean',
300
- default: false
301
- }
302
- ```
303
-
304
- Add getter/setter exports:
305
-
306
- ```javascript
307
- export function getAccountUrl() {
308
- return config.get('accountUrl');
309
- }
310
-
311
- export function setAccountUrl(url) {
312
- config.set('accountUrl', url);
313
- }
314
-
315
- export function getVaultMetadata() {
316
- return config.get('vaultMetadata');
317
- }
318
-
319
- export function setVaultMetadata(metadata) {
320
- config.set('vaultMetadata', metadata);
321
- }
322
-
323
- export function isVaultAvailable() {
324
- return config.get('vaultAvailable');
325
- }
326
-
327
- export function setVaultAvailable(available) {
328
- config.set('vaultAvailable', available);
329
- }
330
- ```
331
-
332
- **Step 2: Update clearApiKey to also clear vault state**
333
-
334
- Rename to `clearCredentials` or update `clearApiKey`:
335
-
336
- ```javascript
337
- export function clearApiKey() {
338
- config.delete('apiKey');
339
- config.delete('vaultMetadata');
340
- config.set('vaultAvailable', false);
341
- }
342
- ```
343
-
344
- **Step 3: Commit**
345
-
346
- ```bash
347
- cd /home/ddnewell/weldedanvil/pc-cli/.worktrees/wa-auth-integration
348
- git add src/lib/config.js
349
- git commit -m "feat: add vault and account config fields"
350
- ```
351
-
352
- ---
353
-
354
- ## Task 7: pc-cli -- Create crypto module
355
-
356
- **Files:**
357
- - Create: `/home/ddnewell/weldedanvil/pc-cli/.worktrees/wa-auth-integration/src/lib/crypto.js`
358
-
359
- **Context:** Must match the encryption format used by the web client in `prompt-registry/static/js/vault.js`. The web client uses:
360
- - AES-256-GCM via WebCrypto
361
- - Random 12-byte IV per encryption
362
- - Base64url encoding (no padding) for encrypted content and IV
363
- - The master key is stored as `encrypted_metadata` JSON containing `wrapped_master_key`, `wrap_iv`, `prf_salt`, `hkdf_info`, `version`
364
-
365
- **Important design note:** The vault master key is wrapped with a WebAuthn PRF-derived key. The CLI cannot perform passkey ceremonies. For the CLI to encrypt, it needs the raw master key. This requires a CLI key export mechanism in the web UI (out of scope for this plan -- see Task 10 for the interim approach).
366
-
367
- For now, implement the encryption functions assuming the CLI has a raw AES-256 key (32 bytes, base64url encoded) stored in config as `vaultMetadata`. The key provisioning mechanism (how the CLI gets this key) will be addressed separately.
368
-
369
- **Step 1: Create crypto.js**
370
-
371
- ```javascript
372
- import { createCipheriv, randomBytes } from 'crypto';
373
-
374
- function b64urlEncode(buffer) {
375
- return Buffer.from(buffer)
376
- .toString('base64')
377
- .replace(/\+/g, '-')
378
- .replace(/\//g, '_')
379
- .replace(/=+$/, '');
380
- }
381
-
382
- function b64urlDecode(str) {
383
- let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
384
- while (base64.length % 4) base64 += '=';
385
- return Buffer.from(base64, 'base64');
386
- }
387
-
388
- export function encryptPrompt(plaintext, keyBase64url) {
389
- const key = b64urlDecode(keyBase64url);
390
- if (key.length !== 32) {
391
- throw new Error('Invalid vault key length');
392
- }
393
-
394
- const iv = randomBytes(12);
395
- const cipher = createCipheriv('aes-256-gcm', key, iv);
396
-
397
- const encoded = Buffer.from(plaintext, 'utf8');
398
- const encrypted = Buffer.concat([cipher.update(encoded), cipher.final()]);
399
- const authTag = cipher.getAuthTag();
400
-
401
- // AES-GCM ciphertext = encrypted + authTag (WebCrypto format)
402
- const ciphertext = Buffer.concat([encrypted, authTag]);
403
-
404
- return {
405
- encrypted_content: b64urlEncode(ciphertext),
406
- content_iv: b64urlEncode(iv),
407
- };
408
- }
409
-
410
- export { b64urlEncode, b64urlDecode };
411
- ```
412
-
413
- **Step 2: Commit**
414
-
415
- ```bash
416
- git add src/lib/crypto.js
417
- git commit -m "feat: add AES-256-GCM encryption module"
418
- ```
419
-
420
- ---
421
-
422
- ## Task 8: pc-cli -- Rewrite login command
423
-
424
- **Files:**
425
- - Modify: `/home/ddnewell/weldedanvil/pc-cli/.worktrees/wa-auth-integration/src/commands/login.js`
426
-
427
- **Step 1: Rewrite login.js**
428
-
429
- The new flow:
430
- 1. Request device code from wa-auth (`POST /device/code` with `client_id`)
431
- 2. Show verification URL to user
432
- 3. Poll wa-auth (`POST /device/poll`) until approved
433
- 4. Exchange JWT for API key at prompt-registry (`POST /api/v1/auth/device-token`)
434
- 5. Store API key, vault metadata, and URLs
435
-
436
- ```javascript
437
- import ora from 'ora';
438
- import chalk from 'chalk';
439
- import {
440
- setApiKey, setApiUrl, setAccountUrl,
441
- setVaultMetadata, setVaultAvailable,
442
- isLoggedIn, getAccountUrl
443
- } from '../lib/config.js';
444
-
445
- const DEFAULT_API_URL = 'https://prompts.weldedanvil.com';
446
- const DEFAULT_ACCOUNT_URL = 'https://account.weldedanvil.com';
447
- const CLIENT_ID = 'prompts-prod';
448
-
449
- async function requestDeviceCode(accountUrl) {
450
- const response = await fetch(`${accountUrl}/device/code`, {
451
- method: 'POST',
452
- headers: { 'Content-Type': 'application/json' },
453
- body: JSON.stringify({ client_id: CLIENT_ID }),
454
- });
455
-
456
- if (!response.ok) {
457
- const data = await response.json().catch(() => ({}));
458
- throw new Error(data.error || 'Failed to request device code');
459
- }
460
-
461
- return response.json();
462
- }
463
-
464
- async function pollForApproval(accountUrl, deviceCode, expiresIn) {
465
- const expiresAt = Date.now() + (expiresIn || 600) * 1000;
466
- const interval = 5000;
467
-
468
- while (Date.now() < expiresAt) {
469
- await sleep(interval);
470
-
471
- const response = await fetch(`${accountUrl}/device/poll`, {
472
- method: 'POST',
473
- headers: { 'Content-Type': 'application/json' },
474
- body: JSON.stringify({ device_code: deviceCode }),
475
- });
476
-
477
- const data = await response.json();
478
-
479
- if (data.status === 'approved' && data.access_token) {
480
- return data;
481
- }
482
-
483
- if (data.status === 'expired') {
484
- throw new Error('Authorization request expired. Please try again.');
485
- }
486
-
487
- // status === 'pending' -- keep polling
488
- }
489
-
490
- throw new Error('Authorization request timed out. Please try again.');
491
- }
492
-
493
- async function exchangeTokenForApiKey(apiUrl, jwt, deviceName) {
494
- const response = await fetch(`${apiUrl}/api/v1/auth/device-token`, {
495
- method: 'POST',
496
- headers: {
497
- 'Authorization': `Bearer ${jwt}`,
498
- 'Content-Type': 'application/json',
499
- },
500
- body: JSON.stringify({ device_name: deviceName }),
501
- });
502
-
503
- if (!response.ok) {
504
- const data = await response.json().catch(() => ({}));
505
- throw new Error(data.error || 'Failed to exchange token');
506
- }
507
-
508
- return response.json();
509
- }
510
-
511
- function getDeviceName() {
512
- const os = process.platform;
513
- const hostname = process.env.HOSTNAME || process.env.COMPUTERNAME || 'CLI';
514
- const osNames = { darwin: 'macOS', linux: 'Linux', win32: 'Windows' };
515
- return `${osNames[os] || os} - ${hostname}`;
516
- }
517
-
518
- function sleep(ms) {
519
- return new Promise(resolve => setTimeout(resolve, ms));
520
- }
521
-
522
- export async function login(options) {
523
- const apiUrl = options?.url || DEFAULT_API_URL;
524
- const accountUrl = options?.accountUrl || DEFAULT_ACCOUNT_URL;
525
-
526
- if (isLoggedIn()) {
527
- console.log(chalk.yellow('Already logged in.'));
528
- console.log('Run ' + chalk.cyan('pc logout') + ' first to switch accounts.\n');
529
- return;
530
- }
531
-
532
- console.log(chalk.bold('\nPromptCellar CLI Login\n'));
533
-
534
- const spinner = ora('Connecting...').start();
535
-
536
- try {
537
- // Step 1: Request device code from wa-auth
538
- const authData = await requestDeviceCode(accountUrl);
539
- spinner.stop();
540
-
541
- // Step 2: Show login URL
542
- const verifyUrl = `${authData.verification_url}?code=${authData.user_code}`;
543
- console.log('Open this URL in your browser to log in:\n');
544
- console.log(chalk.cyan.bold(` ${verifyUrl}\n`));
545
- console.log(chalk.dim(`Or go to ${authData.verification_url} and enter code: ${authData.user_code}\n`));
546
-
547
- // Step 3: Poll for approval
548
- spinner.start('Waiting for you to authorize in browser...');
549
- const approval = await pollForApproval(accountUrl, authData.device_code);
550
- spinner.text = 'Setting up credentials...';
551
-
552
- // Step 4: Exchange JWT for API key
553
- const deviceName = getDeviceName();
554
- const result = await exchangeTokenForApiKey(apiUrl, approval.access_token, deviceName);
555
-
556
- // Step 5: Store credentials
557
- setApiKey(result.api_key);
558
- setApiUrl(apiUrl);
559
- setAccountUrl(accountUrl);
560
-
561
- if (result.vault && result.vault.available) {
562
- setVaultMetadata(result.vault.encrypted_metadata);
563
- setVaultAvailable(true);
564
- } else {
565
- setVaultMetadata('');
566
- setVaultAvailable(false);
567
- }
568
-
569
- spinner.succeed(chalk.green('Logged in successfully!'));
570
- console.log(`\nWelcome, ${chalk.cyan(result.email)}!`);
571
-
572
- if (!result.vault || !result.vault.available) {
573
- console.log(chalk.yellow('\nVault not set up.') + ' Prompt capture requires a vault.');
574
- console.log('Set up your vault at: ' + chalk.cyan(`${apiUrl}/dashboard/settings`));
575
- }
576
-
577
- console.log('\nRun ' + chalk.cyan('pc setup') + ' to configure auto-capture for your CLI tools.\n');
578
-
579
- } catch (error) {
580
- spinner.fail(chalk.red('Login failed: ' + error.message));
581
- console.log('\nPlease try again or visit ' + chalk.cyan(apiUrl) + ' to sign up.\n');
582
- }
583
- }
584
-
585
- export default login;
586
- ```
587
-
588
- **Step 2: Commit**
589
-
590
- ```bash
591
- git add src/commands/login.js
592
- git commit -m "feat: rewrite login to use wa-auth device flow"
593
- ```
594
-
595
- ---
596
-
597
- ## Task 9: pc-cli -- Update hooks and save to use encryption
598
-
599
- **Files:**
600
- - Modify: `/home/ddnewell/weldedanvil/pc-cli/.worktrees/wa-auth-integration/hooks/prompt-capture.js`
601
- - Modify: `/home/ddnewell/weldedanvil/pc-cli/.worktrees/wa-auth-integration/hooks/codex-capture.js`
602
- - Modify: `/home/ddnewell/weldedanvil/pc-cli/.worktrees/wa-auth-integration/hooks/gemini-capture.js`
603
- - Modify: `/home/ddnewell/weldedanvil/pc-cli/.worktrees/wa-auth-integration/src/commands/save.js`
604
-
605
- **Step 1: Update prompt-capture.js**
606
-
607
- Add vault check and encryption. Key changes:
608
- - Import `isVaultAvailable`, `getVaultMetadata` from config
609
- - Import `encryptPrompt` from crypto
610
- - After `isLoggedIn()` check, add `isVaultAvailable()` check -- exit silently if not available
611
- - Before calling `capturePrompt()`, encrypt the content
612
- - Send `{ encrypted_content, content_iv }` instead of `{ content }`
613
-
614
- ```javascript
615
- import { isLoggedIn, isVaultAvailable, getVaultMetadata } from '../src/lib/config.js';
616
- import { encryptPrompt } from '../src/lib/crypto.js';
617
-
618
- // ... (existing code) ...
619
-
620
- // After isLoggedIn check:
621
- if (!isVaultAvailable()) {
622
- process.exit(0);
623
- }
624
-
625
- // Before capturePrompt call:
626
- const vaultKey = getVaultMetadata();
627
- const { encrypted_content, content_iv } = encryptPrompt(initialPrompt.content, vaultKey);
628
-
629
- await capturePrompt({
630
- encrypted_content,
631
- content_iv,
632
- ...context,
633
- captured_at: initialPrompt.timestamp
634
- });
635
- ```
636
-
637
- Apply the same pattern to `codex-capture.js` and `gemini-capture.js`.
638
-
639
- **Step 2: Update save.js**
640
-
641
- Add vault check before save. If vault not available, show actionable error:
642
-
643
- ```javascript
644
- import { isLoggedIn, isVaultAvailable, getVaultMetadata, getApiUrl } from '../lib/config.js';
645
- import { encryptPrompt } from '../lib/crypto.js';
646
-
647
- // In save():
648
- if (!isVaultAvailable()) {
649
- console.log(chalk.red('Vault not set up.') + ' Prompt capture requires a vault.');
650
- console.log('Set up your vault at: ' + chalk.cyan(`${getApiUrl()}/dashboard/settings`));
651
- return;
652
- }
653
-
654
- // Before capturePrompt:
655
- const vaultKey = getVaultMetadata();
656
- const { encrypted_content, content_iv } = encryptPrompt(content.trim(), vaultKey);
657
-
658
- const result = await capturePrompt({
659
- encrypted_content,
660
- content_iv,
661
- ...context
662
- });
663
- ```
664
-
665
- **Step 3: Commit**
666
-
667
- ```bash
668
- git add hooks/prompt-capture.js hooks/codex-capture.js hooks/gemini-capture.js src/commands/save.js
669
- git commit -m "feat: encrypt prompts before capture"
670
- ```
671
-
672
- ---
673
-
674
- ## Task 10: pc-cli -- Update status command to show vault status
675
-
676
- **Files:**
677
- - Modify: `/home/ddnewell/weldedanvil/pc-cli/.worktrees/wa-auth-integration/src/commands/status.js`
678
-
679
- **Step 1: Add vault status display**
680
-
681
- ```javascript
682
- import { isLoggedIn, getApiUrl, getCaptureLevel, getSessionId, isVaultAvailable } from '../lib/config.js';
683
-
684
- // After capture level display:
685
- if (isVaultAvailable()) {
686
- console.log(` Vault: ${chalk.green('configured')}`);
687
- } else {
688
- console.log(` Vault: ${chalk.yellow('not configured')} - capture disabled`);
689
- }
690
- ```
691
-
692
- **Step 2: Commit**
693
-
694
- ```bash
695
- git add src/commands/status.js
696
- git commit -m "feat: show vault status in pc status"
697
- ```
698
-
699
- ---
700
-
701
- ## Task 11: pc-cli -- Update logout to clear vault state
702
-
703
- **Files:**
704
- - Modify: `/home/ddnewell/weldedanvil/pc-cli/.worktrees/wa-auth-integration/src/commands/logout.js`
705
-
706
- **Step 1: Read logout.js and verify it calls clearApiKey**
707
-
708
- The `clearApiKey` function was updated in Task 6 to also clear vault state. Verify logout.js calls it. If it uses a different clearing mechanism, update accordingly.
709
-
710
- **Step 2: Commit if changes needed**
711
-
712
- ```bash
713
- git add src/commands/logout.js
714
- git commit -m "fix: ensure logout clears vault state"
715
- ```
716
-
717
- ---
718
-
719
- ## Task 12: Manual integration test
720
-
721
- **No code changes -- verification only.**
722
-
723
- **Step 1: Verify wa-auth changes**
724
-
725
- ```bash
726
- cd /home/ddnewell/weldedanvil/wa-auth
727
- # Run existing tests if any
728
- python -m pytest tests/ -v 2>/dev/null || echo "Check tests manually"
729
- ```
730
-
731
- **Step 2: Verify prompt-registry changes**
732
-
733
- ```bash
734
- cd /home/ddnewell/weldedanvil/prompt-registry
735
- python -m pytest tests/ -v 2>/dev/null || echo "Check tests manually"
736
- ```
737
-
738
- **Step 3: Verify pc-cli syntax**
739
-
740
- ```bash
741
- cd /home/ddnewell/weldedanvil/pc-cli/.worktrees/wa-auth-integration
742
- node -e "import('./src/lib/crypto.js').then(m => { const r = m.encryptPrompt('test', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); console.log('OK:', r); })"
743
- node -e "import('./src/lib/config.js').then(m => { console.log('Config OK'); })"
744
- node -e "import('./src/commands/login.js').then(m => { console.log('Login OK'); })"
745
- ```
746
-
747
- **Step 4: Verify end-to-end flow description**
748
-
749
- Confirm the flow works conceptually:
750
- 1. `pc login` → calls wa-auth `/device/code` with `client_id=prompts-prod`
751
- 2. User approves in browser
752
- 3. CLI polls → gets JWT with `aud=prompts-prod`
753
- 4. CLI calls prompt-registry `/api/v1/auth/device-token` with JWT
754
- 5. Gets back `pk_*` key + vault metadata
755
- 6. `pc save -m "test"` → encrypts with vault key → sends encrypted blob
756
- 7. `pc status` → shows vault status
757
-
758
- ---
759
-
760
- ## Notes
761
-
762
- ### Vault key provisioning (deferred)
763
-
764
- The vault master key is wrapped with a WebAuthn PRF-derived key. The CLI cannot perform passkey ceremonies. A separate mechanism is needed to export the raw master key to the CLI. Options:
765
-
766
- 1. Web UI "Enable CLI Access" button that re-wraps master key with a CLI-specific passphrase
767
- 2. QR code in web UI containing the raw key for one-time transfer
768
- 3. Temporary API endpoint that returns the raw key during an authenticated browser session
769
-
770
- This is intentionally deferred. For initial development, the `vaultMetadata` config field stores the raw key directly (base64url-encoded 32 bytes). The provisioning UX will be designed separately.
771
-
772
- ### Cross-repo deployment order
773
-
774
- 1. Deploy wa-auth (Tasks 1-3, 5) -- backward compatible, existing device flow still works
775
- 2. Deploy prompt-registry (Task 4) -- new endpoint, no existing behavior changes
776
- 3. Release pc-cli (Tasks 6-11) -- requires both backends deployed first