@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -0
  3. package/dist/agent/agent-resolution.errors.js +42 -0
  4. package/dist/agent/agent-resolution.layer.js +204 -0
  5. package/dist/agent/agent-resolution.service.js +2259 -0
  6. package/dist/agent/agent-resolution.types.js +1 -0
  7. package/dist/agent/controlled-executor.js +704 -0
  8. package/dist/agent/harness-adapters.js +85 -0
  9. package/dist/cli/cli.js +618 -0
  10. package/dist/cli/exit-codes.js +28 -0
  11. package/dist/cli/follower-commands.js +3 -0
  12. package/dist/cli/render.js +56 -0
  13. package/dist/cli/source-commands.js +5 -0
  14. package/dist/domain/brand.js +29 -0
  15. package/dist/domain/identity.js +31 -0
  16. package/dist/domain/npm-package-spec.js +186 -0
  17. package/dist/domain/profile.js +950 -0
  18. package/dist/domain/recipe-versions.js +297 -0
  19. package/dist/domain/resource.js +259 -0
  20. package/dist/domain/synchronization.js +346 -0
  21. package/dist/enrollment/enrollment.errors.js +43 -0
  22. package/dist/enrollment/enrollment.layer.js +724 -0
  23. package/dist/enrollment/enrollment.service.js +3 -0
  24. package/dist/enrollment/enrollment.types.js +59 -0
  25. package/dist/enrollment/follower-client.js +585 -0
  26. package/dist/enrollment/source-server.js +313 -0
  27. package/dist/machine/linux.layer.js +1183 -0
  28. package/dist/machine/machine-state.errors.js +52 -0
  29. package/dist/machine/machine-state.service.js +3 -0
  30. package/dist/machine/machine-state.types.js +1 -0
  31. package/dist/machine/macos.layer.js +470 -0
  32. package/dist/machine/windows.layer.js +879 -0
  33. package/dist/profile/discovery.js +740 -0
  34. package/dist/profile/profile-catalog.errors.js +50 -0
  35. package/dist/profile/profile-catalog.layer.js +20 -0
  36. package/dist/profile/profile-catalog.service.js +7 -0
  37. package/dist/profile/profile-codec.js +153 -0
  38. package/dist/profile/publication.js +298 -0
  39. package/dist/profile/tool-catalog.js +384 -0
  40. package/dist/runtime/doctor.js +306 -0
  41. package/dist/runtime/layers.js +706 -0
  42. package/dist/runtime/main.js +38 -0
  43. package/dist/schedule/linux-schedule.js +24 -0
  44. package/dist/schedule/macos-schedule.js +25 -0
  45. package/dist/schedule/schedule-manager.errors.js +17 -0
  46. package/dist/schedule/schedule-manager.layer.js +205 -0
  47. package/dist/schedule/schedule-manager.service.js +3 -0
  48. package/dist/schedule/schedule-manager.types.js +114 -0
  49. package/dist/schedule/windows-schedule.js +25 -0
  50. package/dist/state/state-repository.errors.js +55 -0
  51. package/dist/state/state-repository.layer.js +1507 -0
  52. package/dist/state/state-repository.service.js +3 -0
  53. package/dist/state/state-repository.types.js +1 -0
  54. package/dist/state/state-schema.js +298 -0
  55. package/dist/synchronization/config-codec.js +97 -0
  56. package/dist/synchronization/executor.js +700 -0
  57. package/dist/synchronization/follower-orchestration.js +939 -0
  58. package/dist/synchronization/follower-sync-config.js +81 -0
  59. package/dist/synchronization/npm-artifact.js +670 -0
  60. package/dist/synchronization/planner.js +378 -0
  61. package/dist/synchronization/recovery.js +397 -0
  62. package/dist/synchronization/resource-executors.js +1198 -0
  63. package/dist/synchronization/resource-plans.js +645 -0
  64. package/dist/synchronization/synchronization.errors.js +102 -0
  65. package/dist/synchronization/synchronization.layer.js +97 -0
  66. package/dist/synchronization/synchronization.service.js +11 -0
  67. package/dist/synchronization/synchronization.types.js +1 -0
  68. 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
+ });