@happyvertical/smrt-secrets 0.49.3 → 0.49.4

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.
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-secrets",
5
- "packageVersion": "0.49.3",
5
+ "packageVersion": "0.49.4",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-secrets:SecretAuditLogCollection": {
8
8
  "name": "secretauditlogcollection",
@@ -220,8 +220,8 @@
220
220
  "exportName": "SecretAuditLogCollection",
221
221
  "collectionExportName": "SecretAuditLogCollectionCollection",
222
222
  "schema": {
223
- "tableName": "secret_audit_log_collections",
224
- "ddl": "CREATE TABLE IF NOT EXISTS \"secret_audit_log_collections\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp\n);",
223
+ "tableName": "secret_audit_logs",
224
+ "ddl": "CREATE TABLE IF NOT EXISTS \"secret_audit_logs\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID,\n \"secret_id\" UUID,\n \"secret_name\" TEXT DEFAULT '',\n \"user_id\" UUID,\n \"action\" TEXT DEFAULT 'read',\n \"result\" TEXT DEFAULT 'success',\n \"ip_address\" TEXT DEFAULT '',\n \"user_agent\" TEXT DEFAULT '',\n \"details\" JSON DEFAULT '{}'\n);",
225
225
  "columns": {
226
226
  "id": {
227
227
  "type": "UUID",
@@ -247,25 +247,94 @@
247
247
  "type": "TIMESTAMP",
248
248
  "notNull": true,
249
249
  "default": "current_timestamp"
250
+ },
251
+ "tenant_id": {
252
+ "type": "UUID",
253
+ "referenceKind": "tenantId",
254
+ "notNull": false,
255
+ "unique": false
256
+ },
257
+ "secret_id": {
258
+ "type": "UUID",
259
+ "referenceKind": "foreignKey",
260
+ "notNull": false,
261
+ "unique": false
262
+ },
263
+ "secret_name": {
264
+ "type": "TEXT",
265
+ "notNull": false,
266
+ "unique": false,
267
+ "default": ""
268
+ },
269
+ "user_id": {
270
+ "type": "UUID",
271
+ "referenceKind": "crossPackageRef",
272
+ "notNull": false,
273
+ "unique": false
274
+ },
275
+ "action": {
276
+ "type": "TEXT",
277
+ "notNull": false,
278
+ "unique": false,
279
+ "default": "read"
280
+ },
281
+ "result": {
282
+ "type": "TEXT",
283
+ "notNull": false,
284
+ "unique": false,
285
+ "default": "success"
286
+ },
287
+ "ip_address": {
288
+ "type": "TEXT",
289
+ "notNull": false,
290
+ "unique": false,
291
+ "default": ""
292
+ },
293
+ "user_agent": {
294
+ "type": "TEXT",
295
+ "notNull": false,
296
+ "unique": false,
297
+ "default": ""
298
+ },
299
+ "details": {
300
+ "type": "JSON",
301
+ "notNull": false,
302
+ "unique": false,
303
+ "default": {}
250
304
  }
251
305
  },
252
306
  "indexes": [
253
307
  {
254
- "name": "secret_audit_log_collections_slug_context_idx",
308
+ "name": "secret_audit_logs_slug_context_idx",
255
309
  "columns": [
310
+ "tenant_id",
256
311
  "slug",
257
312
  "context"
258
313
  ],
259
- "unique": true
314
+ "unique": true,
315
+ "nullsNotDistinct": true
260
316
  },
261
317
  {
262
- "name": "secret_audit_log_collections_created_at_idx",
318
+ "name": "secret_audit_logs_tenant_id_created_at_idx",
263
319
  "columns": [
320
+ "tenant_id",
264
321
  "created_at"
265
322
  ]
323
+ },
324
+ {
325
+ "name": "secret_audit_logs_secret_id_idx",
326
+ "columns": [
327
+ "secret_id"
328
+ ]
329
+ },
330
+ {
331
+ "name": "secret_audit_logs_user_id_idx",
332
+ "columns": [
333
+ "user_id"
334
+ ]
266
335
  }
267
336
  ],
268
- "version": "7bb61130"
337
+ "version": "af0d679d"
269
338
  }
270
339
  },
