@wildo-ai/wildo-module-client 1.1.5 → 1.1.7
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/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/services/_http-client.d.ts +32 -3
- package/dist/esm/services/_http-client.d.ts.map +1 -1
- package/dist/esm/services/_http-client.js +100 -6
- package/dist/esm/services/_http-client.js.map +1 -1
- package/dist/esm/services/_zod-issue-formatting.d.ts.map +1 -1
- package/dist/esm/services/index.d.ts.map +1 -1
- package/dist/esm/services/lockfile-repair.service.d.ts.map +1 -1
- package/dist/esm/services/module-adopter.service.d.ts +1 -1
- package/dist/esm/services/module-adopter.service.d.ts.map +1 -1
- package/dist/esm/services/module-adopter.service.js +7 -8
- package/dist/esm/services/module-adopter.service.js.map +1 -1
- package/dist/esm/services/module-packager.service.d.ts.map +1 -1
- package/dist/esm/services/module-packager.service.js +16 -19
- package/dist/esm/services/module-packager.service.js.map +1 -1
- package/dist/esm/services/provider-materialization-hook.d.ts.map +1 -1
- package/dist/esm/services/provider-sync-runner.d.ts +2 -3
- package/dist/esm/services/provider-sync-runner.d.ts.map +1 -1
- package/dist/esm/services/provider-sync-runner.js +14 -2
- package/dist/esm/services/provider-sync-runner.js.map +1 -1
- package/dist/esm/services/registry-companion-client.d.ts +10 -47
- package/dist/esm/services/registry-companion-client.d.ts.map +1 -1
- package/dist/esm/services/registry-companion-client.js +9 -97
- package/dist/esm/services/registry-companion-client.js.map +1 -1
- package/dist/esm/services/registry-lockfile.service.d.ts +7 -3
- package/dist/esm/services/registry-lockfile.service.d.ts.map +1 -1
- package/dist/esm/services/registry-lockfile.service.js +8 -3
- package/dist/esm/services/registry-lockfile.service.js.map +1 -1
- package/dist/esm/services/registry-server-client.d.ts +35 -9
- package/dist/esm/services/registry-server-client.d.ts.map +1 -1
- package/dist/esm/services/registry-server-client.js +115 -58
- package/dist/esm/services/registry-server-client.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/__tests__/module-adopter.service.test.ts +6 -4
- package/src/__tests__/module-packager.service.test.ts +36 -34
- package/src/__tests__/provider-sync-runner.test.ts +12 -0
- package/src/__tests__/registry-lockfile.service.test.ts +3 -3
- package/src/__tests__/registry-server-client.test.ts +129 -0
- package/src/__tests__/registry-transfer-timeouts.test.ts +94 -0
- package/src/services/_http-client.ts +100 -6
- package/src/services/module-adopter.service.ts +6 -9
- package/src/services/module-packager.service.ts +19 -16
- package/src/services/provider-sync-runner.ts +16 -5
- package/src/services/registry-companion-client.ts +10 -166
- package/src/services/registry-lockfile.service.ts +9 -3
- package/src/services/registry-server-client.ts +147 -57
- 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
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* Auth (KD-11): if `authToken` is supplied, every request adds
|
|
13
13
|
* `Authorization: Bearer <token>`. Public read paths work without
|
|
14
14
|
* a token; restricted paths return 401/403, surfaced as
|
|
15
|
-
* `RegistryAuthRequiredError`
|
|
15
|
+
* `RegistryAuthRequiredError` (no key sent), `RegistryCredentialRejectedError` (a key sent and refused)
|
|
16
|
+
* or `RegistryAuthForbiddenError`; a 429 is `RegistryRateLimitedError`.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { z } from 'zod';
|
|
@@ -23,16 +24,31 @@ import {
|
|
|
23
24
|
RegistryArtifactIntegrityMismatchError,
|
|
24
25
|
RegistryAuthForbiddenError,
|
|
25
26
|
RegistryAuthRequiredError,
|
|
27
|
+
RegistryCredentialRejectedError,
|
|
26
28
|
RegistryError,
|
|
29
|
+
RegistryRateLimitedError,
|
|
30
|
+
isRegistryError,
|
|
31
|
+
isRegistryReleaseStateConflictBody,
|
|
27
32
|
RegistryMalformedPayloadError,
|
|
28
33
|
RegistryModuleNotFoundError,
|
|
29
34
|
RegistryUnexpectedStatusError,
|
|
30
35
|
RegistryVersionNotFoundError,
|
|
36
|
+
RegistryYankNoticeSchema,
|
|
37
|
+
RegistryYankStateConflictError,
|
|
38
|
+
RegistryYankVerb,
|
|
39
|
+
REGISTRY_YANK_REASON_HEADER,
|
|
40
|
+
REGISTRY_YANKED_AT_HEADER,
|
|
31
41
|
Sha256DigestSchema,
|
|
42
|
+
ModuleSearchHitSchema,
|
|
43
|
+
moduleSearchFilterQueryPairs,
|
|
44
|
+
moduleSearchHitMatchesFilter,
|
|
32
45
|
type ModuleManifest,
|
|
46
|
+
type ModuleSearchFilter,
|
|
47
|
+
type ModuleSearchHit,
|
|
48
|
+
type RegistryYankNotice,
|
|
33
49
|
type ModuleVersionRecord,
|
|
34
50
|
} from '@wildo-ai/platform-config-lib';
|
|
35
|
-
import { fetchJsonOk, fetchWithTimeout, joinUrl } from './_http-client';
|
|
51
|
+
import { fetchJsonOk, fetchTransfer, fetchWithTimeout, joinUrl, readJsonBody } from './_http-client';
|
|
36
52
|
|
|
37
53
|
/**
|
|
38
54
|
* Zod schema for the `info()` response payload — mirrors
|
|
@@ -48,13 +64,7 @@ const RegistryInfoResponseSchema = z.object({
|
|
|
48
64
|
});
|
|
49
65
|
|
|
50
66
|
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
|
-
),
|
|
67
|
+
hits: z.array(ModuleSearchHitSchema),
|
|
58
68
|
});
|
|
59
69
|
|
|
60
70
|
const DIAGNOSTIC_LABEL = 'Registry';
|
|
@@ -72,10 +82,32 @@ function authHeaders(options: RegistryServerClientOptions): Record<string, strin
|
|
|
72
82
|
return options.authToken ? { Authorization: `Bearer ${options.authToken}` } : {};
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Throw the typed error for a non-2xx response, after releasing its body (#1763 review: an unread body
|
|
87
|
+
* holds the connection until garbage collection). A 401 is told apart by whether a key was SENT — "log
|
|
88
|
+
* in" and "your key no longer works" are different remedies (#1759 review).
|
|
89
|
+
*/
|
|
90
|
+
async function refuseUnlessOk(
|
|
91
|
+
res: Response,
|
|
92
|
+
options: RegistryServerClientOptions,
|
|
93
|
+
action: string,
|
|
94
|
+
notFound?: () => RegistryError,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
if (res.ok) return;
|
|
97
|
+
let error: RegistryError;
|
|
98
|
+
if (res.status === 404 && notFound) error = notFound();
|
|
99
|
+
else if (res.status === 401) error = options.authToken ? new RegistryCredentialRejectedError(options.baseUrl) : new RegistryAuthRequiredError(options.baseUrl);
|
|
100
|
+
else if (res.status === 403) error = new RegistryAuthForbiddenError(action);
|
|
101
|
+
else if (res.status === 429) error = new RegistryRateLimitedError(options.baseUrl, action, parseRetryAfterSeconds(res.headers.get('retry-after')));
|
|
102
|
+
else error = new RegistryUnexpectedStatusError(options.baseUrl, res.status, res.statusText, action);
|
|
103
|
+
await res.body?.cancel().catch(() => undefined);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `Retry-After` in its delta-seconds form; the HTTP-date form is not produced by a Wildo backend. */
|
|
108
|
+
function parseRetryAfterSeconds(value: string | null): number | undefined {
|
|
109
|
+
if (!value || !/^\d+$/.test(value.trim())) return undefined;
|
|
110
|
+
return Number(value.trim());
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
/**
|
|
@@ -90,10 +122,13 @@ async function parseJsonOrThrow(
|
|
|
90
122
|
res: Response,
|
|
91
123
|
registryUrl: string,
|
|
92
124
|
action: string,
|
|
125
|
+
timeoutMs: number | undefined,
|
|
93
126
|
): Promise<unknown> {
|
|
94
127
|
try {
|
|
95
|
-
return await res
|
|
128
|
+
return await readJsonBody(res, registryUrl, action, timeoutMs);
|
|
96
129
|
} catch (err) {
|
|
130
|
+
// A body cut short by the request budget is a timeout (#1763), not a malformed payload.
|
|
131
|
+
if (isRegistryError(err)) throw err;
|
|
97
132
|
throw new RegistryMalformedPayloadError(
|
|
98
133
|
registryUrl,
|
|
99
134
|
action,
|
|
@@ -106,6 +141,11 @@ export interface RegistryResolveResult {
|
|
|
106
141
|
/** Resolved exact version (matches `Sha256DigestSchema` for digest). */
|
|
107
142
|
version: string;
|
|
108
143
|
artifactDigest: string;
|
|
144
|
+
/**
|
|
145
|
+
* Present when the resolved release is yanked. Only an EXACT-version request can resolve to one
|
|
146
|
+
* (#1758) — ranges and tags never do — so the caller is pinned to it and should warn, not refuse.
|
|
147
|
+
*/
|
|
148
|
+
yanked?: RegistryYankNotice;
|
|
109
149
|
}
|
|
110
150
|
|
|
111
151
|
export interface RegistryInfoResult {
|
|
@@ -116,11 +156,8 @@ export interface RegistryInfoResult {
|
|
|
116
156
|
tags: Readonly<Record<string, string>>;
|
|
117
157
|
}
|
|
118
158
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
description?: string;
|
|
122
|
-
latestVersion: string;
|
|
123
|
-
}
|
|
159
|
+
/** One search hit: the module, its latest version, and (#2014) its kind and provider facts. */
|
|
160
|
+
export type RegistrySearchHit = ModuleSearchHit;
|
|
124
161
|
|
|
125
162
|
export const RegistryServerClient = {
|
|
126
163
|
/**
|
|
@@ -162,19 +199,13 @@ export const RegistryServerClient = {
|
|
|
162
199
|
timeoutMs: options.timeoutMs,
|
|
163
200
|
diagnosticLabel: DIAGNOSTIC_LABEL,
|
|
164
201
|
});
|
|
165
|
-
|
|
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
|
-
}
|
|
202
|
+
await refuseUnlessOk(res, options, `resolve ${args.moduleId}`, () => new RegistryVersionNotFoundError(args.moduleId, args.range));
|
|
173
203
|
// 6th-pass M3: typed JSON-parse (was bare `SyntaxError` for
|
|
174
204
|
// malformed proxy responses).
|
|
175
|
-
const json = (await parseJsonOrThrow(res, options.baseUrl, `resolve ${args.moduleId}
|
|
205
|
+
const json = (await parseJsonOrThrow(res, options.baseUrl, `resolve ${args.moduleId}`, options.timeoutMs)) as {
|
|
176
206
|
version?: unknown;
|
|
177
207
|
artifactDigest?: unknown;
|
|
208
|
+
yanked?: unknown;
|
|
178
209
|
};
|
|
179
210
|
const digest = String(json.artifactDigest ?? '');
|
|
180
211
|
// 6th-pass M4: typed digest validation (was bare `ZodError`).
|
|
@@ -196,7 +227,15 @@ export const RegistryServerClient = {
|
|
|
196
227
|
'response missing required `version` string',
|
|
197
228
|
);
|
|
198
229
|
}
|
|
199
|
-
|
|
230
|
+
let yanked: RegistryYankNotice | undefined;
|
|
231
|
+
if (json.yanked !== undefined) {
|
|
232
|
+
const yankParse = RegistryYankNoticeSchema.safeParse(json.yanked);
|
|
233
|
+
if (!yankParse.success) {
|
|
234
|
+
throw new RegistryMalformedPayloadError(options.baseUrl, `resolve ${args.moduleId}`, `yanked: ${formatZodIssuesAsSingleLine(yankParse.error)}`);
|
|
235
|
+
}
|
|
236
|
+
yanked = yankParse.data;
|
|
237
|
+
}
|
|
238
|
+
return { version: json.version, artifactDigest: digest, ...(yanked ? { yanked } : {}) };
|
|
200
239
|
},
|
|
201
240
|
|
|
202
241
|
/**
|
|
@@ -217,14 +256,9 @@ export const RegistryServerClient = {
|
|
|
217
256
|
timeoutMs: options.timeoutMs,
|
|
218
257
|
diagnosticLabel: DIAGNOSTIC_LABEL,
|
|
219
258
|
});
|
|
220
|
-
|
|
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
|
-
}
|
|
259
|
+
await refuseUnlessOk(res, options, `info ${args.moduleId}`, () => new RegistryModuleNotFoundError(args.moduleId));
|
|
226
260
|
// 6th-pass M3: typed JSON-parse.
|
|
227
|
-
const json = await parseJsonOrThrow(res, options.baseUrl, `info ${args.moduleId}
|
|
261
|
+
const json = await parseJsonOrThrow(res, options.baseUrl, `info ${args.moduleId}`, options.timeoutMs);
|
|
228
262
|
const parsed = RegistryInfoResponseSchema.safeParse(json);
|
|
229
263
|
if (!parsed.success) {
|
|
230
264
|
throw new RegistryMalformedPayloadError(
|
|
@@ -237,16 +271,17 @@ export const RegistryServerClient = {
|
|
|
237
271
|
},
|
|
238
272
|
|
|
239
273
|
/**
|
|
240
|
-
* Search the registry's metadata index
|
|
241
|
-
*
|
|
274
|
+
* Search the registry's metadata index: a module-id substring, and (#2014) the manifest filters —
|
|
275
|
+
* kind, engine capability and provider maturity. The server refuses an unknown filter value.
|
|
242
276
|
*/
|
|
243
277
|
async search(
|
|
244
278
|
options: RegistryServerClientOptions,
|
|
245
|
-
args: { domain: string; query: string },
|
|
279
|
+
args: { domain: string; query: string; filter?: ModuleSearchFilter },
|
|
246
280
|
): Promise<readonly RegistrySearchHit[]> {
|
|
281
|
+
const params = new URLSearchParams([['q', args.query], ...moduleSearchFilterQueryPairs(args.filter ?? {})]);
|
|
247
282
|
const url = joinUrl(
|
|
248
283
|
options.baseUrl,
|
|
249
|
-
`/v1/registry/${encodeURIComponent(args.domain)}/modules
|
|
284
|
+
`/v1/registry/${encodeURIComponent(args.domain)}/modules?${params.toString()}`,
|
|
250
285
|
);
|
|
251
286
|
const res = await fetchWithTimeout(url, {
|
|
252
287
|
method: 'GET',
|
|
@@ -254,13 +289,9 @@ export const RegistryServerClient = {
|
|
|
254
289
|
timeoutMs: options.timeoutMs,
|
|
255
290
|
diagnosticLabel: DIAGNOSTIC_LABEL,
|
|
256
291
|
});
|
|
257
|
-
|
|
258
|
-
if (authErr) throw authErr;
|
|
259
|
-
if (!res.ok) {
|
|
260
|
-
throw new RegistryUnexpectedStatusError(options.baseUrl, res.status, res.statusText, 'search');
|
|
261
|
-
}
|
|
292
|
+
await refuseUnlessOk(res, options, 'search');
|
|
262
293
|
// 6th-pass M3: typed JSON-parse.
|
|
263
|
-
const json = await parseJsonOrThrow(res, options.baseUrl, 'search');
|
|
294
|
+
const json = await parseJsonOrThrow(res, options.baseUrl, 'search', options.timeoutMs);
|
|
264
295
|
const parsed = RegistrySearchResponseSchema.safeParse(json);
|
|
265
296
|
if (!parsed.success) {
|
|
266
297
|
throw new RegistryMalformedPayloadError(
|
|
@@ -269,7 +300,8 @@ export const RegistryServerClient = {
|
|
|
269
300
|
formatZodIssuesAsSingleLine(parsed.error),
|
|
270
301
|
);
|
|
271
302
|
}
|
|
272
|
-
|
|
303
|
+
// Re-applied here: a server that predates the filters ignores them and answers every module (#2014 review F9).
|
|
304
|
+
return parsed.data.hits.filter((hit) => moduleSearchHitMatchesFilter(hit, args.filter ?? {}));
|
|
273
305
|
},
|
|
274
306
|
|
|
275
307
|
/**
|
|
@@ -286,7 +318,7 @@ export const RegistryServerClient = {
|
|
|
286
318
|
version: string;
|
|
287
319
|
expectedDigest: string;
|
|
288
320
|
},
|
|
289
|
-
): Promise<{ bytes: Uint8Array; integrity: string }> {
|
|
321
|
+
): Promise<{ bytes: Uint8Array; integrity: string; yanked?: RegistryYankNotice }> {
|
|
290
322
|
// 7th-pass C-M1: caller-supplied digest is a developer-contract
|
|
291
323
|
// violation if malformed, not a registry-runtime condition.
|
|
292
324
|
// Throw `TypeError` (matching `C-L3` precedent for
|
|
@@ -306,19 +338,16 @@ export const RegistryServerClient = {
|
|
|
306
338
|
options.baseUrl,
|
|
307
339
|
`/v1/registry/${encodeURIComponent(args.domain)}/modules/${encodeURIComponent(args.moduleId)}/${encodeURIComponent(args.version)}/artifact`,
|
|
308
340
|
);
|
|
309
|
-
|
|
341
|
+
// A transfer, not a metadata call: the response is bounded by `timeoutMs`, the body by an IDLE budget
|
|
342
|
+
// reset on every chunk, so a large artifact over a slow link installs and a stalled one fails (#1763).
|
|
343
|
+
const { response: res, readAll } = await fetchTransfer(url, {
|
|
310
344
|
method: 'GET',
|
|
311
345
|
headers: authHeaders(options),
|
|
312
346
|
timeoutMs: options.timeoutMs,
|
|
313
347
|
diagnosticLabel: DIAGNOSTIC_LABEL,
|
|
314
348
|
});
|
|
315
|
-
|
|
316
|
-
const
|
|
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());
|
|
349
|
+
await refuseUnlessOk(res, options, `download ${args.moduleId}@${args.version}`, () => new RegistryVersionNotFoundError(args.moduleId, args.version));
|
|
350
|
+
const buf = await readAll();
|
|
322
351
|
const { createHash } = await import('node:crypto');
|
|
323
352
|
const observed = `sha256:${createHash('sha256').update(buf).digest('hex')}`;
|
|
324
353
|
if (observed !== args.expectedDigest) {
|
|
@@ -329,6 +358,67 @@ export const RegistryServerClient = {
|
|
|
329
358
|
observed,
|
|
330
359
|
);
|
|
331
360
|
}
|
|
332
|
-
|
|
361
|
+
// A yanked release is served by exact version (#1758) and says so in headers.
|
|
362
|
+
const yankedAt = res.headers.get(REGISTRY_YANKED_AT_HEADER);
|
|
363
|
+
const encodedReason = res.headers.get(REGISTRY_YANK_REASON_HEADER);
|
|
364
|
+
let reason: string | undefined;
|
|
365
|
+
if (encodedReason) {
|
|
366
|
+
try { reason = decodeURIComponent(encodedReason); } catch { reason = encodedReason; }
|
|
367
|
+
}
|
|
368
|
+
return { bytes: buf, integrity: observed, ...(yankedAt ? { yanked: { yankedAt, ...(reason ? { reason } : {}) } } : {}) };
|
|
369
|
+
},
|
|
370
|
+
/**
|
|
371
|
+
* Yank a release (#1758): ranges and `latest` stop selecting it, and `latest` moves to the highest
|
|
372
|
+
* live release; an exact pin still installs it, with a warning. Needs a key holding the version UPDATE
|
|
373
|
+
* role in the domain's owning organization. Returns where `latest` points afterwards.
|
|
374
|
+
*/
|
|
375
|
+
async yank(
|
|
376
|
+
options: RegistryServerClientOptions,
|
|
377
|
+
args: { domain: string; moduleId: string; version: string; reason?: string },
|
|
378
|
+
): Promise<{ latest: string | null }> {
|
|
379
|
+
return changeYankState(options, args, RegistryYankVerb.YANK);
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
/** Restore a yanked release (#1758); `latest` returns to it when it outranks the current target. */
|
|
383
|
+
async unyank(
|
|
384
|
+
options: RegistryServerClientOptions,
|
|
385
|
+
args: { domain: string; moduleId: string; version: string },
|
|
386
|
+
): Promise<{ latest: string | null }> {
|
|
387
|
+
return changeYankState(options, args, RegistryYankVerb.UNYANK);
|
|
333
388
|
},
|
|
334
389
|
} as const;
|
|
390
|
+
|
|
391
|
+
const RegistryYankResponseSchema = z.object({ ok: z.literal(true), latest: z.string().nullable().optional() });
|
|
392
|
+
|
|
393
|
+
async function changeYankState(
|
|
394
|
+
options: RegistryServerClientOptions,
|
|
395
|
+
args: { domain: string; moduleId: string; version: string; reason?: string },
|
|
396
|
+
verb: RegistryYankVerb,
|
|
397
|
+
): Promise<{ latest: string | null }> {
|
|
398
|
+
const url = joinUrl(
|
|
399
|
+
options.baseUrl,
|
|
400
|
+
`/v1/registry/${encodeURIComponent(args.domain)}/modules/${encodeURIComponent(args.moduleId)}/${verb}`,
|
|
401
|
+
);
|
|
402
|
+
const action = `${verb} ${args.moduleId}@${args.version}`;
|
|
403
|
+
const res = await fetchWithTimeout(url, {
|
|
404
|
+
method: 'POST',
|
|
405
|
+
headers: { ...authHeaders(options), 'content-type': 'application/json' },
|
|
406
|
+
body: JSON.stringify({ version: args.version, ...(args.reason !== undefined ? { reason: args.reason } : {}) }),
|
|
407
|
+
timeoutMs: options.timeoutMs,
|
|
408
|
+
diagnosticLabel: DIAGNOSTIC_LABEL,
|
|
409
|
+
});
|
|
410
|
+
if (res.status === 409) {
|
|
411
|
+
// Only the release-state refusal is the no-op; a 409 for anything else (the registry giving up after
|
|
412
|
+
// lost optimistic checks) is a FAILED yank and must not be reported as done (#1758 re-review R3).
|
|
413
|
+
const body = await res.json().catch(() => undefined);
|
|
414
|
+
if (isRegistryReleaseStateConflictBody(body)) throw new RegistryYankStateConflictError(args.moduleId, args.version, verb);
|
|
415
|
+
throw new RegistryUnexpectedStatusError(options.baseUrl, res.status, res.statusText, action);
|
|
416
|
+
}
|
|
417
|
+
// A 404 names the module or the version; the sanitized envelope does not say which.
|
|
418
|
+
await refuseUnlessOk(res, options, action, () => new RegistryVersionNotFoundError(args.moduleId, args.version));
|
|
419
|
+
const parsed = RegistryYankResponseSchema.safeParse(await parseJsonOrThrow(res, options.baseUrl, action, options.timeoutMs));
|
|
420
|
+
if (!parsed.success) {
|
|
421
|
+
throw new RegistryMalformedPayloadError(options.baseUrl, action, formatZodIssuesAsSingleLine(parsed.error));
|
|
422
|
+
}
|
|
423
|
+
return { latest: parsed.data.latest ?? null };
|
|
424
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|