@wildo-ai/wildo-module-client 1.1.5 → 1.1.6

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.
Files changed (64) hide show
  1. package/dist/esm/services/_http-client.d.ts +32 -3
  2. package/dist/esm/services/_http-client.d.ts.map +1 -1
  3. package/dist/esm/services/_http-client.js +100 -6
  4. package/dist/esm/services/_http-client.js.map +1 -1
  5. package/dist/esm/services/module-adopter.service.d.ts +1 -1
  6. package/dist/esm/services/module-adopter.service.d.ts.map +1 -1
  7. package/dist/esm/services/module-adopter.service.js +7 -8
  8. package/dist/esm/services/module-adopter.service.js.map +1 -1
  9. package/dist/esm/services/module-packager.service.d.ts.map +1 -1
  10. package/dist/esm/services/module-packager.service.js +16 -19
  11. package/dist/esm/services/module-packager.service.js.map +1 -1
  12. package/dist/esm/services/provider-sync-runner.d.ts +2 -3
  13. package/dist/esm/services/provider-sync-runner.d.ts.map +1 -1
  14. package/dist/esm/services/provider-sync-runner.js +14 -2
  15. package/dist/esm/services/provider-sync-runner.js.map +1 -1
  16. package/dist/esm/services/registry-companion-client.d.ts +10 -47
  17. package/dist/esm/services/registry-companion-client.d.ts.map +1 -1
  18. package/dist/esm/services/registry-companion-client.js +9 -97
  19. package/dist/esm/services/registry-companion-client.js.map +1 -1
  20. package/dist/esm/services/registry-lockfile.service.d.ts +7 -3
  21. package/dist/esm/services/registry-lockfile.service.d.ts.map +1 -1
  22. package/dist/esm/services/registry-lockfile.service.js +8 -3
  23. package/dist/esm/services/registry-lockfile.service.js.map +1 -1
  24. package/dist/esm/services/registry-server-client.d.ts +35 -9
  25. package/dist/esm/services/registry-server-client.d.ts.map +1 -1
  26. package/dist/esm/services/registry-server-client.js +115 -58
  27. package/dist/esm/services/registry-server-client.js.map +1 -1
  28. package/dist/tsconfig.build.tsbuildinfo +1 -1
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +2 -2
  31. package/src/__tests__/module-adopter.service.test.ts +6 -4
  32. package/src/__tests__/module-packager.service.test.ts +36 -34
  33. package/src/__tests__/provider-sync-runner.test.ts +12 -0
  34. package/src/__tests__/registry-lockfile.service.test.ts +3 -3
  35. package/src/__tests__/registry-server-client.test.ts +129 -0
  36. package/src/__tests__/registry-transfer-timeouts.test.ts +94 -0
  37. package/src/services/_http-client.ts +100 -6
  38. package/src/services/module-adopter.service.ts +6 -9
  39. package/src/services/module-packager.service.ts +19 -16
  40. package/src/services/provider-sync-runner.ts +16 -5
  41. package/src/services/registry-companion-client.ts +10 -166
  42. package/src/services/registry-lockfile.service.ts +9 -3
  43. package/src/services/registry-server-client.ts +147 -57
  44. package/dist/esm/__tests__/lockfile-repair.service.test.d.ts +0 -0
  45. package/dist/esm/__tests__/lockfile-repair.service.test.d.ts.map +0 -0
  46. package/dist/esm/__tests__/lockfile-repair.service.test.js.map +0 -0
  47. package/dist/esm/__tests__/module-adopter.service.test.d.ts +0 -0
  48. package/dist/esm/__tests__/module-adopter.service.test.d.ts.map +0 -0
  49. package/dist/esm/__tests__/module-adopter.service.test.js.map +0 -0
  50. package/dist/esm/__tests__/module-packager.service.test.d.ts +0 -0
  51. package/dist/esm/__tests__/module-packager.service.test.d.ts.map +0 -0
  52. package/dist/esm/__tests__/module-packager.service.test.js.map +0 -0
  53. package/dist/esm/__tests__/provider-materialization-hook.test.d.ts +0 -0
  54. package/dist/esm/__tests__/provider-materialization-hook.test.d.ts.map +0 -0
  55. package/dist/esm/__tests__/provider-materialization-hook.test.js.map +0 -0
  56. package/dist/esm/__tests__/provider-sync-runner.test.d.ts +0 -0
  57. package/dist/esm/__tests__/provider-sync-runner.test.d.ts.map +0 -0
  58. package/dist/esm/__tests__/provider-sync-runner.test.js.map +0 -0
  59. package/dist/esm/__tests__/registry-lockfile.service.test.d.ts +0 -0
  60. package/dist/esm/__tests__/registry-lockfile.service.test.d.ts.map +0 -0
  61. package/dist/esm/__tests__/registry-lockfile.service.test.js.map +0 -0
  62. package/dist/esm/__tests__/registry-server-client.test.d.ts +0 -0
  63. package/dist/esm/__tests__/registry-server-client.test.d.ts.map +0 -0
  64. package/dist/esm/__tests__/registry-server-client.test.js.map +0 -0
