haechi 0.9.0 → 1.0.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 +15 -10
- package/README.md +15 -10
- package/docs/current/api-stability.ko.md +87 -41
- package/docs/current/api-stability.md +87 -41
- package/docs/current/release-1.0-implementation-scope.ko.md +170 -0
- package/docs/current/release-1.0-implementation-scope.md +164 -0
- package/docs/current/risk-register-release-gate.ko.md +18 -6
- package/docs/current/risk-register-release-gate.md +18 -6
- package/docs/current/threat-model.ko.md +14 -1
- package/docs/current/threat-model.md +14 -1
- package/package.json +4 -3
- package/packages/audit/index.mjs +13 -1
- package/packages/auth/index.mjs +173 -0
- package/packages/cli/runtime.mjs +184 -5
- package/packages/core/index.mjs +19 -4
- package/packages/plugin/index.mjs +83 -17
- package/packages/plugin/sandbox.mjs +608 -0
- package/packages/plugin/signing.mjs +393 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
+
export {
|
|
4
|
+
signPluginManifest,
|
|
5
|
+
verifySignedPlugin,
|
|
6
|
+
PluginLoadError,
|
|
7
|
+
PLUGIN_LOAD_REASONS
|
|
8
|
+
} from "./signing.mjs";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
createSandboxedAuthProvider,
|
|
12
|
+
createSandboxedAuthProviderSync
|
|
13
|
+
} from "./sandbox.mjs";
|
|
14
|
+
|
|
3
15
|
const VALID_KINDS = new Set([
|
|
4
16
|
"crypto-provider",
|
|
5
17
|
"key-provider",
|
|
@@ -8,7 +20,10 @@ const VALID_KINDS = new Set([
|
|
|
8
20
|
"token-vault",
|
|
9
21
|
"audit-sink",
|
|
10
22
|
"protocol-adapter",
|
|
11
|
-
"classifier-plugin"
|
|
23
|
+
"classifier-plugin",
|
|
24
|
+
// 1.0: the first dynamically-loadable kind, only under the worker-isolated
|
|
25
|
+
// signed/capability-gated/audited sandbox.
|
|
26
|
+
"authProvider"
|
|
12
27
|
]);
|
|
13
28
|
|
|
14
29
|
const CAPABILITY_KEYS = [
|
|
@@ -19,7 +34,10 @@ const CAPABILITY_KEYS = [
|
|
|
19
34
|
"auditWrite",
|
|
20
35
|
"externalSecrets"
|
|
21
36
|
];
|
|
22
|
-
|
|
37
|
+
// manifest-only is the historical, behavior-preserving path. worker-isolated is
|
|
38
|
+
// the 1.0 dynamic-loading runtime — permitted ONLY for kind authProvider and
|
|
39
|
+
// only with the Ed25519 signed envelope (see validateWorkerIsolatedManifest).
|
|
40
|
+
const VALID_RUNTIMES = new Set(["manifest-only", "worker-isolated"]);
|
|
23
41
|
|
|
24
42
|
export async function validatePluginManifestFile(path) {
|
|
25
43
|
const manifest = JSON.parse(await readFile(path, "utf8"));
|
|
@@ -47,26 +65,35 @@ export function validatePluginManifest(manifest) {
|
|
|
47
65
|
errors.push("dynamic plugin execution is not supported; set runtime to manifest-only");
|
|
48
66
|
}
|
|
49
67
|
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
errors.push("missing capabilities");
|
|
68
|
+
if (plugin.runtime === "worker-isolated") {
|
|
69
|
+
// The 1.0 dynamic-loading path: a separate, stricter contract (signed
|
|
70
|
+
// Ed25519 envelope + a validity window + authProvider-only). Kept apart
|
|
71
|
+
// from the manifest-only checks so the historical path is untouched.
|
|
72
|
+
validateWorkerIsolatedManifest(plugin, errors);
|
|
56
73
|
} else {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
// manifest-only (and any other declared-but-rejected runtime): the
|
|
75
|
+
// historical, behavior-preserving contract — UNCHANGED.
|
|
76
|
+
if (!plugin.compatibility?.haechiCore) {
|
|
77
|
+
errors.push("missing compatibility.haechiCore");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!plugin.capabilities || typeof plugin.capabilities !== "object") {
|
|
81
|
+
errors.push("missing capabilities");
|
|
82
|
+
} else {
|
|
83
|
+
for (const key of CAPABILITY_KEYS) {
|
|
84
|
+
if (typeof plugin.capabilities[key] !== "boolean") {
|
|
85
|
+
errors.push(`capabilities.${key} must be boolean`);
|
|
86
|
+
}
|
|
60
87
|
}
|
|
61
88
|
}
|
|
62
|
-
}
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
if (plugin.capabilities?.networkEgress && plugin.capabilities.readsPlaintext && !plugin.dataHandling?.retention) {
|
|
91
|
+
errors.push("plaintext-reading network plugins must declare dataHandling.retention");
|
|
92
|
+
}
|
|
67
93
|
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
if (plugin.dataHandling?.logsRawPayload === true) {
|
|
95
|
+
errors.push("dataHandling.logsRawPayload must not be true");
|
|
96
|
+
}
|
|
70
97
|
}
|
|
71
98
|
}
|
|
72
99
|
|
|
@@ -76,6 +103,45 @@ export function validatePluginManifest(manifest) {
|
|
|
76
103
|
};
|
|
77
104
|
}
|
|
78
105
|
|
|
106
|
+
// The worker-isolated runtime is dynamic code-loading; it is permitted ONLY for
|
|
107
|
+
// kind authProvider and ONLY with the Ed25519 signed envelope fields. A
|
|
108
|
+
// worker-isolated manifest that is not an authProvider, or is missing the signed
|
|
109
|
+
// fields / validity window / readsCredentials, is rejected with a clear error.
|
|
110
|
+
function validateWorkerIsolatedManifest(plugin, errors) {
|
|
111
|
+
if (plugin.kind !== "authProvider") {
|
|
112
|
+
errors.push("worker-isolated runtime is only supported for kind authProvider");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The signed-envelope fields that bind authorship and the exact entry bytes.
|
|
116
|
+
// signature must be a non-empty base64-ish string; entrySha256 must be a
|
|
117
|
+
// 64-char lowercase hex string. Loose shapes signal a malformed/forged manifest.
|
|
118
|
+
if (!plugin.signature || typeof plugin.signature !== "string" || plugin.signature.length === 0) {
|
|
119
|
+
errors.push("missing signature");
|
|
120
|
+
} else if (!/^[A-Za-z0-9+/=]+$/.test(plugin.signature)) {
|
|
121
|
+
errors.push("signature must be a non-empty base64 string");
|
|
122
|
+
}
|
|
123
|
+
requireString(plugin, "signerKeyId", errors);
|
|
124
|
+
if (!plugin.entrySha256 || typeof plugin.entrySha256 !== "string" || plugin.entrySha256.length === 0) {
|
|
125
|
+
errors.push("missing entrySha256");
|
|
126
|
+
} else if (!/^[0-9a-f]{64}$/.test(plugin.entrySha256)) {
|
|
127
|
+
errors.push("entrySha256 must be a 64-character lowercase hex string");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// A validity window is mandatory for a dynamically-loaded artifact.
|
|
131
|
+
const hasNotBefore = plugin.notBefore !== undefined && plugin.notBefore !== null;
|
|
132
|
+
const hasNotAfter = plugin.notAfter !== undefined && plugin.notAfter !== null;
|
|
133
|
+
if (!hasNotBefore && !hasNotAfter) {
|
|
134
|
+
errors.push("worker-isolated manifest requires a validity window (notBefore and/or notAfter)");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!plugin.capabilities || typeof plugin.capabilities !== "object" || Array.isArray(plugin.capabilities)) {
|
|
138
|
+
errors.push("missing capabilities");
|
|
139
|
+
} else if (plugin.capabilities.readsCredentials !== true) {
|
|
140
|
+
// An authProvider sees the bearer token, so it MUST declare readsCredentials.
|
|
141
|
+
errors.push("worker-isolated authProvider must declare capabilities.readsCredentials = true");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
79
145
|
function requireString(object, key, errors) {
|
|
80
146
|
if (!object[key] || typeof object[key] !== "string") {
|
|
81
147
|
errors.push(`missing ${key}`);
|