@openparachute/hub 0.5.13-rc.13 → 0.5.13-rc.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/hub",
3
- "version": "0.5.13-rc.13",
3
+ "version": "0.5.13-rc.14",
4
4
  "description": "parachute — the local hub for the Parachute ecosystem (discovery, ports, lifecycle, soon OAuth).",
5
5
  "license": "AGPL-3.0",
6
6
  "publishConfig": {
@@ -35,7 +35,8 @@ describe("validateModuleManifest", () => {
35
35
 
36
36
  test("rejects missing required fields", () => {
37
37
  expect(() => validateModuleManifest({ ...VALID, name: undefined }, "x")).toThrow(/name/);
38
- expect(() => validateModuleManifest({ ...VALID, kind: "weird" }, "x")).toThrow(/kind/);
38
+ // `kind` is NOT validated as of hub#327 — see the "kind is no longer
39
+ // validated" suite below for the full behavior surface.
39
40
  expect(() => validateModuleManifest({ ...VALID, port: -1 }, "x")).toThrow(/port/);
40
41
  expect(() => validateModuleManifest({ ...VALID, port: 99999 }, "x")).toThrow(/port/);
41
42
  expect(() => validateModuleManifest({ ...VALID, paths: "not-array" }, "x")).toThrow(/paths/);
@@ -44,6 +45,61 @@ describe("validateModuleManifest", () => {
44
45
  );
45
46
  });
46
47
 
48
+ // hub#327 (hub#301 Phase A fold): the validator no longer inspects `kind`.
49
+ // Any value — present, absent, valid string, typo, wrong type — is
50
+ // accepted. The single downstream read site
51
+ // (`commands/upgrade.ts: target.spec?.kind === "frontend"`) handles the
52
+ // absent / non-"frontend" case via falsy-fallthrough into the
53
+ // backend-proxy default.
54
+ describe("kind is no longer validated (hub#327)", () => {
55
+ test("missing kind is accepted and produces an undefined kind", () => {
56
+ const { kind: _ignored, ...withoutKind } = VALID;
57
+ const m = validateModuleManifest(withoutKind, "x");
58
+ expect(m.kind).toBeUndefined();
59
+ });
60
+
61
+ test("explicit kind: 'frontend' passes through unchanged", () => {
62
+ const m = validateModuleManifest({ ...VALID, kind: "frontend" }, "x");
63
+ expect(m.kind).toBe("frontend");
64
+ });
65
+
66
+ test("explicit kind: 'api' passes through unchanged", () => {
67
+ const m = validateModuleManifest({ ...VALID, kind: "api" }, "x");
68
+ expect(m.kind).toBe("api");
69
+ });
70
+
71
+ test("explicit kind: 'tool' passes through unchanged", () => {
72
+ const m = validateModuleManifest({ ...VALID, kind: "tool" }, "x");
73
+ expect(m.kind).toBe("tool");
74
+ });
75
+
76
+ test("invalid kind values are also accepted (validator no longer inspects)", () => {
77
+ // Per Aaron's direction on #327: stop validating kind. Typos,
78
+ // novel strings, wrong types — none of them error. The validator
79
+ // simply doesn't look at the field. Downstream routing branches on
80
+ // `kind === "frontend"` and falls through gracefully for anything
81
+ // else (including these), so accepting them is safe.
82
+ const m1 = validateModuleManifest({ ...VALID, kind: "static" }, "x");
83
+ expect(m1.kind).toBeUndefined();
84
+ const m2 = validateModuleManifest({ ...VALID, kind: "backend" }, "x");
85
+ expect(m2.kind).toBeUndefined();
86
+ const m3 = validateModuleManifest({ ...VALID, kind: null }, "x");
87
+ expect(m3.kind).toBeUndefined();
88
+ const m4 = validateModuleManifest({ ...VALID, kind: 42 }, "x");
89
+ expect(m4.kind).toBeUndefined();
90
+ });
91
+
92
+ test("routing-relevant kind: 'frontend' still survives the validator (upgrade.ts branch intact)", () => {
93
+ // Defensive sanity check: the one downstream branch that reads kind
94
+ // (commands/upgrade.ts checks `target.spec?.kind === "frontend"` to
95
+ // decide whether to run `bun run build`) keeps working — when a
96
+ // module DOES declare `kind: "frontend"`, the value reaches that
97
+ // branch untouched.
98
+ const m = validateModuleManifest({ ...VALID, kind: "frontend" }, "x");
99
+ expect(m.kind === "frontend").toBe(true);
100
+ });
101
+ });
102
+
47
103
  test("rejects invalid name shape", () => {
48
104
  expect(() => validateModuleManifest({ ...VALID, name: "Demo" }, "x")).toThrow(/name/);
49
105
  expect(() => validateModuleManifest({ ...VALID, name: "1demo" }, "x")).toThrow(/name/);
@@ -76,8 +76,17 @@ export interface ModuleManifest {
76
76
  readonly displayName?: string;
77
77
  /** One-line subtitle rendered under displayName. */
78
78
  readonly tagline?: string;
79
- /** Drives card vs. iframe vs. launcher in the hub. */
80
- readonly kind: ModuleKind;
79
+ /**
80
+ * Historically drove card vs. iframe vs. launcher in the hub. As of
81
+ * hub#301 Phase A's fold (#327) the validator no longer inspects `kind` —
82
+ * any value, or no value at all, is accepted and passes through untouched.
83
+ * Routing branches downstream use `=== "frontend"` style checks which
84
+ * treat undefined/other values as the backend-proxy default (so the
85
+ * routing remains correct without validator enforcement). New modules
86
+ * may safely omit the field; existing values are preserved for the
87
+ * narrow `kind === "frontend"` branch in `commands/upgrade.ts`.
88
+ */
89
+ readonly kind?: ModuleKind;
81
90
  /** Default loopback port. CLI warns on conflict, doesn't block. */
82
91
  readonly port: number;
83
92
  /** URL paths the module serves under the hub origin. */
@@ -167,11 +176,19 @@ function asOptionalString(v: unknown, where: string, field: string): string | un
167
176
  return v;
168
177
  }
169
178
 
170
- function asKind(v: unknown, where: string): ModuleKind {
171
- if (v !== "api" && v !== "frontend" && v !== "tool") {
172
- throw new ModuleManifestError(`${where}: "kind" must be "api" | "frontend" | "tool"`);
173
- }
174
- return v;
179
+ /**
180
+ * Pass-through `kind` reader (hub#301 Phase A fold #327).
181
+ *
182
+ * The validator no longer inspects `kind`. Any value, or no value, is
183
+ * accepted. We narrow to the canonical `ModuleKind` only when the input is
184
+ * one of the three known strings — otherwise we drop the field entirely so
185
+ * downstream `kind === "frontend"` branches fall through to the
186
+ * backend-proxy default. Author intent (typo, novel value, omission) is no
187
+ * longer surfaced from this layer; it's not the validator's job anymore.
188
+ */
189
+ function asKind(v: unknown): ModuleKind | undefined {
190
+ if (v === "api" || v === "frontend" || v === "tool") return v;
191
+ return undefined;
175
192
  }
176
193
 
177
194
  function asPort(v: unknown, where: string): number {
@@ -341,9 +358,20 @@ function asDependencies(v: unknown, where: string): Record<string, ModuleDepende
341
358
  /**
342
359
  * Strict validator. Throws `ModuleManifestError` with the source path so
343
360
  * malformed third-party modules get a clear-enough error to fix. Required
344
- * fields are name, manifestName, kind, port, paths, health.
361
+ * fields are name, manifestName, port, paths, health. `kind` is no longer
362
+ * inspected as of hub#301 Phase A's fold (#327) — any value (or none) is
363
+ * accepted and passes through untouched. See `asKind` for the narrowing
364
+ * behavior.
365
+ *
366
+ * The optional `logger` parameter is retained for forward-compatibility
367
+ * with future validator soft-warnings, even though the kind soft-warning
368
+ * it was originally added for has been removed.
345
369
  */
346
- export function validateModuleManifest(raw: unknown, where: string): ModuleManifest {
370
+ export function validateModuleManifest(
371
+ raw: unknown,
372
+ where: string,
373
+ _logger: Pick<Console, "warn"> = console,
374
+ ): ModuleManifest {
347
375
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
348
376
  throw new ModuleManifestError(`${where}: root must be an object`);
349
377
  }
@@ -356,7 +384,7 @@ export function validateModuleManifest(raw: unknown, where: string): ModuleManif
356
384
  );
357
385
  }
358
386
  const manifestName = asString(m.manifestName, where, "manifestName");
359
- const kind = asKind(m.kind, where);
387
+ const kind = asKind(m.kind);
360
388
  const port = asPort(m.port, where);
361
389
  const paths = asStringArray(m.paths, where, "paths");
362
390
  const health = asHealthPath(m.health, where);
@@ -404,7 +432,8 @@ export function validateModuleManifest(raw: unknown, where: string): ModuleManif
404
432
  stripPrefix = m.stripPrefix;
405
433
  }
406
434
 
407
- const out: ModuleManifest = { name, manifestName, kind, port, paths, health };
435
+ const out: ModuleManifest = { name, manifestName, port, paths, health };
436
+ if (kind !== undefined) (out as { kind?: ModuleKind }).kind = kind;
408
437
  if (displayName !== undefined) (out as { displayName?: string }).displayName = displayName;
409
438
  if (tagline !== undefined) (out as { tagline?: string }).tagline = tagline;
410
439
  if (startCmd !== undefined) (out as { startCmd?: readonly string[] }).startCmd = startCmd;
@@ -470,8 +499,14 @@ function asPathOrUrl(v: unknown, where: string, field: string): string | undefin
470
499
  * absent (caller decides whether that's an error — first-party modules fall
471
500
  * back to the vendored manifest; third-party hard-errors). Throws
472
501
  * `ModuleManifestError` on parse / validation failure.
502
+ *
503
+ * The optional `logger` parameter is retained for forward-compatibility
504
+ * with future validator soft-warnings. Defaults to `console`.
473
505
  */
474
- export async function readModuleManifest(packageDir: string): Promise<ModuleManifest | null> {
506
+ export async function readModuleManifest(
507
+ packageDir: string,
508
+ logger: Pick<Console, "warn"> = console,
509
+ ): Promise<ModuleManifest | null> {
475
510
  const path = join(packageDir, ".parachute", "module.json");
476
511
  let buf: string;
477
512
  try {
@@ -488,5 +523,5 @@ export async function readModuleManifest(packageDir: string): Promise<ModuleMani
488
523
  `${path}: failed to parse JSON: ${err instanceof Error ? err.message : String(err)}`,
489
524
  );
490
525
  }
491
- return validateModuleManifest(parsed, path);
526
+ return validateModuleManifest(parsed, path, logger);
492
527
  }
@@ -174,7 +174,14 @@ export interface ServiceSpec {
174
174
  * First service boot overwrites the seed with its own authoritative version.
175
175
  */
176
176
  readonly seedEntry?: () => ServiceEntry;
177
- readonly kind: ServiceKind;
177
+ /**
178
+ * Optional as of hub#327 (Phase A's fold): the validator no longer
179
+ * inspects `kind`, so synthesized + third-party-manifest specs may
180
+ * carry `undefined` here. The single read site
181
+ * (`commands/upgrade.ts: target.spec?.kind === "frontend"`) handles
182
+ * the absent case via the `=== "frontend"` falsy-fallthrough.
183
+ */
184
+ readonly kind?: ServiceKind;
178
185
  readonly hasAuth?: boolean;
179
186
  readonly urlForEntry?: (entry: ServiceEntry) => string | undefined;
180
187
  readonly postInstallFooter?: () => readonly string[];