@helyx/cli 0.3.0 → 0.3.3

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 CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 0229cb1: Add the versioned Helyx privacy-provider contract, strict write and nullable
8
+ read boundaries, guarded attribution migrations, bounded hosted and self-hosted
9
+ provider execution, and the Privacy Centre runtime configuration.
10
+ - Updated dependencies [0229cb1]
11
+ - @helyx/database@0.3.0
12
+ - @helyx/config@0.3.0
13
+ - @helyx/bot@0.3.0
14
+
15
+ ## 0.3.2
16
+
17
+ ### Patch Changes
18
+
19
+ - Correct generated self-host catalogue metadata for modules that are not yet in development.
20
+ - Updated dependencies
21
+ - @helyx/config@0.2.2
22
+ - @helyx/database@0.2.2
23
+ - @helyx/bot@0.2.3
24
+
25
+ ## 0.3.1
26
+
27
+ ### Patch Changes
28
+
29
+ - 9e290e1: Load the current reviewed self-host catalogue from the fixed Helyx API endpoint when generating a downloaded starter project, allowing compatible module-only releases without republishing the CLI. Catalogue requests now reject redirects, non-JSON responses, oversized bodies, invalid schemas and unavailable or stale catalogue data.
30
+
3
31
  ## 0.3.0
4
32
 
5
33
  ### Minor Changes
package/README.md CHANGED
@@ -14,7 +14,7 @@ helyx self-host update --confirm
14
14
 
15
15
  `setup` safely creates `.env` from a generated starter's `.env.example`, never overwrites existing configuration and reports whether Docker Compose is available. Direct Node.js operation remains fully supported with a local or hosted PostgreSQL database. `start` loads local configuration without overriding hosting-platform variables and performs the production bot bootstrap, including database and installed-module migrations. `migrate` applies only core migrations and exits. `doctor` validates the complete bot environment, PostgreSQL connectivity, immutable core migration history and configured module package metadata without logging in to Discord or modifying the database.
16
16
 
17
- `self-host init` validates a build manifest against the reviewed catalogue shipped with this exact CLI version, creates a new starter-project directory, pins the runtime and selected official modules, generates a lockfile with lifecycle scripts disabled, and writes checksums. Add `--install` only when the dependencies should also be installed locally. Unknown, unpublished, duplicated or modified package selections fail closed.
17
+ `self-host init` retrieves the current reviewed catalogue from the fixed Helyx API endpoint, validates a downloaded build manifest against it, creates a new starter-project directory, pins the runtime and selected official modules, generates a lockfile with lifecycle scripts disabled, and writes checksums. The request has strict timeout, response-size, JSON-schema and official-package checks; no arbitrary catalogue URL or stale bundled fallback is accepted. Add `--install` only when the dependencies should also be installed locally. Unknown, unpublished, duplicated, stale or modified package selections fail closed.
18
18
 
19
19
  `self-host update --check` reads the current project and the reviewed catalogue from `api.helyx.gg`, then reports the complete compatible release-set change without writing files. `--confirm` backs up the managed metadata under `.helyx/backups`, updates exact core, CLI and selected live-module versions together, refreshes and validates the lockfile with lifecycle scripts disabled, and restores the original metadata if generation fails. Run `npm ci` and `npm run doctor` after applying an update. The updater never changes `.env` or customer database data.
20
20
 
@@ -1,11 +1,16 @@
1
- import { type GeneratedSelfHostProject } from "./project-generator.js";
1
+ import type { SelfHostCatalogue } from "./catalogue.js";
2
+ import { generateSelfHostProject, type GeneratedSelfHostProject } from "./project-generator.js";
2
3
  import { type SelfHostUpdatePlan } from "./update.js";
3
4
  export interface SelfHostInitArguments {
4
5
  manifestPath: string;
5
6
  outputPath: string;
6
7
  install: boolean;
7
8
  }