271
340
  "@happyvertical/smrt-secrets:SecretCollection": {
@@ -421,8 +490,8 @@
421
490
  "exportName": "SecretCollection",
422
491
  "collectionExportName": "SecretCollectionCollection",
423
492
  "schema": {
424
- "tableName": "secret_collections",
425
- "ddl": "CREATE TABLE IF NOT EXISTS \"secret_collections\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp\n);",
493
+ "tableName": "secrets",
494
+ "ddl": "CREATE TABLE IF NOT EXISTS \"secrets\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID,\n \"name\" TEXT DEFAULT '',\n \"description\" TEXT DEFAULT '',\n \"category\" TEXT DEFAULT '',\n \"encrypted_value\" TEXT DEFAULT '',\n \"key_version\" INTEGER DEFAULT 1,\n \"status\" TEXT DEFAULT 'active',\n \"expires_at\" TIMESTAMP,\n \"last_accessed_at\" TIMESTAMP,\n \"access_count\" INTEGER DEFAULT 0,\n \"metadata\" JSON DEFAULT '{}'\n);",
426
495
  "columns": {
427
496
  "id": {
428
497
  "type": "UUID",
@@ -448,25 +517,92 @@
448
517
  "type": "TIMESTAMP",
449
518
  "notNull": true,
450
519
  "default": "current_timestamp"
520
+ },
521
+ "tenant_id": {
522
+ "type": "UUID",
523
+ "referenceKind": "tenantId",
524
+ "notNull": false,
525
+ "unique": false
526
+ },
527
+ "name": {
528
+ "type": "TEXT",
529
+ "notNull": false,
530
+ "unique": false,
531
+ "default": ""
532
+ },
533
+ "description": {
534
+ "type": "TEXT",
535
+ "notNull": false,
536
+ "unique": false,
537
+ "default": ""
538
+ },
539
+ "category": {
540
+ "type": "TEXT",
541
+ "notNull": false,
542
+ "unique": false,
543
+ "default": ""
544
+ },
545
+ "encrypted_value": {
546
+ "type": "TEXT",
547
+ "notNull": false,
548
+ "unique": false,
549
+ "default": ""
550
+ },
551
+ "key_version": {
552
+ "type": "INTEGER",
553
+ "notNull": false,
554
+ "unique": false,
555
+ "default": 1
556
+ },
557
+ "status": {
558
+ "type": "TEXT",
559
+ "notNull": false,
560
+ "unique": false,
561
+ "default": "active"
562
+ },
563
+ "expires_at": {
564
+ "type": "TIMESTAMP",
565
+ "notNull": false,
566
+ "unique": false
567
+ },
568
+ "last_accessed_at": {
569
+ "type": "TIMESTAMP",
570
+ "notNull": false,
571
+ "unique": false
572
+ },
573
+ "access_count": {
574
+ "type": "INTEGER",
575
+ "notNull": false,
576
+ "unique": false,
577
+ "default": 0
578
+ },
579
+ "metadata": {
580
+ "type": "JSON",
581
+ "notNull": false,
582
+ "unique": false,
583
+ "default": {}
451
584
  }
452
585
  },
453
586
  "indexes": [
454
587
  {
455
- "name": "secret_collections_slug_context_idx",
588
+ "name": "secrets_slug_context_idx",
456
589
  "columns": [
590
+ "tenant_id",
457
591
  "slug",
458
592
  "context"
459
593
  ],
460
- "unique": true
594
+ "unique": true,
595
+ "nullsNotDistinct": true
461
596
  },
462
597
  {
463
- "name": "secret_collections_created_at_idx",
598
+ "name": "secrets_tenant_id_created_at_idx",
464
599
  "columns": [
600
+ "tenant_id",
465
601
  "created_at"
466
602
  ]
467
603
  }
468
604
  ],
469
- "version": "5f18a721"
605
+ "version": "6298a980"
470
606
  }
471
607
  },
