@danielblomma/cortex-mcp 2.4.0 → 2.4.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/CHANGELOG.md +201 -0
- package/README.md +19 -3
- package/bin/cortex.mjs +308 -75
- package/bin/daemon-control.mjs +162 -0
- package/package.json +7 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +321 -200
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
} from "node:fs";
|
|
2
11
|
import { join } from "node:path";
|
|
3
12
|
import { resolveTelemetryStateDir, telemetryStatePath } from "./telemetry/state-dir.js";
|
|
13
|
+
import { isAllowedEnterpriseEndpoint } from "./secure-endpoint.js";
|
|
14
|
+
|
|
15
|
+
export { isAllowedEnterpriseEndpoint as isAllowedLicenseEndpoint };
|
|
4
16
|
|
|
5
17
|
export type LicenseVerification =
|
|
6
18
|
| {
|
|
@@ -20,6 +32,9 @@ export type LicenseVerification =
|
|
|
20
32
|
};
|
|
21
33
|
|
|
22
34
|
type CacheEntry = {
|
|
35
|
+
version: 2;
|
|
36
|
+
endpoint: string;
|
|
37
|
+
api_key_sha256: string;
|
|
23
38
|
result: LicenseVerification;
|
|
24
39
|
// ISO timestamp for cache freshness window
|
|
25
40
|
cached_at: string;
|
|
@@ -34,23 +49,83 @@ function cachePath(contextDir: string): string {
|
|
|
34
49
|
return telemetryStatePath(contextDir, CACHE_FILE);
|
|
35
50
|
}
|
|
36
51
|
|
|
52
|
+
function isIsoTimestamp(value: unknown): value is string {
|
|
53
|
+
if (typeof value !== "string") return false;
|
|
54
|
+
const timestamp = new Date(value).getTime();
|
|
55
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
function readCache(contextDir: string): CacheEntry | null {
|
|
38
59
|
const path = cachePath(contextDir);
|
|
39
60
|
if (!existsSync(path)) return null;
|
|
40
61
|
try {
|
|
41
62
|
const raw = readFileSync(path, "utf8");
|
|
42
|
-
|
|
63
|
+
const parsed = JSON.parse(raw) as Partial<CacheEntry>;
|
|
64
|
+
if (
|
|
65
|
+
!parsed ||
|
|
66
|
+
typeof parsed !== "object" ||
|
|
67
|
+
parsed.version !== 2 ||
|
|
68
|
+
typeof parsed.endpoint !== "string" ||
|
|
69
|
+
!parsed.endpoint ||
|
|
70
|
+
typeof parsed.api_key_sha256 !== "string" ||
|
|
71
|
+
!/^[a-f0-9]{64}$/.test(parsed.api_key_sha256) ||
|
|
72
|
+
!parsed.result ||
|
|
73
|
+
typeof parsed.result !== "object" ||
|
|
74
|
+
typeof parsed.result.valid !== "boolean" ||
|
|
75
|
+
!isIsoTimestamp(parsed.cached_at)
|
|
76
|
+
) {
|
|
77
|
+
deleteCache(contextDir);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (parsed.result.valid) {
|
|
81
|
+
if (
|
|
82
|
+
typeof parsed.result.edition !== "string" ||
|
|
83
|
+
!Array.isArray(parsed.result.features) ||
|
|
84
|
+
!parsed.result.features.every((feature) => typeof feature === "string") ||
|
|
85
|
+
!isIsoTimestamp(parsed.result.expires_at) ||
|
|
86
|
+
typeof parsed.result.max_repos !== "number" ||
|
|
87
|
+
!Number.isFinite(parsed.result.max_repos) ||
|
|
88
|
+
parsed.result.max_repos < 0 ||
|
|
89
|
+
!isIsoTimestamp(parsed.result.verified_at) ||
|
|
90
|
+
!["remote", "cache"].includes(parsed.result.source)
|
|
91
|
+
) {
|
|
92
|
+
deleteCache(contextDir);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
} else if (
|
|
96
|
+
typeof parsed.result.reason !== "string" ||
|
|
97
|
+
!isIsoTimestamp(parsed.result.verified_at) ||
|
|
98
|
+
!["remote", "cache", "grace_expired"].includes(parsed.result.source)
|
|
99
|
+
) {
|
|
100
|
+
deleteCache(contextDir);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return parsed as CacheEntry;
|
|
43
104
|
} catch {
|
|
105
|
+
deleteCache(contextDir);
|
|
44
106
|
return null;
|
|
45
107
|
}
|
|
46
108
|
}
|
|
47
109
|
|
|
48
110
|
function writeCache(contextDir: string, entry: CacheEntry): void {
|
|
49
111
|
const path = cachePath(contextDir);
|
|
112
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
50
113
|
try {
|
|
51
114
|
mkdirSync(resolveTelemetryStateDir(contextDir), { recursive: true });
|
|
52
|
-
writeFileSync(
|
|
115
|
+
writeFileSync(
|
|
116
|
+
temporaryPath,
|
|
117
|
+
JSON.stringify(entry, null, 2) + "\n",
|
|
118
|
+
{ encoding: "utf8", mode: 0o600, flag: "wx" },
|
|
119
|
+
);
|
|
120
|
+
chmodSync(temporaryPath, 0o600);
|
|
121
|
+
renameSync(temporaryPath, path);
|
|
122
|
+
chmodSync(path, 0o600);
|
|
53
123
|
} catch {
|
|
124
|
+
try {
|
|
125
|
+
unlinkSync(temporaryPath);
|
|
126
|
+
} catch {
|
|
127
|
+
// Best effort.
|
|
128
|
+
}
|
|
54
129
|
// Cache failures are non-fatal — license check just won't be cached.
|
|
55
130
|
}
|
|
56
131
|
}
|
|
@@ -69,6 +144,48 @@ function ageMs(isoTimestamp: string): number {
|
|
|
69
144
|
return Date.now() - new Date(isoTimestamp).getTime();
|
|
70
145
|
}
|
|
71
146
|
|
|
147
|
+
export function normalizeLicenseEndpoint(endpoint: string): string {
|
|
148
|
+
return endpoint.trim().replace(/\/+$/, "");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function apiKeyFingerprint(apiKey: string): string {
|
|
152
|
+
return createHash("sha256").update(apiKey, "utf8").digest("hex");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function cacheMatchesCredential(
|
|
156
|
+
cached: CacheEntry,
|
|
157
|
+
endpoint: string,
|
|
158
|
+
apiKey: string,
|
|
159
|
+
): boolean {
|
|
160
|
+
return (
|
|
161
|
+
cached.version === 2 &&
|
|
162
|
+
cached.endpoint === normalizeLicenseEndpoint(endpoint) &&
|
|
163
|
+
cached.api_key_sha256 === apiKeyFingerprint(apiKey)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function enterpriseCredentialId(
|
|
168
|
+
endpoint: string,
|
|
169
|
+
apiKey: string,
|
|
170
|
+
): string {
|
|
171
|
+
return createHash("sha256")
|
|
172
|
+
.update(normalizeLicenseEndpoint(endpoint), "utf8")
|
|
173
|
+
.update("\0", "utf8")
|
|
174
|
+
.update(apiKey, "utf8")
|
|
175
|
+
.digest("hex");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function cacheResultIsUnexpired(cached: CacheEntry): boolean {
|
|
179
|
+
if (!cached.result.valid) return false;
|
|
180
|
+
const expiresAt = new Date(cached.result.expires_at).getTime();
|
|
181
|
+
return Number.isFinite(expiresAt) && expiresAt > Date.now();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function cacheAgeIsWithin(cached: CacheEntry, limitMs: number): boolean {
|
|
185
|
+
const age = ageMs(cached.cached_at);
|
|
186
|
+
return Number.isFinite(age) && age >= 0 && age < limitMs;
|
|
187
|
+
}
|
|
188
|
+
|
|
72
189
|
async function fetchLicense(
|
|
73
190
|
endpoint: string,
|
|
74
191
|
apiKey: string,
|
|
@@ -93,21 +210,41 @@ async function fetchLicense(
|
|
|
93
210
|
signal: controller.signal,
|
|
94
211
|
});
|
|
95
212
|
|
|
213
|
+
if (res.status === 401 || res.status === 403) {
|
|
214
|
+
return {
|
|
215
|
+
valid: false,
|
|
216
|
+
reason: "authentication_rejected",
|
|
217
|
+
verified_at: new Date().toISOString(),
|
|
218
|
+
source: "remote",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
96
222
|
if (!res.ok) {
|
|
97
|
-
//
|
|
223
|
+
// Rate limiting, server failures, and other transport-level responses
|
|
224
|
+
// may use an identity-matched positive cache during the grace period.
|
|
98
225
|
return null;
|
|
99
226
|
}
|
|
100
227
|
|
|
101
228
|
const json = (await res.json()) as Record<string, unknown>;
|
|
102
229
|
const verifiedAt = new Date().toISOString();
|
|
103
230
|
|
|
104
|
-
if (
|
|
231
|
+
if (
|
|
232
|
+
json.valid === true &&
|
|
233
|
+
typeof json.edition === "string" &&
|
|
234
|
+
Array.isArray(json.features) &&
|
|
235
|
+
json.features.every((feature) => typeof feature === "string") &&
|
|
236
|
+
isIsoTimestamp(json.expires_at) &&
|
|
237
|
+
new Date(json.expires_at).getTime() > Date.now() &&
|
|
238
|
+
typeof json.max_repos === "number" &&
|
|
239
|
+
Number.isFinite(json.max_repos) &&
|
|
240
|
+
json.max_repos >= 0
|
|
241
|
+
) {
|
|
105
242
|
return {
|
|
106
243
|
valid: true,
|
|
107
|
-
edition:
|
|
108
|
-
features:
|
|
109
|
-
expires_at:
|
|
110
|
-
max_repos:
|
|
244
|
+
edition: json.edition,
|
|
245
|
+
features: json.features,
|
|
246
|
+
expires_at: json.expires_at,
|
|
247
|
+
max_repos: json.max_repos,
|
|
111
248
|
verified_at: verifiedAt,
|
|
112
249
|
source: "remote",
|
|
113
250
|
};
|
|
@@ -115,7 +252,9 @@ async function fetchLicense(
|
|
|
115
252
|
|
|
116
253
|
return {
|
|
117
254
|
valid: false,
|
|
118
|
-
reason:
|
|
255
|
+
reason: json.valid === true
|
|
256
|
+
? "malformed_license_response"
|
|
257
|
+
: String(json.reason ?? "unknown"),
|
|
119
258
|
verified_at: verifiedAt,
|
|
120
259
|
source: "remote",
|
|
121
260
|
};
|
|
@@ -129,7 +268,8 @@ async function fetchLicense(
|
|
|
129
268
|
|
|
130
269
|
/**
|
|
131
270
|
* Verify the license for the given api_key. Layered fallback:
|
|
132
|
-
* 1. If a positive cache (valid:true) is fresh (<24h)
|
|
271
|
+
* 1. If a positive cache (valid:true) is fresh (<24h), unexpired, and
|
|
272
|
+
* bound to this endpoint + API-key fingerprint → use cache
|
|
133
273
|
* Negative cache entries are never trusted; if one is encountered
|
|
134
274
|
* it's deleted on the spot so a since-fixed remote can heal.
|
|
135
275
|
* 2. Otherwise try remote endpoint
|
|
@@ -138,8 +278,9 @@ async function fetchLicense(
|
|
|
138
278
|
* cache (so a revoked/expired key doesn't keep masquerading as
|
|
139
279
|
* valid past its remote-side fail), return result, do NOT
|
|
140
280
|
* cache the negative.
|
|
141
|
-
* - On transient failure → fall back to
|
|
142
|
-
* grace period (7d).
|
|
281
|
+
* - On transient failure → fall back to the same identity-bound positive
|
|
282
|
+
* cache if within the grace period (7d). Authentication rejection is
|
|
283
|
+
* authoritative and never uses grace.
|
|
143
284
|
* 3. If no usable cache and endpoint unreachable → return invalid
|
|
144
285
|
* (grace_expired).
|
|
145
286
|
*
|
|
@@ -153,6 +294,16 @@ export async function verifyLicense(
|
|
|
153
294
|
apiKey: string,
|
|
154
295
|
options: { instance_id?: string; client_version?: string } = {},
|
|
155
296
|
): Promise<LicenseVerification> {
|
|
297
|
+
if (!isAllowedEnterpriseEndpoint(endpoint)) {
|
|
298
|
+
deleteCache(contextDir);
|
|
299
|
+
return {
|
|
300
|
+
valid: false,
|
|
301
|
+
reason: "insecure_or_invalid_endpoint",
|
|
302
|
+
verified_at: new Date().toISOString(),
|
|
303
|
+
source: "remote",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
156
307
|
let cached = readCache(contextDir);
|
|
157
308
|
|
|
158
309
|
// Defensive: a previous version of this code wrote negative results
|
|
@@ -163,9 +314,19 @@ export async function verifyLicense(
|
|
|
163
314
|
cached = null;
|
|
164
315
|
}
|
|
165
316
|
|
|
317
|
+
const matchingCache =
|
|
318
|
+
cached && cacheMatchesCredential(cached, endpoint, apiKey)
|
|
319
|
+
? cached
|
|
320
|
+
: null;
|
|
321
|
+
|
|
166
322
|
// Fresh positive cache: skip remote.
|
|
167
|
-
if (
|
|
168
|
-
|
|
323
|
+
if (
|
|
324
|
+
matchingCache &&
|
|
325
|
+
matchingCache.result.valid === true &&
|
|
326
|
+
cacheResultIsUnexpired(matchingCache) &&
|
|
327
|
+
cacheAgeIsWithin(matchingCache, CACHE_TTL_MS)
|
|
328
|
+
) {
|
|
329
|
+
return { ...matchingCache.result, source: "cache" };
|
|
169
330
|
}
|
|
170
331
|
|
|
171
332
|
const remote = await fetchLicense(
|
|
@@ -178,6 +339,9 @@ export async function verifyLicense(
|
|
|
178
339
|
if (remote) {
|
|
179
340
|
if (remote.valid) {
|
|
180
341
|
writeCache(contextDir, {
|
|
342
|
+
version: 2,
|
|
343
|
+
endpoint: normalizeLicenseEndpoint(endpoint),
|
|
344
|
+
api_key_sha256: apiKeyFingerprint(apiKey),
|
|
181
345
|
result: remote,
|
|
182
346
|
cached_at: new Date().toISOString(),
|
|
183
347
|
});
|
|
@@ -190,8 +354,13 @@ export async function verifyLicense(
|
|
|
190
354
|
}
|
|
191
355
|
|
|
192
356
|
// Remote unreachable. Fall back to positive cache if within grace.
|
|
193
|
-
if (
|
|
194
|
-
|
|
357
|
+
if (
|
|
358
|
+
matchingCache &&
|
|
359
|
+
matchingCache.result.valid === true &&
|
|
360
|
+
cacheResultIsUnexpired(matchingCache) &&
|
|
361
|
+
cacheAgeIsWithin(matchingCache, GRACE_PERIOD_MS)
|
|
362
|
+
) {
|
|
363
|
+
return { ...matchingCache.result, source: "cache" };
|
|
195
364
|
}
|
|
196
365
|
|
|
197
366
|
return {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise bearer credentials may travel only over TLS. Loopback HTTP is
|
|
3
|
+
* retained for local development because the connection cannot leave the
|
|
4
|
+
* host. Embedded URL credentials are never accepted.
|
|
5
|
+
*/
|
|
6
|
+
export function isAllowedEnterpriseEndpoint(endpoint: string): boolean {
|
|
7
|
+
let parsed: URL;
|
|
8
|
+
try {
|
|
9
|
+
parsed = new URL(endpoint);
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (parsed.username || parsed.password) return false;
|
|
14
|
+
if (parsed.protocol === "https:") return true;
|
|
15
|
+
if (parsed.protocol !== "http:") return false;
|
|
16
|
+
const host = parsed.hostname.toLowerCase();
|
|
17
|
+
return (
|
|
18
|
+
host === "localhost" ||
|
|
19
|
+
host === "127.0.0.1" ||
|
|
20
|
+
host === "::1" ||
|
|
21
|
+
host === "[::1]"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -5,6 +5,7 @@ import { DEFAULT_CAPABILITIES, type CapabilityDefinition } from "./capabilities.
|
|
|
5
5
|
import { workflowDefinitionSchema, type WorkflowDefinition } from "./schemas.js";
|
|
6
6
|
import { DEFAULT_WORKFLOWS } from "./default-workflows.js";
|
|
7
7
|
import { loadSyncedCapabilities } from "./synced-capability-registry.js";
|
|
8
|
+
import { configuredEnterpriseCredentialId } from "../enterprise-identity.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Pre-tool-use enforcement for the harness. Pure function: takes the tool
|
|
@@ -89,7 +90,13 @@ export function evaluateToolCall(options: EvaluateOptions): EnforcementResult {
|
|
|
89
90
|
// capabilities, with synced ones taking precedence on name collisions
|
|
90
91
|
// so org overrides actually override.
|
|
91
92
|
const capabilities =
|
|
92
|
-
options.capabilities ?? {
|
|
93
|
+
options.capabilities ?? {
|
|
94
|
+
...DEFAULT_CAPABILITIES,
|
|
95
|
+
...loadSyncedCapabilities(
|
|
96
|
+
undefined,
|
|
97
|
+
configuredEnterpriseCredentialId(options.cwd) ?? undefined,
|
|
98
|
+
),
|
|
99
|
+
};
|
|
93
100
|
const capability = capabilities[stage.capability];
|
|
94
101
|
if (!capability) {
|
|
95
102
|
return {
|
|
@@ -96,6 +96,7 @@ export function runWorkflowStart(
|
|
|
96
96
|
) {
|
|
97
97
|
const resolved = resolveWorkflowDefinition(input.workflow_id, {
|
|
98
98
|
registry: ctx.workflows,
|
|
99
|
+
cwd: ctx.cwd,
|
|
99
100
|
bundledFallbackPolicy: ctx.bundledFallbackPolicy,
|
|
100
101
|
});
|
|
101
102
|
const workflow = resolved.workflow;
|
|
@@ -130,6 +131,7 @@ export function runWorkflowAdvance(
|
|
|
130
131
|
}
|
|
131
132
|
const workflow = resolveWorkflowDefinition(state.workflow_id, {
|
|
132
133
|
registry: ctx.workflows,
|
|
134
|
+
cwd: ctx.cwd,
|
|
133
135
|
}).workflow;
|
|
134
136
|
const stage = workflow.stages.find((s) => s.name === input.stage);
|
|
135
137
|
if (!stage) {
|
|
@@ -210,6 +212,7 @@ export function runWorkflowEnvelope(
|
|
|
210
212
|
}
|
|
211
213
|
const workflow = resolveWorkflowDefinition(state.workflow_id, {
|
|
212
214
|
registry: ctx.workflows,
|
|
215
|
+
cwd: ctx.cwd,
|
|
213
216
|
}).workflow;
|
|
214
217
|
const envelope = composeStageEnvelope({
|
|
215
218
|
cwd: ctx.cwd,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_WORKFLOWS } from "./default-workflows.js";
|
|
2
2
|
import { loadSyncedWorkflows } from "./synced-registry.js";
|
|
3
|
+
import { configuredEnterpriseCredentialId } from "../enterprise-identity.js";
|
|
3
4
|
import type { WorkflowDefinition } from "./schemas.js";
|
|
4
5
|
|
|
5
6
|
export type WorkflowSource = "bundled" | "synced" | "injected";
|
|
@@ -24,6 +25,7 @@ export type WorkflowResolution = {
|
|
|
24
25
|
type ResolveWorkflowOptions = {
|
|
25
26
|
registry?: Record<string, WorkflowDefinition>;
|
|
26
27
|
syncedDir?: string;
|
|
28
|
+
cwd?: string;
|
|
27
29
|
emitBundledFallbackWarning?: boolean;
|
|
28
30
|
bundledFallbackPolicy?: "allow" | "warn" | "block";
|
|
29
31
|
};
|
|
@@ -59,7 +61,13 @@ export function resolveWorkflowDefinition(
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
const bundled = DEFAULT_WORKFLOWS;
|
|
62
|
-
const
|
|
64
|
+
const expectedCredentialId = options.syncedDir
|
|
65
|
+
? undefined
|
|
66
|
+
: configuredEnterpriseCredentialId(options.cwd ?? process.cwd()) ?? undefined;
|
|
67
|
+
const synced = loadSyncedWorkflows(
|
|
68
|
+
options.syncedDir,
|
|
69
|
+
expectedCredentialId,
|
|
70
|
+
);
|
|
63
71
|
const bundledIds = sortIds(Object.keys(bundled));
|
|
64
72
|
const syncedIds = sortIds(Object.keys(synced));
|
|
65
73
|
const merged = { ...bundled, ...synced };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { configuredEnterpriseCredentialId } from "../enterprise-identity.js";
|
|
4
5
|
import {
|
|
5
6
|
capabilityDefinitionSchema,
|
|
6
7
|
type CapabilityDefinition,
|
|
@@ -27,6 +28,7 @@ type LocalCapabilityRecord = {
|
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
type LocalCapabilitiesState = {
|
|
31
|
+
credential_id?: string;
|
|
30
32
|
capabilities?: Record<string, LocalCapabilityRecord>;
|
|
31
33
|
};
|
|
32
34
|
|
|
@@ -42,6 +44,7 @@ export function syncedCapabilitiesCachePath(dir?: string): string {
|
|
|
42
44
|
*/
|
|
43
45
|
export function loadSyncedCapabilities(
|
|
44
46
|
dir?: string,
|
|
47
|
+
expectedCredentialId?: string,
|
|
45
48
|
): Record<string, CapabilityDefinition> {
|
|
46
49
|
const path = syncedCapabilitiesCachePath(dir);
|
|
47
50
|
if (!existsSync(path)) return {};
|
|
@@ -52,6 +55,18 @@ export function loadSyncedCapabilities(
|
|
|
52
55
|
} catch {
|
|
53
56
|
return {};
|
|
54
57
|
}
|
|
58
|
+
const requiredCredentialId =
|
|
59
|
+
expectedCredentialId ??
|
|
60
|
+
(dir ? undefined : configuredEnterpriseCredentialId(process.cwd()));
|
|
61
|
+
if (!dir && !requiredCredentialId) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
requiredCredentialId &&
|
|
66
|
+
parsed.credential_id !== requiredCredentialId
|
|
67
|
+
) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
55
70
|
const records = parsed.capabilities;
|
|
56
71
|
if (!records || typeof records !== "object") return {};
|
|
57
72
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { configuredEnterpriseCredentialId } from "../enterprise-identity.js";
|
|
4
5
|
import { workflowDefinitionSchema, type WorkflowDefinition } from "./schemas.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -25,6 +26,7 @@ type LocalWorkflowRecord = {
|
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
type LocalWorkflowsState = {
|
|
29
|
+
credential_id?: string;
|
|
28
30
|
workflows?: Record<string, LocalWorkflowRecord>;
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -40,6 +42,7 @@ export function syncedWorkflowsCachePath(dir?: string): string {
|
|
|
40
42
|
*/
|
|
41
43
|
export function loadSyncedWorkflows(
|
|
42
44
|
dir?: string,
|
|
45
|
+
expectedCredentialId?: string,
|
|
43
46
|
): Record<string, WorkflowDefinition> {
|
|
44
47
|
const path = syncedWorkflowsCachePath(dir);
|
|
45
48
|
if (!existsSync(path)) return {};
|
|
@@ -50,6 +53,18 @@ export function loadSyncedWorkflows(
|
|
|
50
53
|
} catch {
|
|
51
54
|
return {};
|
|
52
55
|
}
|
|
56
|
+
const requiredCredentialId =
|
|
57
|
+
expectedCredentialId ??
|
|
58
|
+
(dir ? undefined : configuredEnterpriseCredentialId(process.cwd()));
|
|
59
|
+
if (!dir && !requiredCredentialId) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
if (
|
|
63
|
+
requiredCredentialId &&
|
|
64
|
+
parsed.credential_id !== requiredCredentialId
|
|
65
|
+
) {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
53
68
|
const records = parsed.workflows;
|
|
54
69
|
if (!records || typeof records !== "object") return {};
|
|
55
70
|
|
|
@@ -6,6 +6,11 @@ import {
|
|
|
6
6
|
import { hostname } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { loadEnterpriseConfig } from "../core/config.js";
|
|
9
|
+
import { matchesEnterpriseHostIdentity } from "../core/enterprise-host-identity.js";
|
|
10
|
+
import {
|
|
11
|
+
enterpriseCredentialId,
|
|
12
|
+
isAllowedLicenseEndpoint,
|
|
13
|
+
} from "../core/license.js";
|
|
9
14
|
import {
|
|
10
15
|
capabilityDefinitionSchema,
|
|
11
16
|
type CapabilityDefinition,
|
|
@@ -51,6 +56,7 @@ type LocalCapabilityRecord = {
|
|
|
51
56
|
};
|
|
52
57
|
|
|
53
58
|
type LocalCapabilitiesState = {
|
|
59
|
+
credential_id?: string;
|
|
54
60
|
capabilities: Record<string, LocalCapabilityRecord>;
|
|
55
61
|
last_synced_at?: string;
|
|
56
62
|
};
|
|
@@ -75,6 +81,10 @@ function readSyncedCapabilitiesState(): LocalCapabilitiesState {
|
|
|
75
81
|
try {
|
|
76
82
|
const parsed = JSON.parse(readFileSync(path, "utf8")) as LocalCapabilitiesState;
|
|
77
83
|
return {
|
|
84
|
+
credential_id:
|
|
85
|
+
typeof parsed.credential_id === "string"
|
|
86
|
+
? parsed.credential_id
|
|
87
|
+
: undefined,
|
|
78
88
|
capabilities: parsed.capabilities ?? {},
|
|
79
89
|
last_synced_at: parsed.last_synced_at,
|
|
80
90
|
};
|
|
@@ -145,6 +155,28 @@ export async function runCapabilitySyncOnce(
|
|
|
145
155
|
await writeAudit(cwd, outcome);
|
|
146
156
|
return outcome;
|
|
147
157
|
}
|
|
158
|
+
if (!isAllowedLicenseEndpoint(baseUrl)) {
|
|
159
|
+
const outcome: CapabilitySyncOutcome = {
|
|
160
|
+
kind: "failed",
|
|
161
|
+
error: "insecure or invalid enterprise endpoint",
|
|
162
|
+
};
|
|
163
|
+
await writeAudit(cwd, outcome);
|
|
164
|
+
return outcome;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const credentialId = enterpriseCredentialId(baseUrl, apiKey);
|
|
168
|
+
if (!matchesEnterpriseHostIdentity(credentialId)) {
|
|
169
|
+
const outcome: CapabilitySyncOutcome = {
|
|
170
|
+
kind: "failed",
|
|
171
|
+
error:
|
|
172
|
+
"enterprise identity conflict: this user profile is already enrolled to another endpoint or API key",
|
|
173
|
+
};
|
|
174
|
+
await writeAudit(cwd, outcome);
|
|
175
|
+
return outcome;
|
|
176
|
+
}
|
|
177
|
+
const state = readSyncedCapabilitiesState();
|
|
178
|
+
const identityChanged = state.credential_id !== credentialId;
|
|
179
|
+
state.credential_id = credentialId;
|
|
148
180
|
|
|
149
181
|
let manifest: ManifestEntry[];
|
|
150
182
|
try {
|
|
@@ -158,7 +190,6 @@ export async function runCapabilitySyncOnce(
|
|
|
158
190
|
return outcome;
|
|
159
191
|
}
|
|
160
192
|
|
|
161
|
-
const state = readSyncedCapabilitiesState();
|
|
162
193
|
const remoteByName = new Map(manifest.map((e) => [e.capability_name, e]));
|
|
163
194
|
|
|
164
195
|
const added: string[] = [];
|
|
@@ -169,7 +200,8 @@ export async function runCapabilitySyncOnce(
|
|
|
169
200
|
const local = state.capabilities[entry.capability_name];
|
|
170
201
|
const isNew = !local;
|
|
171
202
|
const isChanged =
|
|
172
|
-
Boolean(local) &&
|
|
203
|
+
Boolean(local) &&
|
|
204
|
+
(identityChanged || local.updated_at !== entry.updated_at);
|
|
173
205
|
if (!isNew && !isChanged) continue;
|
|
174
206
|
|
|
175
207
|
let fetched: FetchedCapability;
|
|
@@ -218,6 +250,10 @@ export async function runCapabilitySyncOnce(
|
|
|
218
250
|
|
|
219
251
|
const totalChanged = added.length + changed.length + removed.length;
|
|
220
252
|
if (totalChanged === 0) {
|
|
253
|
+
if (identityChanged) {
|
|
254
|
+
state.last_synced_at = new Date().toISOString();
|
|
255
|
+
writeSyncedCapabilitiesState(state);
|
|
256
|
+
}
|
|
221
257
|
const outcome: CapabilitySyncOutcome = {
|
|
222
258
|
kind: "unchanged",
|
|
223
259
|
count: manifest.length,
|
|
@@ -14,8 +14,9 @@ import { writeHostAuditEvent } from "./ungoverned-scanner.js";
|
|
|
14
14
|
* Per privacy boundary v3 we never send payload bytes to cortex-web —
|
|
15
15
|
* only SNI + destination + bytes-transferred counters. The audit lands
|
|
16
16
|
* in .context/audit/host-events-YYYY-MM-DD.jsonl as event_type =
|
|
17
|
-
* "egress_connection"
|
|
18
|
-
*
|
|
17
|
+
* "egress_connection" only when a caller supplies an attributable project
|
|
18
|
+
* cwd. The shared daemon proxy intentionally omits cwd and therefore emits
|
|
19
|
+
* no project audit record rather than misattributing cross-project traffic.
|
|
19
20
|
*
|
|
20
21
|
* Plain HTTP (non-CONNECT) is also supported but logged with the Host
|
|
21
22
|
* header in place of SNI.
|
|
@@ -103,7 +104,7 @@ export function parseSni(buf: Buffer): string | null {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
export type ProxyOptions = {
|
|
106
|
-
cwd
|
|
107
|
+
cwd?: string;
|
|
107
108
|
port?: number;
|
|
108
109
|
hostId?: string;
|
|
109
110
|
};
|
|
@@ -118,7 +119,8 @@ const HTTP_OK = "HTTP/1.1 200 Connection Established\r\nProxy-Agent: cortex-egre
|
|
|
118
119
|
const HTTP_BAD = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
|
|
119
120
|
const HTTP_BAD_GATEWAY = "HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n";
|
|
120
121
|
|
|
121
|
-
function emit(cwd: string, evt: EgressEvent): void {
|
|
122
|
+
function emit(cwd: string | undefined, evt: EgressEvent): void {
|
|
123
|
+
if (!cwd) return;
|
|
122
124
|
void writeHostAuditEvent(cwd, evt as unknown as Record<string, unknown>).catch((err) => {
|
|
123
125
|
process.stderr.write(
|
|
124
126
|
`[cortex-egress] audit emit failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
@@ -147,7 +149,7 @@ function pipeWithCounting(
|
|
|
147
149
|
client: Socket,
|
|
148
150
|
upstream: Socket,
|
|
149
151
|
evt: EgressEvent,
|
|
150
|
-
cwd: string,
|
|
152
|
+
cwd: string | undefined,
|
|
151
153
|
start: number,
|
|
152
154
|
): void {
|
|
153
155
|
let firstClientChunk = true;
|
|
@@ -193,7 +195,7 @@ function handleConnect(
|
|
|
193
195
|
client: Socket,
|
|
194
196
|
host: string,
|
|
195
197
|
port: number,
|
|
196
|
-
cwd: string,
|
|
198
|
+
cwd: string | undefined,
|
|
197
199
|
hostId: string,
|
|
198
200
|
): void {
|
|
199
201
|
const evt = newEvent(host, port, "https", hostId, client.remotePort ?? null);
|
|
@@ -220,7 +222,7 @@ function handleHttp(
|
|
|
220
222
|
client: Socket,
|
|
221
223
|
url: URL,
|
|
222
224
|
initialChunk: Buffer,
|
|
223
|
-
cwd: string,
|
|
225
|
+
cwd: string | undefined,
|
|
224
226
|
hostId: string,
|
|
225
227
|
): void {
|
|
226
228
|
const port = url.port ? parseInt(url.port, 10) : 80;
|
|
@@ -245,7 +247,11 @@ function handleHttp(
|
|
|
245
247
|
});
|
|
246
248
|
}
|
|
247
249
|
|
|
248
|
-
function handleConnection(
|
|
250
|
+
function handleConnection(
|
|
251
|
+
client: Socket,
|
|
252
|
+
cwd: string | undefined,
|
|
253
|
+
hostId: string,
|
|
254
|
+
): void {
|
|
249
255
|
let buffer = Buffer.alloc(0);
|
|
250
256
|
|
|
251
257
|
const onFirstChunk = (chunk: Buffer) => {
|