@btravstack/keys 0.0.0-stage → 0.45.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/LICENSE +21 -0
- package/README.md +101 -2
- package/dist/index.cjs +42 -0
- package/dist/index.d.cts +49 -0
- package/dist/index.d.mts +49 -0
- package/dist/index.mjs +37 -0
- package/dist/keys-CGbVjuu3.mjs +29 -0
- package/dist/keys-DpvmIcWy.d.cts +74 -0
- package/dist/keys-DpvmIcWy.d.mts +74 -0
- package/dist/keys-DtMnWJHq.cjs +46 -0
- package/dist/kms.cjs +211 -0
- package/dist/kms.d.cts +60 -0
- package/dist/kms.d.mts +60 -0
- package/dist/kms.mjs +210 -0
- package/package.json +90 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benoit TRAVERS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,102 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @btravstack/keys
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> An `EnvelopeKeys` port for [`@btravstack/core`](../core): a root key per
|
|
4
|
+
> owner, in generations, wrapped by an AWS KMS key of that owner — unwrapped in
|
|
5
|
+
> memory and cached for a TTL, erased with one key deletion — and an HKDF
|
|
6
|
+
> helper giving each purpose its own key.
|
|
7
|
+
|
|
8
|
+
📖 **[Documentation](https://btravstack.github.io/btravstack/reference/keys)** ·
|
|
9
|
+
[API Reference](https://btravstack.github.io/btravstack/api/keys/)
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add @btravstack/keys @btravstack/core @btravstack/config @btravstack/di unthrown
|
|
13
|
+
pnpm add @aws-sdk/client-kms # for @btravstack/keys/kms
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Every dependency is a peer, and `@aws-sdk/client-kms` an optional one: only
|
|
17
|
+
`@btravstack/keys/kms` imports it.
|
|
18
|
+
|
|
19
|
+
## A worked example
|
|
20
|
+
|
|
21
|
+
<!-- doctest: prelude
|
|
22
|
+
import { Module, Port, Provider } from "@btravstack/di";
|
|
23
|
+
import type { AsyncResult } from "unthrown";
|
|
24
|
+
-->
|
|
25
|
+
|
|
26
|
+
A port that seals a tenant's payloads under a key of that tenant's own, and
|
|
27
|
+
the composition that provides the keys:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import {
|
|
31
|
+
EnvelopeKeys,
|
|
32
|
+
deriveKey,
|
|
33
|
+
type InvalidKeyPurpose,
|
|
34
|
+
type KeyErased,
|
|
35
|
+
type KeyGeneration,
|
|
36
|
+
type KeysUnavailable,
|
|
37
|
+
} from "@btravstack/keys";
|
|
38
|
+
import { kmsKeys } from "@btravstack/keys/kms";
|
|
39
|
+
|
|
40
|
+
class PayloadKeys extends Port("ReadmePayloadKeys")<{
|
|
41
|
+
readonly key: (
|
|
42
|
+
generation: KeyGeneration,
|
|
43
|
+
) => AsyncResult<CryptoKey, KeyErased | KeysUnavailable | InvalidKeyPurpose>;
|
|
44
|
+
}> {}
|
|
45
|
+
|
|
46
|
+
export const payloadKeys = Provider(PayloadKeys)({
|
|
47
|
+
inject: { keys: EnvelopeKeys },
|
|
48
|
+
sync: ({ keys }) => ({
|
|
49
|
+
key: (generation) =>
|
|
50
|
+
keys.rootKey(generation).flatMap((root) =>
|
|
51
|
+
deriveKey(root, { namespace: "payloads", purpose: "seal", version: 1 }, {
|
|
52
|
+
algorithm: { name: "AES-GCM", length: 256 },
|
|
53
|
+
usages: ["encrypt", "decrypt"],
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const Sealing = Module("ReadmeSealing")({
|
|
60
|
+
imports: [kmsKeys()],
|
|
61
|
+
provides: [payloadKeys],
|
|
62
|
+
exports: [PayloadKeys],
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Provisioning a generation, rotating, erasing a tenant in one call and sizing
|
|
67
|
+
the cache are
|
|
68
|
+
[Keep envelope keys per owner](https://btravstack.github.io/btravstack/how-to/keep-envelope-keys-per-owner).
|
|
69
|
+
|
|
70
|
+
## Options
|
|
71
|
+
|
|
72
|
+
| Option | Where | What it is |
|
|
73
|
+
| ------------------------------ | ------------------------------------------ | --------------------------------------------------------- |
|
|
74
|
+
| `KEYS_KMS_REGION` | environment | the region of the owners' KMS keys — required |
|
|
75
|
+
| `KEYS_KMS_ALIAS_PREFIX` | environment | an owner's KMS key is `<prefix><owner>` — required |
|
|
76
|
+
| `KEYS_KMS_ENDPOINT` | environment, pinned by `endpoint` | an emulator or a VPC endpoint (default: AWS's own) |
|
|
77
|
+
| `KEYS_CACHE_TTL_MS` | environment, pinned by `cacheTtlMs` | how long an unwrapped root is kept (default five minutes) |
|
|
78
|
+
| `KEYS_KMS_PENDING_WINDOW_DAYS` | environment, pinned by `pendingWindowDays` | the days an erasure can be cancelled (default `30`) |
|
|
79
|
+
| `clock` | `kmsKeys({ … })` | what the cache's TTL is measured against |
|
|
80
|
+
|
|
81
|
+
The full table — defaults, semantics and the reasoning — lives on
|
|
82
|
+
[the reference page](https://btravstack.github.io/btravstack/reference/keys),
|
|
83
|
+
which is this list's one detailed home.
|
|
84
|
+
|
|
85
|
+
## What it decides, and what it does not
|
|
86
|
+
|
|
87
|
+
**It decides** that a generation is a KMS data key bound by the encryption
|
|
88
|
+
context `{ owner, keyId }`, so a blob presented for another owner or key id is
|
|
89
|
+
refused rather than unwrapped; that a disabled, deleted, missing or refusing
|
|
90
|
+
key is a typed `KeyErased` and anything else KMS says a typed
|
|
91
|
+
`KeysUnavailable`; that erasing an owner is one scheduled key deletion; that
|
|
92
|
+
every KMS request is reported to `Observers`; and that a derived key is
|
|
93
|
+
HKDF-SHA-256 with `info = <namespace>/<purpose>/v<version>`.
|
|
94
|
+
|
|
95
|
+
**It does not decide** where the wrapped generations are kept, which one
|
|
96
|
+
encrypts next, or how the owners' KMS keys are created: those are the
|
|
97
|
+
application's and the platform's. The reasoning is in
|
|
98
|
+
[`AGENTS.md`](./AGENTS.md).
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_keys = require("./keys-DtMnWJHq.cjs");
|
|
3
|
+
let unthrown = require("unthrown");
|
|
4
|
+
//#region src/derive.ts
|
|
5
|
+
/** A purpose whose `info` would be ambiguous: a segment outside the alphabet, or a version that is not a positive integer. */
|
|
6
|
+
var InvalidKeyPurpose = class extends (0, unthrown.TaggedError)("InvalidKeyPurpose") {
|
|
7
|
+
message = "the key purpose is not a valid HKDF info";
|
|
8
|
+
};
|
|
9
|
+
const SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
10
|
+
/**
|
|
11
|
+
* A key for one purpose, derived from a root key with HKDF-SHA-256, an empty
|
|
12
|
+
* salt and `info = <namespace>/<purpose>/v<version>`.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* keys.rootKey(generation).flatMap((root) =>
|
|
16
|
+
* deriveKey(root, { namespace: "payloads", purpose: "seal", version: 1 }, {
|
|
17
|
+
* algorithm: { name: "AES-GCM", length: 256 },
|
|
18
|
+
* usages: ["encrypt", "decrypt"],
|
|
19
|
+
* }),
|
|
20
|
+
* );
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Every purpose gets its own key, and so its own nonce budget: two uses of
|
|
24
|
+
* random 96-bit AES-GCM nonces never draw from one key. The salt is empty
|
|
25
|
+
* because the root is already uniformly random (RFC 5869, section 3.1).
|
|
26
|
+
*/
|
|
27
|
+
const deriveKey = (root, purpose, derived) => SEGMENT.test(purpose.namespace) && SEGMENT.test(purpose.purpose) && Number.isSafeInteger(purpose.version) && purpose.version > 0 ? (0, unthrown.fromSafePromise)(crypto.subtle.deriveKey({
|
|
28
|
+
name: "HKDF",
|
|
29
|
+
hash: "SHA-256",
|
|
30
|
+
salt: /* @__PURE__ */ new Uint8Array(0),
|
|
31
|
+
info: new TextEncoder().encode(`${purpose.namespace}/${purpose.purpose}/v${purpose.version}`)
|
|
32
|
+
}, root, derived.algorithm, false, [...derived.usages])) : (0, unthrown.ErrAsync)(new InvalidKeyPurpose({
|
|
33
|
+
namespace: purpose.namespace,
|
|
34
|
+
purpose: purpose.purpose,
|
|
35
|
+
version: purpose.version
|
|
36
|
+
}));
|
|
37
|
+
//#endregion
|
|
38
|
+
exports.EnvelopeKeys = require_keys.EnvelopeKeys;
|
|
39
|
+
exports.InvalidKeyPurpose = InvalidKeyPurpose;
|
|
40
|
+
exports.KeyErased = require_keys.KeyErased;
|
|
41
|
+
exports.KeysUnavailable = require_keys.KeysUnavailable;
|
|
42
|
+
exports.deriveKey = deriveKey;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { a as KeysUnavailable, i as KeyGeneration, n as EnvelopeKeysService, r as KeyErased, t as EnvelopeKeys } from "./keys-DpvmIcWy.cjs";
|
|
2
|
+
import { webcrypto } from "node:crypto";
|
|
3
|
+
import { AsyncResult } from "unthrown";
|
|
4
|
+
//#region src/derive.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What a derived key is for, spelled into HKDF's `info` as
|
|
7
|
+
* `<namespace>/<purpose>/v<version>`. Each segment is ASCII letters, digits,
|
|
8
|
+
* `.`, `_` or `-`, so no two purposes spell the same `info`; `version` is a
|
|
9
|
+
* positive integer, raised to retire a derived key without a new root.
|
|
10
|
+
*/
|
|
11
|
+
type KeyPurpose = {
|
|
12
|
+
readonly namespace: string;
|
|
13
|
+
readonly purpose: string;
|
|
14
|
+
readonly version: number;
|
|
15
|
+
};
|
|
16
|
+
/** What the derived key is, as WebCrypto's `deriveKey` takes it. It is never extractable. */
|
|
17
|
+
type DerivedKey = {
|
|
18
|
+
readonly algorithm: webcrypto.AesDerivedKeyParams | webcrypto.HmacImportParams;
|
|
19
|
+
readonly usages: readonly webcrypto.KeyUsage[];
|
|
20
|
+
};
|
|
21
|
+
declare const InvalidKeyPurpose_base: import("unthrown").TaggedErrorConstructor<"InvalidKeyPurpose">;
|
|
22
|
+
/** A purpose whose `info` would be ambiguous: a segment outside the alphabet, or a version that is not a positive integer. */
|
|
23
|
+
export declare class InvalidKeyPurpose extends InvalidKeyPurpose_base<{
|
|
24
|
+
readonly namespace: string;
|
|
25
|
+
readonly purpose: string;
|
|
26
|
+
readonly version: number;
|
|
27
|
+
}> {
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A key for one purpose, derived from a root key with HKDF-SHA-256, an empty
|
|
32
|
+
* salt and `info = <namespace>/<purpose>/v<version>`.
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* keys.rootKey(generation).flatMap((root) =>
|
|
36
|
+
* deriveKey(root, { namespace: "payloads", purpose: "seal", version: 1 }, {
|
|
37
|
+
* algorithm: { name: "AES-GCM", length: 256 },
|
|
38
|
+
* usages: ["encrypt", "decrypt"],
|
|
39
|
+
* }),
|
|
40
|
+
* );
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* Every purpose gets its own key, and so its own nonce budget: two uses of
|
|
44
|
+
* random 96-bit AES-GCM nonces never draw from one key. The salt is empty
|
|
45
|
+
* because the root is already uniformly random (RFC 5869, section 3.1).
|
|
46
|
+
*/
|
|
47
|
+
export declare const deriveKey: (root: CryptoKey, purpose: KeyPurpose, derived: DerivedKey) => AsyncResult<CryptoKey, InvalidKeyPurpose>;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { type DerivedKey, EnvelopeKeys, type EnvelopeKeysService, KeyErased, type KeyGeneration, type KeyPurpose, KeysUnavailable };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { a as KeysUnavailable, i as KeyGeneration, n as EnvelopeKeysService, r as KeyErased, t as EnvelopeKeys } from "./keys-DpvmIcWy.mjs";
|
|
2
|
+
import { AsyncResult } from "unthrown";
|
|
3
|
+
import { webcrypto } from "node:crypto";
|
|
4
|
+
//#region src/derive.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What a derived key is for, spelled into HKDF's `info` as
|
|
7
|
+
* `<namespace>/<purpose>/v<version>`. Each segment is ASCII letters, digits,
|
|
8
|
+
* `.`, `_` or `-`, so no two purposes spell the same `info`; `version` is a
|
|
9
|
+
* positive integer, raised to retire a derived key without a new root.
|
|
10
|
+
*/
|
|
11
|
+
type KeyPurpose = {
|
|
12
|
+
readonly namespace: string;
|
|
13
|
+
readonly purpose: string;
|
|
14
|
+
readonly version: number;
|
|
15
|
+
};
|
|
16
|
+
/** What the derived key is, as WebCrypto's `deriveKey` takes it. It is never extractable. */
|
|
17
|
+
type DerivedKey = {
|
|
18
|
+
readonly algorithm: webcrypto.AesDerivedKeyParams | webcrypto.HmacImportParams;
|
|
19
|
+
readonly usages: readonly webcrypto.KeyUsage[];
|
|
20
|
+
};
|
|
21
|
+
declare const InvalidKeyPurpose_base: import("unthrown").TaggedErrorConstructor<"InvalidKeyPurpose">;
|
|
22
|
+
/** A purpose whose `info` would be ambiguous: a segment outside the alphabet, or a version that is not a positive integer. */
|
|
23
|
+
export declare class InvalidKeyPurpose extends InvalidKeyPurpose_base<{
|
|
24
|
+
readonly namespace: string;
|
|
25
|
+
readonly purpose: string;
|
|
26
|
+
readonly version: number;
|
|
27
|
+
}> {
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A key for one purpose, derived from a root key with HKDF-SHA-256, an empty
|
|
32
|
+
* salt and `info = <namespace>/<purpose>/v<version>`.
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* keys.rootKey(generation).flatMap((root) =>
|
|
36
|
+
* deriveKey(root, { namespace: "payloads", purpose: "seal", version: 1 }, {
|
|
37
|
+
* algorithm: { name: "AES-GCM", length: 256 },
|
|
38
|
+
* usages: ["encrypt", "decrypt"],
|
|
39
|
+
* }),
|
|
40
|
+
* );
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* Every purpose gets its own key, and so its own nonce budget: two uses of
|
|
44
|
+
* random 96-bit AES-GCM nonces never draw from one key. The salt is empty
|
|
45
|
+
* because the root is already uniformly random (RFC 5869, section 3.1).
|
|
46
|
+
*/
|
|
47
|
+
export declare const deriveKey: (root: CryptoKey, purpose: KeyPurpose, derived: DerivedKey) => AsyncResult<CryptoKey, InvalidKeyPurpose>;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { type DerivedKey, EnvelopeKeys, type EnvelopeKeysService, KeyErased, type KeyGeneration, type KeyPurpose, KeysUnavailable };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { n as KeyErased, r as KeysUnavailable, t as EnvelopeKeys } from "./keys-CGbVjuu3.mjs";
|
|
2
|
+
import { ErrAsync, TaggedError, fromSafePromise } from "unthrown";
|
|
3
|
+
//#region src/derive.ts
|
|
4
|
+
/** A purpose whose `info` would be ambiguous: a segment outside the alphabet, or a version that is not a positive integer. */
|
|
5
|
+
var InvalidKeyPurpose = class extends TaggedError("InvalidKeyPurpose") {
|
|
6
|
+
message = "the key purpose is not a valid HKDF info";
|
|
7
|
+
};
|
|
8
|
+
const SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
9
|
+
/**
|
|
10
|
+
* A key for one purpose, derived from a root key with HKDF-SHA-256, an empty
|
|
11
|
+
* salt and `info = <namespace>/<purpose>/v<version>`.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* keys.rootKey(generation).flatMap((root) =>
|
|
15
|
+
* deriveKey(root, { namespace: "payloads", purpose: "seal", version: 1 }, {
|
|
16
|
+
* algorithm: { name: "AES-GCM", length: 256 },
|
|
17
|
+
* usages: ["encrypt", "decrypt"],
|
|
18
|
+
* }),
|
|
19
|
+
* );
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Every purpose gets its own key, and so its own nonce budget: two uses of
|
|
23
|
+
* random 96-bit AES-GCM nonces never draw from one key. The salt is empty
|
|
24
|
+
* because the root is already uniformly random (RFC 5869, section 3.1).
|
|
25
|
+
*/
|
|
26
|
+
const deriveKey = (root, purpose, derived) => SEGMENT.test(purpose.namespace) && SEGMENT.test(purpose.purpose) && Number.isSafeInteger(purpose.version) && purpose.version > 0 ? fromSafePromise(crypto.subtle.deriveKey({
|
|
27
|
+
name: "HKDF",
|
|
28
|
+
hash: "SHA-256",
|
|
29
|
+
salt: /* @__PURE__ */ new Uint8Array(0),
|
|
30
|
+
info: new TextEncoder().encode(`${purpose.namespace}/${purpose.purpose}/v${purpose.version}`)
|
|
31
|
+
}, root, derived.algorithm, false, [...derived.usages])) : ErrAsync(new InvalidKeyPurpose({
|
|
32
|
+
namespace: purpose.namespace,
|
|
33
|
+
purpose: purpose.purpose,
|
|
34
|
+
version: purpose.version
|
|
35
|
+
}));
|
|
36
|
+
//#endregion
|
|
37
|
+
export { EnvelopeKeys, InvalidKeyPurpose, KeyErased, KeysUnavailable, deriveKey };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TaggedError } from "unthrown";
|
|
2
|
+
import { Port } from "@btravstack/di";
|
|
3
|
+
//#region src/keys.ts
|
|
4
|
+
/**
|
|
5
|
+
* The generation can no longer be unwrapped, and never will be again: the
|
|
6
|
+
* owner's key is disabled (`disabled`), scheduled for deletion
|
|
7
|
+
* (`pending_deletion`), gone (`not_found`), or refused this ciphertext under
|
|
8
|
+
* this owner and key id (`refused`). An answer, not an outage: whatever the
|
|
9
|
+
* generation protected is erased.
|
|
10
|
+
*/
|
|
11
|
+
var KeyErased = class extends TaggedError("KeyErased") {
|
|
12
|
+
message = "the key generation is erased";
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The key-management service could not be asked: throttling, the network, a
|
|
16
|
+
* credential or permission it refused, a server error. Transient or a
|
|
17
|
+
* deployment mistake, never an erasure, so a later call asks again: a failure
|
|
18
|
+
* is never cached.
|
|
19
|
+
*/
|
|
20
|
+
var KeysUnavailable = class extends TaggedError("KeysUnavailable") {
|
|
21
|
+
message = "the key-management service could not be asked";
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The port an application depends on for per-owner envelope keys.
|
|
25
|
+
* `kmsKeys()` from `@btravstack/keys/kms` provides it over AWS KMS.
|
|
26
|
+
*/
|
|
27
|
+
var EnvelopeKeys = class extends Port("EnvelopeKeys") {};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { KeyErased as n, KeysUnavailable as r, EnvelopeKeys as t };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AsyncResult } from "unthrown";
|
|
2
|
+
//#region src/keys.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* One generation of an owner's root key, as it is kept: wrapped by the
|
|
5
|
+
* owner's key-management key, under an encryption context that binds `owner`
|
|
6
|
+
* and `keyId`.
|
|
7
|
+
*
|
|
8
|
+
* `wrapped` is not secret — only the key-management service can unwrap it, and
|
|
9
|
+
* only for this owner and this key id — so it may live wherever the
|
|
10
|
+
* application keeps configuration: a mounted Secret, a file, an object store,
|
|
11
|
+
* a database row.
|
|
12
|
+
*/
|
|
13
|
+
type KeyGeneration = {
|
|
14
|
+
readonly owner: string;
|
|
15
|
+
readonly keyId: string;
|
|
16
|
+
readonly wrapped: Uint8Array;
|
|
17
|
+
};
|
|
18
|
+
declare const KeyErased_base: import("unthrown").TaggedErrorConstructor<"KeyErased">;
|
|
19
|
+
/**
|
|
20
|
+
* The generation can no longer be unwrapped, and never will be again: the
|
|
21
|
+
* owner's key is disabled (`disabled`), scheduled for deletion
|
|
22
|
+
* (`pending_deletion`), gone (`not_found`), or refused this ciphertext under
|
|
23
|
+
* this owner and key id (`refused`). An answer, not an outage: whatever the
|
|
24
|
+
* generation protected is erased.
|
|
25
|
+
*/
|
|
26
|
+
declare class KeyErased extends KeyErased_base<{
|
|
27
|
+
readonly owner: string;
|
|
28
|
+
readonly keyId: string;
|
|
29
|
+
readonly reason: "disabled" | "pending_deletion" | "not_found" | "refused";
|
|
30
|
+
}> {
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
declare const KeysUnavailable_base: import("unthrown").TaggedErrorConstructor<"KeysUnavailable">;
|
|
34
|
+
/**
|
|
35
|
+
* The key-management service could not be asked: throttling, the network, a
|
|
36
|
+
* credential or permission it refused, a server error. Transient or a
|
|
37
|
+
* deployment mistake, never an erasure, so a later call asks again: a failure
|
|
38
|
+
* is never cached.
|
|
39
|
+
*/
|
|
40
|
+
declare class KeysUnavailable extends KeysUnavailable_base<{
|
|
41
|
+
readonly operation: "unwrap" | "provision" | "erase";
|
|
42
|
+
readonly owner: string;
|
|
43
|
+
readonly cause: unknown;
|
|
44
|
+
}> {
|
|
45
|
+
message: string;
|
|
46
|
+
}
|
|
47
|
+
type EnvelopeKeysService = {
|
|
48
|
+
/**
|
|
49
|
+
* The generation's root key, unwrapped in memory: a non-extractable HKDF
|
|
50
|
+
* key, to derive a key per purpose from with `deriveKey`. Kept for the
|
|
51
|
+
* cache's TTL; callers asking while it is being unwrapped share one request.
|
|
52
|
+
*/
|
|
53
|
+
readonly rootKey: (generation: KeyGeneration) => AsyncResult<CryptoKey, KeyErased | KeysUnavailable>;
|
|
54
|
+
/**
|
|
55
|
+
* A new generation under the owner's key, for the application to keep.
|
|
56
|
+
* Rotation is provisioning another key id; the old generations stay
|
|
57
|
+
* readable until the owner is erased.
|
|
58
|
+
*/
|
|
59
|
+
readonly provision: (owner: string, keyId: string) => AsyncResult<KeyGeneration, KeyErased | KeysUnavailable>;
|
|
60
|
+
/**
|
|
61
|
+
* Every generation of the owner, erased at once: the owner's key is
|
|
62
|
+
* scheduled for deletion, so from then on each generation answers
|
|
63
|
+
* `KeyErased`. Erasing an owner already erased answers `Ok`.
|
|
64
|
+
*/
|
|
65
|
+
readonly erase: (owner: string) => AsyncResult<void, KeysUnavailable>;
|
|
66
|
+
};
|
|
67
|
+
declare const EnvelopeKeys_base: import("@btravstack/di").PortClass<"EnvelopeKeys">;
|
|
68
|
+
/**
|
|
69
|
+
* The port an application depends on for per-owner envelope keys.
|
|
70
|
+
* `kmsKeys()` from `@btravstack/keys/kms` provides it over AWS KMS.
|
|
71
|
+
*/
|
|
72
|
+
declare class EnvelopeKeys extends EnvelopeKeys_base<EnvelopeKeysService> {}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { KeysUnavailable as a, KeyGeneration as i, EnvelopeKeysService as n, KeyErased as r, EnvelopeKeys as t };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AsyncResult } from "unthrown";
|
|
2
|
+
//#region src/keys.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* One generation of an owner's root key, as it is kept: wrapped by the
|
|
5
|
+
* owner's key-management key, under an encryption context that binds `owner`
|
|
6
|
+
* and `keyId`.
|
|
7
|
+
*
|
|
8
|
+
* `wrapped` is not secret — only the key-management service can unwrap it, and
|
|
9
|
+
* only for this owner and this key id — so it may live wherever the
|
|
10
|
+
* application keeps configuration: a mounted Secret, a file, an object store,
|
|
11
|
+
* a database row.
|
|
12
|
+
*/
|
|
13
|
+
type KeyGeneration = {
|
|
14
|
+
readonly owner: string;
|
|
15
|
+
readonly keyId: string;
|
|
16
|
+
readonly wrapped: Uint8Array;
|
|
17
|
+
};
|
|
18
|
+
declare const KeyErased_base: import("unthrown").TaggedErrorConstructor<"KeyErased">;
|
|
19
|
+
/**
|
|
20
|
+
* The generation can no longer be unwrapped, and never will be again: the
|
|
21
|
+
* owner's key is disabled (`disabled`), scheduled for deletion
|
|
22
|
+
* (`pending_deletion`), gone (`not_found`), or refused this ciphertext under
|
|
23
|
+
* this owner and key id (`refused`). An answer, not an outage: whatever the
|
|
24
|
+
* generation protected is erased.
|
|
25
|
+
*/
|
|
26
|
+
declare class KeyErased extends KeyErased_base<{
|
|
27
|
+
readonly owner: string;
|
|
28
|
+
readonly keyId: string;
|
|
29
|
+
readonly reason: "disabled" | "pending_deletion" | "not_found" | "refused";
|
|
30
|
+
}> {
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
declare const KeysUnavailable_base: import("unthrown").TaggedErrorConstructor<"KeysUnavailable">;
|
|
34
|
+
/**
|
|
35
|
+
* The key-management service could not be asked: throttling, the network, a
|
|
36
|
+
* credential or permission it refused, a server error. Transient or a
|
|
37
|
+
* deployment mistake, never an erasure, so a later call asks again: a failure
|
|
38
|
+
* is never cached.
|
|
39
|
+
*/
|
|
40
|
+
declare class KeysUnavailable extends KeysUnavailable_base<{
|
|
41
|
+
readonly operation: "unwrap" | "provision" | "erase";
|
|
42
|
+
readonly owner: string;
|
|
43
|
+
readonly cause: unknown;
|
|
44
|
+
}> {
|
|
45
|
+
message: string;
|
|
46
|
+
}
|
|
47
|
+
type EnvelopeKeysService = {
|
|
48
|
+
/**
|
|
49
|
+
* The generation's root key, unwrapped in memory: a non-extractable HKDF
|
|
50
|
+
* key, to derive a key per purpose from with `deriveKey`. Kept for the
|
|
51
|
+
* cache's TTL; callers asking while it is being unwrapped share one request.
|
|
52
|
+
*/
|
|
53
|
+
readonly rootKey: (generation: KeyGeneration) => AsyncResult<CryptoKey, KeyErased | KeysUnavailable>;
|
|
54
|
+
/**
|
|
55
|
+
* A new generation under the owner's key, for the application to keep.
|
|
56
|
+
* Rotation is provisioning another key id; the old generations stay
|
|
57
|
+
* readable until the owner is erased.
|
|
58
|
+
*/
|
|
59
|
+
readonly provision: (owner: string, keyId: string) => AsyncResult<KeyGeneration, KeyErased | KeysUnavailable>;
|
|
60
|
+
/**
|
|
61
|
+
* Every generation of the owner, erased at once: the owner's key is
|
|
62
|
+
* scheduled for deletion, so from then on each generation answers
|
|
63
|
+
* `KeyErased`. Erasing an owner already erased answers `Ok`.
|
|
64
|
+
*/
|
|
65
|
+
readonly erase: (owner: string) => AsyncResult<void, KeysUnavailable>;
|
|
66
|
+
};
|
|
67
|
+
declare const EnvelopeKeys_base: import("@btravstack/di").PortClass<"EnvelopeKeys">;
|
|
68
|
+
/**
|
|
69
|
+
* The port an application depends on for per-owner envelope keys.
|
|
70
|
+
* `kmsKeys()` from `@btravstack/keys/kms` provides it over AWS KMS.
|
|
71
|
+
*/
|
|
72
|
+
declare class EnvelopeKeys extends EnvelopeKeys_base<EnvelopeKeysService> {}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { KeysUnavailable as a, KeyGeneration as i, EnvelopeKeysService as n, KeyErased as r, EnvelopeKeys as t };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
let unthrown = require("unthrown");
|
|
2
|
+
let _btravstack_di = require("@btravstack/di");
|
|
3
|
+
//#region src/keys.ts
|
|
4
|
+
/**
|
|
5
|
+
* The generation can no longer be unwrapped, and never will be again: the
|
|
6
|
+
* owner's key is disabled (`disabled`), scheduled for deletion
|
|
7
|
+
* (`pending_deletion`), gone (`not_found`), or refused this ciphertext under
|
|
8
|
+
* this owner and key id (`refused`). An answer, not an outage: whatever the
|
|
9
|
+
* generation protected is erased.
|
|
10
|
+
*/
|
|
11
|
+
var KeyErased = class extends (0, unthrown.TaggedError)("KeyErased") {
|
|
12
|
+
message = "the key generation is erased";
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The key-management service could not be asked: throttling, the network, a
|
|
16
|
+
* credential or permission it refused, a server error. Transient or a
|
|
17
|
+
* deployment mistake, never an erasure, so a later call asks again: a failure
|
|
18
|
+
* is never cached.
|
|
19
|
+
*/
|
|
20
|
+
var KeysUnavailable = class extends (0, unthrown.TaggedError)("KeysUnavailable") {
|
|
21
|
+
message = "the key-management service could not be asked";
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The port an application depends on for per-owner envelope keys.
|
|
25
|
+
* `kmsKeys()` from `@btravstack/keys/kms` provides it over AWS KMS.
|
|
26
|
+
*/
|
|
27
|
+
var EnvelopeKeys = class extends (0, _btravstack_di.Port)("EnvelopeKeys") {};
|
|
28
|
+
//#endregion
|
|
29
|
+
Object.defineProperty(exports, "EnvelopeKeys", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
get: function() {
|
|
32
|
+
return EnvelopeKeys;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(exports, "KeyErased", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
get: function() {
|
|
38
|
+
return KeyErased;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(exports, "KeysUnavailable", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
get: function() {
|
|
44
|
+
return KeysUnavailable;
|
|
45
|
+
}
|
|
46
|
+
});
|
package/dist/kms.cjs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_keys = require("./keys-DtMnWJHq.cjs");
|
|
3
|
+
let unthrown = require("unthrown");
|
|
4
|
+
let _btravstack_di = require("@btravstack/di");
|
|
5
|
+
let _aws_sdk_client_kms = require("@aws-sdk/client-kms");
|
|
6
|
+
let _btravstack_config = require("@btravstack/config");
|
|
7
|
+
let _btravstack_core = require("@btravstack/core");
|
|
8
|
+
//#region src/kms.ts
|
|
9
|
+
/** What the graph bound from the environment for the KMS adapter. */
|
|
10
|
+
var KeysConfig = class extends (0, _btravstack_di.Port)("KeysConfig") {};
|
|
11
|
+
/** The client, as a port of its own, so the scope closing destroys it. */
|
|
12
|
+
var KmsConnection = class extends (0, _btravstack_di.Port)("KmsConnection") {};
|
|
13
|
+
const optionalUrl = (variable) => {
|
|
14
|
+
const url = _btravstack_config.Config.url(variable);
|
|
15
|
+
return {
|
|
16
|
+
variable,
|
|
17
|
+
parse: (raw) => raw === void 0 ? (0, unthrown.Ok)(void 0) : url.parse(raw),
|
|
18
|
+
check: (value) => url.parse(String(value))
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* `EnvelopeKeys` over AWS KMS, with one KMS key per owner, named by the alias
|
|
23
|
+
* `<KEYS_KMS_ALIAS_PREFIX><owner>`.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* Module("Documents")({ imports: [kmsKeys()], provides: [sealer] });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* - **Configured from `KEYS_KMS_REGION` and `KEYS_KMS_ALIAS_PREFIX`** (an
|
|
30
|
+
* alias prefix, `alias/tenant-` say), and the pinnable variables of
|
|
31
|
+
* {@link KmsKeysOptions}. Credentials are the AWS SDK's default chain: a
|
|
32
|
+
* workload identity, an instance profile, or `AWS_*` variables.
|
|
33
|
+
* - **A generation is a KMS data key**: `provision` asks
|
|
34
|
+
* `GenerateDataKeyWithoutPlaintext` for an AES-256 key under the owner's
|
|
35
|
+
* KMS key, and `rootKey` unwraps it with `Decrypt`, both with the encryption
|
|
36
|
+
* context `{ owner, keyId }` and the owner's alias as the key, so a blob
|
|
37
|
+
* handed in for another owner or key id is refused rather than unwrapped.
|
|
38
|
+
* - **Erasure is one key deletion**: `erase` resolves the owner's alias and
|
|
39
|
+
* schedules its key's deletion, after which KMS refuses every generation of
|
|
40
|
+
* the owner. This process forgets the keys it kept at once; another
|
|
41
|
+
* process keeps serving its own until they expire, which is what
|
|
42
|
+
* `KEYS_CACHE_TTL_MS` bounds.
|
|
43
|
+
* - **Refusals are answers**: a disabled, deleted, missing or refusing key is
|
|
44
|
+
* `KeyErased`; anything else KMS says is `KeysUnavailable`, never cached.
|
|
45
|
+
* - **Every KMS request is reported to `Observers`** (`component: "keys"`); a
|
|
46
|
+
* key served from memory reports nothing.
|
|
47
|
+
*/
|
|
48
|
+
const kmsKeys = (options = {}) => (0, _btravstack_di.Module)("KmsKeys")({
|
|
49
|
+
needs: [_btravstack_config.Env],
|
|
50
|
+
provides: [
|
|
51
|
+
_btravstack_core.noObserverMember,
|
|
52
|
+
_btravstack_config.Config.provider(KeysConfig)(_btravstack_config.Config.object({
|
|
53
|
+
region: _btravstack_config.Config.string("KEYS_KMS_REGION"),
|
|
54
|
+
aliasPrefix: _btravstack_config.Config.string("KEYS_KMS_ALIAS_PREFIX"),
|
|
55
|
+
endpoint: _btravstack_config.Config.pinned(options.endpoint, optionalUrl("KEYS_KMS_ENDPOINT")),
|
|
56
|
+
cacheTtlMs: _btravstack_config.Config.pinned(options.cacheTtlMs, _btravstack_config.Config.integer("KEYS_CACHE_TTL_MS", {
|
|
57
|
+
default: 3e5,
|
|
58
|
+
min: 0
|
|
59
|
+
})),
|
|
60
|
+
pendingWindowDays: _btravstack_config.Config.pinned(options.pendingWindowDays, _btravstack_config.Config.integer("KEYS_KMS_PENDING_WINDOW_DAYS", {
|
|
61
|
+
default: 30,
|
|
62
|
+
min: 7,
|
|
63
|
+
max: 30
|
|
64
|
+
}))
|
|
65
|
+
})),
|
|
66
|
+
(0, _btravstack_di.Provider)(KmsConnection)({
|
|
67
|
+
inject: { config: KeysConfig },
|
|
68
|
+
acquire: ({ config }) => (0, unthrown.Ok)(new _aws_sdk_client_kms.KMSClient({
|
|
69
|
+
region: config.region,
|
|
70
|
+
...config.endpoint === void 0 ? {} : { endpoint: config.endpoint }
|
|
71
|
+
})),
|
|
72
|
+
release: (client) => client.destroy()
|
|
73
|
+
}),
|
|
74
|
+
(0, _btravstack_di.Provider)(require_keys.EnvelopeKeys)({
|
|
75
|
+
inject: {
|
|
76
|
+
config: KeysConfig,
|
|
77
|
+
client: KmsConnection,
|
|
78
|
+
observers: _btravstack_core.Observers
|
|
79
|
+
},
|
|
80
|
+
sync: ({ config, client, observers }) => kmsEnvelopeKeys(client, config, observers, options.clock ?? _btravstack_core.systemClock)
|
|
81
|
+
})
|
|
82
|
+
],
|
|
83
|
+
exports: [require_keys.EnvelopeKeys]
|
|
84
|
+
});
|
|
85
|
+
/** The KMS errors that mean the generation will never be unwrapped again, and the reason each one gives. */
|
|
86
|
+
const ERASED = /* @__PURE__ */ new Map([
|
|
87
|
+
["DisabledException", "disabled"],
|
|
88
|
+
["KMSInvalidStateException", "pending_deletion"],
|
|
89
|
+
["NotFoundException", "not_found"],
|
|
90
|
+
["InvalidCiphertextException", "refused"],
|
|
91
|
+
["IncorrectKeyException", "refused"]
|
|
92
|
+
]);
|
|
93
|
+
const GONE = /* @__PURE__ */ new Set(["NotFoundException", "KMSInvalidStateException"]);
|
|
94
|
+
const nameOf = (cause) => Object(cause).name;
|
|
95
|
+
const kmsEnvelopeKeys = (client, config, observers, clock) => {
|
|
96
|
+
const cached = /* @__PURE__ */ new Map();
|
|
97
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
98
|
+
const aliasOf = (owner) => `${config.aliasPrefix}${owner}`;
|
|
99
|
+
const through = (name, owner, keyId, call) => (0, _btravstack_core.observed)(observers, {
|
|
100
|
+
component: "keys",
|
|
101
|
+
name,
|
|
102
|
+
attributes: { operation: name },
|
|
103
|
+
details: {
|
|
104
|
+
"btravstack.keys.owner": owner,
|
|
105
|
+
...keyId === void 0 ? {} : { "btravstack.keys.key_id": keyId }
|
|
106
|
+
}
|
|
107
|
+
}, call, {
|
|
108
|
+
ok: () => ({
|
|
109
|
+
outcome: "ok",
|
|
110
|
+
attributes: { result: "ok" }
|
|
111
|
+
}),
|
|
112
|
+
failure: settled
|
|
113
|
+
});
|
|
114
|
+
const request = (operation, generation, send) => (0, unthrown.fromPromise)(send(), (cause) => {
|
|
115
|
+
const reason = ERASED.get(nameOf(cause));
|
|
116
|
+
return reason === void 0 ? new require_keys.KeysUnavailable({
|
|
117
|
+
operation,
|
|
118
|
+
owner: generation.owner,
|
|
119
|
+
cause
|
|
120
|
+
}) : new require_keys.KeyErased({
|
|
121
|
+
owner: generation.owner,
|
|
122
|
+
keyId: generation.keyId,
|
|
123
|
+
reason
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
const unwrap = (entry, generation) => {
|
|
127
|
+
const { owner, keyId } = generation;
|
|
128
|
+
const unwrapping = through("unwrap", owner, keyId, () => request("unwrap", generation, () => client.send(new _aws_sdk_client_kms.DecryptCommand({
|
|
129
|
+
CiphertextBlob: generation.wrapped,
|
|
130
|
+
KeyId: aliasOf(owner),
|
|
131
|
+
EncryptionContext: {
|
|
132
|
+
owner,
|
|
133
|
+
keyId
|
|
134
|
+
}
|
|
135
|
+
}))).flatMap(({ Plaintext }) => {
|
|
136
|
+
const plaintext = Plaintext;
|
|
137
|
+
return (0, unthrown.fromSafePromise)(crypto.subtle.importKey("raw", plaintext, "HKDF", false, ["deriveKey", "deriveBits"]).finally(() => plaintext.fill(0)));
|
|
138
|
+
})).tap((root) => {
|
|
139
|
+
inFlight.delete(entry);
|
|
140
|
+
const now = clock.now();
|
|
141
|
+
for (const [key, kept] of cached) if (kept.expiresAt <= now) cached.delete(key);
|
|
142
|
+
cached.set(entry, {
|
|
143
|
+
owner,
|
|
144
|
+
root,
|
|
145
|
+
expiresAt: now + config.cacheTtlMs
|
|
146
|
+
});
|
|
147
|
+
}).tapFailure(() => inFlight.delete(entry));
|
|
148
|
+
inFlight.set(entry, unwrapping);
|
|
149
|
+
return unwrapping;
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
rootKey: (generation) => {
|
|
153
|
+
const entry = JSON.stringify([
|
|
154
|
+
generation.owner,
|
|
155
|
+
generation.keyId,
|
|
156
|
+
Buffer.from(generation.wrapped).toString("base64")
|
|
157
|
+
]);
|
|
158
|
+
const hit = cached.get(entry);
|
|
159
|
+
if (hit !== void 0 && clock.now() < hit.expiresAt) return (0, unthrown.OkAsync)(hit.root);
|
|
160
|
+
cached.delete(entry);
|
|
161
|
+
return inFlight.get(entry) ?? unwrap(entry, generation);
|
|
162
|
+
},
|
|
163
|
+
provision: (owner, keyId) => through("provision", owner, keyId, () => request("provision", {
|
|
164
|
+
owner,
|
|
165
|
+
keyId
|
|
166
|
+
}, () => client.send(new _aws_sdk_client_kms.GenerateDataKeyWithoutPlaintextCommand({
|
|
167
|
+
KeyId: aliasOf(owner),
|
|
168
|
+
KeySpec: "AES_256",
|
|
169
|
+
EncryptionContext: {
|
|
170
|
+
owner,
|
|
171
|
+
keyId
|
|
172
|
+
}
|
|
173
|
+
}))).map(({ CiphertextBlob }) => ({
|
|
174
|
+
owner,
|
|
175
|
+
keyId,
|
|
176
|
+
wrapped: CiphertextBlob
|
|
177
|
+
}))),
|
|
178
|
+
erase: (owner) => {
|
|
179
|
+
const erasing = (cause) => GONE.has(nameOf(cause)) ? "gone" : new require_keys.KeysUnavailable({
|
|
180
|
+
operation: "erase",
|
|
181
|
+
owner,
|
|
182
|
+
cause
|
|
183
|
+
});
|
|
184
|
+
return through("erase", owner, void 0, () => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_kms.DescribeKeyCommand({ KeyId: aliasOf(owner) })), erasing).flatMap(({ KeyMetadata }) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_kms.ScheduleKeyDeletionCommand({
|
|
185
|
+
KeyId: KeyMetadata.KeyId,
|
|
186
|
+
PendingWindowInDays: config.pendingWindowDays
|
|
187
|
+
})), erasing)).map(() => void 0).flatMapErrCases((matcher) => matcher.with("gone", () => (0, unthrown.Ok)(void 0)).with(unthrown.P.tag("KeysUnavailable"), (unavailable) => (0, unthrown.Err)(unavailable)))).tap(() => {
|
|
188
|
+
for (const [key, kept] of cached) if (kept.owner === owner) cached.delete(key);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* **An erased generation settles `ok` with `result: "erased"`, not `error`.**
|
|
195
|
+
* Asking for a key that was erased is the answer erasure exists to give, so
|
|
196
|
+
* it is counted rather than written as a fault line; `KeysUnavailable` is
|
|
197
|
+
* what pages somebody.
|
|
198
|
+
*/
|
|
199
|
+
const settled = (failure) => failure.tag === "Err" && failure.error._tag === "KeyErased" ? {
|
|
200
|
+
outcome: "ok",
|
|
201
|
+
attributes: {
|
|
202
|
+
result: "erased",
|
|
203
|
+
reason: failure.error.reason
|
|
204
|
+
}
|
|
205
|
+
} : {
|
|
206
|
+
outcome: "error",
|
|
207
|
+
attributes: { result: "error" },
|
|
208
|
+
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
209
|
+
};
|
|
210
|
+
//#endregion
|
|
211
|
+
exports.kmsKeys = kmsKeys;
|
package/dist/kms.d.cts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { t as EnvelopeKeys } from "./keys-DpvmIcWy.cjs";
|
|
2
|
+
import { ConfigInvalid, EnvReading } from "@btravstack/config";
|
|
3
|
+
import { Clock } from "@btravstack/core";
|
|
4
|
+
import { Module, Scope } from "@btravstack/di";
|
|
5
|
+
//#region src/kms.d.ts
|
|
6
|
+
/** What {@link kmsKeys} is handed. Each value option pins the variable named beside it. */
|
|
7
|
+
export type KmsKeysOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* Pins `KEYS_KMS_ENDPOINT` (default: AWS's own endpoint for the region): a
|
|
10
|
+
* KMS-compatible emulator, or a VPC endpoint.
|
|
11
|
+
*/
|
|
12
|
+
readonly endpoint?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Pins `KEYS_CACHE_TTL_MS` (default `300000`, five minutes): how long an
|
|
15
|
+
* unwrapped root key is kept. It bounds two things at once: the KMS
|
|
16
|
+
* requests, one per generation per process per TTL, and how long a process
|
|
17
|
+
* that did not erase an owner keeps serving that owner's keys after it was
|
|
18
|
+
* erased. `0` keeps nothing.
|
|
19
|
+
*/
|
|
20
|
+
readonly cacheTtlMs?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Pins `KEYS_KMS_PENDING_WINDOW_DAYS` (default `30`, AWS's own, between 7
|
|
23
|
+
* and 30): the days a scheduled deletion can still be cancelled. Every
|
|
24
|
+
* generation answers `KeyErased` from the moment it is scheduled.
|
|
25
|
+
*/
|
|
26
|
+
readonly pendingWindowDays?: number;
|
|
27
|
+
/** What the cache's TTL is measured against (default: the kernel's `systemClock`). */
|
|
28
|
+
readonly clock?: Clock;
|
|
29
|
+
};
|
|
30
|
+
/** The variables {@link kmsKeys} reads: two required, the rest defaulted or pinnable. */
|
|
31
|
+
export type KmsKeysEnv = EnvReading<"KEYS_KMS_REGION" | "KEYS_KMS_ALIAS_PREFIX", "KEYS_KMS_ENDPOINT" | "KEYS_CACHE_TTL_MS" | "KEYS_KMS_PENDING_WINDOW_DAYS">;
|
|
32
|
+
/**
|
|
33
|
+
* `EnvelopeKeys` over AWS KMS, with one KMS key per owner, named by the alias
|
|
34
|
+
* `<KEYS_KMS_ALIAS_PREFIX><owner>`.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* Module("Documents")({ imports: [kmsKeys()], provides: [sealer] });
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* - **Configured from `KEYS_KMS_REGION` and `KEYS_KMS_ALIAS_PREFIX`** (an
|
|
41
|
+
* alias prefix, `alias/tenant-` say), and the pinnable variables of
|
|
42
|
+
* {@link KmsKeysOptions}. Credentials are the AWS SDK's default chain: a
|
|
43
|
+
* workload identity, an instance profile, or `AWS_*` variables.
|
|
44
|
+
* - **A generation is a KMS data key**: `provision` asks
|
|
45
|
+
* `GenerateDataKeyWithoutPlaintext` for an AES-256 key under the owner's
|
|
46
|
+
* KMS key, and `rootKey` unwraps it with `Decrypt`, both with the encryption
|
|
47
|
+
* context `{ owner, keyId }` and the owner's alias as the key, so a blob
|
|
48
|
+
* handed in for another owner or key id is refused rather than unwrapped.
|
|
49
|
+
* - **Erasure is one key deletion**: `erase` resolves the owner's alias and
|
|
50
|
+
* schedules its key's deletion, after which KMS refuses every generation of
|
|
51
|
+
* the owner. This process forgets the keys it kept at once; another
|
|
52
|
+
* process keeps serving its own until they expire, which is what
|
|
53
|
+
* `KEYS_CACHE_TTL_MS` bounds.
|
|
54
|
+
* - **Refusals are answers**: a disabled, deleted, missing or refusing key is
|
|
55
|
+
* `KeyErased`; anything else KMS says is `KeysUnavailable`, never cached.
|
|
56
|
+
* - **Every KMS request is reported to `Observers`** (`component: "keys"`); a
|
|
57
|
+
* key served from memory reports nothing.
|
|
58
|
+
*/
|
|
59
|
+
export declare const kmsKeys: (options?: KmsKeysOptions) => Module<EnvelopeKeys, ConfigInvalid, KmsKeysEnv | Scope>;
|
|
60
|
+
//#endregion
|
package/dist/kms.d.mts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { t as EnvelopeKeys } from "./keys-DpvmIcWy.mjs";
|
|
2
|
+
import { Module, Scope } from "@btravstack/di";
|
|
3
|
+
import { ConfigInvalid, EnvReading } from "@btravstack/config";
|
|
4
|
+
import { Clock } from "@btravstack/core";
|
|
5
|
+
//#region src/kms.d.ts
|
|
6
|
+
/** What {@link kmsKeys} is handed. Each value option pins the variable named beside it. */
|
|
7
|
+
export type KmsKeysOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* Pins `KEYS_KMS_ENDPOINT` (default: AWS's own endpoint for the region): a
|
|
10
|
+
* KMS-compatible emulator, or a VPC endpoint.
|
|
11
|
+
*/
|
|
12
|
+
readonly endpoint?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Pins `KEYS_CACHE_TTL_MS` (default `300000`, five minutes): how long an
|
|
15
|
+
* unwrapped root key is kept. It bounds two things at once: the KMS
|
|
16
|
+
* requests, one per generation per process per TTL, and how long a process
|
|
17
|
+
* that did not erase an owner keeps serving that owner's keys after it was
|
|
18
|
+
* erased. `0` keeps nothing.
|
|
19
|
+
*/
|
|
20
|
+
readonly cacheTtlMs?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Pins `KEYS_KMS_PENDING_WINDOW_DAYS` (default `30`, AWS's own, between 7
|
|
23
|
+
* and 30): the days a scheduled deletion can still be cancelled. Every
|
|
24
|
+
* generation answers `KeyErased` from the moment it is scheduled.
|
|
25
|
+
*/
|
|
26
|
+
readonly pendingWindowDays?: number;
|
|
27
|
+
/** What the cache's TTL is measured against (default: the kernel's `systemClock`). */
|
|
28
|
+
readonly clock?: Clock;
|
|
29
|
+
};
|
|
30
|
+
/** The variables {@link kmsKeys} reads: two required, the rest defaulted or pinnable. */
|
|
31
|
+
export type KmsKeysEnv = EnvReading<"KEYS_KMS_REGION" | "KEYS_KMS_ALIAS_PREFIX", "KEYS_KMS_ENDPOINT" | "KEYS_CACHE_TTL_MS" | "KEYS_KMS_PENDING_WINDOW_DAYS">;
|
|
32
|
+
/**
|
|
33
|
+
* `EnvelopeKeys` over AWS KMS, with one KMS key per owner, named by the alias
|
|
34
|
+
* `<KEYS_KMS_ALIAS_PREFIX><owner>`.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* Module("Documents")({ imports: [kmsKeys()], provides: [sealer] });
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* - **Configured from `KEYS_KMS_REGION` and `KEYS_KMS_ALIAS_PREFIX`** (an
|
|
41
|
+
* alias prefix, `alias/tenant-` say), and the pinnable variables of
|
|
42
|
+
* {@link KmsKeysOptions}. Credentials are the AWS SDK's default chain: a
|
|
43
|
+
* workload identity, an instance profile, or `AWS_*` variables.
|
|
44
|
+
* - **A generation is a KMS data key**: `provision` asks
|
|
45
|
+
* `GenerateDataKeyWithoutPlaintext` for an AES-256 key under the owner's
|
|
46
|
+
* KMS key, and `rootKey` unwraps it with `Decrypt`, both with the encryption
|
|
47
|
+
* context `{ owner, keyId }` and the owner's alias as the key, so a blob
|
|
48
|
+
* handed in for another owner or key id is refused rather than unwrapped.
|
|
49
|
+
* - **Erasure is one key deletion**: `erase` resolves the owner's alias and
|
|
50
|
+
* schedules its key's deletion, after which KMS refuses every generation of
|
|
51
|
+
* the owner. This process forgets the keys it kept at once; another
|
|
52
|
+
* process keeps serving its own until they expire, which is what
|
|
53
|
+
* `KEYS_CACHE_TTL_MS` bounds.
|
|
54
|
+
* - **Refusals are answers**: a disabled, deleted, missing or refusing key is
|
|
55
|
+
* `KeyErased`; anything else KMS says is `KeysUnavailable`, never cached.
|
|
56
|
+
* - **Every KMS request is reported to `Observers`** (`component: "keys"`); a
|
|
57
|
+
* key served from memory reports nothing.
|
|
58
|
+
*/
|
|
59
|
+
export declare const kmsKeys: (options?: KmsKeysOptions) => Module<EnvelopeKeys, ConfigInvalid, KmsKeysEnv | Scope>;
|
|
60
|
+
//#endregion
|
package/dist/kms.mjs
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { n as KeyErased, r as KeysUnavailable, t as EnvelopeKeys } from "./keys-CGbVjuu3.mjs";
|
|
2
|
+
import { Err, Ok, OkAsync, P, fromPromise, fromSafePromise } from "unthrown";
|
|
3
|
+
import { Module, Port, Provider } from "@btravstack/di";
|
|
4
|
+
import { DecryptCommand, DescribeKeyCommand, GenerateDataKeyWithoutPlaintextCommand, KMSClient, ScheduleKeyDeletionCommand } from "@aws-sdk/client-kms";
|
|
5
|
+
import { Config, Env } from "@btravstack/config";
|
|
6
|
+
import { Observers, noObserverMember, observed, systemClock } from "@btravstack/core";
|
|
7
|
+
//#region src/kms.ts
|
|
8
|
+
/** What the graph bound from the environment for the KMS adapter. */
|
|
9
|
+
var KeysConfig = class extends Port("KeysConfig") {};
|
|
10
|
+
/** The client, as a port of its own, so the scope closing destroys it. */
|
|
11
|
+
var KmsConnection = class extends Port("KmsConnection") {};
|
|
12
|
+
const optionalUrl = (variable) => {
|
|
13
|
+
const url = Config.url(variable);
|
|
14
|
+
return {
|
|
15
|
+
variable,
|
|
16
|
+
parse: (raw) => raw === void 0 ? Ok(void 0) : url.parse(raw),
|
|
17
|
+
check: (value) => url.parse(String(value))
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* `EnvelopeKeys` over AWS KMS, with one KMS key per owner, named by the alias
|
|
22
|
+
* `<KEYS_KMS_ALIAS_PREFIX><owner>`.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* Module("Documents")({ imports: [kmsKeys()], provides: [sealer] });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* - **Configured from `KEYS_KMS_REGION` and `KEYS_KMS_ALIAS_PREFIX`** (an
|
|
29
|
+
* alias prefix, `alias/tenant-` say), and the pinnable variables of
|
|
30
|
+
* {@link KmsKeysOptions}. Credentials are the AWS SDK's default chain: a
|
|
31
|
+
* workload identity, an instance profile, or `AWS_*` variables.
|
|
32
|
+
* - **A generation is a KMS data key**: `provision` asks
|
|
33
|
+
* `GenerateDataKeyWithoutPlaintext` for an AES-256 key under the owner's
|
|
34
|
+
* KMS key, and `rootKey` unwraps it with `Decrypt`, both with the encryption
|
|
35
|
+
* context `{ owner, keyId }` and the owner's alias as the key, so a blob
|
|
36
|
+
* handed in for another owner or key id is refused rather than unwrapped.
|
|
37
|
+
* - **Erasure is one key deletion**: `erase` resolves the owner's alias and
|
|
38
|
+
* schedules its key's deletion, after which KMS refuses every generation of
|
|
39
|
+
* the owner. This process forgets the keys it kept at once; another
|
|
40
|
+
* process keeps serving its own until they expire, which is what
|
|
41
|
+
* `KEYS_CACHE_TTL_MS` bounds.
|
|
42
|
+
* - **Refusals are answers**: a disabled, deleted, missing or refusing key is
|
|
43
|
+
* `KeyErased`; anything else KMS says is `KeysUnavailable`, never cached.
|
|
44
|
+
* - **Every KMS request is reported to `Observers`** (`component: "keys"`); a
|
|
45
|
+
* key served from memory reports nothing.
|
|
46
|
+
*/
|
|
47
|
+
const kmsKeys = (options = {}) => Module("KmsKeys")({
|
|
48
|
+
needs: [Env],
|
|
49
|
+
provides: [
|
|
50
|
+
noObserverMember,
|
|
51
|
+
Config.provider(KeysConfig)(Config.object({
|
|
52
|
+
region: Config.string("KEYS_KMS_REGION"),
|
|
53
|
+
aliasPrefix: Config.string("KEYS_KMS_ALIAS_PREFIX"),
|
|
54
|
+
endpoint: Config.pinned(options.endpoint, optionalUrl("KEYS_KMS_ENDPOINT")),
|
|
55
|
+
cacheTtlMs: Config.pinned(options.cacheTtlMs, Config.integer("KEYS_CACHE_TTL_MS", {
|
|
56
|
+
default: 3e5,
|
|
57
|
+
min: 0
|
|
58
|
+
})),
|
|
59
|
+
pendingWindowDays: Config.pinned(options.pendingWindowDays, Config.integer("KEYS_KMS_PENDING_WINDOW_DAYS", {
|
|
60
|
+
default: 30,
|
|
61
|
+
min: 7,
|
|
62
|
+
max: 30
|
|
63
|
+
}))
|
|
64
|
+
})),
|
|
65
|
+
Provider(KmsConnection)({
|
|
66
|
+
inject: { config: KeysConfig },
|
|
67
|
+
acquire: ({ config }) => Ok(new KMSClient({
|
|
68
|
+
region: config.region,
|
|
69
|
+
...config.endpoint === void 0 ? {} : { endpoint: config.endpoint }
|
|
70
|
+
})),
|
|
71
|
+
release: (client) => client.destroy()
|
|
72
|
+
}),
|
|
73
|
+
Provider(EnvelopeKeys)({
|
|
74
|
+
inject: {
|
|
75
|
+
config: KeysConfig,
|
|
76
|
+
client: KmsConnection,
|
|
77
|
+
observers: Observers
|
|
78
|
+
},
|
|
79
|
+
sync: ({ config, client, observers }) => kmsEnvelopeKeys(client, config, observers, options.clock ?? systemClock)
|
|
80
|
+
})
|
|
81
|
+
],
|
|
82
|
+
exports: [EnvelopeKeys]
|
|
83
|
+
});
|
|
84
|
+
/** The KMS errors that mean the generation will never be unwrapped again, and the reason each one gives. */
|
|
85
|
+
const ERASED = /* @__PURE__ */ new Map([
|
|
86
|
+
["DisabledException", "disabled"],
|
|
87
|
+
["KMSInvalidStateException", "pending_deletion"],
|
|
88
|
+
["NotFoundException", "not_found"],
|
|
89
|
+
["InvalidCiphertextException", "refused"],
|
|
90
|
+
["IncorrectKeyException", "refused"]
|
|
91
|
+
]);
|
|
92
|
+
const GONE = /* @__PURE__ */ new Set(["NotFoundException", "KMSInvalidStateException"]);
|
|
93
|
+
const nameOf = (cause) => Object(cause).name;
|
|
94
|
+
const kmsEnvelopeKeys = (client, config, observers, clock) => {
|
|
95
|
+
const cached = /* @__PURE__ */ new Map();
|
|
96
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
97
|
+
const aliasOf = (owner) => `${config.aliasPrefix}${owner}`;
|
|
98
|
+
const through = (name, owner, keyId, call) => observed(observers, {
|
|
99
|
+
component: "keys",
|
|
100
|
+
name,
|
|
101
|
+
attributes: { operation: name },
|
|
102
|
+
details: {
|
|
103
|
+
"btravstack.keys.owner": owner,
|
|
104
|
+
...keyId === void 0 ? {} : { "btravstack.keys.key_id": keyId }
|
|
105
|
+
}
|
|
106
|
+
}, call, {
|
|
107
|
+
ok: () => ({
|
|
108
|
+
outcome: "ok",
|
|
109
|
+
attributes: { result: "ok" }
|
|
110
|
+
}),
|
|
111
|
+
failure: settled
|
|
112
|
+
});
|
|
113
|
+
const request = (operation, generation, send) => fromPromise(send(), (cause) => {
|
|
114
|
+
const reason = ERASED.get(nameOf(cause));
|
|
115
|
+
return reason === void 0 ? new KeysUnavailable({
|
|
116
|
+
operation,
|
|
117
|
+
owner: generation.owner,
|
|
118
|
+
cause
|
|
119
|
+
}) : new KeyErased({
|
|
120
|
+
owner: generation.owner,
|
|
121
|
+
keyId: generation.keyId,
|
|
122
|
+
reason
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
const unwrap = (entry, generation) => {
|
|
126
|
+
const { owner, keyId } = generation;
|
|
127
|
+
const unwrapping = through("unwrap", owner, keyId, () => request("unwrap", generation, () => client.send(new DecryptCommand({
|
|
128
|
+
CiphertextBlob: generation.wrapped,
|
|
129
|
+
KeyId: aliasOf(owner),
|
|
130
|
+
EncryptionContext: {
|
|
131
|
+
owner,
|
|
132
|
+
keyId
|
|
133
|
+
}
|
|
134
|
+
}))).flatMap(({ Plaintext }) => {
|
|
135
|
+
const plaintext = Plaintext;
|
|
136
|
+
return fromSafePromise(crypto.subtle.importKey("raw", plaintext, "HKDF", false, ["deriveKey", "deriveBits"]).finally(() => plaintext.fill(0)));
|
|
137
|
+
})).tap((root) => {
|
|
138
|
+
inFlight.delete(entry);
|
|
139
|
+
const now = clock.now();
|
|
140
|
+
for (const [key, kept] of cached) if (kept.expiresAt <= now) cached.delete(key);
|
|
141
|
+
cached.set(entry, {
|
|
142
|
+
owner,
|
|
143
|
+
root,
|
|
144
|
+
expiresAt: now + config.cacheTtlMs
|
|
145
|
+
});
|
|
146
|
+
}).tapFailure(() => inFlight.delete(entry));
|
|
147
|
+
inFlight.set(entry, unwrapping);
|
|
148
|
+
return unwrapping;
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
rootKey: (generation) => {
|
|
152
|
+
const entry = JSON.stringify([
|
|
153
|
+
generation.owner,
|
|
154
|
+
generation.keyId,
|
|
155
|
+
Buffer.from(generation.wrapped).toString("base64")
|
|
156
|
+
]);
|
|
157
|
+
const hit = cached.get(entry);
|
|
158
|
+
if (hit !== void 0 && clock.now() < hit.expiresAt) return OkAsync(hit.root);
|
|
159
|
+
cached.delete(entry);
|
|
160
|
+
return inFlight.get(entry) ?? unwrap(entry, generation);
|
|
161
|
+
},
|
|
162
|
+
provision: (owner, keyId) => through("provision", owner, keyId, () => request("provision", {
|
|
163
|
+
owner,
|
|
164
|
+
keyId
|
|
165
|
+
}, () => client.send(new GenerateDataKeyWithoutPlaintextCommand({
|
|
166
|
+
KeyId: aliasOf(owner),
|
|
167
|
+
KeySpec: "AES_256",
|
|
168
|
+
EncryptionContext: {
|
|
169
|
+
owner,
|
|
170
|
+
keyId
|
|
171
|
+
}
|
|
172
|
+
}))).map(({ CiphertextBlob }) => ({
|
|
173
|
+
owner,
|
|
174
|
+
keyId,
|
|
175
|
+
wrapped: CiphertextBlob
|
|
176
|
+
}))),
|
|
177
|
+
erase: (owner) => {
|
|
178
|
+
const erasing = (cause) => GONE.has(nameOf(cause)) ? "gone" : new KeysUnavailable({
|
|
179
|
+
operation: "erase",
|
|
180
|
+
owner,
|
|
181
|
+
cause
|
|
182
|
+
});
|
|
183
|
+
return through("erase", owner, void 0, () => fromPromise(client.send(new DescribeKeyCommand({ KeyId: aliasOf(owner) })), erasing).flatMap(({ KeyMetadata }) => fromPromise(client.send(new ScheduleKeyDeletionCommand({
|
|
184
|
+
KeyId: KeyMetadata.KeyId,
|
|
185
|
+
PendingWindowInDays: config.pendingWindowDays
|
|
186
|
+
})), erasing)).map(() => void 0).flatMapErrCases((matcher) => matcher.with("gone", () => Ok(void 0)).with(P.tag("KeysUnavailable"), (unavailable) => Err(unavailable)))).tap(() => {
|
|
187
|
+
for (const [key, kept] of cached) if (kept.owner === owner) cached.delete(key);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* **An erased generation settles `ok` with `result: "erased"`, not `error`.**
|
|
194
|
+
* Asking for a key that was erased is the answer erasure exists to give, so
|
|
195
|
+
* it is counted rather than written as a fault line; `KeysUnavailable` is
|
|
196
|
+
* what pages somebody.
|
|
197
|
+
*/
|
|
198
|
+
const settled = (failure) => failure.tag === "Err" && failure.error._tag === "KeyErased" ? {
|
|
199
|
+
outcome: "ok",
|
|
200
|
+
attributes: {
|
|
201
|
+
result: "erased",
|
|
202
|
+
reason: failure.error.reason
|
|
203
|
+
}
|
|
204
|
+
} : {
|
|
205
|
+
outcome: "error",
|
|
206
|
+
attributes: { result: "error" },
|
|
207
|
+
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
208
|
+
};
|
|
209
|
+
//#endregion
|
|
210
|
+
export { kmsKeys };
|
package/package.json
CHANGED
|
@@ -1,6 +1,93 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/keys",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.45.0",
|
|
4
|
+
"description": "An EnvelopeKeys port for @btravstack/core: per-owner root keys wrapped by an AWS KMS key of that owner, unwrapped in memory and cached for a TTL, erased with one key deletion, and an HKDF helper giving each purpose its own key",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"crypto-shredding",
|
|
7
|
+
"dependency-injection",
|
|
8
|
+
"envelope-encryption",
|
|
9
|
+
"hkdf",
|
|
10
|
+
"kms",
|
|
11
|
+
"typescript",
|
|
12
|
+
"unthrown"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/btravstack/btravstack#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/btravstack/btravstack/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Benoit TRAVERS <benoit.travers.fr@gmail.com>",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/btravstack/btravstack.git",
|
|
23
|
+
"directory": "packages/keys"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.mjs",
|
|
32
|
+
"types": "./dist/index.d.cts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"import": {
|
|
36
|
+
"types": "./dist/index.d.mts",
|
|
37
|
+
"default": "./dist/index.mjs"
|
|
38
|
+
},
|
|
39
|
+
"require": {
|
|
40
|
+
"types": "./dist/index.d.cts",
|
|
41
|
+
"default": "./dist/index.cjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"./kms": {
|
|
45
|
+
"import": {
|
|
46
|
+
"types": "./dist/kms.d.mts",
|
|
47
|
+
"default": "./dist/kms.mjs"
|
|
48
|
+
},
|
|
49
|
+
"require": {
|
|
50
|
+
"types": "./dist/kms.d.cts",
|
|
51
|
+
"default": "./dist/kms.cjs"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"./package.json": "./package.json"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@aws-sdk/client-kms": "3.1152.0",
|
|
58
|
+
"@btravstack/config": "0.45.0",
|
|
59
|
+
"@btravstack/core": "0.45.0",
|
|
60
|
+
"@btravstack/di": "0.45.0",
|
|
61
|
+
"@btravstack/internal-test-infra": "0.0.0",
|
|
62
|
+
"@btravstack/testing": "0.45.0",
|
|
63
|
+
"@btravstack/tsconfig": "0.4.0",
|
|
64
|
+
"@types/node": "26.6.5",
|
|
65
|
+
"@unthrown/vitest": "5.12.1",
|
|
66
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
67
|
+
"tsdown": "0.23.1",
|
|
68
|
+
"typescript": "7.0.2",
|
|
69
|
+
"unthrown": "5.12.1",
|
|
70
|
+
"vitest": "5.0.3"
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"@aws-sdk/client-kms": "^3.1000.0",
|
|
74
|
+
"@btravstack/config": "^0.45.0",
|
|
75
|
+
"@btravstack/core": "^0.45.0",
|
|
76
|
+
"@btravstack/di": "^0.45.0",
|
|
77
|
+
"unthrown": "^5.0.0"
|
|
78
|
+
},
|
|
79
|
+
"peerDependenciesMeta": {
|
|
80
|
+
"@aws-sdk/client-kms": {
|
|
81
|
+
"optional": true
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"engines": {
|
|
85
|
+
"node": ">=22"
|
|
86
|
+
},
|
|
87
|
+
"scripts": {
|
|
88
|
+
"build": "tsdown src/index.ts src/kms.ts --format cjs,esm --dts --clean",
|
|
89
|
+
"dev": "tsdown src/index.ts src/kms.ts --format cjs,esm --dts --watch",
|
|
90
|
+
"test": "vitest run --coverage",
|
|
91
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json"
|
|
92
|
+
}
|
|
6
93
|
}
|