@@ -31,14 +31,17 @@ import { createHash } from 'node:crypto';
31
31
  import { promises as fs } from 'node:fs';
32
32
  import * as path from 'node:path';
33
33
  import {
34
+ flattenModuleBoundaries,
35
+ isModuleArtifactPathAllowed,
36
+ isModuleId,
34
37
  ModuleManifestSchema,
35
38
  RegistryBoundaryViolationError,
36
39
  type ModuleManifest,
37
40
  type ModuleManifestBoundaries,
41
+ MODULE_MANIFEST_FILE_NAME as MANIFEST_FILE_NAME,
38
42
  } from '@wildo-ai/platform-config-lib';
39
43
 
40
44
  export const MODULES_ARTIFACTS_RELATIVE_DIR = '.wildo-saas/generated/modules-artifacts';
41
- const MANIFEST_FILE_NAME = 'wildo.module.json';
42
45
 
43
46
  export interface StagingPaths {
44
47
  /** Absolute path to the per-module staging root. */
@@ -52,7 +55,9 @@ export interface StagingPaths {
52
55
  * fs side effects.
53
56
  */
54
57
  export function getModuleStagingPaths(saasRoot: string, moduleId: string): StagingPaths {
55
- if (!moduleId.includes('/')) {
58
+ // The full `<scope>/<name>` shape, not merely "contains a `/`": `stageFresh` runs a recursive delete
59
+ // on the path built here, and `a/../../../..` contained a slash.
60
+ if (!isModuleId(moduleId)) {
56
61
  // C-L3 (4th-pass review): `TypeError` is Node's standard for
57
62
  // argument-shape violations. The typed `RegistryError` subclasses
58
63
  // model registry-runtime conditions (auth, integrity, version
@@ -62,10 +67,14 @@ export function getModuleStagingPaths(saasRoot: string, moduleId: string): Stagi
62
67
  // and stops the typed-error vocabulary from absorbing pure
63
68
  // argument-validation concerns.
64
69
  throw new TypeError(
65
- `Module id "${moduleId}" must be in <domain>/<name> form (e.g. "acme/billing").`,
70
+ `Module id "${moduleId}" must be in <scope>/<name> form (e.g. "acme/billing"), each half lowercase letters, digits, ".", "_" or "-".`,
66
71
  );
67
72
  }
68
- const stagingRoot = path.resolve(saasRoot, MODULES_ARTIFACTS_RELATIVE_DIR, moduleId);
73
+ const artifactsRoot = path.resolve(saasRoot, MODULES_ARTIFACTS_RELATIVE_DIR);
74
+ const stagingRoot = path.resolve(artifactsRoot, moduleId);
75
+ if (!isPathInside(stagingRoot, artifactsRoot) || stagingRoot === artifactsRoot) {
76
+ throw new TypeError(`Module id "${moduleId}" resolves outside ${MODULES_ARTIFACTS_RELATIVE_DIR}.`);
77
+ }
69
78
  return {
70
79
  stagingRoot,
71
80
  manifestPath: path.join(stagingRoot, MANIFEST_FILE_NAME),
@@ -114,20 +123,14 @@ export function assertWithinBoundaries(
114
123
  boundaries: ModuleManifestBoundaries | undefined,
115
124
  ): string {
116
125
  const absolute = path.resolve(saasRoot, appRelativePath);
117
- // Path must stay inside saasRoot (no `..` escapes).
118
- if (!isPathInside(absolute, path.resolve(saasRoot))) {
126
+ // Path must stay inside saasRoot (no `..` escapes), and be a path a module artifact may carry at
127
+ // all — no dot-segment (`.env`, `.git`), no `node_modules` — whatever the boundaries say.
128
+ if (!isPathInside(absolute, path.resolve(saasRoot)) || !isModuleArtifactPathAllowed(appRelativePath.split(path.sep).join('/'))) {
119
129
  throw new RegistryBoundaryViolationError(moduleId, appRelativePath);
120
130
  }
121
- if (!boundaries) return absolute;
122
- const allowed: readonly string[] = [
123
- ...boundaries.specifications,
124
- ...boundaries.shared,
125
- ...boundaries.frontend,
126
- ...boundaries.backend,
127
- ...boundaries.minions,
128
- ...boundaries.workers,
129
- ];
130
- if (allowed.length === 0) return absolute;
131
+ // No declared boundary means the module may write NOWHERE. This used to return early and admit
132
+ // every path in the application — the fail-open reading of an absent declaration.
133
+ const allowed: readonly string[] = flattenModuleBoundaries(boundaries);
131
134
  // The candidate must be inside at least one declared boundary.
132
135
  const isInside = allowed.some((rel) => isPathInside(absolute, path.resolve(saasRoot, rel)));
133
136
  if (!isInside) {
@@ -41,7 +41,16 @@ import {
41
41
  } from '@wildo-ai/platform-config-lib';
42
42
  import type { RegistryClientLogger } from './types';
43
43
 
44
- const DEFAULT_TIMEOUT_MS = 120_000;
44
+ /**
45
+ * How long the post-mutation `wildo config sync` may run before it is treated as wedged and killed.
46
+ *
47
+ * Five minutes, not two: the budget exists to stop a WEDGED sync, and a wall clock cannot tell wedged
48
+ * from slow. Measured 2026-09-27 on a scaffolded application (#1765): the same sync took 14.6 s on a
49
+ * quiet machine, 91 s while the framework's watchers were recompiling, and exceeded the old 120 s during
50
+ * an install under that load — so a healthy sync was killed part-way and the install reported a provider
51
+ * sync failure it did not have. The kill stays: a genuinely stuck sync still ends, five minutes on.
52
+ */
53
+ const DEFAULT_TIMEOUT_MS = 300_000;
45
54
 
46
55
  export interface ProviderSyncRunnerInput {
47
56
  /** Absolute path to the SaaS application root. */
@@ -57,9 +66,8 @@ export interface ProviderSyncRunnerInput {
57
66
  */
58
67
  wildoBinPath?: string;
59
68
  /**
60
- * Maximum wall-time before SIGTERM. Default 120s — provider sync
61
- * can be heavy on first run (artifact generation + cleanup),
62
- * but every additional second is operator-visible.
69
+ * Maximum wall-time before SIGTERM. Default {@link DEFAULT_TIMEOUT_MS} (five minutes): it only ever
70
+ * elapses on a wedged sync, and a healthy one under load has been measured past two minutes.
63
71
  */
64
72
  timeoutMs?: number;
65
73
  /**
@@ -178,7 +186,10 @@ function runWildoConfigSync(
178
186
  const startedAt = Date.now();
179
187
  const child = spawn(binPath, ['config', 'sync', '--domain', 'config'], {
180
188
  cwd,
181
- stdio: ['ignore', 'pipe', 'pipe'],
189
+ // stdout is IGNORED, not piped: nothing here reads it, and an unread pipe fills at the OS buffer
190
+ // (~64 KB) and blocks the child mid-write — a finished sync then looked like a wedged one and
191
+ // was killed at the timeout. stderr stays piped because the diagnostic reads it (bounded below).
192
+ stdio: ['ignore', 'ignore', 'pipe'],
182
193
  });
183
194
  let stderr = '';
184
195
  let signaled = false;
@@ -1,116 +1,25 @@
1
1
  /**
2
- * `RegistryCompanionClient` — HTTP client for the local dev
3
- * companion's registry routes.
2
+ * `RegistryCompanionClient` — liveness probe for the local dev companion, used by
3
+ * `wildo registry ping` beside the registry-server probe.
4
4
  *
5
- * Distinct from `RegistryServerClient` (which talks to the central
6
- * `platform-module-registry` server). The companion client targets
7
- * `/api/companion/registry/*` on `http://localhost:<companion-port>`
8
- * for AST/source mutations the CLI delegates to the companion
9
- * (KD-6).
10
- *
11
- * Phase 2 ships the four mutation routes as method calls; the
12
- * companion controller (under
13
- * `platform/factory/wildo-dev-companion/src/companion-api/controllers/`)
14
- * implements them with the actual ts-morph + filesystem work in
15
- * Phase 5+.
16
- *
17
- * The four mutations are GATED: the companion refuses any non-`GET` route that does not present the
18
- * machine-local token, so they take `RegistryCompanionMutationOptions` and `ping` — a `GET` — does
19
- * not. See that type for why the credential is required rather than optional.
5
+ * That is all it is. It used to carry four mutation methods (install / update / remove / publish)
6
+ * against `/api/companion/registry/*` routes that only ever answered a "Phase 5/6 deliverable"
7
+ * not-implemented envelope and had no caller. Installation shipped CLI-direct instead
8
+ * (`wildo registry add` / `update` / `remove` / `publish` do the work themselves), so the routes,
9
+ * these methods and their wire schemas were deleted on 2026-09-25 rather than left describing a
10
+ * design that was never built. A future companion-owned mutation lane starts from the CLI commands,
11
+ * not from this file.
20
12
  */
21
13
 
22
- import {
23
- REGISTRY_COMPANION_ROUTES,
24
- RegistryMalformedPayloadError,
25
- RegistryRouteResultSchema,
26
- RegistryUnexpectedStatusError,
27
- type RegistryInstallRequest,
28
- type RegistryPublishRequest,
29
- type RegistryRemoveRequest,
30
- type RegistryRouteResult,
31
- type RegistryUpdateRequest,
32
- } from '@wildo-ai/platform-config-lib';
33
14
  import { fetchWithTimeout, joinUrl } from './_http-client';
34
- import { formatZodIssuesAsSingleLine } from './_zod-issue-formatting';
35
15
 
36
16
  const DIAGNOSTIC_LABEL = 'Companion registry';
37
17
 
38
- /**
39
- * The header the companion's P-29b token floor reads on a mutating route.
40
- *
41
- * A wire contract between two processes that share no package, so it is named at both ends rather
42
- * than at neither. Its server twin is `COMPANION_TOKEN_HEADER` in the companion's
43
- * `core/auth/companion-auth-floor.companion.service.ts`; the CLI's is in
44
- * `wildo-cli/src/services/companion/companion-local-token.utils.ts`. Move one, move all three.
45
- */
46
- // semantic-literal-boundary: private companion HTTP protocol, translated only at this client edge.
47
- const COMPANION_TOKEN_HEADER = 'x-wildo-companion-token';
48
-
49
18
  export interface RegistryCompanionClientOptions {
50
19
  baseUrl: string;
51
20
  timeoutMs?: number;
52
21
  }
53
22
 
54
- /**
55
- * What a MUTATING companion registry call needs, over and above an address.
56
- *
57
- * `companionToken` is REQUIRED, and that is the whole point of this second type. Every non-`GET`
58
- * route under `/api/companion/**` is refused without it, and the four methods below have no caller
59
- * yet (the routes answer a not-implemented envelope until Phase 5) — so making it required now costs
60
- * nothing and means the caller Phase 5 writes cannot be written without the credential. An optional
61
- * field would have been a rule to remember; a required one is a compile error.
62
- *
63
- * The value is the content of `<appRoot>/.wildo-saas/state/companion.token` (mode 0600), which the
64
- * companion mints on every boot. This package has no notion of an application root, so reading it is
65
- * deliberately the caller's job: `wildo-cli` already owns a hardened no-follow reader
66
- * (`readCompanionTokenNoFollow`), and a second copy of a permission-checked read is precisely where
67
- * the checks quietly diverge.
68
- */
69
- export interface RegistryCompanionMutationOptions extends RegistryCompanionClientOptions {
70
- companionToken: string;
71
- }
72
-
73
- async function postJson<R>(
74
- options: RegistryCompanionMutationOptions,
75
- routePath: string,
76
- body: unknown,
77
- ): Promise<R> {
78
- const url = joinUrl(options.baseUrl, routePath);
79
- const res = await fetchWithTimeout(url, {
80
- method: 'POST',
81
- headers: { 'content-type': 'application/json', [COMPANION_TOKEN_HEADER]: options.companionToken },
82
- body: JSON.stringify(body),
83
- timeoutMs: options.timeoutMs,
84
- diagnosticLabel: DIAGNOSTIC_LABEL,
85
- });
86
- if (!res.ok) {
87
- // 3rd-pass M5: typed `RegistryUnexpectedStatusError` instead of
88
- // bare `new Error(...)` so callers can `isRegistryError(err)` and
89
- // discriminate by status code without re-parsing message text.
90
- // The companion deliberately reuses the same error vocabulary
91
- // as `RegistryServerClient` since the wire shape (status + URL +
92
- // action) is identical even though the lane is different.
93
- throw new RegistryUnexpectedStatusError(
94
- options.baseUrl,
95
- res.status,
96
- res.statusText,
97
- `companion ${routePath}`,
98
- );
99
- }
100
- // 6th-pass M3: typed JSON-parse — a misconfigured proxy returning
101
- // HTML at 2xx would otherwise throw a bare `SyntaxError` that
102
- // bypasses the typed-error contract.
103
- try {
104
- return (await res.json()) as R;
105
- } catch (err) {
106
- throw new RegistryMalformedPayloadError(
107
- options.baseUrl,
108
- `companion ${routePath}`,
109
- `response is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
110
- );
111
- }
112
- }
113
-
114
23
  export const RegistryCompanionClient = {
115
24
  /**
116
25
  * Liveness probe against `/health` (the same endpoint the
@@ -129,69 +38,4 @@ export const RegistryCompanionClient = {
129
38
  return false;
130
39
  }
131
40
  },
132
-
133
- /**
134
- * Ask the companion to install a module: download bytes, copy
135
- * source, mutate `wildo.saas.config.ts > moduleRegistries` +
136
- * the three `modules-registry.*.ts` files, refresh
137
- * `sections.modules`, run provider sync.
138
- */
139
- async install(
140
- options: RegistryCompanionMutationOptions,
141
- request: RegistryInstallRequest,
142
- ): Promise<RegistryRouteResult> {
143
- const raw = await postJson<unknown>(options, REGISTRY_COMPANION_ROUTES.install, request);
144
- return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.install, raw);
145
- },
146
-
147
- async update(
148
- options: RegistryCompanionMutationOptions,
149
- request: RegistryUpdateRequest,
150
- ): Promise<RegistryRouteResult> {
151
- const raw = await postJson<unknown>(options, REGISTRY_COMPANION_ROUTES.update, request);
152
- return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.update, raw);
153
- },
154
-
155
- async remove(
156
- options: RegistryCompanionMutationOptions,
157
- request: RegistryRemoveRequest,
158
- ): Promise<RegistryRouteResult> {
159
- const raw = await postJson<unknown>(options, REGISTRY_COMPANION_ROUTES.remove, request);
160
- return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.remove, raw);
161
- },
162
-
163
- async publish(
164
- options: RegistryCompanionMutationOptions,
165
- request: RegistryPublishRequest,
166
- ): Promise<RegistryRouteResult> {
167
- const raw = await postJson<unknown>(options, REGISTRY_COMPANION_ROUTES.publish, request);
168
- return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.publish, raw);
169
- },
170
- } as const;
171
-
172
- /**
173
- * M5: thin wrapper around `RegistryRouteResultSchema.parse(raw)` that
174
- * converts a Zod parse failure into the typed
175
- * `RegistryMalformedPayloadError` instead of letting a bare `ZodError`
176
- * bubble up. Keeps the typed-handler contract aligned with
177
- * `RegistryServerClient`.
178
- */
179
- function parseCompanionResult(
180
- baseUrl: string,
181
- routePath: string,
182
- raw: unknown,
183
- ): RegistryRouteResult {
184
- const parsed = RegistryRouteResultSchema.safeParse(raw);
185
- if (!parsed.success) {
186
- // 7th-pass C-L2: format the issues array for operator
187
- // readability instead of using Zod 4's JSON-stringified
188
- // `error.message`.
189
- const detail = formatZodIssuesAsSingleLine(parsed.error);
190
- throw new RegistryMalformedPayloadError(
191
- baseUrl,
192
- `companion ${routePath}`,
193
- detail,
194
- );
195
- }
196
- return parsed.data;
197
- }
41
+ };
@@ -25,7 +25,8 @@ import {
25
25
  withLockfileSectionMutate,
26
26
  readWildoSaasLock,
27
27
  type RegistryModulesLockSection,
28
- type RegistryModulesLockEntry,
28
+ RegistryModulesLockEntrySchema,
29
+ type RegistryModulesLockEntryInput,
29
30
  } from '@wildo-ai/platform-config-lib';
30
31
  // L6 (third-pass review): the package's barrel-discipline rule says
31
32
  // "Never re-export (except in the root `index.ts` of each package)."
@@ -81,8 +82,13 @@ export const RegistryLockfileService = {
81
82
  );
82
83
  },
83
84
 
84
- /** Convenience helper used by Phase 5+ install flow. */
85
- async upsertEntry(saasRoot: string, entry: RegistryModulesLockEntry): Promise<void> {
85
+ /**
86
+ * Insert or replace one module's entry. The entry is parsed here, so its defaults (`distribution`,
87
+ * `files`) apply in one place and a malformed entry is refused naming the module, before the lockfile
88
+ * is touched (`mutate` then re-validates the whole section).
89
+ */
90
+ async upsertEntry(saasRoot: string, input: RegistryModulesLockEntryInput): Promise<void> {
91
+ const entry = RegistryModulesLockEntrySchema.parse(input);
86
92
  await this.mutate(saasRoot, (current) => ({
87
93
  ...current,
88
94
  installed: { ...current.installed, [entry.moduleId]: entry },