472
608
  "@happyvertical/smrt-secrets:TenantKeyCollection": {
@@ -589,8 +725,8 @@
589
725
  "exportName": "TenantKeyCollection",
590
726
  "collectionExportName": "TenantKeyCollectionCollection",
591
727
  "schema": {
592
- "tableName": "tenant_key_collections",
593
- "ddl": "CREATE TABLE IF NOT EXISTS \"tenant_key_collections\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp\n);",
728
+ "tableName": "tenant_keys",
729
+ "ddl": "CREATE TABLE IF NOT EXISTS \"tenant_keys\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" TEXT DEFAULT '',\n \"wrapped_key\" TEXT DEFAULT '',\n \"amk_key_id\" TEXT DEFAULT '',\n \"status\" TEXT DEFAULT 'active',\n \"version\" INTEGER DEFAULT 1,\n \"rotate_after\" TIMESTAMP,\n \"retired_at\" TIMESTAMP\n);",
594
730
  "columns": {
595
731
  "id": {
596
732
  "type": "UUID",
@@ -616,11 +752,51 @@
616
752
  "type": "TIMESTAMP",
617
753
  "notNull": true,
618
754
  "default": "current_timestamp"
755
+ },
756
+ "tenant_id": {
757
+ "type": "TEXT",
758
+ "notNull": false,
759
+ "unique": false,
760
+ "default": ""
761
+ },
762
+ "wrapped_key": {
763
+ "type": "TEXT",
764
+ "notNull": false,
765
+ "unique": false,
766
+ "default": ""
767
+ },
768
+ "amk_key_id": {
769
+ "type": "TEXT",
770
+ "notNull": false,
771
+ "unique": false,
772
+ "default": ""
773
+ },
774
+ "status": {
775
+ "type": "TEXT",
776
+ "notNull": false,
777
+ "unique": false,
778
+ "default": "active"
779
+ },
780
+ "version": {
781
+ "type": "INTEGER",
782
+ "notNull": false,
783
+ "unique": false,
784
+ "default": 1
785
+ },
786
+ "rotate_after": {
787
+ "type": "TIMESTAMP",
788
+ "notNull": false,
789
+ "unique": false
790
+ },
791
+ "retired_at": {
792
+ "type": "TIMESTAMP",
793
+ "notNull": false,
794
+ "unique": false
619
795
  }
620
796
  },
621
797
  "indexes": [
622
798
  {
623
- "name": "tenant_key_collections_slug_context_idx",
799
+ "name": "tenant_keys_slug_context_idx",
624
800
  "columns": [
625
801
  "slug",
626
802
  "context"
@@ -628,13 +804,13 @@
628
804
  "unique": true
629
805
  },
630
806
  {
631
- "name": "tenant_key_collections_created_at_idx",
807
+ "name": "tenant_keys_created_at_idx",
632
808
  "columns": [
633
809
  "created_at"
634
810
  ]
635
811
  }
636
812
  ],
637
- "version": "26616d13"
813
+ "version": "79fd7e46"
638
814
  }
639
815
  },
640
816
  "@happyvertical/smrt-secrets:Secret": {
@@ -885,7 +1061,8 @@
885
1061
  "slug",
886
1062
  "context"
887
1063
  ],
888
- "unique": true
1064
+ "unique": true,
1065
+ "nullsNotDistinct": true
889
1066
  },
890
1067
  {
891
1068
  "name": "secrets_tenant_id_created_at_idx",
@@ -1130,7 +1307,8 @@
1130
1307
  "slug",
1131
1308
  "context"
1132
1309
  ],
1133
- "unique": true
1310
+ "unique": true,
1311
+ "nullsNotDistinct": true
1134
1312
  },
1135
1313
  {
1136
1314
  "name": "secret_audit_logs_tenant_id_created_at_idx",
@@ -1,2 +1,2 @@
1
- import { i as createAuditEntry, n as Secret, r as SecretAuditLog, t as TenantKey } from "../chunks/TenantKey-by07m170.js";
1
+ import { i as createAuditEntry, n as Secret, r as SecretAuditLog, t as TenantKey } from "../chunks/TenantKey-TATS2F3C.js";
2
2
  export { Secret, SecretAuditLog, TenantKey, createAuditEntry };
@@ -1,2 +1,2 @@
1
- import { n as SecretService, t as SecretKeyDriftError } from "../chunks/SecretService-gOQN_9Fc.js";
1
+ import { n as SecretService, t as SecretKeyDriftError } from "../chunks/SecretService-Cw26e9sn.js";
2
2
  export { SecretKeyDriftError, SecretService };
@@ -3,12 +3,12 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-secrets",
6
- "packageVersion": "0.49.3",
6
+ "packageVersion": "0.49.4",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "81582ae7af0a8e20e460f3eb765dd332d1d1fe500a005a7923f5225c67e832e0",
11
- "packageJson": "3fd61ff08285a36283671cde4acfffd8d76b800fd0cddda34958de2986725363",
10
+ "manifest": "84c7876610eec3502df83ddeb8145a89f885a7bfd9d49e0ec6365124382bb372",
11
+ "packageJson": "a20ae4afedf8bb306f7fe2ef7f7605d1d091704a65483496ef7d615ebb150722",
12
12
  "agents": "96fe8095cb8a2673fb8752fdc2940b53ce09d83c42715396ba6812ce215e990b"
13
13
  },
