@wildo-ai/wildo-module-client 1.1.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.
Files changed (98) hide show
  1. package/LICENSE +34 -0
  2. package/dist/esm/.builder.pid +9 -0
  3. package/dist/esm/__tests__/lockfile-repair.service.test.d.ts +0 -0
  4. package/dist/esm/__tests__/lockfile-repair.service.test.d.ts.map +0 -0
  5. package/dist/esm/__tests__/lockfile-repair.service.test.js.map +0 -0
  6. package/dist/esm/__tests__/module-adopter.service.test.d.ts +0 -0
  7. package/dist/esm/__tests__/module-adopter.service.test.d.ts.map +0 -0
  8. package/dist/esm/__tests__/module-adopter.service.test.js.map +0 -0
  9. package/dist/esm/__tests__/module-packager.service.test.d.ts +0 -0
  10. package/dist/esm/__tests__/module-packager.service.test.d.ts.map +0 -0
  11. package/dist/esm/__tests__/module-packager.service.test.js.map +0 -0
  12. package/dist/esm/__tests__/provider-materialization-hook.test.d.ts +0 -0
  13. package/dist/esm/__tests__/provider-materialization-hook.test.d.ts.map +0 -0
  14. package/dist/esm/__tests__/provider-materialization-hook.test.js.map +0 -0
  15. package/dist/esm/__tests__/provider-sync-runner.test.d.ts +0 -0
  16. package/dist/esm/__tests__/provider-sync-runner.test.d.ts.map +0 -0
  17. package/dist/esm/__tests__/provider-sync-runner.test.js.map +0 -0
  18. package/dist/esm/__tests__/registry-lockfile.service.test.d.ts +0 -0
  19. package/dist/esm/__tests__/registry-lockfile.service.test.d.ts.map +0 -0
  20. package/dist/esm/__tests__/registry-lockfile.service.test.js.map +0 -0
  21. package/dist/esm/__tests__/registry-server-client.test.d.ts +0 -0
  22. package/dist/esm/__tests__/registry-server-client.test.d.ts.map +0 -0
  23. package/dist/esm/__tests__/registry-server-client.test.js.map +0 -0
  24. package/dist/esm/index.d.ts +18 -0
  25. package/dist/esm/index.d.ts.map +1 -0
  26. package/dist/esm/index.js +18 -0
  27. package/dist/esm/index.js.map +1 -0
  28. package/dist/esm/services/_http-client.d.ts +44 -0
  29. package/dist/esm/services/_http-client.d.ts.map +1 -0
  30. package/dist/esm/services/_http-client.js +75 -0
  31. package/dist/esm/services/_http-client.js.map +1 -0
  32. package/dist/esm/services/_zod-issue-formatting.d.ts +12 -0
  33. package/dist/esm/services/_zod-issue-formatting.d.ts.map +1 -0
  34. package/dist/esm/services/_zod-issue-formatting.js +47 -0
  35. package/dist/esm/services/_zod-issue-formatting.js.map +1 -0
  36. package/dist/esm/services/index.d.ts +10 -0
  37. package/dist/esm/services/index.d.ts.map +1 -0
  38. package/dist/esm/services/index.js +10 -0
  39. package/dist/esm/services/index.js.map +1 -0
  40. package/dist/esm/services/lockfile-repair.service.d.ts +68 -0
  41. package/dist/esm/services/lockfile-repair.service.d.ts.map +1 -0
  42. package/dist/esm/services/lockfile-repair.service.js +160 -0
  43. package/dist/esm/services/lockfile-repair.service.js.map +1 -0
  44. package/dist/esm/services/module-adopter.service.d.ts +59 -0
  45. package/dist/esm/services/module-adopter.service.d.ts.map +1 -0
  46. package/dist/esm/services/module-adopter.service.js +126 -0
  47. package/dist/esm/services/module-adopter.service.js.map +1 -0
  48. package/dist/esm/services/module-packager.service.d.ts +105 -0
  49. package/dist/esm/services/module-packager.service.d.ts.map +1 -0
  50. package/dist/esm/services/module-packager.service.js +212 -0
  51. package/dist/esm/services/module-packager.service.js.map +1 -0
  52. package/dist/esm/services/provider-materialization-hook.d.ts +32 -0
  53. package/dist/esm/services/provider-materialization-hook.d.ts.map +1 -0
  54. package/dist/esm/services/provider-materialization-hook.js +33 -0
  55. package/dist/esm/services/provider-materialization-hook.js.map +1 -0
  56. package/dist/esm/services/provider-sync-runner.d.ts +87 -0
  57. package/dist/esm/services/provider-sync-runner.d.ts.map +1 -0
  58. package/dist/esm/services/provider-sync-runner.js +207 -0
  59. package/dist/esm/services/provider-sync-runner.js.map +1 -0
  60. package/dist/esm/services/registry-companion-client.d.ts +39 -0
  61. package/dist/esm/services/registry-companion-client.d.ts.map +1 -0
  62. package/dist/esm/services/registry-companion-client.js +109 -0
  63. package/dist/esm/services/registry-companion-client.js.map +1 -0
  64. package/dist/esm/services/registry-lockfile.service.d.ts +56 -0
  65. package/dist/esm/services/registry-lockfile.service.d.ts.map +1 -0
  66. package/dist/esm/services/registry-lockfile.service.js +99 -0
  67. package/dist/esm/services/registry-lockfile.service.js.map +1 -0
  68. package/dist/esm/services/registry-server-client.d.ts +94 -0
  69. package/dist/esm/services/registry-server-client.d.ts.map +1 -0
  70. package/dist/esm/services/registry-server-client.js +225 -0
  71. package/dist/esm/services/registry-server-client.js.map +1 -0
  72. package/dist/esm/services/types.d.ts +22 -0
  73. package/dist/esm/services/types.d.ts.map +1 -0
  74. package/dist/esm/services/types.js +16 -0
  75. package/dist/esm/services/types.js.map +1 -0
  76. package/dist/tsconfig.build.tsbuildinfo +1 -0
  77. package/dist/tsconfig.tsbuildinfo +1 -0
  78. package/package.json +51 -0
  79. package/src/__tests__/lockfile-repair.service.test.ts +106 -0
  80. package/src/__tests__/module-adopter.service.test.ts +96 -0
  81. package/src/__tests__/module-packager.service.test.ts +258 -0
  82. package/src/__tests__/provider-materialization-hook.test.ts +47 -0
  83. package/src/__tests__/provider-sync-runner.test.ts +113 -0
  84. package/src/__tests__/registry-lockfile.service.test.ts +134 -0
  85. package/src/__tests__/registry-server-client.test.ts +312 -0
  86. package/src/index.ts +18 -0
  87. package/src/services/_http-client.ts +91 -0
  88. package/src/services/_zod-issue-formatting.ts +50 -0
  89. package/src/services/index.ts +9 -0
  90. package/src/services/lockfile-repair.service.ts +215 -0
  91. package/src/services/module-adopter.service.ts +155 -0
  92. package/src/services/module-packager.service.ts +251 -0
  93. package/src/services/provider-materialization-hook.ts +54 -0
  94. package/src/services/provider-sync-runner.ts +298 -0
  95. package/src/services/registry-companion-client.ts +163 -0
  96. package/src/services/registry-lockfile.service.ts +122 -0
  97. package/src/services/registry-server-client.ts +334 -0
  98. package/src/services/types.ts +22 -0
