machine-bridge-mcp 2.0.0 → 3.0.0-beta.9
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 +101 -0
- package/CONTRIBUTING.md +20 -25
- package/README.md +21 -14
- package/SECURITY.md +147 -123
- package/browser-extension/manifest.json +3 -2
- package/browser-extension/service-worker.js +31 -12
- package/docs/ARCHITECTURE.md +27 -13
- package/docs/AUDIT.md +85 -2
- package/docs/CLIENTS.md +2 -2
- package/docs/ENGINEERING.md +9 -3
- package/docs/GETTING_STARTED.md +10 -9
- package/docs/LOCAL_AUTHORIZATION.md +115 -72
- package/docs/LOGGING.md +12 -3
- package/docs/MANAGED_JOBS.md +24 -29
- package/docs/MULTI_ACCOUNT.md +98 -37
- package/docs/OPERATIONS.md +33 -22
- package/docs/OVERVIEW.md +7 -7
- package/docs/PROJECT_STANDARDS.md +9 -5
- package/docs/RELEASING.md +171 -64
- package/docs/TESTING.md +19 -7
- package/docs/THREAT_MODEL.md +138 -67
- package/docs/TOOL_REFERENCE.md +2 -2
- package/docs/UPGRADING.md +102 -18
- package/native/macos/MachineBridgeTrustBroker.swift +233 -0
- package/package.json +36 -5
- package/scripts/candidate-runtime-store.mjs +38 -0
- package/scripts/check-plan.mjs +19 -0
- package/scripts/check-runner.mjs +4 -1
- package/scripts/commit-message-check.mjs +5 -2
- package/scripts/coverage-check.mjs +30 -2
- package/scripts/github-push.mjs +11 -0
- package/scripts/github-release.mjs +38 -7
- package/scripts/install-published-prerelease.mjs +167 -0
- package/scripts/local-release-acceptance.mjs +23 -12
- package/scripts/npm-publication-policy.mjs +16 -0
- package/scripts/prerelease-activation.mjs +79 -0
- package/scripts/privacy-check.mjs +7 -5
- package/scripts/promotion-digest.mjs +121 -0
- package/scripts/publish-npm.mjs +51 -0
- package/scripts/published-release.mjs +80 -0
- package/scripts/release-acceptance.mjs +30 -1
- package/scripts/release-candidate-manifest.mjs +40 -0
- package/scripts/release-channel.mjs +106 -0
- package/scripts/release-impact-check.mjs +12 -22
- package/scripts/release-soak.mjs +237 -0
- package/scripts/start-release-candidate.mjs +113 -7
- package/src/local/account-access.mjs +6 -0
- package/src/local/account-admin.mjs +34 -16
- package/src/local/agent-context.mjs +45 -42
- package/src/local/app-automation.mjs +9 -4
- package/src/local/authority-context.mjs +106 -0
- package/src/local/browser-bridge-http.mjs +4 -1
- package/src/local/browser-bridge.mjs +14 -9
- package/src/local/browser-broker-routes.mjs +4 -0
- package/src/local/browser-broker-server.mjs +5 -4
- package/src/local/browser-extension-identity.mjs +50 -0
- package/src/local/browser-extension-protocol.mjs +9 -4
- package/src/local/browser-operation-service.mjs +7 -0
- package/src/local/browser-pairing-http.mjs +36 -0
- package/src/local/browser-pairing-store.mjs +56 -47
- package/src/local/cli-account-admin.mjs +41 -7
- package/src/local/cli-activate.mjs +90 -0
- package/src/local/cli-approval.mjs +14 -58
- package/src/local/cli-local-admin.mjs +5 -15
- package/src/local/cli-options.mjs +11 -7
- package/src/local/cli-service.mjs +35 -5
- package/src/local/cli.mjs +112 -58
- package/src/local/daemon-process.mjs +6 -0
- package/src/local/delegated-process-sandbox.mjs +147 -0
- package/src/local/device-identity.mjs +157 -48
- package/src/local/device-root-provider.mjs +87 -0
- package/src/local/errors.mjs +25 -3
- package/src/local/git-service.mjs +16 -15
- package/src/local/job-runner.mjs +35 -27
- package/src/local/log.mjs +14 -1
- package/src/local/macos-trust-broker.mjs +320 -0
- package/src/local/managed-job-lock.mjs +18 -0
- package/src/local/managed-job-projection.mjs +9 -1
- package/src/local/managed-job-runner.mjs +13 -1
- package/src/local/managed-job-storage.mjs +2 -1
- package/src/local/managed-job-terminal.mjs +138 -0
- package/src/local/managed-jobs.mjs +88 -32
- package/src/local/operation-authorization.mjs +75 -239
- package/src/local/operation-risk.mjs +28 -0
- package/src/local/operation-state-lock.mjs +7 -86
- package/src/local/owner-state-lock.mjs +104 -0
- package/src/local/policy.mjs +19 -0
- package/src/local/process-execution.mjs +21 -9
- package/src/local/process-sessions.mjs +27 -17
- package/src/local/process-tracker.mjs +30 -6
- package/src/local/process-tree-ownership.mjs +59 -0
- package/src/local/process-tree.mjs +22 -18
- package/src/local/relay-connection.mjs +47 -35
- package/src/local/relay-diagnostics.mjs +65 -0
- package/src/local/remote-configuration.mjs +48 -0
- package/src/local/runtime-activation.mjs +141 -0
- package/src/local/runtime-diagnostics.mjs +21 -0
- package/src/local/runtime-relay.mjs +9 -8
- package/src/local/runtime-reporting.mjs +8 -3
- package/src/local/runtime-tool-handlers.mjs +8 -8
- package/src/local/runtime.mjs +73 -39
- package/src/local/secure-file.mjs +5 -1
- package/src/local/security-audit-log.mjs +204 -0
- package/src/local/service-convergence.mjs +17 -1
- package/src/local/service-ownership.mjs +17 -0
- package/src/local/service-restart-handoff.mjs +50 -0
- package/src/local/service-restart-scheduler.mjs +49 -0
- package/src/local/service-status.mjs +47 -0
- package/src/local/service.mjs +142 -43
- package/src/local/state-inventory.mjs +21 -3
- package/src/local/state.mjs +155 -17
- package/src/local/stdio.mjs +5 -0
- package/src/local/tool-executor.mjs +39 -6
- package/src/local/tool-result-boundary.mjs +51 -0
- package/src/local/tools.mjs +1 -0
- package/src/local/trusted-executable.mjs +53 -0
- package/src/local/trusted-git-executable.mjs +34 -0
- package/src/local/trusted-github-cli.mjs +24 -0
- package/src/local/windows-service.mjs +20 -0
- package/src/local/worker-deployment.mjs +3 -4
- package/src/local/worker-secret-file.mjs +2 -2
- package/src/local/workspace-file-service.mjs +34 -31
- package/src/shared/admin-auth.d.mts +2 -1
- package/src/shared/admin-auth.mjs +3 -3
- package/src/shared/device-session-auth.d.mts +20 -0
- package/src/shared/device-session-auth.mjs +51 -0
- package/src/shared/server-metadata.json +1 -1
- package/src/shared/tool-catalog.json +2 -2
- package/src/worker/account-admin.ts +23 -22
- package/src/worker/authority.ts +9 -2
- package/src/worker/daemon-auth.ts +45 -67
- package/src/worker/daemon-sockets.ts +12 -2
- package/src/worker/device-session-verifier.ts +129 -0
- package/src/worker/dpop.ts +151 -0
- package/src/worker/http.ts +47 -16
- package/src/worker/index.ts +53 -16
- package/src/worker/nonce-store.ts +1 -5
- package/src/worker/oauth-client-admin.ts +45 -0
- package/src/worker/oauth-controller.ts +55 -6
- package/src/worker/oauth-refresh-families.ts +21 -7
- package/src/worker/oauth-state.ts +6 -0
- package/src/worker/oauth-tokens.ts +28 -8
- package/src/worker/websocket-protocol.ts +8 -2
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, sign, webcrypto } from "node:crypto";
|
|
2
2
|
import { DAEMON_AUTH_SCHEME, DAEMON_PREFLIGHT_SCHEME, daemonAuthTranscript, daemonPreflightTranscript } from "../shared/daemon-auth.mjs";
|
|
3
|
+
import {
|
|
4
|
+
DEVICE_SESSION_CERTIFICATE_SCHEME,
|
|
5
|
+
DEVICE_SESSION_MAX_LIFETIME_SECONDS,
|
|
6
|
+
canonicalPublicJwk,
|
|
7
|
+
deviceSessionCertificateTranscript,
|
|
8
|
+
} from "../shared/device-session-auth.mjs";
|
|
3
9
|
|
|
4
10
|
const DEVICE_KEY_TYPE = "EC";
|
|
5
11
|
const DEVICE_CURVE = "P-256";
|
|
12
|
+
const SESSION_CERTIFICATE_HEADER = "X-Bridge-Device-Certificate";
|
|
6
13
|
|
|
7
14
|
export function createDeviceIdentity() {
|
|
8
15
|
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "prime256v1" });
|
|
@@ -19,90 +26,192 @@ export function createDeviceIdentity() {
|
|
|
19
26
|
};
|
|
20
27
|
}
|
|
21
28
|
|
|
29
|
+
export function createDeviceSessionIdentity(rootIdentity, workerOrigin, server, version, now = Date.now()) {
|
|
30
|
+
validateDeviceIdentity(rootIdentity);
|
|
31
|
+
const draft = createDeviceSessionDraft(rootIdentity, workerOrigin, server, version, now);
|
|
32
|
+
return finalizeDeviceSessionIdentity(draft, signDeviceTranscript(rootIdentity, draft.transcript));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createDeviceSessionDraft(rootIdentity, workerOrigin, server, version, now = Date.now()) {
|
|
36
|
+
validatePublicDeviceRoot(rootIdentity);
|
|
37
|
+
const issuedAt = Math.floor(Number(now) / 1000);
|
|
38
|
+
if (!Number.isSafeInteger(issuedAt) || issuedAt <= 0) throw new Error("device session timestamp is invalid");
|
|
39
|
+
const expiresAt = issuedAt + DEVICE_SESSION_MAX_LIFETIME_SECONDS;
|
|
40
|
+
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "prime256v1" });
|
|
41
|
+
const privateJwk = privateKey.export({ format: "jwk" });
|
|
42
|
+
const publicJwk = publicKey.export({ format: "jwk" });
|
|
43
|
+
validatePrivateDeviceJwk(privateJwk);
|
|
44
|
+
validatePublicDeviceJwk(publicJwk);
|
|
45
|
+
const nonce = randomBytes(24).toString("base64url");
|
|
46
|
+
const certificateBody = {
|
|
47
|
+
scheme: DEVICE_SESSION_CERTIFICATE_SCHEME,
|
|
48
|
+
root_key_id: rootIdentity.keyId,
|
|
49
|
+
public_jwk: canonicalPublicJwk(publicJwk),
|
|
50
|
+
issued_at: issuedAt,
|
|
51
|
+
expires_at: expiresAt,
|
|
52
|
+
nonce,
|
|
53
|
+
};
|
|
54
|
+
const transcript = deviceSessionCertificateTranscript({
|
|
55
|
+
workerOrigin,
|
|
56
|
+
server,
|
|
57
|
+
version,
|
|
58
|
+
rootKeyId: rootIdentity.keyId,
|
|
59
|
+
publicJwk,
|
|
60
|
+
issuedAt,
|
|
61
|
+
expiresAt,
|
|
62
|
+
nonce,
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
transcript,
|
|
66
|
+
certificateBody,
|
|
67
|
+
session: {
|
|
68
|
+
scheme: DAEMON_AUTH_SCHEME,
|
|
69
|
+
privateJwk,
|
|
70
|
+
publicJwk,
|
|
71
|
+
keyId: deviceKeyId(publicJwk),
|
|
72
|
+
createdAt: new Date(Number(now)).toISOString(),
|
|
73
|
+
expiresAt: new Date(expiresAt * 1000).toISOString(),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function finalizeDeviceSessionIdentity(draft, signature) {
|
|
79
|
+
if (!draft || typeof draft !== "object" || !draft.session || !draft.certificateBody) throw new Error("device session draft is invalid");
|
|
80
|
+
if (!/^[A-Za-z0-9_-]{86}$/.test(String(signature || ""))) throw new Error("device session root signature is invalid");
|
|
81
|
+
const identity = {
|
|
82
|
+
...draft.session,
|
|
83
|
+
certificate: { ...draft.certificateBody, signature },
|
|
84
|
+
};
|
|
85
|
+
return validateDeviceSessionIdentity(identity);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function validatePublicDeviceRoot(identity) {
|
|
89
|
+
if (!identity || typeof identity !== "object" || Array.isArray(identity)) throw new Error("device root identity is missing");
|
|
90
|
+
if (identity.scheme !== DAEMON_AUTH_SCHEME) throw new Error("device root identity scheme is invalid");
|
|
91
|
+
validatePublicDeviceJwk(identity.publicJwk);
|
|
92
|
+
if (identity.keyId !== deviceKeyId(identity.publicJwk)) throw new Error("device root identity key id is invalid");
|
|
93
|
+
if (!Number.isFinite(Date.parse(String(identity.createdAt || "")))) throw new Error("device root identity creation time is invalid");
|
|
94
|
+
return identity;
|
|
95
|
+
}
|
|
96
|
+
|
|
22
97
|
export function publicDeviceJwkJson(identity) {
|
|
23
98
|
const publicJwk = identity?.publicJwk;
|
|
24
99
|
validatePublicDeviceJwk(publicJwk);
|
|
25
100
|
return JSON.stringify(publicJwk);
|
|
26
101
|
}
|
|
27
102
|
|
|
28
|
-
|
|
29
103
|
export function createDaemonPreflightHeaders(identity, workerOrigin, server, version, now = Date.now()) {
|
|
30
|
-
|
|
104
|
+
validateDeviceSessionIdentity(identity, now);
|
|
31
105
|
const issuedAt = Math.floor(Number(now) / 1000);
|
|
32
106
|
if (!Number.isSafeInteger(issuedAt) || issuedAt <= 0) throw new Error("device preflight timestamp is invalid");
|
|
33
107
|
const nonce = randomBytes(24).toString("base64url");
|
|
34
108
|
const transcript = daemonPreflightTranscript({ workerOrigin, server, version, nonce, issuedAt });
|
|
35
|
-
const
|
|
36
|
-
const signature = sign("sha256", Buffer.from(transcript, "utf8"), { key, dsaEncoding: "ieee-p1363" }).toString("base64url");
|
|
109
|
+
const signature = signDeviceTranscript(identity, transcript);
|
|
37
110
|
return {
|
|
38
111
|
"X-Bridge-Device-Scheme": DAEMON_PREFLIGHT_SCHEME,
|
|
39
|
-
"X-Bridge-Device-Key":
|
|
112
|
+
"X-Bridge-Device-Key": identity.keyId,
|
|
40
113
|
"X-Bridge-Device-Nonce": nonce,
|
|
41
114
|
"X-Bridge-Device-Time": String(issuedAt),
|
|
42
115
|
"X-Bridge-Device-Signature": signature,
|
|
116
|
+
[SESSION_CERTIFICATE_HEADER]: encodeDeviceSessionCertificate(identity),
|
|
43
117
|
};
|
|
44
118
|
}
|
|
45
119
|
|
|
120
|
+
export function encodeDeviceSessionCertificate(identity) {
|
|
121
|
+
validateDeviceSessionIdentity(identity);
|
|
122
|
+
return Buffer.from(JSON.stringify(identity.certificate), "utf8").toString("base64url");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function signWithDeviceSessionIdentity(identity, transcript) {
|
|
126
|
+
validateDeviceSessionIdentity(identity);
|
|
127
|
+
const text = String(transcript || "");
|
|
128
|
+
if (!text || Buffer.byteLength(text) > 64 * 1024) throw new Error("device session signing transcript is empty or too large");
|
|
129
|
+
return signDeviceTranscript(identity, text);
|
|
130
|
+
}
|
|
131
|
+
|
|
46
132
|
export async function createDaemonAuthentication(identity, welcome, instanceId) {
|
|
47
|
-
|
|
133
|
+
validateDeviceSessionIdentity(identity);
|
|
48
134
|
const auth = welcome?.authentication;
|
|
49
|
-
if (!auth || auth.scheme !== DAEMON_AUTH_SCHEME) throw new Error("Worker did not
|
|
50
|
-
const
|
|
135
|
+
if (!auth || auth.scheme !== DAEMON_AUTH_SCHEME) throw new Error("Worker did not provide a supported device challenge");
|
|
136
|
+
const challenge = String(auth.challenge || "");
|
|
137
|
+
const issuedAt = Number(auth.issued_at);
|
|
138
|
+
const expiresAt = Number(auth.expires_at);
|
|
139
|
+
if (!/^daemon_challenge_[A-Za-z0-9_-]{40,96}$/.test(challenge)) throw new Error("Worker device challenge is invalid");
|
|
140
|
+
if (!Number.isSafeInteger(issuedAt) || !Number.isSafeInteger(expiresAt) || expiresAt <= issuedAt) throw new Error("Worker device challenge lifetime is invalid");
|
|
141
|
+
if (Math.floor(Date.now() / 1000) > expiresAt) throw new Error("Worker device challenge expired");
|
|
51
142
|
const transcript = daemonAuthTranscript({
|
|
52
|
-
challenge
|
|
53
|
-
workerOrigin,
|
|
54
|
-
server: welcome.server,
|
|
55
|
-
version: welcome.version,
|
|
143
|
+
challenge,
|
|
144
|
+
workerOrigin: String(welcome.worker_origin || ""),
|
|
145
|
+
server: String(welcome.server || ""),
|
|
146
|
+
version: String(welcome.version || ""),
|
|
56
147
|
instanceId,
|
|
57
|
-
issuedAt
|
|
148
|
+
issuedAt,
|
|
58
149
|
});
|
|
59
|
-
const privateKey = await webcrypto.subtle.importKey(
|
|
60
|
-
"jwk",
|
|
61
|
-
identity.privateJwk,
|
|
62
|
-
{ name: "ECDSA", namedCurve: DEVICE_CURVE },
|
|
63
|
-
false,
|
|
64
|
-
["sign"],
|
|
65
|
-
);
|
|
66
|
-
const signature = await webcrypto.subtle.sign(
|
|
67
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
68
|
-
privateKey,
|
|
69
|
-
new TextEncoder().encode(transcript),
|
|
70
|
-
);
|
|
71
150
|
return {
|
|
72
151
|
scheme: DAEMON_AUTH_SCHEME,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
signature:
|
|
152
|
+
key_id: identity.keyId,
|
|
153
|
+
challenge,
|
|
154
|
+
issued_at: issuedAt,
|
|
155
|
+
signature: signDeviceTranscript(identity, transcript),
|
|
77
156
|
};
|
|
78
157
|
}
|
|
79
158
|
|
|
80
|
-
export function deviceKeyId(publicJwk) {
|
|
81
|
-
validatePublicDeviceJwk(publicJwk);
|
|
82
|
-
const canonical = JSON.stringify({ crv: publicJwk.crv, kty: publicJwk.kty, x: publicJwk.x, y: publicJwk.y });
|
|
83
|
-
return `device_${createHash("sha256").update(canonical).digest("base64url").slice(0, 32)}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
159
|
export function validateDeviceIdentity(identity) {
|
|
87
160
|
if (!identity || typeof identity !== "object" || Array.isArray(identity)) throw new Error("device identity is missing");
|
|
88
|
-
if (identity.scheme !== DAEMON_AUTH_SCHEME) throw new Error("device identity scheme is
|
|
161
|
+
if (identity.scheme !== DAEMON_AUTH_SCHEME) throw new Error("device identity scheme is invalid");
|
|
89
162
|
validatePrivateDeviceJwk(identity.privateJwk);
|
|
90
163
|
validatePublicDeviceJwk(identity.publicJwk);
|
|
91
|
-
const
|
|
92
|
-
if (identity.
|
|
164
|
+
const expectedPublic = publicFromPrivate(identity.privateJwk);
|
|
165
|
+
if (!samePublicJwk(expectedPublic, identity.publicJwk)) throw new Error("device identity public and private keys do not match");
|
|
166
|
+
if (identity.keyId !== deviceKeyId(identity.publicJwk)) throw new Error("device identity key id is invalid");
|
|
167
|
+
if (!Number.isFinite(Date.parse(String(identity.createdAt || "")))) throw new Error("device identity creation time is invalid");
|
|
93
168
|
return identity;
|
|
94
169
|
}
|
|
95
170
|
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
171
|
+
export function validateDeviceSessionIdentity(identity, now = Date.now()) {
|
|
172
|
+
validateDeviceIdentity(identity);
|
|
173
|
+
const certificate = identity.certificate;
|
|
174
|
+
if (!certificate || typeof certificate !== "object" || Array.isArray(certificate)) throw new Error("device session certificate is missing");
|
|
175
|
+
if (certificate.scheme !== DEVICE_SESSION_CERTIFICATE_SCHEME) throw new Error("device session certificate scheme is invalid");
|
|
176
|
+
if (identity.keyId !== deviceKeyId(certificate.public_jwk) || !samePublicJwk(identity.publicJwk, certificate.public_jwk)) {
|
|
177
|
+
throw new Error("device session certificate key mismatch");
|
|
178
|
+
}
|
|
179
|
+
const expiresAt = Number(certificate.expires_at);
|
|
180
|
+
if (!Number.isSafeInteger(expiresAt) || Math.floor(Number(now) / 1000) > expiresAt) throw new Error("device session certificate expired");
|
|
181
|
+
if (!/^[A-Za-z0-9_-]{86}$/.test(String(certificate.signature || ""))) throw new Error("device session certificate signature is invalid");
|
|
182
|
+
return identity;
|
|
99
183
|
}
|
|
100
184
|
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
185
|
+
export function deviceKeyId(publicJwk) {
|
|
186
|
+
validatePublicDeviceJwk(publicJwk);
|
|
187
|
+
const canonical = JSON.stringify(canonicalPublicJwk(publicJwk));
|
|
188
|
+
return `device_${createHash("sha256").update(canonical).digest("base64url").slice(0, 32)}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function signDeviceTranscript(identity, transcript) {
|
|
192
|
+
validatePrivateDeviceJwk(identity?.privateJwk);
|
|
193
|
+
const key = createPrivateKey({ key: identity.privateJwk, format: "jwk" });
|
|
194
|
+
return sign("sha256", Buffer.from(transcript, "utf8"), { key, dsaEncoding: "ieee-p1363" }).toString("base64url");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function publicFromPrivate(privateJwk) {
|
|
198
|
+
const privateKey = createPrivateKey({ key: privateJwk, format: "jwk" });
|
|
199
|
+
return privateKey.export({ format: "jwk" });
|
|
108
200
|
}
|
|
201
|
+
|
|
202
|
+
function validatePrivateDeviceJwk(value) {
|
|
203
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("device private key is invalid");
|
|
204
|
+
if (value.kty !== DEVICE_KEY_TYPE || value.crv !== DEVICE_CURVE || typeof value.d !== "string") throw new Error("device private key is invalid");
|
|
205
|
+
validatePublicDeviceJwk(value);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function validatePublicDeviceJwk(value) {
|
|
209
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("device public key is invalid");
|
|
210
|
+
if (value.kty !== DEVICE_KEY_TYPE || value.crv !== DEVICE_CURVE || typeof value.x !== "string" || typeof value.y !== "string") throw new Error("device public key is invalid");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function samePublicJwk(left, right) {
|
|
214
|
+
return left?.kty === right?.kty && left?.crv === right?.crv && left?.x === right?.x && left?.y === right?.y;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const deviceWebCrypto = webcrypto;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDeviceIdentity,
|
|
3
|
+
createDeviceSessionDraft,
|
|
4
|
+
createDeviceSessionIdentity,
|
|
5
|
+
finalizeDeviceSessionIdentity,
|
|
6
|
+
publicDeviceJwkJson,
|
|
7
|
+
validateDeviceIdentity,
|
|
8
|
+
validatePublicDeviceRoot,
|
|
9
|
+
} from "./device-identity.mjs";
|
|
10
|
+
import {
|
|
11
|
+
configuredMacosTrustBrokerPath,
|
|
12
|
+
ensureMacosSecureDeviceRoot,
|
|
13
|
+
isMacosSecureDeviceRoot,
|
|
14
|
+
signWithMacosSecureDeviceRoot,
|
|
15
|
+
} from "./macos-trust-broker.mjs";
|
|
16
|
+
|
|
17
|
+
export function validateDeviceRootIdentity(identity) {
|
|
18
|
+
if (isMacosSecureDeviceRoot(identity)) return identity;
|
|
19
|
+
return validateDeviceIdentity(identity);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function publicDeviceRootJwkJson(identity) {
|
|
23
|
+
validateDeviceRootIdentity(identity);
|
|
24
|
+
return publicDeviceJwkJson(identity);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function ensurePreferredDeviceRoot({
|
|
28
|
+
profileDir,
|
|
29
|
+
workspaceHash,
|
|
30
|
+
existing = null,
|
|
31
|
+
rotate = false,
|
|
32
|
+
platform = process.platform,
|
|
33
|
+
brokerPath = undefined,
|
|
34
|
+
env = process.env,
|
|
35
|
+
secureRootFactory = ensureMacosSecureDeviceRoot,
|
|
36
|
+
} = {}) {
|
|
37
|
+
const configuredBroker = brokerPath === undefined ? configuredMacosTrustBrokerPath(env) : brokerPath;
|
|
38
|
+
const selectedBroker = configuredBroker || (isMacosSecureDeviceRoot(existing) ? existing.brokerPath : null);
|
|
39
|
+
if (platform === "darwin" && selectedBroker) {
|
|
40
|
+
return secureRootFactory({
|
|
41
|
+
profileDir,
|
|
42
|
+
workspaceHash,
|
|
43
|
+
existing,
|
|
44
|
+
rotate,
|
|
45
|
+
brokerPath: selectedBroker,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (!rotate && existing) return validateDeviceRootIdentity(existing);
|
|
49
|
+
return createDeviceIdentity();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function createDeviceSessionForRoot(identity, workerOrigin, server, version, {
|
|
53
|
+
profileDir,
|
|
54
|
+
now = Date.now(),
|
|
55
|
+
reason = "Authorize Machine Bridge startup",
|
|
56
|
+
} = {}) {
|
|
57
|
+
if (!isMacosSecureDeviceRoot(identity)) return createDeviceSessionIdentity(validateDeviceIdentity(identity), workerOrigin, server, version, now);
|
|
58
|
+
validatePublicDeviceRoot(identity);
|
|
59
|
+
const draft = createDeviceSessionDraft(identity, workerOrigin, server, version, now);
|
|
60
|
+
const signature = signWithMacosSecureDeviceRoot(identity, draft.transcript, { profileDir, reason });
|
|
61
|
+
return finalizeDeviceSessionIdentity(draft, signature);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function deviceRootProviderStatus(identity, { env = process.env } = {}) {
|
|
65
|
+
if (isMacosSecureDeviceRoot(identity)) {
|
|
66
|
+
return {
|
|
67
|
+
provider: identity.provider,
|
|
68
|
+
private_key_exportable: false,
|
|
69
|
+
root_storage: "Secure Enclave",
|
|
70
|
+
broker_provisioned: true,
|
|
71
|
+
session_signing: "one user-presence operation per daemon start",
|
|
72
|
+
reconnect_signing: "ephemeral in-memory key",
|
|
73
|
+
key_id: identity.keyId,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
validateDeviceIdentity(identity);
|
|
77
|
+
return {
|
|
78
|
+
provider: "portable-jwk-v1",
|
|
79
|
+
private_key_exportable: true,
|
|
80
|
+
root_storage: "owner-only local state",
|
|
81
|
+
broker_provisioned: Boolean(String(env?.MBM_MACOS_TRUST_BROKER || "").trim()),
|
|
82
|
+
secure_enclave_enrollment: "requires a provisioning-profile-validated app-like broker configured through MBM_MACOS_TRUST_BROKER",
|
|
83
|
+
session_signing: "local file-backed root",
|
|
84
|
+
reconnect_signing: "ephemeral in-memory key",
|
|
85
|
+
key_id: identity.keyId,
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/local/errors.mjs
CHANGED
|
@@ -6,6 +6,9 @@ const ERROR_CODES = new Set([
|
|
|
6
6
|
]);
|
|
7
7
|
|
|
8
8
|
const RETRYABLE_CODES = new Set(["timeout", "network_error", "unavailable"]);
|
|
9
|
+
const MAX_PUBLIC_ERROR_MESSAGE_CHARS = 2000;
|
|
10
|
+
const MAX_PUBLIC_ERROR_DETAILS_BYTES = 512 * 1024;
|
|
11
|
+
const UNSAFE_ERROR_CONTROLS = /[\u0000-\u001f\u007f\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g;
|
|
9
12
|
|
|
10
13
|
export class BridgeError extends Error {
|
|
11
14
|
constructor(code, message, options = {}) {
|
|
@@ -29,7 +32,7 @@ export function normalizeBridgeError(error, options = {}) {
|
|
|
29
32
|
: safeFallbackMessage(error, code);
|
|
30
33
|
return new BridgeError(code, message, {
|
|
31
34
|
cause: error instanceof Error ? error : undefined,
|
|
32
|
-
expose: options.expose
|
|
35
|
+
expose: options.expose === true,
|
|
33
36
|
retryable: options.retryable,
|
|
34
37
|
});
|
|
35
38
|
}
|
|
@@ -50,11 +53,12 @@ export function errorCode(error, fallback = "execution_failed") {
|
|
|
50
53
|
|
|
51
54
|
export function publicError(error, options = {}) {
|
|
52
55
|
const normalized = normalizeBridgeError(error, options);
|
|
56
|
+
const details = normalized.expose ? publicErrorDetails(normalized.details) : undefined;
|
|
53
57
|
return {
|
|
54
58
|
code: normalized.code,
|
|
55
|
-
message: normalized.expose ? normalized.message : defaultMessage(normalized.code),
|
|
59
|
+
message: publicErrorMessage(normalized.expose ? normalized.message : defaultMessage(normalized.code)),
|
|
56
60
|
retryable: normalized.retryable,
|
|
57
|
-
...(
|
|
61
|
+
...(details ? { details } : {}),
|
|
58
62
|
};
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -106,3 +110,21 @@ function defaultMessage(code) {
|
|
|
106
110
|
function isRecord(value) {
|
|
107
111
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
108
112
|
}
|
|
113
|
+
|
|
114
|
+
function publicErrorMessage(value) {
|
|
115
|
+
const normalized = String(value || "").replace(UNSAFE_ERROR_CONTROLS, " ").replace(/\s+/g, " ").trim();
|
|
116
|
+
return (normalized || "operation failed").slice(0, MAX_PUBLIC_ERROR_MESSAGE_CHARS);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function publicErrorDetails(value) {
|
|
120
|
+
if (!isRecord(value)) return undefined;
|
|
121
|
+
try {
|
|
122
|
+
const serialized = JSON.stringify(value, (_key, current) => {
|
|
123
|
+
if (["bigint", "function", "symbol", "undefined"].includes(typeof current)) throw new TypeError("unsupported error detail");
|
|
124
|
+
if (typeof current === "number" && !Number.isFinite(current)) throw new TypeError("unsupported error detail");
|
|
125
|
+
return current;
|
|
126
|
+
});
|
|
127
|
+
if (!serialized || Buffer.byteLength(serialized) > MAX_PUBLIC_ERROR_DETAILS_BYTES) return undefined;
|
|
128
|
+
return JSON.parse(serialized);
|
|
129
|
+
} catch { return undefined; }
|
|
130
|
+
}
|
|
@@ -4,10 +4,11 @@ import { BridgeError } from "./errors.mjs";
|
|
|
4
4
|
import { clampInteger } from "./numbers.mjs";
|
|
5
5
|
|
|
6
6
|
export class GitService {
|
|
7
|
-
constructor({ resolveExistingPath, displayPath,
|
|
7
|
+
constructor({ resolveExistingPath, displayPath, runInternalProcess, gitExecutable, maximumBytes }) {
|
|
8
8
|
this.resolveExistingPath = resolveExistingPath;
|
|
9
9
|
this.displayPath = displayPath;
|
|
10
|
-
this.
|
|
10
|
+
this.runInternalProcess = runInternalProcess;
|
|
11
|
+
this.gitExecutable = typeof gitExecutable === "function" ? gitExecutable : () => String(gitExecutable || "");
|
|
11
12
|
this.maximumBytes = maximumBytes;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -16,54 +17,54 @@ export class GitService {
|
|
|
16
17
|
if (!git.ok) return git.result;
|
|
17
18
|
const commandArgs = ["-c", "core.fsmonitor=false", "-C", git.root, "status", "--short", "--branch"];
|
|
18
19
|
if (git.pathspec) commandArgs.push("--", git.pathspec);
|
|
19
|
-
const result = await this.
|
|
20
|
-
return { ...result, path: this.displayPath(git.target), gitRoot: this.displayPath(git.root) };
|
|
20
|
+
const result = await this.runInternalProcess(this.gitExecutable(), commandArgs, 30_000, true, 512 * 1024, context);
|
|
21
|
+
return { ...result, path: this.displayPath(git.target, context), gitRoot: this.displayPath(git.root, context) };
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
async diff(args = {}, context = {}) {
|
|
24
25
|
const maxBytes = clampInteger(args.max_bytes, 1024 * 1024, 1, this.maximumBytes);
|
|
25
26
|
const git = await this.context(args.path || ".", context);
|
|
26
|
-
if (!git.ok) return { ...git.result, path: this.displayPath(git.target) };
|
|
27
|
+
if (!git.ok) return { ...git.result, path: this.displayPath(git.target, context) };
|
|
27
28
|
const commandArgs = ["-c", "core.fsmonitor=false", "-c", "diff.external=", "-C", git.root, "diff", "--no-ext-diff", "--no-textconv"];
|
|
28
29
|
if (args.staged) commandArgs.push("--cached");
|
|
29
30
|
if (git.pathspec) commandArgs.push("--", git.pathspec);
|
|
30
|
-
const result = await this.
|
|
31
|
-
return { ...result, path: this.displayPath(git.target), gitRoot: this.displayPath(git.root), staged: args.staged === true };
|
|
31
|
+
const result = await this.runInternalProcess(this.gitExecutable(), commandArgs, 60_000, true, maxBytes, context);
|
|
32
|
+
return { ...result, path: this.displayPath(git.target, context), gitRoot: this.displayPath(git.root, context), staged: args.staged === true };
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
async log(args = {}, context = {}) {
|
|
35
36
|
const git = await this.context(args.path || ".", context);
|
|
36
|
-
if (!git.ok) return { ...git.result, path: this.displayPath(git.target) };
|
|
37
|
+
if (!git.ok) return { ...git.result, path: this.displayPath(git.target, context) };
|
|
37
38
|
const maxCount = clampInteger(args.max_count, 20, 1, 100);
|
|
38
39
|
const format = "%H%x1f%h%x1f%aI%x1f%an%x1f%ae%x1f%s%x1e";
|
|
39
40
|
const commandArgs = ["-c", "core.fsmonitor=false", "-C", git.root, "log", `--max-count=${maxCount}`, `--format=${format}`];
|
|
40
41
|
if (git.pathspec) commandArgs.push("--", git.pathspec);
|
|
41
|
-
const result = await this.
|
|
42
|
+
const result = await this.runInternalProcess(this.gitExecutable(), commandArgs, 30_000, true, 1024 * 1024, context);
|
|
42
43
|
const commits = result.stdout.split("\x1e").map((record) => record.trim()).filter(Boolean).map((record) => {
|
|
43
44
|
const [hash, short, authored_at, author_name, author_email, subject] = record.split("\x1f");
|
|
44
45
|
const commit = { hash, short, authored_at, author_name, subject };
|
|
45
46
|
if (args.include_author_email === true) commit.author_email = author_email;
|
|
46
47
|
return commit;
|
|
47
48
|
});
|
|
48
|
-
return { code: result.code, stderr: result.stderr, commits, path: this.displayPath(git.target), gitRoot: this.displayPath(git.root) };
|
|
49
|
+
return { code: result.code, stderr: result.stderr, commits, path: this.displayPath(git.target, context), gitRoot: this.displayPath(git.root, context) };
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
async show(args = {}, context = {}) {
|
|
52
53
|
const git = await this.context(args.path || ".", context);
|
|
53
|
-
if (!git.ok) return { ...git.result, path: this.displayPath(git.target) };
|
|
54
|
+
if (!git.ok) return { ...git.result, path: this.displayPath(git.target, context) };
|
|
54
55
|
const revision = validateRevision(args.revision || "HEAD");
|
|
55
56
|
const maxBytes = clampInteger(args.max_bytes, 1024 * 1024, 1, this.maximumBytes);
|
|
56
57
|
const commandArgs = ["-c", "core.fsmonitor=false", "-c", "diff.external=", "-C", git.root, "show", "--no-ext-diff", "--no-textconv", "--decorate=no", revision];
|
|
57
58
|
if (git.pathspec) commandArgs.push("--", git.pathspec);
|
|
58
|
-
const result = await this.
|
|
59
|
-
return { ...result, revision, path: this.displayPath(git.target), gitRoot: this.displayPath(git.root) };
|
|
59
|
+
const result = await this.runInternalProcess(this.gitExecutable(), commandArgs, 60_000, true, maxBytes, context);
|
|
60
|
+
return { ...result, revision, path: this.displayPath(git.target, context), gitRoot: this.displayPath(git.root, context) };
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
async context(inputPath, context = {}) {
|
|
63
|
-
const target = await this.resolveExistingPath(inputPath);
|
|
64
|
+
const target = await this.resolveExistingPath(inputPath, context);
|
|
64
65
|
const info = await stat(target);
|
|
65
66
|
const cwd = info.isDirectory() ? target : dirname(target);
|
|
66
|
-
const result = await this.
|
|
67
|
+
const result = await this.runInternalProcess(this.gitExecutable(), ["-c", "core.fsmonitor=false", "-C", cwd, "rev-parse", "--show-toplevel"], 10_000, true, 512 * 1024, context);
|
|
67
68
|
if (result.code !== 0) return { ok: false, result, target };
|
|
68
69
|
const root = result.stdout.trim();
|
|
69
70
|
const repoRelative = relative(root, target);
|
package/src/local/job-runner.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { createExclusiveFileSync, removeOwnedJsonFileSync, replaceFileAtomically
|
|
|
9
9
|
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
10
10
|
import { currentProcessStartTimeMs } from "./process-identity.mjs";
|
|
11
11
|
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
12
|
+
import { persistManagedJobTerminal } from "./managed-job-terminal.mjs";
|
|
12
13
|
|
|
13
14
|
const RESOURCE_TOKEN = /\{\{resource:([a-z][a-z0-9._-]{0,63})\}\}/g;
|
|
14
15
|
const TEMP_TOKEN = /\{\{temp:([a-z][a-z0-9._-]{0,63})\}\}/g;
|
|
@@ -55,14 +56,15 @@ for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
|
55
56
|
|
|
56
57
|
const initial = readJson(statusFile, MAX_STATUS_BYTES);
|
|
57
58
|
assertLaunchState(initial);
|
|
58
|
-
createExclusiveFileSync(runnerPidFile, `${JSON.stringify({ pid: process.pid, processStartedAt: RUNNER_PROCESS_STARTED_AT })}\n`, { mode: 0o600 });
|
|
59
59
|
try {
|
|
60
|
+
createExclusiveFileSync(runnerPidFile, `${JSON.stringify({ pid: process.pid, processStartedAt: RUNNER_PROCESS_STARTED_AT })}\n`, { mode: 0o600 });
|
|
60
61
|
if (recover) await releaseRecoveryClaim();
|
|
61
62
|
const plan = readJson(planFile, 1024 * 1024);
|
|
62
63
|
assertPlanIntegrity(plan, initial);
|
|
63
64
|
await main(plan, initial);
|
|
64
65
|
} catch (error) {
|
|
65
66
|
recordFatalRunnerError(error);
|
|
67
|
+
process.exitCode = 1;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
async function releaseRecoveryClaim() {
|
|
@@ -125,7 +127,6 @@ async function main(plan, initial) {
|
|
|
125
127
|
} catch (error) {
|
|
126
128
|
cleanupError ||= error;
|
|
127
129
|
}
|
|
128
|
-
rmSync(runtimeDir, { recursive: true, force: true });
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
const cancelled = mainError instanceof JobCancelledError || existsSync(cancelFile);
|
|
@@ -148,17 +149,16 @@ async function main(plan, initial) {
|
|
|
148
149
|
capture_remaining_bytes: captureBudget.remaining,
|
|
149
150
|
finished_at: new Date().toISOString(),
|
|
150
151
|
};
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
152
|
+
const terminal = persistManagedJobTerminal({
|
|
153
|
+
statusFile, resultFile,
|
|
154
|
+
artifacts: [runtimeDir, planFile, runnerPidFile, cancelFile],
|
|
155
|
+
status: { ...status, runner_pid: process.pid, runner_process_started_at: RUNNER_PROCESS_STARTED_AT },
|
|
156
|
+
result, writeJson,
|
|
157
|
+
removeFile: (file) => rmSync(file, { recursive: true, force: true }),
|
|
158
|
+
maxStatusBytes: MAX_STATUS_BYTES, maxResultBytes: MAX_RESULT_BYTES,
|
|
158
159
|
});
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
try { rmSync(cancelFile, { force: true }); } catch {}
|
|
160
|
+
Object.assign(status, terminal.status);
|
|
161
|
+
reportTerminalPersistenceFailure(terminal);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
function assertLaunchState(status) {
|
|
@@ -176,7 +176,6 @@ function assertPlanIntegrity(plan, status) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
function recordFatalRunnerError(error) {
|
|
179
|
-
rmSync(runtimeDir, { recursive: true, force: true });
|
|
180
179
|
const now = new Date().toISOString();
|
|
181
180
|
let status = {};
|
|
182
181
|
try { status = readJson(statusFile, MAX_STATUS_BYTES); } catch {}
|
|
@@ -192,24 +191,33 @@ function recordFatalRunnerError(error) {
|
|
|
192
191
|
cleanup_error_class: recover ? classifyError(error) : null,
|
|
193
192
|
finished_at: now,
|
|
194
193
|
};
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
const terminal = persistManagedJobTerminal({
|
|
195
|
+
statusFile, resultFile,
|
|
196
|
+
artifacts: [runtimeDir, planFile, runnerPidFile, cancelFile],
|
|
197
|
+
status: {
|
|
198
198
|
...status,
|
|
199
|
-
status: finalStatus,
|
|
200
|
-
current_phase: null,
|
|
201
|
-
current_step: null,
|
|
202
199
|
runner_pid: process.pid,
|
|
203
200
|
runner_process_started_at: RUNNER_PROCESS_STARTED_AT,
|
|
204
|
-
updated_at: now,
|
|
205
|
-
finished_at: now,
|
|
206
|
-
error_class: result.error_class,
|
|
207
201
|
cleanup_guarantee: "best-effort-finally-and-recovery",
|
|
208
|
-
},
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
202
|
+
},
|
|
203
|
+
result, writeJson,
|
|
204
|
+
removeFile: (file) => rmSync(file, { recursive: true, force: true }),
|
|
205
|
+
maxStatusBytes: MAX_STATUS_BYTES, maxResultBytes: MAX_RESULT_BYTES,
|
|
206
|
+
});
|
|
207
|
+
reportTerminalPersistenceFailure(terminal);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function reportTerminalPersistenceFailure(terminal) {
|
|
211
|
+
if (terminal.statusPersisted && terminal.artifactsScrubbed && !terminal.statusErrorClass) return;
|
|
212
|
+
const fields = [
|
|
213
|
+
`status_persisted=${terminal.statusPersisted}`,
|
|
214
|
+
`result_persisted=${terminal.resultPersisted}`,
|
|
215
|
+
`artifacts_scrubbed=${terminal.artifactsScrubbed}`,
|
|
216
|
+
`status_error=${terminal.statusErrorClass || "none"}`,
|
|
217
|
+
`result_error=${terminal.resultErrorClass || "none"}`,
|
|
218
|
+
`cleanup_error=${terminal.cleanupErrorClass || "none"}`,
|
|
219
|
+
];
|
|
220
|
+
process.stderr.write(`managed job terminal persistence incomplete: ${fields.join(" ")}\n`);
|
|
213
221
|
}
|
|
214
222
|
|
|
215
223
|
async function runStep(step, index, phase, plan, resourceContext, cancellationAware, captureBudget) {
|
package/src/local/log.mjs
CHANGED
|
@@ -46,9 +46,21 @@ export function createLogger(options = {}) {
|
|
|
46
46
|
|
|
47
47
|
const write = (stream, level, label, color, message, fields) => {
|
|
48
48
|
if (LEVEL_RANK[level] < LEVEL_RANK[minimumLevel]) return;
|
|
49
|
+
const sanitizedMessage = sanitizeLogText(message, MAX_LOG_MESSAGE_CHARS);
|
|
50
|
+
if (options.format === "json") {
|
|
51
|
+
const entry = sanitizeLogValue({
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
level: level === "success" ? "info" : level,
|
|
54
|
+
component,
|
|
55
|
+
message: sanitizedMessage,
|
|
56
|
+
...(fields && typeof fields === "object" ? fields : {}),
|
|
57
|
+
});
|
|
58
|
+
stream.write(`${JSON.stringify(entry)}\n`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
49
61
|
const prefix = useColor ? `${color}${label}${COLORS.reset}` : label;
|
|
50
62
|
const suffix = formatFields(fields);
|
|
51
|
-
stream.write(`${prefix} ${component}: ${
|
|
63
|
+
stream.write(`${prefix} ${component}: ${sanitizedMessage}${suffix}\n`);
|
|
52
64
|
};
|
|
53
65
|
|
|
54
66
|
const event = (level, name, fields = {}, message = "") => {
|
|
@@ -83,6 +95,7 @@ export function createLogger(options = {}) {
|
|
|
83
95
|
return {
|
|
84
96
|
verbose: minimumLevel === "debug",
|
|
85
97
|
level: minimumLevel,
|
|
98
|
+
format: options.format === "json" ? "json" : "text",
|
|
86
99
|
child(childComponent) {
|
|
87
100
|
return createLogger({ ...options, component: childComponent });
|
|
88
101
|
},
|