@happyvertical/smrt-secrets 0.30.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/AGENTS.md +66 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +144 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/chunks/SecretService-C91H6WJK.js +1275 -0
- package/dist/chunks/SecretService-C91H6WJK.js.map +1 -0
- package/dist/chunks/TenantKey-DzglnpAV.js +377 -0
- package/dist/chunks/TenantKey-DzglnpAV.js.map +1 -0
- package/dist/collections/SecretAuditLogCollection.d.ts +71 -0
- package/dist/collections/SecretAuditLogCollection.d.ts.map +1 -0
- package/dist/collections/SecretCollection.d.ts +63 -0
- package/dist/collections/SecretCollection.d.ts.map +1 -0
- package/dist/collections/TenantKeyCollection.d.ts +42 -0
- package/dist/collections/TenantKeyCollection.d.ts.map +1 -0
- package/dist/collections/index.d.ts +8 -0
- package/dist/collections/index.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +1272 -0
- package/dist/models/Secret.d.ts +104 -0
- package/dist/models/Secret.d.ts.map +1 -0
- package/dist/models/SecretAuditLog.d.ts +123 -0
- package/dist/models/SecretAuditLog.d.ts.map +1 -0
- package/dist/models/TenantKey.d.ts +101 -0
- package/dist/models/TenantKey.d.ts.map +1 -0
- package/dist/models/index.d.ts +4 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +8 -0
- package/dist/models/index.js.map +1 -0
- package/dist/services/SecretService.d.ts +266 -0
- package/dist/services/SecretService.d.ts.map +1 -0
- package/dist/services/SecretService.js +9 -0
- package/dist/services/SecretService.js.map +1 -0
- package/dist/smrt-knowledge.json +447 -0
- package/package.json +71 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @happyvertical/smrt-secrets
|
|
2
|
+
|
|
3
|
+
Envelope encryption for per-tenant secret storage with key rotation and audit logging.
|
|
4
|
+
|
|
5
|
+
## Encryption Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Secret value → encrypted with TDEK (per-tenant Data Encryption Key)
|
|
9
|
+
TDEK → wrapped with AMK (Application Master Key, from env SMRT_SECRET_MASTER_KEY)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Models
|
|
13
|
+
|
|
14
|
+
- **Secret**: `encryptedValue` (JSON envelope), `category`, `status` (active/disabled/expired), `expiresAt`, `accessCount`, `lastAccessedAt`. **No API/MCP exposure** (security). CLI: list-only.
|
|
15
|
+
- **TenantKey**: per-tenant TDEK in wrapped form. Status: active/rotating/retired/compromised. **Not tenant-scoped itself** — it tracks keys FOR tenants.
|
|
16
|
+
- **SecretAuditLog**: action (create/read/update/delete/rotate_key/disable/enable), result (success/failure/denied), user/IP tracking. CLI: list-only.
|
|
17
|
+
|
|
18
|
+
## SecretService
|
|
19
|
+
|
|
20
|
+
| Method | Behavior |
|
|
21
|
+
|--------|----------|
|
|
22
|
+
| `store(name, value, options)` | Encrypt + save. Upsert if exists. Uses `context=tenantId` for per-tenant uniqueness. |
|
|
23
|
+
| `retrieve(name)` | Decrypt + audit + increment accessCount |
|
|
24
|
+
| `diagnoseTenantSecretKeyDrift(tenantId)` | Report active secret/key drift without exposing values. Checks `secrets`, SDK `tenant_encryption_keys`, and SMRT `tenant_keys`. |
|
|
25
|
+
| `repairTenantSecretKeyDrift(tenantId, opts)` | Explicit repair path for unrecoverable drift. Use `dryRun` first; destructive cleanup requires `confirmDeleteUnrecoverableData: true`. |
|
|
26
|
+
| `rotateKey()` | Create new TDEK, mark old as retired |
|
|
27
|
+
| `reencryptAll()` | Decrypt with old TDEK, re-encrypt with new — **must call separately after rotateKey()** |
|
|
28
|
+
| `disable(name)` / `enable(name)` | Toggle status |
|
|
29
|
+
|
|
30
|
+
## Gotchas
|
|
31
|
+
|
|
32
|
+
- **Key rotation doesn't auto-re-encrypt**: call `reencryptAll()` separately after `rotateKey()`
|
|
33
|
+
- **retrieve() increments accessCount**: every read is tracked
|
|
34
|
+
- **TenantKey NOT tenant-scoped**: it stores keys for tenants but isn't filtered by tenant context
|
|
35
|
+
- **Two key tables**: `tenant_encryption_keys` is the lower-level SDK table used by `SecretService` encryption; `tenant_keys` is the SMRT model table. Drift diagnosis reports both so an empty `tenant_keys` view is not confused with missing SDK key material.
|
|
36
|
+
- **Repair is destructive only by confirmation**: `repairTenantSecretKeyDrift()` deletes unrecoverable encrypted rows only when `confirmDeleteUnrecoverableData: true` is explicit. Do not silently discard encrypted secrets.
|
|
37
|
+
- **Expired secrets filtered by default**: pass `includeExpired: true` to list them
|
|
38
|
+
- **TenantKeyCollection.cleanupRetiredKeys()**: hard-deletes after 90 days
|
|
39
|
+
- **Audit logging optional but default**: failures logged to console, not thrown
|
|
40
|
+
|
|
41
|
+
## Known exceptions to monorepo standards
|
|
42
|
+
|
|
43
|
+
Per `docs/content/standards.md §7`, tenant-aware models should normally apply
|
|
44
|
+
`@TenantScoped({ mode: 'optional' })` from `@happyvertical/smrt-tenancy`. The three
|
|
45
|
+
models in this package deviate intentionally; each `@smrt(...)` block carries an
|
|
46
|
+
inline comment pointing back to this section.
|
|
47
|
+
|
|
48
|
+
- **`Secret` (`src/models/Secret.ts`)** — uses the inline `tenantScoped: true` form
|
|
49
|
+
on `@smrt()` instead of the `@TenantScoped` decorator. `SecretService.store()`
|
|
50
|
+
performs manual scoping by populating `context = tenantId` on each row, so the
|
|
51
|
+
`(slug, context)` upsert key from the base `SmrtObject` is what isolates secret
|
|
52
|
+
names per tenant. Switching to the decorator without rethinking the upsert key
|
|
53
|
+
would surface false-positive name collisions across tenants.
|
|
54
|
+
- **`TenantKey` (`src/models/TenantKey.ts`)** — deliberately NOT tenant-scoped at
|
|
55
|
+
all. The row carries a `tenantId` column because each TDEK belongs to a tenant,
|
|
56
|
+
but key-rotation tooling, AMK rewrap jobs, and super-admin audits must query
|
|
57
|
+
across tenants; the tenancy interceptor would silently filter rows those flows
|
|
58
|
+
rely on.
|
|
59
|
+
- **`SecretAuditLog` (`src/models/SecretAuditLog.ts`)** — uses the inline
|
|
60
|
+
`tenantScoped: true` form rather than the decorator. Audit reads run in mixed
|
|
61
|
+
contexts (tenant-scoped reports vs. super-admin compliance review). Cross-
|
|
62
|
+
tenant audit queries should be wrapped in `withSuperAdminBypass()` from
|
|
63
|
+
`@happyvertical/smrt-tenancy` at the call site — there are no such cross-
|
|
64
|
+
tenant call sites in this package today, but consumers building compliance
|
|
65
|
+
tooling should adopt that pattern explicitly rather than relying on
|
|
66
|
+
decorator-implicit filtering.
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2025> <Happy Vertical Corporation>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @happyvertical/smrt-secrets
|
|
2
|
+
|
|
3
|
+
Per-tenant secret management with envelope encryption for the SMRT framework. Uses a three-layer encryption chain: Application Master Key (AMK) wraps per-tenant Data Encryption Keys (TDEK), which encrypt individual secret values.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @happyvertical/smrt-secrets
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires the `SMRT_SECRET_MASTER_KEY` environment variable (64 hex characters) as the AMK.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { SecretService } from '@happyvertical/smrt-secrets';
|
|
17
|
+
import { withTenant } from '@happyvertical/smrt-tenancy';
|
|
18
|
+
|
|
19
|
+
// Create the service (reads AMK from env by default)
|
|
20
|
+
const service = await SecretService.create({ db });
|
|
21
|
+
|
|
22
|
+
await withTenant({ tenantId: 'tenant-123' }, async () => {
|
|
23
|
+
// Store a secret (upserts if name already exists)
|
|
24
|
+
await service.store('stripe-api-key', 'sk_live_xxx', {
|
|
25
|
+
category: 'api-keys',
|
|
26
|
+
description: 'Stripe production key',
|
|
27
|
+
expiresAt: new Date('2027-01-01'),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Retrieve and decrypt
|
|
31
|
+
const { value, accessCount } = await service.retrieve('stripe-api-key');
|
|
32
|
+
|
|
33
|
+
// List secret names (values never included)
|
|
34
|
+
const secrets = await service.list({ category: 'api-keys' });
|
|
35
|
+
|
|
36
|
+
// Disable/enable without deleting
|
|
37
|
+
await service.disable('stripe-api-key');
|
|
38
|
+
await service.enable('stripe-api-key');
|
|
39
|
+
|
|
40
|
+
// Rotate the tenant's encryption key
|
|
41
|
+
await service.rotateKey();
|
|
42
|
+
// Re-encrypt all secrets with the new key (separate step)
|
|
43
|
+
await service.reencryptAll();
|
|
44
|
+
|
|
45
|
+
// Query audit logs
|
|
46
|
+
const logs = await service.getAuditLogs({ secretName: 'stripe-api-key' });
|
|
47
|
+
|
|
48
|
+
// Diagnose tenant secret/key drift without exposing values
|
|
49
|
+
const drift = await service.diagnoseTenantSecretKeyDrift('tenant-123');
|
|
50
|
+
|
|
51
|
+
// Preview and then explicitly repair unrecoverable drifted rows
|
|
52
|
+
await service.repairTenantSecretKeyDrift('tenant-123', { dryRun: true });
|
|
53
|
+
await service.repairTenantSecretKeyDrift('tenant-123', {
|
|
54
|
+
confirmDeleteUnrecoverableData: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Hard delete
|
|
58
|
+
await service.delete('stripe-api-key');
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Encryption chain
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
AMK (env var, 256-bit)
|
|
66
|
+
└─ wraps TDEK (per-tenant, auto-generated)
|
|
67
|
+
└─ encrypts secret value (stored as JSON envelope)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The AMK is loaded from `SMRT_SECRET_MASTER_KEY` (configurable via `amkEnvVar` option). Each tenant gets its own TDEK on first secret storage. TDEKs are stored in wrapped form -- they can only be unwrapped using the AMK. Secret values are encrypted by the unwrapped TDEK and persisted as `EncryptedEnvelope` JSON.
|
|
71
|
+
|
|
72
|
+
### Key rotation
|
|
73
|
+
|
|
74
|
+
`rotateKey()` creates a new TDEK version and retires the old one. Existing secrets remain readable because retired keys are kept for decryption. Call `reencryptAll()` separately after rotation to re-encrypt all secrets with the new key. The method returns `{ success, failed }` counts.
|
|
75
|
+
|
|
76
|
+
TenantKey statuses: `active` (current encryption key), `rotating` (transitional), `retired` (kept for decryption), `compromised` (should not be used).
|
|
77
|
+
|
|
78
|
+
### Drift diagnosis and repair
|
|
79
|
+
|
|
80
|
+
`diagnoseTenantSecretKeyDrift(tenantId)` reports tenant secret/key drift without
|
|
81
|
+
returning secret values. It checks active `secrets` rows, lower-level
|
|
82
|
+
`tenant_encryption_keys` rows used by `@happyvertical/secrets`, and SMRT-visible
|
|
83
|
+
`tenant_keys` rows so operators can distinguish missing secrets from stale or
|
|
84
|
+
unusable key material.
|
|
85
|
+
|
|
86
|
+
`repairTenantSecretKeyDrift(tenantId, { dryRun: true })` previews rows that would
|
|
87
|
+
be deleted. A destructive repair requires
|
|
88
|
+
`confirmDeleteUnrecoverableData: true`; it deletes only encrypted secret/key rows
|
|
89
|
+
identified as unrecoverable with the currently configured AMK. After repair,
|
|
90
|
+
store fresh secret values with `storeForTenant()` or `store()`.
|
|
91
|
+
|
|
92
|
+
### Audit logging
|
|
93
|
+
|
|
94
|
+
Every secret operation (create, read, update, delete, rotate_key, disable, enable) is logged to `SecretAuditLog` with user ID, action, result (success/failure/denied), and optional IP/user-agent. Audit logging is enabled by default; failures are logged to console rather than thrown so they do not block secret operations.
|
|
95
|
+
|
|
96
|
+
### Security model
|
|
97
|
+
|
|
98
|
+
The `Secret` and `TenantKey` models have no API or MCP exposure to prevent accidental leakage. CLI access is limited to listing names. The `TenantKey` model is deliberately not tenant-scoped -- it tracks keys FOR tenants rather than being owned BY them.
|
|
99
|
+
|
|
100
|
+
## API
|
|
101
|
+
|
|
102
|
+
### Models
|
|
103
|
+
|
|
104
|
+
| Export | Description |
|
|
105
|
+
|--------|------------|
|
|
106
|
+
| `Secret` | Encrypted secret record with status, expiration, and access tracking |
|
|
107
|
+
| `SecretAuditLog` | Immutable audit trail for all secret operations |
|
|
108
|
+
| `TenantKey` | Per-tenant data encryption key in wrapped form |
|
|
109
|
+
|
|
110
|
+
### Collections
|
|
111
|
+
|
|
112
|
+
| Export | Description |
|
|
113
|
+
|--------|------------|
|
|
114
|
+
| `SecretCollection` | Secret queries with tenant filtering and category support |
|
|
115
|
+
| `SecretAuditLogCollection` | Audit log queries with filtering and pagination |
|
|
116
|
+
| `TenantKeyCollection` | Key management including retired key cleanup |
|
|
117
|
+
|
|
118
|
+
### Services
|
|
119
|
+
|
|
120
|
+
| Export | Description |
|
|
121
|
+
|--------|------------|
|
|
122
|
+
| `SecretService` | High-level API: store, retrieve, rotate, delete, audit, diagnose and repair key drift |
|
|
123
|
+
|
|
124
|
+
### Functions
|
|
125
|
+
|
|
126
|
+
| Export | Description |
|
|
127
|
+
|--------|------------|
|
|
128
|
+
| `createAuditEntry` | Build an audit log entry for a secret operation |
|
|
129
|
+
|
|
130
|
+
### Error Classes (re-exported from `@happyvertical/secrets`)
|
|
131
|
+
|
|
132
|
+
`SecretError`, `AMKUnavailableError`, `DecryptionError`, `EncryptionError`, `InvalidKeyFormatError`, `KeyNotFoundError`, `KeyRotationError`, `StoreNotInitializedError`, `TenantKeyMissingError`
|
|
133
|
+
|
|
134
|
+
### Key Types
|
|
135
|
+
|
|
136
|
+
`SecretServiceOptions`, `StoreSecretOptions`, `RetrievedSecret`, `DiagnoseTenantSecretKeyDriftOptions`, `SecretKeyDriftReport`, `SecretKeyDriftIssue`, `RepairTenantSecretKeyDriftOptions`, `SecretKeyDriftRepairResult`, `ListSecretsOptions`, `ListAuditLogsOptions`, `SecretStatus`, `SecretAuditAction`, `SecretAuditResult`, `TenantKeyStatus`, `ApplicationMasterKey`, `EncryptedEnvelope`, `TenantDataEncryptionKey`, `SecretStore`
|
|
137
|
+
|
|
138
|
+
## Dependencies
|
|
139
|
+
|
|
140
|
+
- `@happyvertical/smrt-core` -- ORM and code generation
|
|
141
|
+
- `@happyvertical/smrt-tenancy` -- tenant context for scoped operations
|
|
142
|
+
- `@happyvertical/secrets` -- envelope encryption primitives
|
|
143
|
+
- `@happyvertical/sql` -- database interface
|
|
144
|
+
- `@happyvertical/utils` -- shared utilities
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"__smrt-register__.d.ts","sourceRoot":"","sources":["../src/__smrt-register__.ts"],"names":[],"mappings":""}
|