14
14
  "exports": [
@@ -49,7 +49,7 @@
49
49
  "name": "SecretAuditLogCollection",
50
50
  "qualifiedName": "@happyvertical/smrt-secrets:SecretAuditLogCollection",
51
51
  "collection": "secretauditlogs",
52
- "tableName": "secret_audit_log_collections",
52
+ "tableName": "secret_audit_logs",
53
53
  "packageName": "@happyvertical/smrt-secrets",
54
54
  "extends": "SmrtCollection",
55
55
  "fields": [],
@@ -310,7 +310,7 @@
310
310
  "name": "SecretCollection",
311
311
  "qualifiedName": "@happyvertical/smrt-secrets:SecretCollection",
312
312
  "collection": "secrets",
313
- "tableName": "secret_collections",
313
+ "tableName": "secrets",
314
314
  "packageName": "@happyvertical/smrt-secrets",
315
315
  "extends": "SmrtCollection",
316
316
  "fields": [],
@@ -568,7 +568,7 @@
568
568
  "name": "TenantKeyCollection",
569
569
  "qualifiedName": "@happyvertical/smrt-secrets:TenantKeyCollection",
570
570
  "collection": "tenantkeys",
571
- "tableName": "tenant_key_collections",
571
+ "tableName": "tenant_keys",
572
572
  "packageName": "@happyvertical/smrt-secrets",
573
573
  "extends": "SmrtCollection",
574
574
  "fields": [],
@@ -1881,7 +1881,7 @@
1881
1881
  "junctionCollections": 0,
1882
1882
  "hierarchicalObjects": 0,
1883
1883
  "polymorphicAssociations": 0,
1884
- "uuidColumns": 10
1884
+ "uuidColumns": 14
1885
1885
  },