8
- export declare function runSelfHostCommand(arguments_: readonly string[]): Promise<GeneratedSelfHostProject | SelfHostUpdatePlan>;
9
+ export interface SelfHostCommandDependencies {
10
+ catalogueLoader?: () => Promise<SelfHostCatalogue>;
11
+ projectGenerator?: typeof generateSelfHostProject;
12
+ }
13
+ export declare function runSelfHostCommand(arguments_: readonly string[], dependencies?: SelfHostCommandDependencies): Promise<GeneratedSelfHostProject | SelfHostUpdatePlan>;
9
14
  export interface SelfHostUpdateArguments {
10
15
  projectPath: string;
11
16
  check: boolean;
@@ -1,16 +1,18 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
- import { loadBundledSelfHostCatalogue } from "./catalogue.js";
4
3
  import { generateSelfHostProject, } from "./project-generator.js";
5
- import { updateSelfHostProject } from "./update.js";
4
+ import { loadCurrentSelfHostCatalogue, updateSelfHostProject, } from "./update.js";
6
5
  const maximumManifestBytes = 16 * 1_024;
7
- export async function runSelfHostCommand(arguments_) {
6
+ export async function runSelfHostCommand(arguments_, dependencies = {}) {
8
7
  const [subcommand, ...rest] = arguments_;
9
8
  if (subcommand === "update") {
10
9
  const parsed = parseSelfHostUpdateArguments(rest);
11
10
  const plan = await updateSelfHostProject({
12
11
  projectDirectory: parsed.projectPath,
13
12
  apply: parsed.confirm,
13
+ ...(dependencies.catalogueLoader
14
+ ? { catalogueLoader: dependencies.catalogueLoader }
15
+ : {}),
14
16
  });
15
17
  printUpdatePlan(plan);
16
18
  return plan;
@@ -33,8 +35,8 @@ export async function runSelfHostCommand(arguments_) {
33
35
  cause: error,
34
36
  });
35
37
  }
36
- const catalogue = await loadBundledSelfHostCatalogue();
37
- const generated = await generateSelfHostProject({
38
+ const catalogue = await (dependencies.catalogueLoader ?? loadCurrentSelfHostCatalogue)();
39
+ const generated = await (dependencies.projectGenerator ?? generateSelfHostProject)({
38
40
  catalogue,
39
41
  manifest,
40
42
  targetDirectory: parsed.outputPath,
@@ -240,6 +240,15 @@ export const selfHostEnvironmentContract = {
240
240
  optional: true,
241
241
  },
242
242
  HELYX_BOT_CONTROL_PORT: { value: "3001", optional: true },
243
+ HELYX_PRIVACY_ENABLED: { value: "false", optional: false },
244
+ HELYX_PRIVACY_EXECUTOR_DATABASE_URL: {
245
+ value: "<dedicated-privacy-executor-postgresql-url>",
246
+ optional: true,
247
+ },
248
+ HELYX_PRIVACY_EXECUTOR_DATABASE_SSL: {
249
+ value: "disable",
250
+ optional: true,
251
+ },
243
252
  HELYX_CLOUD_CONNECT_ENABLED: { value: "true", optional: false },
244
253
  HELYX_CLOUD_ENROLMENT_TOKEN: {
245
254
  value: "replace-with-a-short-lived-enrolment-code",
@@ -6,6 +6,8 @@ import { parse, resolve } from "node:path";
6
6
  import { z } from "zod";
7
7
  import { parseSelfHostCatalogue } from "./catalogue.js";
8
8
  export const DEFAULT_SELF_HOST_CATALOGUE_URL = "https://api.helyx.gg/self-host/catalogue";
9
+ const maximumCatalogueBytes = 512 * 1_024;
10
+ const catalogueTimeoutMilliseconds = 15_000;
9
11
  const packageName = z.string().regex(/^@helyx\/[a-z0-9][a-z0-9-]*$/u);
10
12
  const modulePackage = z.string().regex(/^@helyx\/module-[a-z0-9][a-z0-9-]*$/u);
11
13
  const exactVersion = z
@@ -100,34 +102,74 @@ export async function loadCurrentSelfHostCatalogue(fetcher = fetch) {
100
102
  try {
101
103
  response = await fetcher(DEFAULT_SELF_HOST_CATALOGUE_URL, {
102
104
  headers: { Accept: "application/json" },
103
- signal: AbortSignal.timeout(15_000),
105
+ redirect: "error",
106
+ signal: AbortSignal.timeout(catalogueTimeoutMilliseconds),
104
107
  });
105
108
  }
106
109
  catch (error) {
107
- throw new Error("The reviewed Helyx update catalogue is unavailable", {
110
+ throw new Error("The reviewed Helyx self-host catalogue is unavailable", {
108
111
  cause: error,
109
112
  });
110
113
  }
111
114
  if (!response.ok) {
112
- throw new Error(`The reviewed Helyx update catalogue returned HTTP ${response.status}`);
115
+ throw new Error(`The reviewed Helyx self-host catalogue returned HTTP ${response.status}`);
113
116
  }
114
- const declaredLength = Number(response.headers.get("content-length") ?? "0");
115
- if (declaredLength > 512 * 1_024) {
116
- throw new Error("The reviewed Helyx update catalogue was too large");
117
+ const contentType = response.headers.get("content-type")?.toLowerCase();
118
+ if (!contentType?.startsWith("application/json")) {
119
+ throw new Error("The reviewed Helyx self-host catalogue did not return JSON");
117
120
  }
118
- const text = await response.text();
119
- if (Buffer.byteLength(text, "utf8") > 512 * 1_024) {
120
- throw new Error("The reviewed Helyx update catalogue was too large");
121
+ const declaredLengthHeader = response.headers.get("content-length");
122
+ if (declaredLengthHeader !== null) {
123
+ const declaredLength = Number(declaredLengthHeader);
124
+ if (!Number.isSafeInteger(declaredLength) ||
125
+ declaredLength < 0 ||
126
+ declaredLength > maximumCatalogueBytes) {
127
+ throw new Error("The reviewed Helyx self-host catalogue had an invalid size");
128
+ }
121
129
  }
130
+ const text = await readBoundedCatalogueBody(response);
122
131
  try {
123
132
  return parseSelfHostCatalogue(JSON.parse(text));
124
133
  }
125
134
  catch (error) {
126
- throw new Error("The reviewed Helyx update catalogue was invalid", {
135
+ throw new Error("The reviewed Helyx self-host catalogue was invalid", {
127
136
  cause: error,
128
137
  });
129
138
  }
130
139
  }
140
+ async function readBoundedCatalogueBody(response) {
141
+ if (!response.body) {
142
+ throw new Error("The reviewed Helyx self-host catalogue was empty");
143
+ }
144
+ const reader = response.body.getReader();
145
+ const chunks = [];
146
+ let totalBytes = 0;
147
+ try {
148
+ while (true) {
149
+ const { done, value } = await reader.read();
150
+ if (done)
151
+ break;
152
+ totalBytes += value.byteLength;
153
+ if (totalBytes > maximumCatalogueBytes) {
154
+ throw new Error("The reviewed Helyx self-host catalogue was too large");
155
+ }
156
+ chunks.push(value);
157
+ }
158
+ }
159
+ finally {
160
+ reader.releaseLock();
161
+ }
162
+ if (totalBytes === 0) {
163
+ throw new Error("The reviewed Helyx self-host catalogue was empty");
164
+ }
165
+ const bytes = new Uint8Array(totalBytes);
166
+ let offset = 0;
167
+ for (const chunk of chunks) {
168
+ bytes.set(chunk, offset);
169
+ offset += chunk.byteLength;
170
+ }
171
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
172
+ }
131
173
  function targetHelyxDependencies(catalogue, selection) {
132
174
  if (catalogue.core.availability !== "published" ||
133
175
  catalogue.generator.availability !== "published") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.3",
4
4
  "description": "Command-line tools for running and generating self-hosted Helyx deployments.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -52,9 +52,9 @@
52
52
  "build": "tsc -b"
53
53
  },
54
54
  "dependencies": {
55
- "@helyx/bot": "0.2.2",
56
- "@helyx/config": "0.2.1",
57
- "@helyx/database": "0.2.1",
55
+ "@helyx/bot": "0.3.0",
56
+ "@helyx/config": "0.3.0",
57
+ "@helyx/database": "0.3.0",
58
58
  "semver": "7.8.5",
59
59
  "zod": "4.4.3"
60
60
  },
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "catalogueVersion": 11,
3
+ "catalogueVersion": 12,
4
4
  "core": {
5
5
  "package": "@helyx/bot",
6
- "version": "0.2.2",
6
+ "version": "0.3.0",
7
7
  "availability": "published"
8
8
  },
9
9
  "generator": {
10
10
  "package": "@helyx/cli",
11
- "version": "0.3.0",
11
+ "version": "0.3.3",
12
12
  "command": "helyx",
13
13
  "availability": "published"
14
14
  },
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "id": "helyx.feedback",
18
18
  "package": "@helyx/module-feedback",
19
- "version": "1.0.5",
19
+ "version": "1.1.0",
20
20
  "name": "Feedback",
21
21
  "description": "Collects simple feedback from Discord server members.",
22
22
  "category": "Community",
@@ -26,7 +26,7 @@
26
26
  {
27
27
  "id": "helyx.welcome",
28
28
  "package": "@helyx/module-welcome",
29
- "version": "1.0.5",
29
+ "version": "1.1.0",
30
30
  "name": "Welcome",
31
31
  "description": "Greets new members and can give them a starting role.",
32
32
  "category": "Community",
@@ -36,7 +36,7 @@
36
36
  {
37
37
  "id": "helyx.suggestions",
38
38
  "package": "@helyx/module-suggestions",
39
- "version": "1.0.5",
39
+ "version": "1.1.0",
40
40
  "name": "Suggestions",
41
41
  "description": "Collects community ideas with configurable reaction or button voting.",
42
42
  "category": "Community",
@@ -101,15 +101,14 @@
101
101
  "description": "Add a deliberate member verification step.",
102
102
  "category": "Safety",
103
103
  "availability": "planned",
104
- "releaseStatus": "testing",
105
- "tier": "premium"
104
+ "releaseStatus": "planned"
106
105
  },
107
106
  {
108
- "id": "helyx.antiraid",
109
- "package": "@helyx/module-antiraid",
107
+ "id": "helyx.raid-protection",
108
+ "package": "@helyx/module-raid-protection",
110
109
  "version": null,
111
- "name": "Raid protection",
112
- "description": "Detect and slow unusual join activity.",
110
+ "name": "Raid Protection",
111
+ "description": "Manually remove selected new activity during a raid.",
113
112
  "category": "Safety",
114
113
  "availability": "planned",
115
114
  "releaseStatus": "planned"
@@ -237,7 +236,7 @@
237
236
  {
238
237
  "id": "helyx.logging",
239
238
  "package": "@helyx/module-logging",
240
- "version": "1.0.5",
239
+ "version": "1.1.0",
241
240
  "name": "Logging",
242
241
  "description": "Posts configurable activity records from enabled Helyx modules.",
243
242
  "category": "Insights",
@@ -37,6 +37,12 @@ LOG_LEVEL=info
37
37
  # HELYX_BOT_CONTROL_SECRET=replace-with-a-base64-encoded-32-byte-key
38
38
  # HELYX_BOT_CONTROL_PORT=3001
39
39
 
40
+ # Privacy tooling ships with self-hosted Helyx but stays disabled until the
41
+ # dedicated least-privilege executor connection has been configured.
42
+ HELYX_PRIVACY_ENABLED=false
43
+ # HELYX_PRIVACY_EXECUTOR_DATABASE_URL=<dedicated-privacy-executor-postgresql-url>
44
+ # HELYX_PRIVACY_EXECUTOR_DATABASE_SSL=disable
45
+
40
46
  # Alternative Cloud Connect enrolment — the Discord command is preferred
41
47
  # HELYX_CLOUD_ENROLMENT_TOKEN=replace-with-a-short-lived-enrolment-code
42
48
  # HELYX_CLOUD_ENROLMENT_FILE=./helyx.cloud-enrolment.json