@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.
- package/LICENSE +34 -0
- package/dist/esm/.builder.pid +9 -0
- package/dist/esm/__tests__/lockfile-repair.service.test.d.ts +0 -0
- package/dist/esm/__tests__/lockfile-repair.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/lockfile-repair.service.test.js.map +0 -0
- package/dist/esm/__tests__/module-adopter.service.test.d.ts +0 -0
- package/dist/esm/__tests__/module-adopter.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/module-adopter.service.test.js.map +0 -0
- package/dist/esm/__tests__/module-packager.service.test.d.ts +0 -0
- package/dist/esm/__tests__/module-packager.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/module-packager.service.test.js.map +0 -0
- package/dist/esm/__tests__/provider-materialization-hook.test.d.ts +0 -0
- package/dist/esm/__tests__/provider-materialization-hook.test.d.ts.map +0 -0
- package/dist/esm/__tests__/provider-materialization-hook.test.js.map +0 -0
- package/dist/esm/__tests__/provider-sync-runner.test.d.ts +0 -0
- package/dist/esm/__tests__/provider-sync-runner.test.d.ts.map +0 -0
- package/dist/esm/__tests__/provider-sync-runner.test.js.map +0 -0
- package/dist/esm/__tests__/registry-lockfile.service.test.d.ts +0 -0
- package/dist/esm/__tests__/registry-lockfile.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/registry-lockfile.service.test.js.map +0 -0
- package/dist/esm/__tests__/registry-server-client.test.d.ts +0 -0
- package/dist/esm/__tests__/registry-server-client.test.d.ts.map +0 -0
- package/dist/esm/__tests__/registry-server-client.test.js.map +0 -0
- package/dist/esm/index.d.ts +18 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/services/_http-client.d.ts +44 -0
- package/dist/esm/services/_http-client.d.ts.map +1 -0
- package/dist/esm/services/_http-client.js +75 -0
- package/dist/esm/services/_http-client.js.map +1 -0
- package/dist/esm/services/_zod-issue-formatting.d.ts +12 -0
- package/dist/esm/services/_zod-issue-formatting.d.ts.map +1 -0
- package/dist/esm/services/_zod-issue-formatting.js +47 -0
- package/dist/esm/services/_zod-issue-formatting.js.map +1 -0
- package/dist/esm/services/index.d.ts +10 -0
- package/dist/esm/services/index.d.ts.map +1 -0
- package/dist/esm/services/index.js +10 -0
- package/dist/esm/services/index.js.map +1 -0
- package/dist/esm/services/lockfile-repair.service.d.ts +68 -0
- package/dist/esm/services/lockfile-repair.service.d.ts.map +1 -0
- package/dist/esm/services/lockfile-repair.service.js +160 -0
- package/dist/esm/services/lockfile-repair.service.js.map +1 -0
- package/dist/esm/services/module-adopter.service.d.ts +59 -0
- package/dist/esm/services/module-adopter.service.d.ts.map +1 -0
- package/dist/esm/services/module-adopter.service.js +126 -0
- package/dist/esm/services/module-adopter.service.js.map +1 -0
- package/dist/esm/services/module-packager.service.d.ts +105 -0
- package/dist/esm/services/module-packager.service.d.ts.map +1 -0
- package/dist/esm/services/module-packager.service.js +212 -0
- package/dist/esm/services/module-packager.service.js.map +1 -0
- package/dist/esm/services/provider-materialization-hook.d.ts +32 -0
- package/dist/esm/services/provider-materialization-hook.d.ts.map +1 -0
- package/dist/esm/services/provider-materialization-hook.js +33 -0
- package/dist/esm/services/provider-materialization-hook.js.map +1 -0
- package/dist/esm/services/provider-sync-runner.d.ts +87 -0
- package/dist/esm/services/provider-sync-runner.d.ts.map +1 -0
- package/dist/esm/services/provider-sync-runner.js +207 -0
- package/dist/esm/services/provider-sync-runner.js.map +1 -0
- package/dist/esm/services/registry-companion-client.d.ts +39 -0
- package/dist/esm/services/registry-companion-client.d.ts.map +1 -0
- package/dist/esm/services/registry-companion-client.js +109 -0
- package/dist/esm/services/registry-companion-client.js.map +1 -0
- package/dist/esm/services/registry-lockfile.service.d.ts +56 -0
- package/dist/esm/services/registry-lockfile.service.d.ts.map +1 -0
- package/dist/esm/services/registry-lockfile.service.js +99 -0
- package/dist/esm/services/registry-lockfile.service.js.map +1 -0
- package/dist/esm/services/registry-server-client.d.ts +94 -0
- package/dist/esm/services/registry-server-client.d.ts.map +1 -0
- package/dist/esm/services/registry-server-client.js +225 -0
- package/dist/esm/services/registry-server-client.js.map +1 -0
- package/dist/esm/services/types.d.ts +22 -0
- package/dist/esm/services/types.d.ts.map +1 -0
- package/dist/esm/services/types.js +16 -0
- package/dist/esm/services/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +51 -0
- package/src/__tests__/lockfile-repair.service.test.ts +106 -0
- package/src/__tests__/module-adopter.service.test.ts +96 -0
- package/src/__tests__/module-packager.service.test.ts +258 -0
- package/src/__tests__/provider-materialization-hook.test.ts +47 -0
- package/src/__tests__/provider-sync-runner.test.ts +113 -0
- package/src/__tests__/registry-lockfile.service.test.ts +134 -0
- package/src/__tests__/registry-server-client.test.ts +312 -0
- package/src/index.ts +18 -0
- package/src/services/_http-client.ts +91 -0
- package/src/services/_zod-issue-formatting.ts +50 -0
- package/src/services/index.ts +9 -0
- package/src/services/lockfile-repair.service.ts +215 -0
- package/src/services/module-adopter.service.ts +155 -0
- package/src/services/module-packager.service.ts +251 -0
- package/src/services/provider-materialization-hook.ts +54 -0
- package/src/services/provider-sync-runner.ts +298 -0
- package/src/services/registry-companion-client.ts +163 -0
- package/src/services/registry-lockfile.service.ts +122 -0
- package/src/services/registry-server-client.ts +334 -0
- 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
|
+
}
|