@openparachute/hub 0.5.10 → 0.5.12-rc.2

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/src/well-known.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { CONFIG_DIR } from "./config.ts";
4
- import type { ServiceEntry } from "./services-manifest.ts";
4
+ import { type ModuleManifest, readModuleManifest } from "./module-manifest.ts";
5
+ import { type ServiceEntry, readManifest } from "./services-manifest.ts";
5
6
 
6
7
  export interface WellKnownServiceEntry {
7
8
  url: string;
@@ -214,3 +215,83 @@ export function writeWellKnownFile(doc: WellKnownDocument, path: string = WELL_K
214
215
  renameSync(tmp, path);
215
216
  return path;
216
217
  }
218
+
219
+ export interface RegenerateWellKnownOpts {
220
+ /** Path to services.json. Defaults to `SERVICES_MANIFEST_PATH`. */
221
+ manifestPath: string;
222
+ /**
223
+ * Origin to embed in the doc's `url` fields. The hub HTTP path uses
224
+ * `configuredIssuer ?? new URL(req.url).origin`; module-ops callers don't
225
+ * have a request, so they pass `issuer` (the configured hub origin from
226
+ * `ApiModulesOpsDeps`) — same canonical URL the per-request build would
227
+ * emit.
228
+ */
229
+ canonicalOrigin: string;
230
+ /** Override the on-disk well-known path (test seam). Defaults to `WELL_KNOWN_PATH`. */
231
+ wellKnownPath?: string;
232
+ /**
233
+ * Reader for a module's `.parachute/module.json`. Production uses
234
+ * `readModuleManifest`; tests inject a stub. Mirrors hub-server's
235
+ * `readModuleManifest` seam so the disk regen and the per-request build
236
+ * stay in lockstep.
237
+ */
238
+ readModuleManifest?: (installDir: string) => Promise<ModuleManifest | null>;
239
+ }
240
+
241
+ /**
242
+ * Regenerate `/.well-known/parachute.json` on disk from current services.json.
243
+ *
244
+ * Mirrors the dynamic build inside `hub-server.ts`'s
245
+ * `/.well-known/parachute.json` handler so the on-disk doc tracks the same
246
+ * `uiUrl` / `displayName` / `managementUrl` shape the live discovery page
247
+ * fetches. Returns the path written + the resulting doc so callers can log
248
+ * and tests can assert without re-reading from disk.
249
+ *
250
+ * Used by `/api/modules/:short/{install,upgrade,uninstall}` post-mutation so
251
+ * the on-disk doc stays current after lifecycle ops. The per-request build
252
+ * in hub-server.ts remains the source of truth for live HTTP reads — this
253
+ * disk write is the inspection / debug artifact (and a belt-and-suspenders
254
+ * canary for anything that reads the file directly).
255
+ */
256
+ export async function regenerateWellKnown(
257
+ opts: RegenerateWellKnownOpts,
258
+ ): Promise<{ path: string; doc: WellKnownDocument }> {
259
+ const read = opts.readModuleManifest ?? readModuleManifest;
260
+ const path = opts.wellKnownPath ?? WELL_KNOWN_PATH;
261
+ const services = readManifest(opts.manifestPath).services;
262
+ // Build the resolver maps the same way hub-server does — read each
263
+ // module's `.parachute/module.json` from `installDir` and harvest
264
+ // managementUrl (vault rows) + uiUrl + displayName (non-vault rows).
265
+ // Per-entry errors land in console.warn so one malformed manifest doesn't
266
+ // block the regen for everyone else.
267
+ const managementUrls = new Map<string, string>();
268
+ const uiUrls = new Map<string, string>();
269
+ const displayNames = new Map<string, string>();
270
+ await Promise.all(
271
+ services.map(async (s) => {
272
+ if (!s.installDir) return;
273
+ try {
274
+ const m = await read(s.installDir);
275
+ if (!m) return;
276
+ if (isVaultEntry(s)) {
277
+ if (m.managementUrl) managementUrls.set(s.name, m.managementUrl);
278
+ } else {
279
+ if (m.uiUrl) uiUrls.set(s.name, m.uiUrl);
280
+ if (m.displayName) displayNames.set(s.name, m.displayName);
281
+ }
282
+ } catch (err) {
283
+ const msg = err instanceof Error ? err.message : String(err);
284
+ console.warn(`well-known regen: skipping module metadata for ${s.name}: ${msg}`);
285
+ }
286
+ }),
287
+ );
288
+ const doc = buildWellKnown({
289
+ services,
290
+ canonicalOrigin: opts.canonicalOrigin,
291
+ managementUrlFor: (entry) => managementUrls.get(entry.name),
292
+ uiUrlFor: (entry) => uiUrls.get(entry.name),
293
+ displayNameFor: (entry) => displayNames.get(entry.name),
294
+ });
295
+ writeWellKnownFile(doc, path);
296
+ return { path, doc };
297
+ }