1886
1886
  "agentDoc": "# @happyvertical/smrt-secrets\n\nEnvelope encryption for per-tenant secret storage with key rotation and audit logging.\n\n## Encryption Architecture\n\n```\nSecret value → encrypted with TDEK (per-tenant Data Encryption Key)\n TDEK → wrapped with AMK (Application Master Key, from env SMRT_SECRET_MASTER_KEY)\n```\n\n## Models\n\n- **Secret**: `encryptedValue` (JSON envelope), `category`, `status` (active/disabled/expired), `expiresAt`, `accessCount`, `lastAccessedAt`. **No API/MCP exposure** (security). CLI: list-only.\n- **TenantKey**: per-tenant TDEK in wrapped form. Status: active/rotating/retired/compromised. **Not tenant-scoped itself** — it tracks keys FOR tenants.\n- **SecretAuditLog**: action (create/read/update/delete/rotate_key/disable/enable), result (success/failure/denied), user/IP tracking. CLI: list-only.\n\n## SecretService\n\n| Method | Behavior |\n|--------|----------|\n| `store(name, value, options)` | Encrypt + save. Upsert if exists. Uses `context=tenantId` for per-tenant uniqueness. |\n| `retrieve(name)` | Decrypt + audit + increment accessCount |\n| `diagnoseTenantSecretKeyDrift(tenantId)` | Report active secret/key drift without exposing values. Checks `secrets`, SDK `tenant_encryption_keys`, and SMRT `tenant_keys`. |\n| `repairTenantSecretKeyDrift(tenantId, opts)` | Explicit repair path for unrecoverable drift. Use `dryRun` first; destructive cleanup requires `confirmDeleteUnrecoverableData: true`. |\n| `rotateKey()` | Create new TDEK, mark old as retired |\n| `reencryptAll()` | Decrypt with old TDEK, re-encrypt with new — **must call separately after rotateKey()** |\n| `disable(name)` / `enable(name)` | Toggle status |\n\n## Gotchas\n\n- **Key rotation doesn't auto-re-encrypt**: call `reencryptAll()` separately after `rotateKey()`\n- **retrieve() increments accessCount**: every read is tracked\n- **TenantKey NOT tenant-scoped**: it stores keys for tenants but isn't filtered by tenant context\n- **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.\n- **Repair is destructive only by confirmation**: `repairTenantSecretKeyDrift()` deletes unrecoverable encrypted rows only when `confirmDeleteUnrecoverableData: true` is explicit. Do not silently discard encrypted secrets.\n- **Expired secrets filtered by default**: pass `includeExpired: true` to list them\n- **TenantKeyCollection.cleanupRetiredKeys()**: hard-deletes after 90 days\n- **Audit logging optional but default**: failures logged to console, not thrown\n- **Audit rows outlive secrets**: `SecretAuditLog.secretId` uses the explicit\n same-package app-side-only FK option. It retains relationship metadata and an\n index, but emits no physical constraint because compliance logs are retained\n for 1-7 years while secrets may be deleted or purged during key-drift repair.\n\n## Known exceptions to monorepo standards\n\nPer `docs/content/standards.md §7`, tenant-aware models should normally apply\n`@TenantScoped({ mode: 'optional' })` from `@happyvertical/smrt-tenancy`. The three\nmodels in this package deviate intentionally; each `@smrt(...)` block carries an\ninline comment pointing back to this section.\n\n- **`Secret` (`src/models/Secret.ts`)** — uses the inline `tenantScoped: true` form\n on `@smrt()` instead of the `@TenantScoped` decorator. `SecretService.store()`\n performs manual scoping by populating `context = tenantId` on each row, so the\n `(slug, context)` upsert key from the base `SmrtObject` is what isolates secret\n names per tenant. Switching to the decorator without rethinking the upsert key\n would surface false-positive name collisions across tenants.\n- **`TenantKey` (`src/models/TenantKey.ts`)** — deliberately NOT tenant-scoped at\n all. The row carries a `tenantId` column because each TDEK belongs to a tenant,\n but key-rotation tooling, AMK rewrap jobs, and super-admin audits must query\n across tenants; the tenancy interceptor would silently filter rows those flows\n rely on.\n- **`SecretAuditLog` (`src/models/SecretAuditLog.ts`)** — uses the inline\n `tenantScoped: true` form rather than the decorator. Audit reads run in mixed\n contexts (tenant-scoped reports vs. super-admin compliance review). Cross-\n tenant audit queries should be wrapped in `withSuperAdminBypass()` from\n `@happyvertical/smrt-tenancy` at the call site — there are no such cross-\n tenant call sites in this package today, but consumers building compliance\n tooling should adopt that pattern explicitly rather than relying on\n decorator-implicit filtering. Its `secretId` relationship is also deliberately\n app-side-only so immutable audit history can survive deletion of the secret.\n"
1887
1887
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-secrets",
3
- "version": "0.49.3",
3
+ "version": "0.49.4",
4
4
  "description": "Per-tenant secret management with envelope encryption for SMRT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,13 +29,13 @@
29
29
  "dependencies": {
30
30
  "@happyvertical/logger": "^0.89.6",
31
31
  "@happyvertical/secrets": "^0.89.6",
32
- "@happyvertical/smrt-core": "0.49.3",
33
- "@happyvertical/smrt-tenancy": "0.49.3",
32
+ "@happyvertical/smrt-core": "0.49.4",
33
+ "@happyvertical/smrt-tenancy": "0.49.4",
34
34
  "@happyvertical/sql": "^0.89.6",
35
35
  "@happyvertical/utils": "^0.89.6"
36
36
  },
37
37
  "devDependencies": {
38
- "@happyvertical/smrt-vitest": "0.49.3",
38
+ "@happyvertical/smrt-vitest": "0.49.4",
39
39
  "@types/node": "24.13.2",
40
40
  "typescript": "5.9.3",
41
41
  "vite": "8.1.4",