haechi 0.7.0 → 0.9.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/README.ko.md +13 -2
- package/README.md +13 -2
- package/docs/current/api-stability.ko.md +14 -1
- package/docs/current/api-stability.md +14 -1
- package/docs/current/configuration.ko.md +106 -2
- package/docs/current/configuration.md +106 -2
- package/docs/current/release-0.6-implementation-scope.ko.md +4 -4
- package/docs/current/release-0.6-implementation-scope.md +4 -4
- package/docs/current/release-0.7-implementation-scope.ko.md +4 -4
- package/docs/current/release-0.7-implementation-scope.md +4 -4
- package/docs/current/release-0.8-implementation-scope.ko.md +145 -0
- package/docs/current/release-0.8-implementation-scope.md +145 -0
- package/docs/current/release-0.9-implementation-scope.ko.md +231 -0
- package/docs/current/release-0.9-implementation-scope.md +231 -0
- package/docs/current/release-process.ko.md +42 -5
- package/docs/current/release-process.md +42 -5
- package/docs/current/risk-register-release-gate.ko.md +18 -5
- package/docs/current/risk-register-release-gate.md +16 -4
- package/docs/current/threat-model.ko.md +16 -1
- package/docs/current/threat-model.md +16 -1
- package/examples/crypto-kms-reference/README.md +6 -40
- package/haechi.config.example.json +2 -1
- package/package.json +7 -1
- package/packages/audit/index.mjs +12 -1
- package/packages/auth/index.mjs +45 -0
- package/packages/cli/runtime.mjs +5 -1
- package/packages/core/index.mjs +4 -0
- package/packages/filter/index.mjs +58 -3
- package/packages/proxy/index.mjs +3 -0
- package/examples/crypto-kms-reference/index.mjs +0 -133
- package/examples/crypto-kms-reference/package.json +0 -19
package/packages/proxy/index.mjs
CHANGED
|
@@ -438,6 +438,9 @@ async function maybeProtectResponse({ upstreamResponse, routeContext, runtime, a
|
|
|
438
438
|
...authContext,
|
|
439
439
|
operation: `response:${routeContext.operation}`,
|
|
440
440
|
direction: "response",
|
|
441
|
+
// Opt-in: scan bare number leaves on the response (off by default — they are
|
|
442
|
+
// inference-server metadata; see the filter engine's number-leaf skip).
|
|
443
|
+
scanNumbers: runtime.config.responseProtection.scanNumbers,
|
|
441
444
|
mode: runtime.config.responseProtection.mode ?? runtime.config.policy.mode ?? runtime.config.mode
|
|
442
445
|
});
|
|
443
446
|
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
// Reference KMS-backed cryptoProvider for Haechi (keys.provider: external).
|
|
2
|
-
//
|
|
3
|
-
// This is the *shape* a published @haechi/crypto-kms satellite (0.8) takes. It
|
|
4
|
-
// uses envelope encryption: a fresh data key per record encrypts the plaintext
|
|
5
|
-
// locally with AES-256-GCM, and the data key is wrapped by the KMS. The master
|
|
6
|
-
// key never leaves the KMS. The `kms` client is injected, so this file has zero
|
|
7
|
-
// real dependencies — a real adapter swaps createInMemoryKms() for an AWS KMS /
|
|
8
|
-
// HashiCorp Vault client implementing the same small interface.
|
|
9
|
-
//
|
|
10
|
-
// Inject it: createRuntime(config, { cryptoProvider: createKmsCryptoProvider({ kms }) })
|
|
11
|
-
// and set keys.provider: "external".
|
|
12
|
-
|
|
13
|
-
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from "node:crypto";
|
|
14
|
-
|
|
15
|
-
// The published @haechi/crypto-kms satellite imports this from `haechi/crypto`;
|
|
16
|
-
// it is inlined here so the reference example is fully self-contained (no
|
|
17
|
-
// cross-package import) and matches Haechi's canonical AAD exactly.
|
|
18
|
-
function canonicalize(value) {
|
|
19
|
-
if (Array.isArray(value)) {
|
|
20
|
-
return `[${value.map((item) => canonicalize(item)).join(",")}]`;
|
|
21
|
-
}
|
|
22
|
-
if (value && typeof value === "object") {
|
|
23
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`).join(",")}}`;
|
|
24
|
-
}
|
|
25
|
-
return JSON.stringify(value);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const ALG = "AES-256-GCM";
|
|
29
|
-
const HMAC_KEY_DOMAIN = "haechi:crypto-kms:hmac-root:v1";
|
|
30
|
-
|
|
31
|
-
// The injected KMS client must implement:
|
|
32
|
-
// keyId: string
|
|
33
|
-
// async wrap(dataKey: Buffer) -> string (KMS-encrypt a data key)
|
|
34
|
-
// async unwrap(wrapped: string) -> Buffer (KMS-decrypt it back)
|
|
35
|
-
// async deriveHmacKey(domain: string) -> Buffer (KMS-derived per-domain key)
|
|
36
|
-
export function createKmsCryptoProvider({ kms }) {
|
|
37
|
-
if (!kms || typeof kms.wrap !== "function" || typeof kms.unwrap !== "function" || typeof kms.deriveHmacKey !== "function") {
|
|
38
|
-
throw new Error("createKmsCryptoProvider requires a kms client with wrap/unwrap/deriveHmacKey");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function sha256(value) {
|
|
42
|
-
// Plain SHA-256, matching Haechi's core aadHash (defence-in-depth; GCM
|
|
43
|
-
// already authenticates the AAD via the tag).
|
|
44
|
-
return createHash("sha256").update(value).digest("base64url");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
id: "haechi.crypto.kms-reference",
|
|
49
|
-
version: "0.1.0",
|
|
50
|
-
capabilities: {
|
|
51
|
-
readsPlaintext: true,
|
|
52
|
-
networkEgress: true, // a real KMS adapter calls out to the KMS
|
|
53
|
-
keyCustody: "external-kms"
|
|
54
|
-
},
|
|
55
|
-
async encrypt({ plaintext, aad }) {
|
|
56
|
-
const dataKey = randomBytes(32);
|
|
57
|
-
const iv = randomBytes(12);
|
|
58
|
-
const cipher = createCipheriv("aes-256-gcm", dataKey, iv);
|
|
59
|
-
const aadBytes = Buffer.from(canonicalize(aad), "utf8");
|
|
60
|
-
cipher.setAAD(aadBytes);
|
|
61
|
-
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
62
|
-
const tag = cipher.getAuthTag();
|
|
63
|
-
return {
|
|
64
|
-
v: 1,
|
|
65
|
-
alg: ALG,
|
|
66
|
-
kid: kms.keyId,
|
|
67
|
-
iv: iv.toString("base64url"),
|
|
68
|
-
ct: ciphertext.toString("base64url"),
|
|
69
|
-
tag: tag.toString("base64url"),
|
|
70
|
-
wrappedKey: await kms.wrap(dataKey),
|
|
71
|
-
aadHash: sha256(aadBytes)
|
|
72
|
-
};
|
|
73
|
-
},
|
|
74
|
-
async decrypt({ envelope, aad }) {
|
|
75
|
-
if (envelope.alg && envelope.alg !== ALG) {
|
|
76
|
-
throw new Error(`Unsupported algorithm: ${envelope.alg}`);
|
|
77
|
-
}
|
|
78
|
-
const aadBytes = Buffer.from(canonicalize(aad), "utf8");
|
|
79
|
-
if (envelope.aadHash && envelope.aadHash !== sha256(aadBytes)) {
|
|
80
|
-
throw new Error("AAD hash mismatch");
|
|
81
|
-
}
|
|
82
|
-
const dataKey = await kms.unwrap(envelope.wrappedKey);
|
|
83
|
-
const decipher = createDecipheriv("aes-256-gcm", dataKey, Buffer.from(envelope.iv, "base64url"));
|
|
84
|
-
decipher.setAAD(aadBytes);
|
|
85
|
-
decipher.setAuthTag(Buffer.from(envelope.tag, "base64url"));
|
|
86
|
-
return Buffer.concat([
|
|
87
|
-
decipher.update(Buffer.from(envelope.ct, "base64url")),
|
|
88
|
-
decipher.final()
|
|
89
|
-
]).toString("utf8");
|
|
90
|
-
},
|
|
91
|
-
async hmac({ data, domain }) {
|
|
92
|
-
if (!domain || typeof domain !== "string") {
|
|
93
|
-
throw new Error("hmac requires a non-empty domain string");
|
|
94
|
-
}
|
|
95
|
-
// Domain-separated: derive a per-domain key from the KMS, then HMAC.
|
|
96
|
-
const derived = await kms.deriveHmacKey(domain);
|
|
97
|
-
return createHmac("sha256", derived).update(data).digest("hex");
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// In-memory stand-in for AWS KMS / Vault — for examples and tests only. A real
|
|
103
|
-
// deployment injects a client backed by the cloud KMS.
|
|
104
|
-
//
|
|
105
|
-
// WARNING: the default masterKey is a fresh random key PER PROCESS. Anything
|
|
106
|
-
// encrypted in one run cannot be decrypted in the next — exactly the silent
|
|
107
|
-
// data-loss footgun that key rotation must avoid. For any persistence across
|
|
108
|
-
// restarts, supply a stable `masterKey` (or, in production, use a real KMS that
|
|
109
|
-
// holds the master key). This fake is NOT a production key provider.
|
|
110
|
-
export function createInMemoryKms({ keyId = "kms-ref-local", masterKey = randomBytes(32) } = {}) {
|
|
111
|
-
return {
|
|
112
|
-
keyId,
|
|
113
|
-
async wrap(dataKey) {
|
|
114
|
-
const iv = randomBytes(12);
|
|
115
|
-
const cipher = createCipheriv("aes-256-gcm", masterKey, iv);
|
|
116
|
-
const ct = Buffer.concat([cipher.update(dataKey), cipher.final()]);
|
|
117
|
-
const tag = cipher.getAuthTag();
|
|
118
|
-
return Buffer.concat([iv, tag, ct]).toString("base64url");
|
|
119
|
-
},
|
|
120
|
-
async unwrap(wrapped) {
|
|
121
|
-
const buffer = Buffer.from(wrapped, "base64url");
|
|
122
|
-
const iv = buffer.subarray(0, 12);
|
|
123
|
-
const tag = buffer.subarray(12, 28);
|
|
124
|
-
const ct = buffer.subarray(28);
|
|
125
|
-
const decipher = createDecipheriv("aes-256-gcm", masterKey, iv);
|
|
126
|
-
decipher.setAuthTag(tag);
|
|
127
|
-
return Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
128
|
-
},
|
|
129
|
-
async deriveHmacKey(domain) {
|
|
130
|
-
return createHmac("sha256", masterKey).update(`${HMAC_KEY_DOMAIN}:${domain}`).digest();
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@haechi/crypto-kms",
|
|
3
|
-
"version": "0.0.0-reference",
|
|
4
|
-
"private": true,
|
|
5
|
-
"description": "Reference KMS-backed cryptoProvider for Haechi (keys.provider: external). Promoted to a published @haechi/* satellite in 0.8.",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./index.mjs"
|
|
9
|
-
},
|
|
10
|
-
"peerDependencies": {
|
|
11
|
-
"haechi": ">=0.7.0"
|
|
12
|
-
},
|
|
13
|
-
"optionalDependencies": {
|
|
14
|
-
"@aws-sdk/client-kms": "^3"
|
|
15
|
-
},
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=22"
|
|
18
|
-
}
|
|
19
|
-
}
|