@helyx/cli 0.4.41 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/self-host/catalogue.d.ts +186 -2
- package/dist/self-host/catalogue.js +67 -2
- package/dist/self-host/manifest.d.ts +27 -3
- package/dist/self-host/manifest.js +122 -2
- package/dist/self-host/project-generator.d.ts +1 -1
- package/dist/self-host/project-generator.js +25 -0
- package/package.json +1 -1
- package/self-host-catalogue.json +417 -77
- package/templates/self-host/.env.example +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Update the embedded self-host catalogue for the launch-readiness runtime and module patch releases.
|
|
8
|
+
|
|
9
|
+
## 0.5.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 26b4c3e: Add versioned hidden module dependencies, automatic installation lifecycle,
|
|
14
|
+
delegated customer health, and transitive self-host build composition for the
|
|
15
|
+
Nexus foundation while preserving legacy manifest and catalogue behavior. Add
|
|
16
|
+
closed, owner-aware composition for the shared `/nexus` command namespace and
|
|
17
|
+
bounded Core reads for terminology, member history and confirmed member value
|
|
18
|
+
changes. Add resumable, idempotent role and server bulk-award jobs.
|
|
19
|
+
Add bounded Nexus privacy capture, a durable hashed erasure fence and atomic
|
|
20
|
+
member detachment that journals any remaining balance as a privacy burn before
|
|
21
|
+
removing account and bulk-recipient identifiers.
|
|
22
|
+
Generalise the durable Achievement contribution bus as member contributions
|
|
23
|
+
while retaining the existing service names as compatibility aliases.
|
|
24
|
+
Add the private Nexus Rewards Testing package, complete initial record schema
|
|
25
|
+
and an atomic timezone-aware Daily Claim path backed by the shared ledger.
|
|
26
|
+
Add reviewed, default-off Nexus eligibility metadata to existing contribution
|
|
27
|
+
catalogues and atomically apply configured fixed rewards with source, role,
|
|
28
|
+
rate-cap, replay, balance-cap and privacy safeguards.
|
|
29
|
+
Add definition-owned Achievement reward amounts and live repeatable-award
|
|
30
|
+
high-water processing so retries credit only newly committed occurrences.
|
|
31
|
+
Add the private Nexus Economy Testing package and its complete initial Exchange,
|
|
32
|
+
item, redemption, inventory, role-grant and durable fulfilment record schema.
|
|
33
|
+
Add a typed atomic Economy redemption path with exact replay, finite stock,
|
|
34
|
+
member limits, role eligibility, collectible inventory and durable fulfilment.
|
|
35
|
+
Add coalesced leased role fulfilment with reviewed Discord safety, observed
|
|
36
|
+
delivery reconciliation, bounded retries and Core-linked automatic refunds.
|
|
37
|
+
|
|
38
|
+
## 0.4.42
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- Add fail-open production status heartbeat configuration and reporting support.
|
|
43
|
+
|
|
3
44
|
## 0.4.41
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
|
@@ -39,7 +39,88 @@ export declare const selfHostModuleSchema: z.ZodObject<{
|
|
|
39
39
|
premium: "premium";
|
|
40
40
|
}>>;
|
|
41
41
|
}, z.core.$strict>;
|
|
42
|
-
export declare const
|
|
42
|
+
export declare const selfHostModuleV2Schema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
43
|
+
id: z.ZodString;
|
|
44
|
+
package: z.ZodString;
|
|
45
|
+
version: z.ZodNullable<z.ZodString>;
|
|
46
|
+
name: z.ZodString;
|
|
47
|
+
description: z.ZodString;
|
|
48
|
+
category: z.ZodEnum<{
|
|
49
|
+
Community: "Community";
|
|
50
|
+
Safety: "Safety";
|
|
51
|
+
"Support & Staff": "Support & Staff";
|
|
52
|
+
"Auto Tools": "Auto Tools";
|
|
53
|
+
Automation: "Automation";
|
|
54
|
+
Engagement: "Engagement";
|
|
55
|
+
Gaming: "Gaming";
|
|
56
|
+
Insights: "Insights";
|
|
57
|
+
Integrations: "Integrations";
|
|
58
|
+
}>;
|
|
59
|
+
availability: z.ZodEnum<{
|
|
60
|
+
planned: "planned";
|
|
61
|
+
unpublished: "unpublished";
|
|
62
|
+
published: "published";
|
|
63
|
+
}>;
|
|
64
|
+
releaseStatus: z.ZodEnum<{
|
|
65
|
+
planned: "planned";
|
|
66
|
+
testing: "testing";
|
|
67
|
+
beta: "beta";
|
|
68
|
+
live: "live";
|
|
69
|
+
}>;
|
|
70
|
+
tier: z.ZodOptional<z.ZodEnum<{
|
|
71
|
+
free: "free";
|
|
72
|
+
premium: "premium";
|
|
73
|
+
}>>;
|
|
74
|
+
dependencies: z.ZodArray<z.ZodObject<{
|
|
75
|
+
id: z.ZodString;
|
|
76
|
+
version: z.ZodString;
|
|
77
|
+
}, z.core.$strict>>;
|
|
78
|
+
moduleKind: z.ZodLiteral<"customer_feature">;
|
|
79
|
+
customerSelectable: z.ZodLiteral<true>;
|
|
80
|
+
countsAsInstalledModule: z.ZodLiteral<true>;
|
|
81
|
+
healthExposure: z.ZodLiteral<"direct">;
|
|
82
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
83
|
+
id: z.ZodString;
|
|
84
|
+
package: z.ZodString;
|
|
85
|
+
version: z.ZodNullable<z.ZodString>;
|
|
86
|
+
name: z.ZodString;
|
|
87
|
+
description: z.ZodString;
|
|
88
|
+
category: z.ZodEnum<{
|
|
89
|
+
Community: "Community";
|
|
90
|
+
Safety: "Safety";
|
|
91
|
+
"Support & Staff": "Support & Staff";
|
|
92
|
+
"Auto Tools": "Auto Tools";
|
|
93
|
+
Automation: "Automation";
|
|
94
|
+
Engagement: "Engagement";
|
|
95
|
+
Gaming: "Gaming";
|
|
96
|
+
Insights: "Insights";
|
|
97
|
+
Integrations: "Integrations";
|
|
98
|
+
}>;
|
|
99
|
+
availability: z.ZodEnum<{
|
|
100
|
+
planned: "planned";
|
|
101
|
+
unpublished: "unpublished";
|
|
102
|
+
published: "published";
|
|
103
|
+
}>;
|
|
104
|
+
releaseStatus: z.ZodEnum<{
|
|
105
|
+
planned: "planned";
|
|
106
|
+
testing: "testing";
|
|
107
|
+
beta: "beta";
|
|
108
|
+
live: "live";
|
|
109
|
+
}>;
|
|
110
|
+
tier: z.ZodOptional<z.ZodEnum<{
|
|
111
|
+
free: "free";
|
|
112
|
+
premium: "premium";
|
|
113
|
+
}>>;
|
|
114
|
+
dependencies: z.ZodArray<z.ZodObject<{
|
|
115
|
+
id: z.ZodString;
|
|
116
|
+
version: z.ZodString;
|
|
117
|
+
}, z.core.$strict>>;
|
|
118
|
+
moduleKind: z.ZodLiteral<"internal_dependency">;
|
|
119
|
+
customerSelectable: z.ZodLiteral<false>;
|
|
120
|
+
countsAsInstalledModule: z.ZodLiteral<false>;
|
|
121
|
+
healthExposure: z.ZodLiteral<"delegated">;
|
|
122
|
+
}, z.core.$strict>], "moduleKind">;
|
|
123
|
+
export declare const selfHostCatalogueSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
43
124
|
schemaVersion: z.ZodLiteral<1>;
|
|
44
125
|
catalogueVersion: z.ZodNumber;
|
|
45
126
|
core: z.ZodObject<{
|
|
@@ -92,9 +173,112 @@ export declare const selfHostCatalogueSchema: z.ZodObject<{
|
|
|
92
173
|
premium: "premium";
|
|
93
174
|
}>>;
|
|
94
175
|
}, z.core.$strict>>;
|
|
95
|
-
}, z.core.$strict
|
|
176
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
177
|
+
schemaVersion: z.ZodLiteral<2>;
|
|
178
|
+
catalogueVersion: z.ZodNumber;
|
|
179
|
+
core: z.ZodObject<{
|
|
180
|
+
package: z.ZodString;
|
|
181
|
+
version: z.ZodString;
|
|
182
|
+
availability: z.ZodEnum<{
|
|
183
|
+
unpublished: "unpublished";
|
|
184
|
+
published: "published";
|
|
185
|
+
}>;
|
|
186
|
+
}, z.core.$strict>;
|
|
187
|
+
generator: z.ZodObject<{
|
|
188
|
+
package: z.ZodString;
|
|
189
|
+
version: z.ZodString;
|
|
190
|
+
availability: z.ZodEnum<{
|
|
191
|
+
unpublished: "unpublished";
|
|
192
|
+
published: "published";
|
|
193
|
+
}>;
|
|
194
|
+
command: z.ZodString;
|
|
195
|
+
}, z.core.$strict>;
|
|
196
|
+
modules: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
197
|
+
id: z.ZodString;
|
|
198
|
+
package: z.ZodString;
|
|
199
|
+
version: z.ZodNullable<z.ZodString>;
|
|
200
|
+
name: z.ZodString;
|
|
201
|
+
description: z.ZodString;
|
|
202
|
+
category: z.ZodEnum<{
|
|
203
|
+
Community: "Community";
|
|
204
|
+
Safety: "Safety";
|
|
205
|
+
"Support & Staff": "Support & Staff";
|
|
206
|
+
"Auto Tools": "Auto Tools";
|
|
207
|
+
Automation: "Automation";
|
|
208
|
+
Engagement: "Engagement";
|
|
209
|
+
Gaming: "Gaming";
|
|
210
|
+
Insights: "Insights";
|
|
211
|
+
Integrations: "Integrations";
|
|
212
|
+
}>;
|
|
213
|
+
availability: z.ZodEnum<{
|
|
214
|
+
planned: "planned";
|
|
215
|
+
unpublished: "unpublished";
|
|
216
|
+
published: "published";
|
|
217
|
+
}>;
|
|
218
|
+
releaseStatus: z.ZodEnum<{
|
|
219
|
+
planned: "planned";
|
|
220
|
+
testing: "testing";
|
|
221
|
+
beta: "beta";
|
|
222
|
+
live: "live";
|
|
223
|
+
}>;
|
|
224
|
+
tier: z.ZodOptional<z.ZodEnum<{
|
|
225
|
+
free: "free";
|
|
226
|
+
premium: "premium";
|
|
227
|
+
}>>;
|
|
228
|
+
dependencies: z.ZodArray<z.ZodObject<{
|
|
229
|
+
id: z.ZodString;
|
|
230
|
+
version: z.ZodString;
|
|
231
|
+
}, z.core.$strict>>;
|
|
232
|
+
moduleKind: z.ZodLiteral<"customer_feature">;
|
|
233
|
+
customerSelectable: z.ZodLiteral<true>;
|
|
234
|
+
countsAsInstalledModule: z.ZodLiteral<true>;
|
|
235
|
+
healthExposure: z.ZodLiteral<"direct">;
|
|
236
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
237
|
+
id: z.ZodString;
|
|
238
|
+
package: z.ZodString;
|
|
239
|
+
version: z.ZodNullable<z.ZodString>;
|
|
240
|
+
name: z.ZodString;
|
|
241
|
+
description: z.ZodString;
|
|
242
|
+
category: z.ZodEnum<{
|
|
243
|
+
Community: "Community";
|
|
244
|
+
Safety: "Safety";
|
|
245
|
+
"Support & Staff": "Support & Staff";
|
|
246
|
+
"Auto Tools": "Auto Tools";
|
|
247
|
+
Automation: "Automation";
|
|
248
|
+
Engagement: "Engagement";
|
|
249
|
+
Gaming: "Gaming";
|
|
250
|
+
Insights: "Insights";
|
|
251
|
+
Integrations: "Integrations";
|
|
252
|
+
}>;
|
|
253
|
+
availability: z.ZodEnum<{
|
|
254
|
+
planned: "planned";
|
|
255
|
+
unpublished: "unpublished";
|
|
256
|
+
published: "published";
|
|
257
|
+
}>;
|
|
258
|
+
releaseStatus: z.ZodEnum<{
|
|
259
|
+
planned: "planned";
|
|
260
|
+
testing: "testing";
|
|
261
|
+
beta: "beta";
|
|
262
|
+
live: "live";
|
|
263
|
+
}>;
|
|
264
|
+
tier: z.ZodOptional<z.ZodEnum<{
|
|
265
|
+
free: "free";
|
|
266
|
+
premium: "premium";
|
|
267
|
+
}>>;
|
|
268
|
+
dependencies: z.ZodArray<z.ZodObject<{
|
|
269
|
+
id: z.ZodString;
|
|
270
|
+
version: z.ZodString;
|
|
271
|
+
}, z.core.$strict>>;
|
|
272
|
+
moduleKind: z.ZodLiteral<"internal_dependency">;
|
|
273
|
+
customerSelectable: z.ZodLiteral<false>;
|
|
274
|
+
countsAsInstalledModule: z.ZodLiteral<false>;
|
|
275
|
+
healthExposure: z.ZodLiteral<"delegated">;
|
|
276
|
+
}, z.core.$strict>], "moduleKind">>;
|
|
277
|
+
}, z.core.$strict>], "schemaVersion">;
|
|
96
278
|
export type SelfHostModule = z.infer<typeof selfHostModuleSchema>;
|
|
279
|
+
export type SelfHostModuleV2 = z.infer<typeof selfHostModuleV2Schema>;
|
|
97
280
|
export type SelfHostCatalogue = z.infer<typeof selfHostCatalogueSchema>;
|
|
281
|
+
export declare function isSelfHostModuleCustomerSelectable(module: SelfHostModule | SelfHostModuleV2): boolean;
|
|
98
282
|
export declare function parseSelfHostCatalogue(input: unknown): SelfHostCatalogue;
|
|
99
283
|
export declare function loadSelfHostCatalogue(path: string | URL, reader?: (path: string | URL, encoding: "utf8") => Promise<string>): Promise<SelfHostCatalogue>;
|
|
100
284
|
export declare function bundledSelfHostCatalogueUrl(): URL;
|
|
@@ -24,6 +24,14 @@ const modulePackage = z
|
|
|
24
24
|
const moduleId = z
|
|
25
25
|
.string()
|
|
26
26
|
.regex(/^helyx\.[a-z0-9][a-z0-9-]*$/u, "must be a Helyx module ID");
|
|
27
|
+
const moduleDependency = z
|
|
28
|
+
.object({
|
|
29
|
+
id: moduleId,
|
|
30
|
+
version: z
|
|
31
|
+
.string()
|
|
32
|
+
.refine((value) => semver.validRange(value) !== null, "must be semver"),
|
|
33
|
+
})
|
|
34
|
+
.strict();
|
|
27
35
|
const availability = z.enum(["planned", "unpublished", "published"]);
|
|
28
36
|
export const selfHostReleaseStatusSchema = z.enum([
|
|
29
37
|
"planned",
|
|
@@ -92,7 +100,25 @@ export const selfHostModuleSchema = z
|
|
|
92
100
|
});
|
|
93
101
|
}
|
|
94
102
|
});
|
|
95
|
-
|
|
103
|
+
const selfHostCustomerModuleV2Schema = selfHostModuleSchema.safeExtend({
|
|
104
|
+
dependencies: z.array(moduleDependency),
|
|
105
|
+
moduleKind: z.literal("customer_feature"),
|
|
106
|
+
customerSelectable: z.literal(true),
|
|
107
|
+
countsAsInstalledModule: z.literal(true),
|
|
108
|
+
healthExposure: z.literal("direct"),
|
|
109
|
+
});
|
|
110
|
+
const selfHostInternalModuleV2Schema = selfHostModuleSchema.safeExtend({
|
|
111
|
+
dependencies: z.array(moduleDependency),
|
|
112
|
+
moduleKind: z.literal("internal_dependency"),
|
|
113
|
+
customerSelectable: z.literal(false),
|
|
114
|
+
countsAsInstalledModule: z.literal(false),
|
|
115
|
+
healthExposure: z.literal("delegated"),
|
|
116
|
+
});
|
|
117
|
+
export const selfHostModuleV2Schema = z.discriminatedUnion("moduleKind", [
|
|
118
|
+
selfHostCustomerModuleV2Schema,
|
|
119
|
+
selfHostInternalModuleV2Schema,
|
|
120
|
+
]);
|
|
121
|
+
const selfHostCatalogueV1Schema = z
|
|
96
122
|
.object({
|
|
97
123
|
schemaVersion: z.literal(1),
|
|
98
124
|
catalogueVersion: z.number().int().positive(),
|
|
@@ -100,7 +126,21 @@ export const selfHostCatalogueSchema = z
|
|
|
100
126
|
generator: generatorSchema,
|
|
101
127
|
modules: z.array(selfHostModuleSchema),
|
|
102
128
|
})
|
|
103
|
-
.strict()
|
|
129
|
+
.strict();
|
|
130
|
+
const selfHostCatalogueV2Schema = z
|
|
131
|
+
.object({
|
|
132
|
+
schemaVersion: z.literal(2),
|
|
133
|
+
catalogueVersion: z.number().int().positive(),
|
|
134
|
+
core: componentSchema,
|
|
135
|
+
generator: generatorSchema,
|
|
136
|
+
modules: z.array(selfHostModuleV2Schema),
|
|
137
|
+
})
|
|
138
|
+
.strict();
|
|
139
|
+
export const selfHostCatalogueSchema = z
|
|
140
|
+
.discriminatedUnion("schemaVersion", [
|
|
141
|
+
selfHostCatalogueV1Schema,
|
|
142
|
+
selfHostCatalogueV2Schema,
|
|
143
|
+
])
|
|
104
144
|
.superRefine((catalogue, context) => {
|
|
105
145
|
const ids = new Set();
|
|
106
146
|
const packages = new Set([
|
|
@@ -125,7 +165,32 @@ export const selfHostCatalogueSchema = z
|
|
|
125
165
|
ids.add(module.id);
|
|
126
166
|
packages.add(module.package);
|
|
127
167
|
}
|
|
168
|
+
if (catalogue.schemaVersion === 2) {
|
|
169
|
+
for (const [index, module] of catalogue.modules.entries()) {
|
|
170
|
+
const dependencyIds = new Set();
|
|
171
|
+
for (const dependency of module.dependencies) {
|
|
172
|
+
if (dependencyIds.has(dependency.id)) {
|
|
173
|
+
context.addIssue({
|
|
174
|
+
code: "custom",
|
|
175
|
+
path: ["modules", index, "dependencies"],
|
|
176
|
+
message: `duplicate dependency: ${dependency.id}`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (dependency.id === module.id) {
|
|
180
|
+
context.addIssue({
|
|
181
|
+
code: "custom",
|
|
182
|
+
path: ["modules", index, "dependencies"],
|
|
183
|
+
message: "a module cannot depend on itself",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
dependencyIds.add(dependency.id);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
128
190
|
});
|
|
191
|
+
export function isSelfHostModuleCustomerSelectable(module) {
|
|
192
|
+
return !("customerSelectable" in module) || module.customerSelectable;
|
|
193
|
+
}
|
|
129
194
|
export function parseSelfHostCatalogue(input) {
|
|
130
195
|
return selfHostCatalogueSchema.parse(input);
|
|
131
196
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type
|
|
3
|
-
export declare const selfHostBuildManifestSchema: z.ZodObject<{
|
|
2
|
+
import { type SelfHostCatalogue } from "./catalogue.js";
|
|
3
|
+
export declare const selfHostBuildManifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
4
4
|
schemaVersion: z.ZodLiteral<1>;
|
|
5
5
|
catalogueVersion: z.ZodNumber;
|
|
6
6
|
core: z.ZodObject<{
|
|
@@ -16,7 +16,31 @@ export declare const selfHostBuildManifestSchema: z.ZodObject<{
|
|
|
16
16
|
package: z.ZodString;
|
|
17
17
|
version: z.ZodString;
|
|
18
18
|
}, z.core.$strict>>;
|
|
19
|
-
}, z.core.$strict
|
|
19
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
20
|
+
schemaVersion: z.ZodLiteral<2>;
|
|
21
|
+
catalogueVersion: z.ZodNumber;
|
|
22
|
+
core: z.ZodObject<{
|
|
23
|
+
package: z.ZodString;
|
|
24
|
+
version: z.ZodString;
|
|
25
|
+
}, z.core.$strict>;
|
|
26
|
+
generator: z.ZodObject<{
|
|
27
|
+
package: z.ZodString;
|
|
28
|
+
version: z.ZodString;
|
|
29
|
+
}, z.core.$strict>;
|
|
30
|
+
modules: z.ZodArray<z.ZodObject<{
|
|
31
|
+
id: z.ZodString;
|
|
32
|
+
package: z.ZodString;
|
|
33
|
+
version: z.ZodString;
|
|
34
|
+
moduleKind: z.ZodEnum<{
|
|
35
|
+
customer_feature: "customer_feature";
|
|
36
|
+
internal_dependency: "internal_dependency";
|
|
37
|
+
}>;
|
|
38
|
+
dependencies: z.ZodArray<z.ZodObject<{
|
|
39
|
+
id: z.ZodString;
|
|
40
|
+
version: z.ZodString;
|
|
41
|
+
}, z.core.$strict>>;
|
|
42
|
+
}, z.core.$strict>>;
|
|
43
|
+
}, z.core.$strict>], "schemaVersion">;
|
|
20
44
|
export type SelfHostBuildManifest = z.infer<typeof selfHostBuildManifestSchema>;
|
|
21
45
|
export declare function createSelfHostBuildManifest(catalogue: SelfHostCatalogue, selectedModuleIds: readonly string[]): SelfHostBuildManifest;
|
|
22
46
|
export declare function parseSelfHostBuildManifest(input: unknown, catalogue: SelfHostCatalogue): SelfHostBuildManifest;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import semver from "semver";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
3
|
import { z } from "zod";
|
|
4
|
+
import { isSelfHostModuleCustomerSelectable, } from "./catalogue.js";
|
|
3
5
|
const exactVersion = z
|
|
4
6
|
.string()
|
|
5
7
|
.refine((value) => semver.valid(value) === value, "must be exact semver");
|
|
@@ -18,7 +20,7 @@ const buildModuleSchema = z
|
|
|
18
20
|
version: exactVersion,
|
|
19
21
|
})
|
|
20
22
|
.strict();
|
|
21
|
-
|
|
23
|
+
const selfHostBuildManifestV1Schema = z
|
|
22
24
|
.object({
|
|
23
25
|
schemaVersion: z.literal(1),
|
|
24
26
|
catalogueVersion: z.number().int().positive(),
|
|
@@ -26,7 +28,34 @@ export const selfHostBuildManifestSchema = z
|
|
|
26
28
|
generator: buildComponentSchema,
|
|
27
29
|
modules: z.array(buildModuleSchema),
|
|
28
30
|
})
|
|
29
|
-
.strict()
|
|
31
|
+
.strict();
|
|
32
|
+
const buildModuleV2Schema = buildModuleSchema
|
|
33
|
+
.extend({
|
|
34
|
+
moduleKind: z.enum(["customer_feature", "internal_dependency"]),
|
|
35
|
+
dependencies: z.array(z
|
|
36
|
+
.object({
|
|
37
|
+
id: moduleId,
|
|
38
|
+
version: z
|
|
39
|
+
.string()
|
|
40
|
+
.refine((value) => semver.validRange(value) !== null, "must be semver"),
|
|
41
|
+
})
|
|
42
|
+
.strict()),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
const selfHostBuildManifestV2Schema = z
|
|
46
|
+
.object({
|
|
47
|
+
schemaVersion: z.literal(2),
|
|
48
|
+
catalogueVersion: z.number().int().positive(),
|
|
49
|
+
core: buildComponentSchema,
|
|
50
|
+
generator: buildComponentSchema,
|
|
51
|
+
modules: z.array(buildModuleV2Schema),
|
|
52
|
+
})
|
|
53
|
+
.strict();
|
|
54
|
+
export const selfHostBuildManifestSchema = z
|
|
55
|
+
.discriminatedUnion("schemaVersion", [
|
|
56
|
+
selfHostBuildManifestV1Schema,
|
|
57
|
+
selfHostBuildManifestV2Schema,
|
|
58
|
+
])
|
|
30
59
|
.superRefine((manifest, context) => {
|
|
31
60
|
const ids = new Set();
|
|
32
61
|
const packages = new Set();
|
|
@@ -55,6 +84,9 @@ export function createSelfHostBuildManifest(catalogue, selectedModuleIds) {
|
|
|
55
84
|
if (new Set(selectedModuleIds).size !== selectedModuleIds.length) {
|
|
56
85
|
throw new Error("Self-host selection contains duplicate module IDs");
|
|
57
86
|
}
|
|
87
|
+
if (catalogue.schemaVersion === 2) {
|
|
88
|
+
return createSelfHostBuildManifestV2(catalogue, selectedModuleIds);
|
|
89
|
+
}
|
|
58
90
|
const byId = new Map(catalogue.modules.map((module) => [module.id, module]));
|
|
59
91
|
const modules = [...selectedModuleIds]
|
|
60
92
|
.sort((left, right) => left.localeCompare(right, "en"))
|
|
@@ -87,8 +119,76 @@ export function createSelfHostBuildManifest(catalogue, selectedModuleIds) {
|
|
|
87
119
|
modules,
|
|
88
120
|
};
|
|
89
121
|
}
|
|
122
|
+
function createSelfHostBuildManifestV2(catalogue, selectedModuleIds) {
|
|
123
|
+
const byId = new Map(catalogue.modules.map((module) => [module.id, module]));
|
|
124
|
+
const explicitlySelected = new Set(selectedModuleIds);
|
|
125
|
+
const modules = [];
|
|
126
|
+
const visiting = new Set();
|
|
127
|
+
const included = new Set();
|
|
128
|
+
for (const id of selectedModuleIds) {
|
|
129
|
+
const module = byId.get(id);
|
|
130
|
+
if (!module)
|
|
131
|
+
throw new Error(`Unknown self-host module: ${id}`);
|
|
132
|
+
if (!isSelfHostModuleCustomerSelectable(module))
|
|
133
|
+
throw new Error(`Self-host module cannot be selected directly: ${id}`);
|
|
134
|
+
}
|
|
135
|
+
const include = (id) => {
|
|
136
|
+
if (included.has(id))
|
|
137
|
+
return;
|
|
138
|
+
if (visiting.has(id))
|
|
139
|
+
throw new Error(`Self-host module dependency cycle detected at ${id}`);
|
|
140
|
+
const module = byId.get(id);
|
|
141
|
+
if (!module)
|
|
142
|
+
throw new Error(`Unknown self-host module: ${id}`);
|
|
143
|
+
assertLiveModule(module, id);
|
|
144
|
+
visiting.add(id);
|
|
145
|
+
for (const dependency of module.dependencies) {
|
|
146
|
+
const required = byId.get(dependency.id);
|
|
147
|
+
if (!required)
|
|
148
|
+
throw new Error(`Self-host module ${id} is missing dependency ${dependency.id}`);
|
|
149
|
+
if (required.version === null ||
|
|
150
|
+
!semver.satisfies(required.version, dependency.version)) {
|
|
151
|
+
throw new Error(`Self-host module ${id} requires ${dependency.id} ${dependency.version}`);
|
|
152
|
+
}
|
|
153
|
+
if (isSelfHostModuleCustomerSelectable(required) &&
|
|
154
|
+
!explicitlySelected.has(required.id)) {
|
|
155
|
+
throw new Error(`Select required module first: ${required.name}`);
|
|
156
|
+
}
|
|
157
|
+
include(required.id);
|
|
158
|
+
}
|
|
159
|
+
visiting.delete(id);
|
|
160
|
+
included.add(id);
|
|
161
|
+
modules.push({
|
|
162
|
+
id: module.id,
|
|
163
|
+
package: module.package,
|
|
164
|
+
version: module.version,
|
|
165
|
+
moduleKind: module.moduleKind,
|
|
166
|
+
dependencies: module.dependencies.map((dependency) => ({
|
|
167
|
+
...dependency,
|
|
168
|
+
})),
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
for (const id of [...selectedModuleIds].sort((left, right) => left.localeCompare(right, "en")))
|
|
172
|
+
include(id);
|
|
173
|
+
return {
|
|
174
|
+
schemaVersion: 2,
|
|
175
|
+
catalogueVersion: catalogue.catalogueVersion,
|
|
176
|
+
core: {
|
|
177
|
+
package: catalogue.core.package,
|
|
178
|
+
version: catalogue.core.version,
|
|
179
|
+
},
|
|
180
|
+
generator: {
|
|
181
|
+
package: catalogue.generator.package,
|
|
182
|
+
version: catalogue.generator.version,
|
|
183
|
+
},
|
|
184
|
+
modules,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
90
187
|
export function parseSelfHostBuildManifest(input, catalogue) {
|
|
91
188
|
const manifest = selfHostBuildManifestSchema.parse(input);
|
|
189
|
+
if (manifest.schemaVersion !== catalogue.schemaVersion) {
|
|
190
|
+
throw new Error(`Build manifest schema does not match catalogue schema ${String(catalogue.schemaVersion)}`);
|
|
191
|
+
}
|
|
92
192
|
if (manifest.catalogueVersion !== catalogue.catalogueVersion) {
|
|
93
193
|
throw new Error(`Unsupported self-host catalogue version: ${manifest.catalogueVersion}`);
|
|
94
194
|
}
|
|
@@ -96,6 +196,19 @@ export function parseSelfHostBuildManifest(input, catalogue) {
|
|
|
96
196
|
assertPublishedComponent(catalogue.generator, "Helyx generator");
|
|
97
197
|
assertComponentMatches(manifest.core, catalogue.core, "runtime");
|
|
98
198
|
assertComponentMatches(manifest.generator, catalogue.generator, "generator");
|
|
199
|
+
if (manifest.schemaVersion === 2 && catalogue.schemaVersion === 2) {
|
|
200
|
+
const selectedModuleIds = manifest.modules
|
|
201
|
+
.filter(({ moduleKind }) => moduleKind === "customer_feature")
|
|
202
|
+
.map(({ id }) => id);
|
|
203
|
+
const expected = createSelfHostBuildManifest(catalogue, selectedModuleIds);
|
|
204
|
+
if (!isDeepStrictEqual(manifest, expected)) {
|
|
205
|
+
throw new Error("Build manifest modules do not match the trusted catalogue dependency graph");
|
|
206
|
+
}
|
|
207
|
+
return expected;
|
|
208
|
+
}
|
|
209
|
+
if (manifest.schemaVersion !== 1 || catalogue.schemaVersion !== 1) {
|
|
210
|
+
throw new Error("Build manifest and catalogue schemas are incompatible");
|
|
211
|
+
}
|
|
99
212
|
const byId = new Map(catalogue.modules.map((module) => [module.id, module]));
|
|
100
213
|
for (const selected of manifest.modules) {
|
|
101
214
|
const module = byId.get(selected.id);
|
|
@@ -116,6 +229,13 @@ export function parseSelfHostBuildManifest(input, catalogue) {
|
|
|
116
229
|
modules: [...manifest.modules].sort((left, right) => left.id.localeCompare(right.id, "en")),
|
|
117
230
|
};
|
|
118
231
|
}
|
|
232
|
+
function assertLiveModule(module, id) {
|
|
233
|
+
if (module.availability !== "published" ||
|
|
234
|
+
module.releaseStatus !== "live" ||
|
|
235
|
+
module.version === null) {
|
|
236
|
+
throw new Error(`Self-host module is not live: ${id}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
119
239
|
function assertPublishedComponent(component, label) {
|
|
120
240
|
if (component.availability !== "published") {
|
|
121
241
|
throw new Error(`${label} is not published`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BotEnvironmentInput } from "@helyx/config";
|
|
2
|
-
import type
|
|
2
|
+
import { type SelfHostCatalogue } from "./catalogue.js";
|
|
3
3
|
import { type SelfHostBuildManifest } from "./manifest.js";
|
|
4
4
|
export type SelfHostCommandRunner = (arguments_: readonly string[], workingDirectory: string, signal: AbortSignal) => Promise<void>;
|
|
5
5
|
export interface GenerateSelfHostProjectOptions {
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { mkdir, readFile, readdir, rm, stat, writeFile, } from "node:fs/promises";
|
|
4
4
|
import { dirname, parse, resolve } from "node:path";
|
|
5
|
+
import { isSelfHostModuleCustomerSelectable, } from "./catalogue.js";
|
|
5
6
|
import { parseSelfHostBuildManifest, } from "./manifest.js";
|
|
6
7
|
const generatedFileNames = [
|
|
7
8
|
".env.example",
|
|
@@ -102,8 +103,11 @@ function renderReadme(template, manifest, catalogue) {
|
|
|
102
103
|
? manifest.modules
|
|
103
104
|
.map((module) => {
|
|
104
105
|
const definition = modulesById.get(module.id);
|
|
106
|
+
if (definition && !isSelfHostModuleCustomerSelectable(definition))
|
|
107
|
+
return null;
|
|
105
108
|
return `- ${definition?.name ?? module.id} (\`${module.package}@${module.version}\`)`;
|
|
106
109
|
})
|
|
110
|
+
.filter((line) => line !== null)
|
|
107
111
|
.join("\n")
|
|
108
112
|
: "- No feature modules selected. This is a core-only installation.";
|
|
109
113
|
return normalizeCrLf(template.replace("{{SELECTED_MODULES}}", selected));
|
|
@@ -376,6 +380,27 @@ export const selfHostEnvironmentContract = {
|
|
|
376
380
|
value: "365",
|
|
377
381
|
optional: true,
|
|
378
382
|
},
|
|
383
|
+
HELYX_STATUS_ENABLED: { value: "false", optional: false },
|
|
384
|
+
HELYX_STATUS_URL: {
|
|
385
|
+
value: "https://status.helyx.gg",
|
|
386
|
+
optional: true,
|
|
387
|
+
},
|
|
388
|
+
HELYX_STATUS_HEARTBEAT_SECRET: {
|
|
389
|
+
value: "replace-with-a-base64-encoded-32-byte-key",
|
|
390
|
+
optional: true,
|
|
391
|
+
},
|
|
392
|
+
HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS: {
|
|
393
|
+
value: "60",
|
|
394
|
+
optional: true,
|
|
395
|
+
},
|
|
396
|
+
CF_ACCESS_CLIENT_ID: {
|
|
397
|
+
value: "replace-with-access-client-id",
|
|
398
|
+
optional: true,
|
|
399
|
+
},
|
|
400
|
+
CF_ACCESS_CLIENT_SECRET: {
|
|
401
|
+
value: "replace-with-access-client-secret",
|
|
402
|
+
optional: true,
|
|
403
|
+
},
|
|
379
404
|
LOG_LEVEL: { value: "info", optional: false },
|
|
380
405
|
NODE_ENV: { value: "production", optional: false },
|
|
381
406
|
};
|