@microck/canonfig 2.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/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/agent/agent-resolution.errors.js +42 -0
- package/dist/agent/agent-resolution.layer.js +204 -0
- package/dist/agent/agent-resolution.service.js +2259 -0
- package/dist/agent/agent-resolution.types.js +1 -0
- package/dist/agent/controlled-executor.js +704 -0
- package/dist/agent/harness-adapters.js +85 -0
- package/dist/cli/cli.js +618 -0
- package/dist/cli/exit-codes.js +28 -0
- package/dist/cli/follower-commands.js +3 -0
- package/dist/cli/render.js +56 -0
- package/dist/cli/source-commands.js +5 -0
- package/dist/domain/brand.js +29 -0
- package/dist/domain/identity.js +31 -0
- package/dist/domain/npm-package-spec.js +186 -0
- package/dist/domain/profile.js +950 -0
- package/dist/domain/recipe-versions.js +297 -0
- package/dist/domain/resource.js +259 -0
- package/dist/domain/synchronization.js +346 -0
- package/dist/enrollment/enrollment.errors.js +43 -0
- package/dist/enrollment/enrollment.layer.js +724 -0
- package/dist/enrollment/enrollment.service.js +3 -0
- package/dist/enrollment/enrollment.types.js +59 -0
- package/dist/enrollment/follower-client.js +585 -0
- package/dist/enrollment/source-server.js +313 -0
- package/dist/machine/linux.layer.js +1183 -0
- package/dist/machine/machine-state.errors.js +52 -0
- package/dist/machine/machine-state.service.js +3 -0
- package/dist/machine/machine-state.types.js +1 -0
- package/dist/machine/macos.layer.js +470 -0
- package/dist/machine/windows.layer.js +879 -0
- package/dist/profile/discovery.js +740 -0
- package/dist/profile/profile-catalog.errors.js +50 -0
- package/dist/profile/profile-catalog.layer.js +20 -0
- package/dist/profile/profile-catalog.service.js +7 -0
- package/dist/profile/profile-codec.js +153 -0
- package/dist/profile/publication.js +298 -0
- package/dist/profile/tool-catalog.js +384 -0
- package/dist/runtime/doctor.js +306 -0
- package/dist/runtime/layers.js +706 -0
- package/dist/runtime/main.js +38 -0
- package/dist/schedule/linux-schedule.js +24 -0
- package/dist/schedule/macos-schedule.js +25 -0
- package/dist/schedule/schedule-manager.errors.js +17 -0
- package/dist/schedule/schedule-manager.layer.js +205 -0
- package/dist/schedule/schedule-manager.service.js +3 -0
- package/dist/schedule/schedule-manager.types.js +114 -0
- package/dist/schedule/windows-schedule.js +25 -0
- package/dist/state/state-repository.errors.js +55 -0
- package/dist/state/state-repository.layer.js +1507 -0
- package/dist/state/state-repository.service.js +3 -0
- package/dist/state/state-repository.types.js +1 -0
- package/dist/state/state-schema.js +298 -0
- package/dist/synchronization/config-codec.js +97 -0
- package/dist/synchronization/executor.js +700 -0
- package/dist/synchronization/follower-orchestration.js +939 -0
- package/dist/synchronization/follower-sync-config.js +81 -0
- package/dist/synchronization/npm-artifact.js +670 -0
- package/dist/synchronization/planner.js +378 -0
- package/dist/synchronization/recovery.js +397 -0
- package/dist/synchronization/resource-executors.js +1198 -0
- package/dist/synchronization/resource-plans.js +645 -0
- package/dist/synchronization/synchronization.errors.js +102 -0
- package/dist/synchronization/synchronization.layer.js +97 -0
- package/dist/synchronization/synchronization.service.js +11 -0
- package/dist/synchronization/synchronization.types.js +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { createServer } from "node:https";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { Effect, Redacted, Schema } from "effect";
|
|
4
|
+
import { MachineState } from "../machine/machine-state.service.js";
|
|
5
|
+
import { DuplicateFollowerIdentityError, EnrollmentConfigurationError, EnrollmentFingerprintMismatchError, EnrollmentSourceMismatchError, InvitationExpiredError, InvitationNotFoundError, InvitationReplayError, InvalidFollowerCredentialError, MalformedEnrollmentRequestError, RevokedFollowerCredentialError, SourceNotInitializedError, TransportIntegrityError, TransportResourceNotFoundError, TransportSizeLimitError, TransportUnauthorizedError, } from "./enrollment.errors.js";
|
|
6
|
+
import { Enrollment } from "./enrollment.service.js";
|
|
7
|
+
import { EnrollFollowerRequestSchema, } from "./enrollment.types.js";
|
|
8
|
+
const maximumRequestBytes = 64 * 1024;
|
|
9
|
+
const defaultMaximumMetadataBytes = 1024 * 1024;
|
|
10
|
+
const defaultMaximumBlobBytes = 8 * 1024 * 1024;
|
|
11
|
+
const AddressInfoSchema = Schema.Struct({
|
|
12
|
+
address: Schema.String,
|
|
13
|
+
family: Schema.String,
|
|
14
|
+
port: Schema.Number,
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Convert only unambiguous loopback host spellings to the host passed to
|
|
18
|
+
* `listen`. DNS names, wildcard addresses, encoded values, and IPv6 zone
|
|
19
|
+
* identifiers are deliberately excluded from this boundary.
|
|
20
|
+
*/
|
|
21
|
+
export const canonicalLoopbackHostname = (value) => {
|
|
22
|
+
if (value.length === 0 || value.trim() !== value) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
if (value.toLowerCase() === "localhost")
|
|
26
|
+
return "127.0.0.1";
|
|
27
|
+
if (isIP(value) === 4 && value.startsWith("127.")) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
if (isIP(value) !== 6)
|
|
31
|
+
return undefined;
|
|
32
|
+
try {
|
|
33
|
+
const canonical = new URL(`https://[${value}]`).hostname;
|
|
34
|
+
return canonical === "[::1]" ? "::1" : undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const sendJson = (response, status, body) => {
|
|
41
|
+
const encoded = JSON.stringify(body);
|
|
42
|
+
response.writeHead(status, {
|
|
43
|
+
"content-type": "application/json; charset=utf-8",
|
|
44
|
+
"content-length": Buffer.byteLength(encoded),
|
|
45
|
+
"cache-control": "no-store",
|
|
46
|
+
});
|
|
47
|
+
response.end(encoded);
|
|
48
|
+
};
|
|
49
|
+
const errorStatus = (error) => {
|
|
50
|
+
if (error instanceof MalformedEnrollmentRequestError)
|
|
51
|
+
return 400;
|
|
52
|
+
if (error instanceof EnrollmentSourceMismatchError
|
|
53
|
+
|| error instanceof EnrollmentFingerprintMismatchError)
|
|
54
|
+
return 409;
|
|
55
|
+
if (error instanceof InvitationNotFoundError
|
|
56
|
+
|| error instanceof InvalidFollowerCredentialError)
|
|
57
|
+
return 401;
|
|
58
|
+
if (error instanceof InvitationExpiredError
|
|
59
|
+
|| error instanceof InvitationReplayError
|
|
60
|
+
|| error instanceof DuplicateFollowerIdentityError)
|
|
61
|
+
return 410;
|
|
62
|
+
if (error instanceof RevokedFollowerCredentialError)
|
|
63
|
+
return 403;
|
|
64
|
+
if (error instanceof TransportResourceNotFoundError
|
|
65
|
+
|| error instanceof TransportUnauthorizedError)
|
|
66
|
+
return 404;
|
|
67
|
+
if (error instanceof TransportSizeLimitError)
|
|
68
|
+
return 413;
|
|
69
|
+
if (error instanceof TransportIntegrityError)
|
|
70
|
+
return 422;
|
|
71
|
+
return 500;
|
|
72
|
+
};
|
|
73
|
+
const bearerCredential = (authorization) => {
|
|
74
|
+
const match = /^Bearer ([A-Za-z0-9_-]{32,512})$/u.exec(authorization ?? "");
|
|
75
|
+
if (match === null) {
|
|
76
|
+
throw new InvalidFollowerCredentialError({
|
|
77
|
+
message: "the follower credential is invalid",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return match[1];
|
|
81
|
+
};
|
|
82
|
+
const readBody = (request) => new Promise((resolveBody, rejectBody) => {
|
|
83
|
+
const chunks = [];
|
|
84
|
+
let bytes = 0;
|
|
85
|
+
request.on("data", (chunk) => {
|
|
86
|
+
bytes += chunk.byteLength;
|
|
87
|
+
if (bytes > maximumRequestBytes) {
|
|
88
|
+
rejectBody(new Error("request body exceeds the size limit"));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
chunks.push(chunk);
|
|
92
|
+
});
|
|
93
|
+
request.on("end", () => {
|
|
94
|
+
try {
|
|
95
|
+
resolveBody(Schema.decodeUnknownSync(EnrollFollowerRequestSchema)(JSON.parse(Buffer.concat(chunks).toString("utf8"))));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
rejectBody(new Error("request body is not valid JSON"));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
request.on("error", rejectBody);
|
|
102
|
+
});
|
|
103
|
+
const runRequestEffect = async (effect) => {
|
|
104
|
+
const result = await Effect.runPromise(Effect.result(effect));
|
|
105
|
+
if (result._tag === "Failure")
|
|
106
|
+
throw result.failure;
|
|
107
|
+
return result.success;
|
|
108
|
+
};
|
|
109
|
+
const asEnrollmentError = (error) => {
|
|
110
|
+
if (error instanceof DuplicateFollowerIdentityError
|
|
111
|
+
|| error instanceof EnrollmentConfigurationError
|
|
112
|
+
|| error instanceof EnrollmentFingerprintMismatchError
|
|
113
|
+
|| error instanceof EnrollmentSourceMismatchError
|
|
114
|
+
|| error instanceof InvitationExpiredError
|
|
115
|
+
|| error instanceof InvitationNotFoundError
|
|
116
|
+
|| error instanceof InvitationReplayError
|
|
117
|
+
|| error instanceof InvalidFollowerCredentialError
|
|
118
|
+
|| error instanceof MalformedEnrollmentRequestError
|
|
119
|
+
|| error instanceof RevokedFollowerCredentialError
|
|
120
|
+
|| error instanceof SourceNotInitializedError
|
|
121
|
+
|| error instanceof TransportIntegrityError
|
|
122
|
+
|| error instanceof TransportResourceNotFoundError
|
|
123
|
+
|| error instanceof TransportSizeLimitError
|
|
124
|
+
|| error instanceof TransportUnauthorizedError)
|
|
125
|
+
return error;
|
|
126
|
+
return new EnrollmentConfigurationError({
|
|
127
|
+
operation: "serve enrollment request",
|
|
128
|
+
message: "the enrollment request failed",
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
export const startSourceServer = (input = {}) => Effect.gen(function* () {
|
|
132
|
+
const enrollment = yield* Enrollment;
|
|
133
|
+
const machine = yield* MachineState;
|
|
134
|
+
const source = yield* enrollment.source().pipe(Effect.mapError(() => new EnrollmentConfigurationError({
|
|
135
|
+
operation: "start enrollment server",
|
|
136
|
+
message: "source enrollment identity is unavailable",
|
|
137
|
+
})));
|
|
138
|
+
const key = yield* machine.loadCredential({
|
|
139
|
+
reference: source.tlsKeyReference,
|
|
140
|
+
}).pipe(Effect.mapError(() => new EnrollmentConfigurationError({
|
|
141
|
+
operation: "load source TLS key",
|
|
142
|
+
message: "source TLS credentials are unavailable",
|
|
143
|
+
})));
|
|
144
|
+
const certificate = yield* machine.loadCredential({
|
|
145
|
+
reference: source.tlsCertificateReference,
|
|
146
|
+
}).pipe(Effect.mapError(() => new EnrollmentConfigurationError({
|
|
147
|
+
operation: "load source TLS certificate",
|
|
148
|
+
message: "source TLS credentials are unavailable",
|
|
149
|
+
})));
|
|
150
|
+
const requestedHostname = input.hostname === undefined
|
|
151
|
+
? "127.0.0.1"
|
|
152
|
+
: input.hostname;
|
|
153
|
+
const decodedHostname = Schema.decodeUnknownOption(Schema.String)(requestedHostname);
|
|
154
|
+
const hostname = decodedHostname._tag === "Some"
|
|
155
|
+
? canonicalLoopbackHostname(decodedHostname.value)
|
|
156
|
+
: undefined;
|
|
157
|
+
if (hostname === undefined) {
|
|
158
|
+
return yield* new EnrollmentConfigurationError({
|
|
159
|
+
operation: "start enrollment server",
|
|
160
|
+
message: "the source server host must be an unambiguous loopback address",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const maximumMetadataBytes = input.maximumMetadataBytes
|
|
164
|
+
?? defaultMaximumMetadataBytes;
|
|
165
|
+
const maximumBlobBytes = input.maximumBlobBytes ?? defaultMaximumBlobBytes;
|
|
166
|
+
let blobRequests = 0;
|
|
167
|
+
const server = createServer({
|
|
168
|
+
key: Redacted.value(key),
|
|
169
|
+
cert: Redacted.value(certificate),
|
|
170
|
+
minVersion: "TLSv1.2",
|
|
171
|
+
}, (request, response) => {
|
|
172
|
+
const route = async () => {
|
|
173
|
+
const requestUrl = new URL(request.url ?? "/", "https://loopback.invalid");
|
|
174
|
+
if (request.method === "GET" && request.url === "/v1/enrollment/source") {
|
|
175
|
+
sendJson(response, 200, {
|
|
176
|
+
source: source.source,
|
|
177
|
+
tlsFingerprint: source.tlsFingerprint,
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (request.method === "POST" && request.url === "/v1/enrollment") {
|
|
182
|
+
const body = await readBody(request).catch(() => {
|
|
183
|
+
throw new MalformedEnrollmentRequestError({
|
|
184
|
+
message: "the enrollment request is malformed",
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
const enrolled = await runRequestEffect(enrollment.enrollFollower(body));
|
|
188
|
+
sendJson(response, 201, enrolled);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (request.method === "POST"
|
|
192
|
+
&& request.url === "/v1/enrollment/finalize") {
|
|
193
|
+
await runRequestEffect(enrollment.finalizeFollower(bearerCredential(request.headers.authorization)));
|
|
194
|
+
sendJson(response, 200, { ok: true });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (request.method === "POST"
|
|
198
|
+
&& request.url === "/v1/enrollment/cancel") {
|
|
199
|
+
await runRequestEffect(enrollment.cancelPendingEnrollment(bearerCredential(request.headers.authorization)));
|
|
200
|
+
sendJson(response, 200, { ok: true });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (request.method === "POST"
|
|
204
|
+
&& request.url === "/v1/enrollment/revoke") {
|
|
205
|
+
await runRequestEffect(enrollment.revokeAuthenticatedFollower(bearerCredential(request.headers.authorization)));
|
|
206
|
+
sendJson(response, 200, { ok: true });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (request.method === "GET"
|
|
210
|
+
&& request.url === "/v1/enrollment/authenticate") {
|
|
211
|
+
const authenticated = await runRequestEffect(enrollment.authenticate(bearerCredential(request.headers.authorization)));
|
|
212
|
+
sendJson(response, 200, authenticated);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (request.method === "GET"
|
|
216
|
+
&& requestUrl.pathname === "/v1/transport/revisions") {
|
|
217
|
+
const revisions = await runRequestEffect(enrollment.listAuthorizedRevisions(bearerCredential(request.headers.authorization)));
|
|
218
|
+
if (Buffer.byteLength(JSON.stringify(revisions)) > maximumMetadataBytes) {
|
|
219
|
+
throw new TransportSizeLimitError({
|
|
220
|
+
artifact: "revision-list",
|
|
221
|
+
limit: maximumMetadataBytes,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
sendJson(response, 200, revisions);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const revisionMatch = /^\/v1\/transport\/revisions\/([^/]+)$/u.exec(requestUrl.pathname);
|
|
228
|
+
if (request.method === "GET" && revisionMatch !== null) {
|
|
229
|
+
const metadata = await runRequestEffect(enrollment.getAuthorizedRevision(bearerCredential(request.headers.authorization), decodeURIComponent(revisionMatch[1])));
|
|
230
|
+
if (Buffer.byteLength(JSON.stringify(metadata)) > maximumMetadataBytes) {
|
|
231
|
+
throw new TransportSizeLimitError({
|
|
232
|
+
artifact: "revision-metadata",
|
|
233
|
+
limit: maximumMetadataBytes,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
sendJson(response, 200, metadata);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const blobMatch = /^\/v1\/transport\/blobs\/([a-f0-9]{64})$/u.exec(requestUrl.pathname);
|
|
240
|
+
if (request.method === "GET" && blobMatch !== null) {
|
|
241
|
+
blobRequests += 1;
|
|
242
|
+
const blob = await runRequestEffect(enrollment.getAuthorizedBlob(bearerCredential(request.headers.authorization), blobMatch[1]));
|
|
243
|
+
if (blob.byteLength > maximumBlobBytes) {
|
|
244
|
+
throw new TransportSizeLimitError({
|
|
245
|
+
artifact: "blob",
|
|
246
|
+
limit: maximumBlobBytes,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
response.writeHead(200, {
|
|
250
|
+
"content-type": "application/octet-stream",
|
|
251
|
+
"content-length": blob.byteLength,
|
|
252
|
+
"cache-control": "no-store",
|
|
253
|
+
});
|
|
254
|
+
response.end(blob);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
sendJson(response, 404, {
|
|
258
|
+
error: "NotFound",
|
|
259
|
+
message: "the enrollment endpoint does not exist",
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
route().catch((cause) => {
|
|
263
|
+
const error = cause instanceof Error
|
|
264
|
+
? cause
|
|
265
|
+
: new EnrollmentConfigurationError({
|
|
266
|
+
operation: "serve enrollment request",
|
|
267
|
+
message: "the enrollment request failed",
|
|
268
|
+
});
|
|
269
|
+
const enrollmentError = asEnrollmentError(error);
|
|
270
|
+
sendJson(response, errorStatus(enrollmentError), {
|
|
271
|
+
error: enrollmentError._tag ?? "EnrollmentConfigurationError",
|
|
272
|
+
message: "message" in enrollmentError
|
|
273
|
+
&& String(enrollmentError.message).length > 0
|
|
274
|
+
? String(enrollmentError.message)
|
|
275
|
+
: "the transport request failed",
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
server.requestTimeout = 10_000;
|
|
280
|
+
server.headersTimeout = 10_000;
|
|
281
|
+
server.keepAliveTimeout = 1_000;
|
|
282
|
+
const address = yield* Effect.tryPromise({
|
|
283
|
+
try: () => new Promise((resolveAddress, rejectAddress) => {
|
|
284
|
+
const onError = (cause) => rejectAddress(cause);
|
|
285
|
+
server.once("error", onError);
|
|
286
|
+
server.listen(input.port ?? 0, hostname, () => {
|
|
287
|
+
server.off("error", onError);
|
|
288
|
+
const bound = Schema.decodeUnknownSync(AddressInfoSchema)(server.address());
|
|
289
|
+
resolveAddress(bound);
|
|
290
|
+
});
|
|
291
|
+
}),
|
|
292
|
+
catch: () => new EnrollmentConfigurationError({
|
|
293
|
+
operation: "start enrollment server",
|
|
294
|
+
message: "the loopback HTTPS server could not start",
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
297
|
+
const host = address.family === "IPv6"
|
|
298
|
+
? `[${address.address}]`
|
|
299
|
+
: address.address;
|
|
300
|
+
return {
|
|
301
|
+
endpoint: `https://${host}:${address.port}`,
|
|
302
|
+
fingerprint: source.tlsFingerprint,
|
|
303
|
+
blobRequests: () => blobRequests,
|
|
304
|
+
close: () => new Promise((resolveClose, rejectClose) => {
|
|
305
|
+
server.close((cause) => {
|
|
306
|
+
if (cause === undefined)
|
|
307
|
+
resolveClose();
|
|
308
|
+
else
|
|
309
|
+
rejectClose(cause);
|
|
310
|
+
});
|
|
311
|
+
}),
|
|
312
|
+
};
|
|
313
|
+
});
|