@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,56 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
const secretField = /^(?:credential|credentialValue|password|secret|privateKey|signingKey|tlsKey|accessToken|refreshToken|apiKey|authorization|cookie)$/iu;
|
|
3
|
+
const redact = (value) => {
|
|
4
|
+
if (Array.isArray(value))
|
|
5
|
+
return value.map(redact);
|
|
6
|
+
if (value === null
|
|
7
|
+
|| Schema.is(Schema.String)(value)
|
|
8
|
+
|| Schema.is(Schema.Number)(value)
|
|
9
|
+
|| Schema.is(Schema.Boolean)(value))
|
|
10
|
+
return value;
|
|
11
|
+
const result = {};
|
|
12
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
13
|
+
if (entry === undefined)
|
|
14
|
+
continue;
|
|
15
|
+
result[key] = secretField.test(key) ? "[REDACTED]" : redact(entry);
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
const ordered = (value) => {
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return value.map(ordered);
|
|
22
|
+
if (value === null
|
|
23
|
+
|| Schema.is(Schema.String)(value)
|
|
24
|
+
|| Schema.is(Schema.Number)(value)
|
|
25
|
+
|| Schema.is(Schema.Boolean)(value))
|
|
26
|
+
return value;
|
|
27
|
+
const result = {};
|
|
28
|
+
for (const [key, entry] of Object.entries(value)
|
|
29
|
+
.sort(([left], [right]) => left.localeCompare(right))) {
|
|
30
|
+
if (entry === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
result[key] = ordered(entry);
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
export const sanitizeCliData = (value) => ordered(redact(value));
|
|
37
|
+
export const renderCliResult = (result, format) => {
|
|
38
|
+
const data = result.data === undefined
|
|
39
|
+
? undefined
|
|
40
|
+
: sanitizeCliData(result.data);
|
|
41
|
+
if (format === "json") {
|
|
42
|
+
const envelope = {
|
|
43
|
+
schema: "canonfig.cli/v1",
|
|
44
|
+
command: result.command,
|
|
45
|
+
status: result.exitCode === 0 ? "success" : "error",
|
|
46
|
+
exitCode: result.exitCode,
|
|
47
|
+
message: result.message,
|
|
48
|
+
};
|
|
49
|
+
if (data !== undefined)
|
|
50
|
+
envelope.data = data;
|
|
51
|
+
return `${JSON.stringify(envelope)}\n`;
|
|
52
|
+
}
|
|
53
|
+
if (data === undefined)
|
|
54
|
+
return `${result.message}\n`;
|
|
55
|
+
return `${result.message}\n${JSON.stringify(data, null, 2)}\n`;
|
|
56
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* Branded identifier schemas. Every identifier that crosses a boundary or is
|
|
4
|
+
* persisted is branded so an `AgentName` cannot be used where a
|
|
5
|
+
* `FollowerId` is required.
|
|
6
|
+
*/
|
|
7
|
+
const IdentifierText = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u, {
|
|
8
|
+
expected: "a non-empty portable identifier",
|
|
9
|
+
}));
|
|
10
|
+
const ReferenceText = Schema.NonEmptyString;
|
|
11
|
+
export const ProfileId = IdentifierText.pipe(Schema.brand("ProfileId"));
|
|
12
|
+
export const ProfileRevisionId = IdentifierText.pipe(Schema.brand("ProfileRevisionId"));
|
|
13
|
+
export const ResourceId = IdentifierText.pipe(Schema.brand("ResourceId"));
|
|
14
|
+
export const FollowerId = IdentifierText.pipe(Schema.brand("FollowerId"));
|
|
15
|
+
export const GroupName = IdentifierText.pipe(Schema.brand("GroupName"));
|
|
16
|
+
export const RunId = IdentifierText.pipe(Schema.brand("RunId"));
|
|
17
|
+
export const ActionId = IdentifierText.pipe(Schema.brand("ActionId"));
|
|
18
|
+
export const AgentTaskId = IdentifierText.pipe(Schema.brand("AgentTaskId"));
|
|
19
|
+
export const InvitationCode = ReferenceText.pipe(Schema.brand("InvitationCode"));
|
|
20
|
+
export const CredentialReference = ReferenceText.pipe(Schema.brand("CredentialReference"));
|
|
21
|
+
export const ContentDigest = Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/u, { expected: "a lowercase SHA-256 digest" })).pipe(Schema.brand("ContentDigest"));
|
|
22
|
+
export const BlobId = ContentDigest.pipe(Schema.brand("BlobId"));
|
|
23
|
+
export const CertificateFingerprint = ReferenceText.pipe(Schema.brand("CertificateFingerprint"));
|
|
24
|
+
export const SourceSignature = ReferenceText.pipe(Schema.brand("SourceSignature"));
|
|
25
|
+
export const ToolId = IdentifierText.pipe(Schema.brand("ToolId"));
|
|
26
|
+
/** RFC 3339 timestamp kept as a string at persistence and wire boundaries. */
|
|
27
|
+
export const Timestamp = Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u, { expected: "an RFC 3339 timestamp" }));
|
|
28
|
+
/** Decode helpers that keep branding at the boundary. */
|
|
29
|
+
export const decode = Schema.decodeUnknownSync;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { CertificateFingerprint, FollowerId, GroupName, InvitationCode, Timestamp, } from "./brand.js";
|
|
3
|
+
/** Source Machine identity: an ed25519 signing key with a public fingerprint. */
|
|
4
|
+
export const SourceIdentity = Schema.Struct({
|
|
5
|
+
keyId: Schema.NonEmptyString,
|
|
6
|
+
publicKeyFingerprint: CertificateFingerprint,
|
|
7
|
+
});
|
|
8
|
+
/** One issued Follower Identity with its independently revocable credential reference. */
|
|
9
|
+
export const FollowerReference = Schema.Struct({
|
|
10
|
+
id: FollowerId,
|
|
11
|
+
name: Schema.NonEmptyString,
|
|
12
|
+
});
|
|
13
|
+
export const FollowerIdentity = Schema.Struct({
|
|
14
|
+
id: FollowerId,
|
|
15
|
+
name: Schema.NonEmptyString,
|
|
16
|
+
groups: Schema.Array(GroupName),
|
|
17
|
+
revoked: Schema.Boolean,
|
|
18
|
+
credentialReference: Schema.NonEmptyString,
|
|
19
|
+
enrolledAt: Timestamp,
|
|
20
|
+
});
|
|
21
|
+
/** A short-lived, single-use enrollment invitation. */
|
|
22
|
+
export const EnrollmentInvitation = Schema.Struct({
|
|
23
|
+
code: InvitationCode,
|
|
24
|
+
endpoint: Schema.NonEmptyString,
|
|
25
|
+
fingerprint: CertificateFingerprint,
|
|
26
|
+
groups: Schema.Array(GroupName),
|
|
27
|
+
expiresAt: Timestamp,
|
|
28
|
+
used: Schema.Boolean,
|
|
29
|
+
});
|
|
30
|
+
/** Agent execution policy on a follower. */
|
|
31
|
+
export const AgentPolicy = Schema.Literals(["deterministic-only", "agent-propose", "agent-apply"]);
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const hostedOrigins = {
|
|
2
|
+
bitbucket: "https://bitbucket.org",
|
|
3
|
+
github: "https://github.com",
|
|
4
|
+
gitlab: "https://gitlab.com",
|
|
5
|
+
};
|
|
6
|
+
const hostedProtocol = /^(github|gitlab|bitbucket):/iu;
|
|
7
|
+
const localProtocol = /^(?:file|link|workspace):/iu;
|
|
8
|
+
const githubShorthand = /^[^/@\s:]+\/[^/@\s:]+(?:#.*)?$/u;
|
|
9
|
+
const originForHttpUrl = (value) => {
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(value);
|
|
12
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
13
|
+
return undefined;
|
|
14
|
+
if (url.username.length > 0 || url.password.length > 0)
|
|
15
|
+
return undefined;
|
|
16
|
+
return url.origin;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const hostedOriginForUrl = (value) => {
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(value);
|
|
25
|
+
if (!["git:", "http:", "https:", "ssh:"].includes(url.protocol))
|
|
26
|
+
return undefined;
|
|
27
|
+
const host = url.hostname.toLowerCase();
|
|
28
|
+
const hosted = Object.entries(hostedOrigins).find(([, origin]) => new URL(origin).hostname === host);
|
|
29
|
+
if (hosted === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (url.username.length > 0 && url.username.toLowerCase() !== "git")
|
|
32
|
+
return undefined;
|
|
33
|
+
if (url.password.length > 0)
|
|
34
|
+
return undefined;
|
|
35
|
+
const defaultPort = (url.protocol === "http:" && url.port === "80")
|
|
36
|
+
|| (url.protocol === "https:" && url.port === "443");
|
|
37
|
+
const port = url.port.length === 0 || defaultPort ? "" : `:${url.port}`;
|
|
38
|
+
return `${hosted[1]}${port}`;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const hostedNameForHost = (host) => {
|
|
45
|
+
const normalized = host.toLowerCase();
|
|
46
|
+
if (normalized === "github.com" || normalized === "www.github.com")
|
|
47
|
+
return "github";
|
|
48
|
+
if (normalized === "gitlab.com" || normalized === "www.gitlab.com")
|
|
49
|
+
return "gitlab";
|
|
50
|
+
if (normalized === "bitbucket.org" || normalized === "www.bitbucket.org")
|
|
51
|
+
return "bitbucket";
|
|
52
|
+
return undefined;
|
|
53
|
+
};
|
|
54
|
+
const remoteOrigin = (value) => {
|
|
55
|
+
if (/^git@/iu.test(value)) {
|
|
56
|
+
const match = /^git@([^/:?#\s]+):([^?#\s]+)$/iu.exec(value);
|
|
57
|
+
if (match === null)
|
|
58
|
+
return undefined;
|
|
59
|
+
const host = match[1].toLowerCase();
|
|
60
|
+
const hostedName = hostedNameForHost(host);
|
|
61
|
+
const hosted = hostedName === undefined ? undefined : hostedOrigins[hostedName];
|
|
62
|
+
return hosted === undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: hosted;
|
|
65
|
+
}
|
|
66
|
+
const withoutGitPrefix = value.replace(/^git\+/iu, "");
|
|
67
|
+
const hosted = hostedOriginForUrl(withoutGitPrefix);
|
|
68
|
+
if (hosted !== undefined)
|
|
69
|
+
return hosted;
|
|
70
|
+
return originForHttpUrl(withoutGitPrefix);
|
|
71
|
+
};
|
|
72
|
+
export const isNpmRegistryPackageName = (value) => /^[A-Za-z0-9][A-Za-z0-9._~-]*$/u.test(value)
|
|
73
|
+
|| /^@[A-Za-z0-9._~-]+\/[A-Za-z0-9._~-]+$/u.test(value);
|
|
74
|
+
const isRegistryVersion = (value) => value.length > 0
|
|
75
|
+
&& !/[\s/:\\]/u.test(value)
|
|
76
|
+
&& !value.startsWith("#");
|
|
77
|
+
const packageNameAndTarget = (value) => {
|
|
78
|
+
const separator = value.startsWith("@")
|
|
79
|
+
? value.indexOf("@", 1)
|
|
80
|
+
: value.indexOf("@");
|
|
81
|
+
if (separator <= 0)
|
|
82
|
+
return undefined;
|
|
83
|
+
const name = value.slice(0, separator);
|
|
84
|
+
const target = value.slice(separator + 1);
|
|
85
|
+
return target.length === 0 ? undefined : { name, target };
|
|
86
|
+
};
|
|
87
|
+
const classifyRemoteTarget = (value) => {
|
|
88
|
+
const hosted = hostedProtocol.exec(value);
|
|
89
|
+
if (hosted !== null) {
|
|
90
|
+
const protocol = hosted[1].toLowerCase();
|
|
91
|
+
const rest = value.slice(hosted[0].length);
|
|
92
|
+
if (!/^[^/#\s?@:]+\/[^#\s?@:]+(?:#.*)?$/u.test(rest)) {
|
|
93
|
+
return { kind: "ambiguous" };
|
|
94
|
+
}
|
|
95
|
+
const origin = protocol === "github"
|
|
96
|
+
? hostedOrigins.github
|
|
97
|
+
: protocol === "gitlab"
|
|
98
|
+
? hostedOrigins.gitlab
|
|
99
|
+
: hostedOrigins.bitbucket;
|
|
100
|
+
return { kind: "remote", origin };
|
|
101
|
+
}
|
|
102
|
+
if (/^(?:git\+|git:\/\/|git@|https?:\/\/|ssh:\/\/)/iu.test(value)) {
|
|
103
|
+
const origin = remoteOrigin(value);
|
|
104
|
+
return origin === undefined
|
|
105
|
+
? { kind: "ambiguous" }
|
|
106
|
+
: { kind: "remote", origin };
|
|
107
|
+
}
|
|
108
|
+
if (githubShorthand.test(value)) {
|
|
109
|
+
return { kind: "remote", origin: hostedOrigins.github };
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
};
|
|
113
|
+
const classifyRegistryOrAlias = (value) => {
|
|
114
|
+
const remote = classifyRemoteTarget(value);
|
|
115
|
+
if (remote !== undefined)
|
|
116
|
+
return remote;
|
|
117
|
+
if (localProtocol.test(value))
|
|
118
|
+
return localSpecification(value);
|
|
119
|
+
if (value.startsWith("npm:")) {
|
|
120
|
+
const target = value.slice("npm:".length);
|
|
121
|
+
if (!isNpmRegistryPackageName(target) && !packageNameAndTarget(target)) {
|
|
122
|
+
return { kind: "ambiguous" };
|
|
123
|
+
}
|
|
124
|
+
return classifyRegistryOrAlias(target);
|
|
125
|
+
}
|
|
126
|
+
const alias = packageNameAndTarget(value);
|
|
127
|
+
if (alias !== undefined) {
|
|
128
|
+
if (!isNpmRegistryPackageName(alias.name))
|
|
129
|
+
return { kind: "ambiguous" };
|
|
130
|
+
if (alias.target.startsWith("npm:")) {
|
|
131
|
+
return classifyRegistryOrAlias(alias.target.slice("npm:".length));
|
|
132
|
+
}
|
|
133
|
+
const remote = classifyRemoteTarget(alias.target);
|
|
134
|
+
if (remote !== undefined)
|
|
135
|
+
return remote;
|
|
136
|
+
if (localProtocol.test(alias.target))
|
|
137
|
+
return localSpecification(alias.target);
|
|
138
|
+
return isRegistryVersion(alias.target)
|
|
139
|
+
? { kind: "registry" }
|
|
140
|
+
: { kind: "ambiguous" };
|
|
141
|
+
}
|
|
142
|
+
if (isNpmRegistryPackageName(value))
|
|
143
|
+
return { kind: "registry" };
|
|
144
|
+
return { kind: "ambiguous" };
|
|
145
|
+
};
|
|
146
|
+
const localSpecification = (value) => {
|
|
147
|
+
const separator = value.indexOf(":");
|
|
148
|
+
if (separator < 0)
|
|
149
|
+
return { kind: "local" };
|
|
150
|
+
const protocol = value.slice(0, separator).toLowerCase();
|
|
151
|
+
const path = value.slice(separator + 1);
|
|
152
|
+
if (protocol === "workspace" && (path === "*" || path === "^" || path === "~")) {
|
|
153
|
+
return { kind: "local" };
|
|
154
|
+
}
|
|
155
|
+
if (path.length === 0 || /\s/u.test(path))
|
|
156
|
+
return { kind: "ambiguous" };
|
|
157
|
+
return { kind: "local", path };
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Classify one value in an npm package position.
|
|
161
|
+
*
|
|
162
|
+
* `foo/bar` is npm's GitHub shorthand. Explicit relative, absolute, and
|
|
163
|
+
* workspace forms remain local, while unsupported protocol-like values are
|
|
164
|
+
* ambiguous and must not be treated as registry packages.
|
|
165
|
+
*/
|
|
166
|
+
export const parseNpmPackageSpecification = (value) => {
|
|
167
|
+
if (value.length === 0 || /\s/u.test(value))
|
|
168
|
+
return { kind: "ambiguous" };
|
|
169
|
+
if (localProtocol.test(value))
|
|
170
|
+
return localSpecification(value);
|
|
171
|
+
if (value.startsWith("/")
|
|
172
|
+
|| value.startsWith("./")
|
|
173
|
+
|| value.startsWith("../")
|
|
174
|
+
|| value.startsWith("~/")
|
|
175
|
+
|| /^[A-Za-z]:[\\/]/u.test(value)) {
|
|
176
|
+
return { kind: "local", path: value };
|
|
177
|
+
}
|
|
178
|
+
const remote = classifyRemoteTarget(value);
|
|
179
|
+
if (remote !== undefined)
|
|
180
|
+
return remote;
|
|
181
|
+
if ((value.includes("://") || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value))
|
|
182
|
+
&& packageNameAndTarget(value) === undefined) {
|
|
183
|
+
return { kind: "ambiguous" };
|
|
184
|
+
}
|
|
185
|
+
return classifyRegistryOrAlias(value);
|
|
186
|
+
};
|