@@ -0,0 +1,334 @@
1
+ /**
2
+ * `RegistryServerClient` — HTTP client for the central registry
3
+ * server (`platform-module-registry`, KD-7).
4
+ *
5
+ * Phase 2 fills out the read + download surface (`resolve`,
6
+ * `info`, `search`, `download`). The wire shapes mirror the
7
+ * gateway endpoints documented in the analysis plan's "Registry
8
+ * HTTP API" section. The actual server (Phase 3) ships these
9
+ * routes; until it does, the client throws typed errors when it
10
+ * can't reach the server.
11
+ *
12
+ * Auth (KD-11): if `authToken` is supplied, every request adds
13
+ * `Authorization: Bearer <token>`. Public read paths work without
14
+ * a token; restricted paths return 401/403, surfaced as
15
+ * `RegistryAuthRequiredError` / `RegistryAuthForbiddenError`.
16
+ */
17
+
18
+ import { z } from 'zod';
19
+ import { formatZodIssuesAsSingleLine } from './_zod-issue-formatting';
20
+ import {
21
+ ModuleManifestSchema,
22
+ ModuleVersionRecordSchema,
23
+ RegistryArtifactIntegrityMismatchError,
24
+ RegistryAuthForbiddenError,
25
+ RegistryAuthRequiredError,
26
+ RegistryError,
27
+ RegistryMalformedPayloadError,
28
+ RegistryModuleNotFoundError,
29
+ RegistryUnexpectedStatusError,
30
+ RegistryVersionNotFoundError,
31
+ Sha256DigestSchema,
32
+ type ModuleManifest,
33
+ type ModuleVersionRecord,
34
+ } from '@wildo-ai/platform-config-lib';
35
+ import { fetchJsonOk, fetchWithTimeout, joinUrl } from './_http-client';
36
+
37
+ /**
38
+ * Zod schema for the `info()` response payload — mirrors
39
+ * `RegistryInfoResult` and is enforced at the client boundary so a
40
+ * server schema drift fails fast instead of returning malformed data
41
+ * to downstream consumers.
42
+ */
43
+ const RegistryInfoResponseSchema = z.object({
44
+ moduleId: z.string().min(1),
45
+ manifest: ModuleManifestSchema,
46
+ versions: z.array(ModuleVersionRecordSchema),
47
+ tags: z.record(z.string(), z.string()),
48
+ });
49
+
50
+ const RegistrySearchResponseSchema = z.object({
51
+ hits: z.array(
52
+ z.object({
53
+ moduleId: z.string().min(1),
54
+ description: z.string().optional(),
55
+ latestVersion: z.string(),
56
+ }),
57
+ ),
58
+ });
59
+
60
+ const DIAGNOSTIC_LABEL = 'Registry';
61
+
62
+ export interface RegistryServerClientOptions {
63
+ /** Registry origin (e.g. `https://registry.wildo.ai`). */
64
+ baseUrl: string;
65
+ /** Optional bearer / API-key. CLI credentials are per-machine (KD-11). */
66
+ authToken?: string;
67
+ /** Request timeout in ms. */
68
+ timeoutMs?: number;
69
+ }
70
+
71
+ function authHeaders(options: RegistryServerClientOptions): Record<string, string> {
72
+ return options.authToken ? { Authorization: `Bearer ${options.authToken}` } : {};
73
+ }
74
+
75
+ async function classifyAuthError(res: Response, registryUrl: string, action: string): Promise<RegistryError | null> {
76
+ if (res.status === 401) return new RegistryAuthRequiredError(registryUrl);
77
+ if (res.status === 403) return new RegistryAuthForbiddenError(action);
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * 6th-pass M3: typed JSON-parse helper. A misconfigured proxy that
83
+ * returns HTML at 2xx (or a truncated stream) would otherwise throw a
84
+ * bare `SyntaxError` from `await res.json()`, bypassing every
85
+ * `isRegistryError(err)` handler. This helper translates the parse
86
+ * failure into a `RegistryMalformedPayloadError` so the typed-error
87
+ * contract is universal across the client surface.
88
+ */
89
+ async function parseJsonOrThrow(
90
+ res: Response,
91
+ registryUrl: string,
92
+ action: string,
93
+ ): Promise<unknown> {
94
+ try {
95
+ return await res.json();
96
+ } catch (err) {
97
+ throw new RegistryMalformedPayloadError(
98
+ registryUrl,
99
+ action,
100
+ `response is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
101
+ );
102
+ }
103
+ }
104
+
105
+ export interface RegistryResolveResult {
106
+ /** Resolved exact version (matches `Sha256DigestSchema` for digest). */
107
+ version: string;
108
+ artifactDigest: string;
109
+ }
110
+
111
+ export interface RegistryInfoResult {
112
+ moduleId: string;
113
+ manifest: ModuleManifest;
114
+ versions: readonly ModuleVersionRecord[];
115
+ /** dist-tag → version map (e.g. `latest → 1.2.3`). */
116
+ tags: Readonly<Record<string, string>>;
117
+ }
118
+
119
+ export interface RegistrySearchHit {
120
+ moduleId: string;
121
+ description?: string;
122
+ latestVersion: string;
123
+ }
124
+
125
+ export const RegistryServerClient = {
126
+ /**
127
+ * Liveness probe against `/health`. Returns boolean — used by
128
+ * `wildo registry ping` smoke. The registry server now relies on
129
+ * the framework's auto-mounted `HealthController` at the engine
130
+ * root (`GET /health` always 200; `GET /health/ready` enforces
131
+ * dependency readiness), so the path has NO `/v1` prefix.
132
+ */
133
+ async ping(options: RegistryServerClientOptions): Promise<boolean> {
134
+ try {
135
+ await fetchJsonOk(joinUrl(options.baseUrl, '/health'), {
136
+ method: 'GET',
137
+ timeoutMs: options.timeoutMs,
138
+ diagnosticLabel: DIAGNOSTIC_LABEL,
139
+ });
140
+ return true;
141
+ } catch {
142
+ return false;
143
+ }
144
+ },
145
+
146
+ /**
147
+ * Resolve a semver range / tag against a registry domain. The
148
+ * server returns the concrete version + artifact digest so the
149
+ * caller can verify the download against `artifactDigest`.
150
+ */
151
+ async resolve(
152
+ options: RegistryServerClientOptions,
153
+ args: { domain: string; moduleId: string; range: string },
154
+ ): Promise<RegistryResolveResult> {
155
+ const url = joinUrl(
156
+ options.baseUrl,
157
+ `/v1/registry/${encodeURIComponent(args.domain)}/modules/${encodeURIComponent(args.moduleId)}/resolve?range=${encodeURIComponent(args.range)}`,
158
+ );
159
+ const res = await fetchWithTimeout(url, {
160
+ method: 'GET',
161
+ headers: authHeaders(options),
162
+ timeoutMs: options.timeoutMs,
163
+ diagnosticLabel: DIAGNOSTIC_LABEL,
164
+ });
165
+ if (res.status === 404) {
166
+ throw new RegistryVersionNotFoundError(args.moduleId, args.range);
167
+ }
168
+ const authErr = await classifyAuthError(res, options.baseUrl, `resolve ${args.moduleId}`);
169
+ if (authErr) throw authErr;
170
+ if (!res.ok) {
171
+ throw new RegistryUnexpectedStatusError(options.baseUrl, res.status, res.statusText, `resolve ${args.moduleId}`);
172
+ }
173
+ // 6th-pass M3: typed JSON-parse (was bare `SyntaxError` for
174
+ // malformed proxy responses).
175
+ const json = (await parseJsonOrThrow(res, options.baseUrl, `resolve ${args.moduleId}`)) as {
176
+ version?: unknown;
177
+ artifactDigest?: unknown;
178
+ };
179
+ const digest = String(json.artifactDigest ?? '');
180
+ // 6th-pass M4: typed digest validation (was bare `ZodError`).
181
+ const digestParse = Sha256DigestSchema.safeParse(digest);
182
+ if (!digestParse.success) {
183
+ throw new RegistryMalformedPayloadError(
184
+ options.baseUrl,
185
+ `resolve ${args.moduleId}`,
186
+ `artifactDigest is not a valid sha256:<64-hex> string: ${formatZodIssuesAsSingleLine(digestParse.error)}`,
187
+ );
188
+ }
189
+ if (typeof json.version !== 'string' || json.version.length === 0) {
190
+ // 3rd-pass M6: typed `RegistryMalformedPayloadError` instead of
191
+ // bare `Error` so callers' `isRegistryError(err)` handlers fire
192
+ // correctly.
193
+ throw new RegistryMalformedPayloadError(
194
+ options.baseUrl,
195
+ `resolve ${args.moduleId}`,
196
+ 'response missing required `version` string',
197
+ );
198
+ }
199
+ return { version: json.version, artifactDigest: digest };
200
+ },
201
+
202
+ /**
203
+ * Fetch full info for a module: manifest, every published
204
+ * version, dist-tags. Used by `wildo registry info` (Phase 4).
205
+ */
206
+ async info(
207
+ options: RegistryServerClientOptions,
208
+ args: { domain: string; moduleId: string },
209
+ ): Promise<RegistryInfoResult> {
210
+ const url = joinUrl(
211
+ options.baseUrl,
212
+ `/v1/registry/${encodeURIComponent(args.domain)}/modules/${encodeURIComponent(args.moduleId)}`,
213
+ );
214
+ const res = await fetchWithTimeout(url, {
215
+ method: 'GET',
216
+ headers: authHeaders(options),
217
+ timeoutMs: options.timeoutMs,
218
+ diagnosticLabel: DIAGNOSTIC_LABEL,
219
+ });
220
+ if (res.status === 404) throw new RegistryModuleNotFoundError(args.moduleId);
221
+ const authErr = await classifyAuthError(res, options.baseUrl, `info ${args.moduleId}`);
222
+ if (authErr) throw authErr;
223
+ if (!res.ok) {
224
+ throw new RegistryUnexpectedStatusError(options.baseUrl, res.status, res.statusText, `info ${args.moduleId}`);
225
+ }
226
+ // 6th-pass M3: typed JSON-parse.
227
+ const json = await parseJsonOrThrow(res, options.baseUrl, `info ${args.moduleId}`);
228
+ const parsed = RegistryInfoResponseSchema.safeParse(json);
229
+ if (!parsed.success) {
230
+ throw new RegistryMalformedPayloadError(
231
+ options.baseUrl,
232
+ `info ${args.moduleId}`,
233
+ formatZodIssuesAsSingleLine(parsed.error),
234
+ );
235
+ }
236
+ return parsed.data;
237
+ },
238
+
239
+ /**
240
+ * Search the registry's metadata index. The shape is
241
+ * intentionally narrow until Phase 4 designs the search UX.
242
+ */
243
+ async search(
244
+ options: RegistryServerClientOptions,
245
+ args: { domain: string; query: string },
246
+ ): Promise<readonly RegistrySearchHit[]> {
247
+ const url = joinUrl(
248
+ options.baseUrl,
249
+ `/v1/registry/${encodeURIComponent(args.domain)}/modules?q=${encodeURIComponent(args.query)}`,
250
+ );
251
+ const res = await fetchWithTimeout(url, {
252
+ method: 'GET',
253
+ headers: authHeaders(options),
254
+ timeoutMs: options.timeoutMs,
255
+ diagnosticLabel: DIAGNOSTIC_LABEL,
256
+ });
257
+ const authErr = await classifyAuthError(res, options.baseUrl, `search`);
258
+ if (authErr) throw authErr;
259
+ if (!res.ok) {
260
+ throw new RegistryUnexpectedStatusError(options.baseUrl, res.status, res.statusText, 'search');
261
+ }
262
+ // 6th-pass M3: typed JSON-parse.
263
+ const json = await parseJsonOrThrow(res, options.baseUrl, 'search');
264
+ const parsed = RegistrySearchResponseSchema.safeParse(json);
265
+ if (!parsed.success) {
266
+ throw new RegistryMalformedPayloadError(
267
+ options.baseUrl,
268
+ 'search',
269
+ formatZodIssuesAsSingleLine(parsed.error),
270
+ );
271
+ }
272
+ return parsed.data.hits;
273
+ },
274
+
275
+ /**
276
+ * Stream-download a published artifact's bytes and verify the
277
+ * sha256 integrity matches `expectedDigest`. Throws
278
+ * `RegistryArtifactIntegrityMismatchError` on mismatch — the
279
+ * digest check is non-negotiable per KD-9 / Security model.
280
+ */
281
+ async download(
282
+ options: RegistryServerClientOptions,
283
+ args: {
284
+ domain: string;
285
+ moduleId: string;
286
+ version: string;
287
+ expectedDigest: string;
288
+ },
289
+ ): Promise<{ bytes: Uint8Array; integrity: string }> {
290
+ // 7th-pass C-M1: caller-supplied digest is a developer-contract
291
+ // violation if malformed, not a registry-runtime condition.
292
+ // Throw `TypeError` (matching `C-L3` precedent for
293
+ // `getModuleStagingPaths` argument-shape violations) so the
294
+ // typed `RegistryError` vocabulary stays reserved for
295
+ // registry-runtime errors. Prior `Sha256DigestSchema.parse(...)`
296
+ // leaked a bare `ZodError` that bypassed
297
+ // `isRegistryError(err)` handlers but ALSO confused
298
+ // typed-handler maintainers about the contract.
299
+ const digestParse = Sha256DigestSchema.safeParse(args.expectedDigest);
300
+ if (!digestParse.success) {
301
+ throw new TypeError(
302
+ `download() expectedDigest must be a valid sha256:<64-hex> string (got: ${JSON.stringify(args.expectedDigest)}). ${formatZodIssuesAsSingleLine(digestParse.error)}`,
303
+ );
304
+ }
305
+ const url = joinUrl(
306
+ options.baseUrl,
307
+ `/v1/registry/${encodeURIComponent(args.domain)}/modules/${encodeURIComponent(args.moduleId)}/${encodeURIComponent(args.version)}/artifact`,
308
+ );
309
+ const res = await fetchWithTimeout(url, {
310
+ method: 'GET',
311
+ headers: authHeaders(options),
312
+ timeoutMs: options.timeoutMs,
313
+ diagnosticLabel: DIAGNOSTIC_LABEL,
314
+ });
315
+ if (res.status === 404) throw new RegistryVersionNotFoundError(args.moduleId, args.version);
316
+ const authErr = await classifyAuthError(res, options.baseUrl, `download ${args.moduleId}@${args.version}`);
317
+ if (authErr) throw authErr;
318
+ if (!res.ok) {
319
+ throw new RegistryUnexpectedStatusError(options.baseUrl, res.status, res.statusText, `download ${args.moduleId}@${args.version}`);
320
+ }
321
+ const buf = new Uint8Array(await res.arrayBuffer());
322
+ const { createHash } = await import('node:crypto');
323
+ const observed = `sha256:${createHash('sha256').update(buf).digest('hex')}`;
324
+ if (observed !== args.expectedDigest) {
325
+ throw new RegistryArtifactIntegrityMismatchError(
326
+ args.moduleId,
327
+ args.version,
328
+ args.expectedDigest,
329
+ observed,
330
+ );
331
+ }
332
+ return { bytes: buf, integrity: observed };
333
+ },
334
+ } as const;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shared structural types consumed across `wildo-module-client`
3
+ * services.
4
+ *
5
+ * The package intentionally avoids importing the CLI's full
6
+ * `CommandContext` (defined in `wildo-cli/src/kernel/types.ts`)
7
+ * to keep the dependency arrow CLI → wildo-module-client (KD-3
8
+ * forbids a reverse edge). Phase 2 services accept a structural
9
+ * subset of whatever the host (CLI / companion) needs to pass
10
+ * down — typically just `saasRoot` + a logger sink.
11
+ *
12
+ * The single shared type today is `RegistryClientLogger`. Adding
13
+ * more shared structural types here is fine; importing
14
+ * `wildo-cli` types is not.
15
+ */
16
+
17
+ export interface RegistryClientLogger {
18
+ info: (msg: string) => void;
19
+ success?: (msg: string) => void;
20
+ warn: (msg: string) => void;
21
+ error: (msg: string) => void;
22
+ }