@happyvertical/secrets 0.87.0 → 0.88.1
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/README.md +142 -2
- package/dist/adapters/database.d.ts +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1069 -3
- package/dist/index.js.map +1 -1
- package/dist/shared/custody.d.ts +264 -0
- package/dist/shared/custody.d.ts.map +1 -0
- package/dist/shared/factory.d.ts +1 -1
- package/package.json +3 -3
- package/LICENSE +0 -7
package/README.md
CHANGED
|
@@ -55,11 +55,11 @@ const store = await getSecretStore({
|
|
|
55
55
|
await store.createTenantKey('tenant-123');
|
|
56
56
|
|
|
57
57
|
// Encrypt a secret
|
|
58
|
-
const envelope = await store.encrypt('tenant-123', 'api-key', '
|
|
58
|
+
const envelope = await store.encrypt('tenant-123', 'api-key', 'synthetic-secret');
|
|
59
59
|
|
|
60
60
|
// Decrypt the secret
|
|
61
61
|
const { value } = await store.decrypt('tenant-123', envelope);
|
|
62
|
-
|
|
62
|
+
// Use value without logging or retaining plaintext.
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
## Core Concepts
|
|
@@ -215,6 +215,146 @@ try {
|
|
|
215
215
|
}
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
+
## Credential custody orchestration
|
|
219
|
+
|
|
220
|
+
`CredentialCustody` coordinates a provider-neutral issuer, verifier, optional
|
|
221
|
+
secret sink, and receipt ledger. It supports two explicit modes:
|
|
222
|
+
|
|
223
|
+
- `ephemeral` keeps opaque `SecretMaterial` in memory for a bounded lifetime,
|
|
224
|
+
automatically revokes it at expiry, and never stores it in a sink.
|
|
225
|
+
- `durable` requires a `CredentialSecretSink`, then completes
|
|
226
|
+
issue → store → retrieve → verify before returning a lease. Any failure
|
|
227
|
+
removes the stored value when present and revokes the issued credential.
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import {
|
|
231
|
+
CredentialCustody,
|
|
232
|
+
type CredentialReceiptAttestor,
|
|
233
|
+
type CredentialIssuer,
|
|
234
|
+
type CredentialCustodyFinalizer,
|
|
235
|
+
type CredentialSecretSink,
|
|
236
|
+
type CredentialVerifier,
|
|
237
|
+
type CustodyLedger,
|
|
238
|
+
} from '@happyvertical/secrets';
|
|
239
|
+
|
|
240
|
+
const custody = new CredentialCustody({
|
|
241
|
+
issuer: myIssuer satisfies CredentialIssuer,
|
|
242
|
+
verifier: myVerifier satisfies CredentialVerifier,
|
|
243
|
+
sink: mySink satisfies CredentialSecretSink,
|
|
244
|
+
ledger: myLedger satisfies CustodyLedger,
|
|
245
|
+
attestor: myAttestor satisfies CredentialReceiptAttestor,
|
|
246
|
+
finalizer: myFinalizer satisfies CredentialCustodyFinalizer,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const lease = await custody.issue({
|
|
250
|
+
mode: 'durable',
|
|
251
|
+
subject: 'deployment-agent',
|
|
252
|
+
attribution: {
|
|
253
|
+
actor: 'automation',
|
|
254
|
+
runtime: 'scheduler',
|
|
255
|
+
session: 'job-123',
|
|
256
|
+
},
|
|
257
|
+
metadata: { purpose: 'repository-maintenance' },
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await lease.withEnvironment('SERVICE_CREDENTIAL', async () => {
|
|
261
|
+
// The variable exists only for this callback and is restored afterward.
|
|
262
|
+
await runAuthenticatedOperation();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const child = await lease.withChildProcess({
|
|
266
|
+
trust: 'cooperative-process-group',
|
|
267
|
+
command: 'service-cli',
|
|
268
|
+
args: ['verify'],
|
|
269
|
+
environmentVariable: 'SERVICE_CREDENTIAL',
|
|
270
|
+
timeoutMs: 30_000,
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The returned `CustodyReceipt` contains identifiers, attribution, verification
|
|
275
|
+
state, sink reference, rotation lineage, and an Ed25519 attestation only.
|
|
276
|
+
Plaintext is represented by
|
|
277
|
+
`SecretMaterial`, whose string, JSON, and inspection forms are always redacted.
|
|
278
|
+
Adapters can access it only inside `SecretMaterial.use(...)` and should avoid
|
|
279
|
+
copying or retaining the supplied string. Both `SecretMaterial.use(...)` and
|
|
280
|
+
`withEnvironment(...)` return `Promise<void>` so callback code cannot return
|
|
281
|
+
plaintext through the custody API. Environment callbacks are serialized and
|
|
282
|
+
always restore the prior value. Because JavaScript callbacks cannot be forcibly
|
|
283
|
+
cancelled, expiring credentials fail closed for `withEnvironment(...)`; use
|
|
284
|
+
`withChildProcess(...)` for bounded execution.
|
|
285
|
+
|
|
286
|
+
Attestation binds every receipt field—including the stable sink tuple
|
|
287
|
+
`sinkName`, `reference`, `version`, and `storedAt`—to an internal SHA-256
|
|
288
|
+
commitment of the credential. The commitment is signed but is not included in
|
|
289
|
+
the receipt. `CredentialCustodyOptions.attestor` is required, so unsigned
|
|
290
|
+
issuance fails closed. `Ed25519CustodyReceiptAttestor` accepts a Node `KeyObject`
|
|
291
|
+
private key; remote signers can implement `CredentialReceiptAttestor` without
|
|
292
|
+
exposing their key.
|
|
293
|
+
|
|
294
|
+
Consumers verify a presented credential without issuer or sink access by
|
|
295
|
+
constructing its `SecretMaterial` and calling
|
|
296
|
+
`verifyCustodyReceiptAttestation(receipt, material, publicKey)`. Resolve
|
|
297
|
+
`receipt.attestation.keyId` only through trusted configuration, never from
|
|
298
|
+
receipt-supplied key material. Import a trusted PEM/SPKI public key with Node's
|
|
299
|
+
`createPublicKey(...)`; verification requires the resulting public `KeyObject`.
|
|
300
|
+
Always destroy the temporary `SecretMaterial` after verification.
|
|
301
|
+
|
|
302
|
+
The required `CredentialCustodyFinalizer` is a staged, idempotent transaction.
|
|
303
|
+
The signed receipt is first recorded as `finalization-pending`; `prepare` then
|
|
304
|
+
receives it with bounded `SecretMaterial`. The receipt binds the finalizer name
|
|
305
|
+
and SDK-generated `finalizationId`. `commit` activates the prepared record, and
|
|
306
|
+
the ledger atomically marks it issued while recording any predecessor cleanup.
|
|
307
|
+
Only then does rotation retire the predecessor. Cleanup failure keeps the new
|
|
308
|
+
credential active and is retried by `recoverPendingRollbacks()`. The finalizer's
|
|
309
|
+
`status` resolves crash ambiguity; `abort` and issuer/sink cleanup run
|
|
310
|
+
independently. Implement `prepare`, `commit`, `abort`, and `status` idempotently
|
|
311
|
+
by `finalizationId`. Fresh pending transactions are protected from concurrent
|
|
312
|
+
takeover for `finalizationTakeoverMs` (30 seconds by default); a one-shot
|
|
313
|
+
recovery call schedules takeover automatically at that deadline.
|
|
314
|
+
|
|
315
|
+
Prefer `withChildProcess(...)` for trusted command-line consumers. It requires
|
|
316
|
+
the explicit `trust: 'cooperative-process-group'` acknowledgement, injects the
|
|
317
|
+
credential into the child environment without mutating the parent, disables
|
|
318
|
+
shell interpretation, bounds runtime and captured output, owns a detached
|
|
319
|
+
process group so descendants are terminated, confirms that group is gone
|
|
320
|
+
before returning, and exact/token-redacts stdout and stderr. The trusted
|
|
321
|
+
command and every credential-bearing descendant must remain in that group;
|
|
322
|
+
daemonizing, calling `setsid`, or spawning a detached child violates this
|
|
323
|
+
boundary because portable POSIX process groups cannot contain a process that
|
|
324
|
+
creates a new session. Use only audited executables that honor this contract.
|
|
325
|
+
Process-group custody fails closed on Windows, where the required POSIX group
|
|
326
|
+
semantics are unavailable. If group cleanup cannot be verified, the lease is
|
|
327
|
+
immediately invalidated and finalizer/issuer/sink cleanup runs through the same
|
|
328
|
+
persisted, restart-safe rollback path used by failed issuance.
|
|
329
|
+
|
|
330
|
+
Use `rotate(receiptId, request)` to issue and verify a replacement before
|
|
331
|
+
retiring its predecessor. `reconcile()` compares active durable receipts with
|
|
332
|
+
the sink inventory; `recoverOrphans(report)` removes unowned sink records while
|
|
333
|
+
retaining attributable history in the ledger. Missing records can be replaced
|
|
334
|
+
through `rotate`, preserving `replacesReceiptId` and `rotationRootReceiptId`.
|
|
335
|
+
The ledger's `recordIssuance` operation atomically persists the receipt with a
|
|
336
|
+
`finalization-pending` event and rejects duplicate or branching replacements.
|
|
337
|
+
`commitIssuance` idempotently appends the `issued` event and, for rotation, the
|
|
338
|
+
`retirement-pending` event in the same transaction. Issuer revocation
|
|
339
|
+
and exact-version sink removal must be idempotent so failed cleanup can retry.
|
|
340
|
+
`appendEvent` is idempotent by `eventId`: exact replay succeeds, while binding
|
|
341
|
+
the same identifier to different content fails closed.
|
|
342
|
+
Transient finalizer-status or ledger-commit errors retain
|
|
343
|
+
`finalization-pending` state and retry ambiguity resolution; only an explicit
|
|
344
|
+
non-committed finalizer status triggers rollback. Sink identity comparison uses
|
|
345
|
+
the full attested `sinkName`, `reference`, `version`, and `storedAt` tuple.
|
|
346
|
+
Sinks must also implement idempotent `removeByCredentialId(...)` for ambiguous
|
|
347
|
+
store failures, and every inventory entry must identify its credential.
|
|
348
|
+
Orphan recovery re-runs reconciliation and applies a configurable age grace
|
|
349
|
+
before deleting an unchanged sink version. Failed pre-receipt rollback records a
|
|
350
|
+
non-secret pending event, retries automatically, and can be resumed after a
|
|
351
|
+
restart with `recoverPendingRollbacks()`.
|
|
352
|
+
|
|
353
|
+
`redactCredentialText` and `redactCredentialValues` remove common bearer-token
|
|
354
|
+
shapes and known plaintext values from strings, structured outputs, errors, and
|
|
355
|
+
metadata. `CustodyError` serializes only its safe code, stage, redacted message,
|
|
356
|
+
and redacted details; underlying provider causes are intentionally not retained.
|
|
357
|
+
|
|
218
358
|
## License
|
|
219
359
|
|
|
220
360
|
MIT
|
|
@@ -22,7 +22,7 @@ import { ApplicationMasterKey, DatabaseSecretStoreOptions, DecryptedSecret, Encr
|
|
|
22
22
|
* await store.initialize();
|
|
23
23
|
*
|
|
24
24
|
* // Encrypt a secret for a tenant
|
|
25
|
-
* const envelope = await store.encrypt('tenant-123', 'api-key', '
|
|
25
|
+
* const envelope = await store.encrypt('tenant-123', 'api-key', 'synthetic-secret');
|
|
26
26
|
*
|
|
27
27
|
* // Decrypt
|
|
28
28
|
* const { value } = await store.decrypt('tenant-123', envelope);
|
package/dist/index.d.ts
CHANGED
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
* });
|
|
24
24
|
*
|
|
25
25
|
* // Encrypt a secret for a tenant
|
|
26
|
-
* const envelope = await store.encrypt('tenant-123', 'api-key', '
|
|
26
|
+
* const envelope = await store.encrypt('tenant-123', 'api-key', 'synthetic-secret');
|
|
27
27
|
*
|
|
28
28
|
* // Decrypt the secret
|
|
29
29
|
* const { value } = await store.decrypt('tenant-123', envelope);
|
|
30
|
-
*
|
|
30
|
+
* // Use value without logging or retaining plaintext.
|
|
31
31
|
*
|
|
32
32
|
* // Rotate tenant's encryption key
|
|
33
33
|
* await store.rotateTenantKey('tenant-123');
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
* @packageDocumentation
|
|
37
37
|
*/
|
|
38
38
|
export { DatabaseSecretStore } from './adapters/database.js';
|
|
39
|
+
export type { CredentialChildProcessOptions, CredentialChildProcessResult, CredentialCustodyFinalizer, CredentialCustodyOptions, CredentialIssuanceMode, CredentialIssueRequest, CredentialIssuer, CredentialLease, CredentialReceiptAttestor, CredentialSecretSink, CredentialVerifier, CustodyAttribution, CustodyEvent, CustodyEventType, CustodyIssuanceRequest, CustodyLedger, CustodyReceipt, CustodyReceiptAttestation, CustodyReconciliation, CustodyStage, IssuedCredential, SecretSinkInventoryEntry, SecretSinkRecord, } from './shared/custody.js';
|
|
40
|
+
export { CredentialCustody, CustodyError, Ed25519CustodyReceiptAttestor, InMemoryCustodyLedger, redactCredentialText, redactCredentialValues, runCredentialChildProcess, SecretMaterial, verifyCustodyReceiptAttestation, withEnvironmentSecret, } from './shared/custody.js';
|
|
39
41
|
export { EnvelopeEncryption } from './shared/envelope.js';
|
|
40
42
|
export { AMKUnavailableError, DecryptionError, EncryptionError, InvalidKeyFormatError, KeyNotFoundError, KeyRotationError, SecretError, StoreNotInitializedError, TenantKeyMissingError, } from './shared/errors.js';
|
|
41
43
|
export { getSecretStore, isAWSKMSOptions, isAzureKeyVaultOptions, isDatabaseOptions, isVaultOptions, } from './shared/factory.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EACV,6BAA6B,EAC7B,4BAA4B,EAC5B,0BAA0B,EAC1B,wBAAwB,EACxB,sBAAsB,EACtB,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,yBAAyB,EACzB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,cAAc,EACd,yBAAyB,EACzB,qBAAqB,EACrB,YAAY,EACZ,gBAAgB,EAChB,wBAAwB,EACxB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,6BAA6B,EAC7B,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,yBAAyB,EACzB,cAAc,EACd,+BAA+B,EAC/B,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,cAAc,EACd,eAAe,EACf,sBAAsB,EACtB,iBAAiB,EACjB,cAAc,GACf,MAAM,qBAAqB,CAAC;AAE7B,YAAY,EAEV,SAAS,EACT,oBAAoB,EACpB,wBAAwB,EACxB,+BAA+B,EAC/B,0BAA0B,EAC1B,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,uBAAuB,EACvB,WAAW,EACX,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,gBAAgB;AAChB,eAAO,MAAM,2BAA2B,OAAO,CAAC"}
|