@agentxm/registry-client 0.28.4-bootstrap.0
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 +110 -0
- package/README.md +9 -0
- package/dist/src/__generated__/registry-client.d.ts +6501 -0
- package/dist/src/__generated__/registry-client.js +2270 -0
- package/dist/src/admin-client.d.ts +181 -0
- package/dist/src/admin-client.js +165 -0
- package/dist/src/archive-cache.d.ts +46 -0
- package/dist/src/archive-cache.js +179 -0
- package/dist/src/atomic-write.d.ts +49 -0
- package/dist/src/atomic-write.js +60 -0
- package/dist/src/axm-package-meta.d.ts +31 -0
- package/dist/src/axm-package-meta.js +31 -0
- package/dist/src/cache-root.d.ts +11 -0
- package/dist/src/cache-root.js +43 -0
- package/dist/src/client.d.ts +270 -0
- package/dist/src/client.js +43 -0
- package/dist/src/deprecation-warning.d.ts +3 -0
- package/dist/src/deprecation-warning.js +16 -0
- package/dist/src/error-mapping.d.ts +82 -0
- package/dist/src/error-mapping.js +186 -0
- package/dist/src/errors.d.ts +107 -0
- package/dist/src/errors.js +98 -0
- package/dist/src/failure-mapping.d.ts +14 -0
- package/dist/src/failure-mapping.js +100 -0
- package/dist/src/fs-helpers.d.ts +12 -0
- package/dist/src/fs-helpers.js +13 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +37 -0
- package/dist/src/integrity.d.ts +13 -0
- package/dist/src/integrity.js +17 -0
- package/dist/src/local-client.d.ts +24 -0
- package/dist/src/local-client.js +815 -0
- package/dist/src/network.d.ts +6 -0
- package/dist/src/network.js +6 -0
- package/dist/src/path-safety.d.ts +18 -0
- package/dist/src/path-safety.js +26 -0
- package/dist/src/purl-match.d.ts +28 -0
- package/dist/src/purl-match.js +35 -0
- package/dist/src/registry-url.d.ts +14 -0
- package/dist/src/registry-url.js +12 -0
- package/dist/src/remote-client.d.ts +27 -0
- package/dist/src/remote-client.js +725 -0
- package/dist/src/request-policy.d.ts +29 -0
- package/dist/src/request-policy.js +190 -0
- package/dist/src/response-body.d.ts +11 -0
- package/dist/src/response-body.js +32 -0
- package/dist/src/retry-after.d.ts +9 -0
- package/dist/src/retry-after.js +28 -0
- package/dist/src/translate.d.ts +20 -0
- package/dist/src/translate.js +170 -0
- package/dist/src/utils.d.ts +60 -0
- package/dist/src/utils.js +187 -0
- package/package.json +55 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as Duration from "effect/Duration";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { type RegistryClientFailure, type RegistryRequestMetadata } from "./errors.js";
|
|
4
|
+
export interface RegistryRequestPolicy {
|
|
5
|
+
readonly requestTimeout: Duration.Input;
|
|
6
|
+
readonly totalDeadline: Duration.Input;
|
|
7
|
+
readonly maxAttempts: number;
|
|
8
|
+
readonly initialBackoff: Duration.Input;
|
|
9
|
+
readonly maxBackoff: Duration.Input;
|
|
10
|
+
}
|
|
11
|
+
export declare const DEFAULT_REGISTRY_REQUEST_POLICY: RegistryRequestPolicy;
|
|
12
|
+
/** A publish may legitimately spend longer validating and persisting an archive. */
|
|
13
|
+
export declare const PUBLISH_REGISTRY_REQUEST_POLICY: RegistryRequestPolicy;
|
|
14
|
+
export type RegistryRequestReplaySafety = {
|
|
15
|
+
readonly kind: "safe";
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: "mutation";
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "idempotency-keyed";
|
|
20
|
+
readonly idempotencyKey: string;
|
|
21
|
+
};
|
|
22
|
+
export declare const executeRegistryRequest: <A, E, R>(effect: Effect.Effect<A, E, R>, args: {
|
|
23
|
+
readonly operation: string;
|
|
24
|
+
readonly request: RegistryRequestMetadata;
|
|
25
|
+
readonly replaySafety: RegistryRequestReplaySafety;
|
|
26
|
+
readonly mapError: (error: E) => RegistryClientFailure;
|
|
27
|
+
readonly policy?: RegistryRequestPolicy;
|
|
28
|
+
}) => Effect.Effect<A, RegistryClientFailure, R>;
|
|
29
|
+
//# sourceMappingURL=request-policy.d.ts.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as Cause from "effect/Cause";
|
|
2
|
+
import * as Clock from "effect/Clock";
|
|
3
|
+
import * as Duration from "effect/Duration";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Ref from "effect/Ref";
|
|
6
|
+
import * as Schedule from "effect/Schedule";
|
|
7
|
+
import { isAnyRegistryClientError, isHttpClientError } from "./error-mapping.js";
|
|
8
|
+
import { RegistryOperationFailed, RegistryProblem, RegistryRequestFailed, } from "./errors.js";
|
|
9
|
+
import { registryRetryAfterSeconds } from "./retry-after.js";
|
|
10
|
+
export const DEFAULT_REGISTRY_REQUEST_POLICY = {
|
|
11
|
+
requestTimeout: "10 seconds",
|
|
12
|
+
totalDeadline: "30 seconds",
|
|
13
|
+
maxAttempts: 3,
|
|
14
|
+
initialBackoff: "200 millis",
|
|
15
|
+
maxBackoff: "2 seconds",
|
|
16
|
+
};
|
|
17
|
+
/** A publish may legitimately spend longer validating and persisting an archive. */
|
|
18
|
+
export const PUBLISH_REGISTRY_REQUEST_POLICY = {
|
|
19
|
+
requestTimeout: "5 minutes",
|
|
20
|
+
totalDeadline: "5 minutes",
|
|
21
|
+
maxAttempts: 1,
|
|
22
|
+
initialBackoff: "200 millis",
|
|
23
|
+
maxBackoff: "2 seconds",
|
|
24
|
+
};
|
|
25
|
+
const retryEvidence = (error) => {
|
|
26
|
+
if (isHttpClientError(error) && error.reason._tag === "StatusCodeError") {
|
|
27
|
+
return { response: error.reason.response, body: undefined };
|
|
28
|
+
}
|
|
29
|
+
if (isHttpClientError(error))
|
|
30
|
+
return undefined;
|
|
31
|
+
if (isAnyRegistryClientError(error)) {
|
|
32
|
+
return { response: error.response, body: error.cause };
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
};
|
|
36
|
+
const isRetryableRegistryError = (error) => {
|
|
37
|
+
if (Cause.isTimeoutError(error))
|
|
38
|
+
return true;
|
|
39
|
+
if (isHttpClientError(error)) {
|
|
40
|
+
if (error.reason._tag === "TransportError")
|
|
41
|
+
return true;
|
|
42
|
+
if (error.reason._tag !== "StatusCodeError")
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const evidence = retryEvidence(error);
|
|
46
|
+
if (evidence === undefined)
|
|
47
|
+
return false;
|
|
48
|
+
return [408, 429, 500, 502, 503, 504].includes(evidence.response.status);
|
|
49
|
+
};
|
|
50
|
+
const getStringField = (value, field) => {
|
|
51
|
+
if (typeof value !== "object" || value === null)
|
|
52
|
+
return undefined;
|
|
53
|
+
const fieldValue = Reflect.get(value, field);
|
|
54
|
+
return typeof fieldValue === "string" ? fieldValue : undefined;
|
|
55
|
+
};
|
|
56
|
+
const requestIdFromError = (error) => {
|
|
57
|
+
const body = retryEvidence(error)?.body;
|
|
58
|
+
return getStringField(body, "requestId") ?? getStringField(body, "request_id");
|
|
59
|
+
};
|
|
60
|
+
const retryAfter = (error) => Effect.gen(function* () {
|
|
61
|
+
const evidence = retryEvidence(error);
|
|
62
|
+
if (evidence === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
const nowMillis = yield* Clock.currentTimeMillis;
|
|
65
|
+
return registryRetryAfterSeconds({
|
|
66
|
+
status: evidence.response.status,
|
|
67
|
+
body: evidence.body,
|
|
68
|
+
response: evidence.response,
|
|
69
|
+
nowMillis,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
const retrySchedule = (policy, operation) => {
|
|
73
|
+
const totalDeadlineMillis = Duration.toMillis(Duration.fromInputUnsafe(policy.totalDeadline));
|
|
74
|
+
const maxBackoffMillis = Duration.toMillis(Duration.fromInputUnsafe(policy.maxBackoff));
|
|
75
|
+
return Schedule.exponential(policy.initialBackoff).pipe(Schedule.setInputType(), Schedule.jittered, Schedule.modifyDelay(({ duration, input }) => Effect.map(retryAfter(input), (serverSeconds) => {
|
|
76
|
+
const cappedBackoff = Math.min(Duration.toMillis(duration), maxBackoffMillis);
|
|
77
|
+
return Duration.millis(serverSeconds === undefined
|
|
78
|
+
? cappedBackoff
|
|
79
|
+
: Math.max(cappedBackoff, serverSeconds * 1_000));
|
|
80
|
+
})), Schedule.while(({ input, elapsed, duration }) => isRetryableRegistryError(input) &&
|
|
81
|
+
elapsed + Duration.toMillis(duration) <= totalDeadlineMillis), Schedule.upTo({ times: Math.max(0, policy.maxAttempts - 1) }), Schedule.tap(({ attempt, duration, input }) => Effect.logDebug("Retrying Registry request", {
|
|
82
|
+
service: "registry",
|
|
83
|
+
operation,
|
|
84
|
+
nextAttempt: attempt + 1,
|
|
85
|
+
delayMillis: Duration.toMillis(duration),
|
|
86
|
+
...(requestIdFromError(input) === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: { requestId: requestIdFromError(input) }),
|
|
89
|
+
})));
|
|
90
|
+
};
|
|
91
|
+
const isReplaySafe = (safety) => safety.kind === "safe" ||
|
|
92
|
+
(safety.kind === "idempotency-keyed" && safety.idempotencyKey.length > 0);
|
|
93
|
+
const retryStopReason = (args) => {
|
|
94
|
+
if (!args.retryable)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (!args.replaySafe)
|
|
97
|
+
return "replay-unsafe";
|
|
98
|
+
if (args.deadlineExpired || args.attemptCount < args.maxAttempts)
|
|
99
|
+
return "deadline";
|
|
100
|
+
return "attempt-limit";
|
|
101
|
+
};
|
|
102
|
+
const withRequestPolicyMetadata = (error, args) => {
|
|
103
|
+
const replaySafe = isReplaySafe(args.replaySafety);
|
|
104
|
+
const stoppedBy = retryStopReason({
|
|
105
|
+
retryable: args.retryable,
|
|
106
|
+
replaySafe,
|
|
107
|
+
attemptCount: args.attemptCount,
|
|
108
|
+
maxAttempts: args.maxAttempts,
|
|
109
|
+
deadlineExpired: args.deadlineExpired,
|
|
110
|
+
});
|
|
111
|
+
const metadata = {
|
|
112
|
+
...error.metadata,
|
|
113
|
+
request: error.metadata?.request ?? args.request,
|
|
114
|
+
requestPolicy: {
|
|
115
|
+
retryable: args.retryable,
|
|
116
|
+
attemptCount: args.attemptCount,
|
|
117
|
+
maxAttempts: replaySafe ? args.maxAttempts : 1,
|
|
118
|
+
exhausted: args.retryable,
|
|
119
|
+
...(stoppedBy === undefined ? {} : { stoppedBy }),
|
|
120
|
+
replaySafety: args.replaySafety.kind,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
switch (error._tag) {
|
|
124
|
+
case "RegistryProblem":
|
|
125
|
+
return new RegistryProblem({
|
|
126
|
+
category: error.category,
|
|
127
|
+
...(error.title === undefined ? {} : { title: error.title }),
|
|
128
|
+
...(error.detail === undefined ? {} : { detail: error.detail }),
|
|
129
|
+
metadata,
|
|
130
|
+
...(error.suggestions === undefined ? {} : { suggestions: error.suggestions }),
|
|
131
|
+
cause: error.cause,
|
|
132
|
+
});
|
|
133
|
+
case "RegistryRequestFailed":
|
|
134
|
+
return new RegistryRequestFailed({
|
|
135
|
+
category: error.category,
|
|
136
|
+
detail: error.detail,
|
|
137
|
+
metadata,
|
|
138
|
+
...(error.suggestions === undefined ? {} : { suggestions: error.suggestions }),
|
|
139
|
+
...(error.cause === undefined ? {} : { cause: error.cause }),
|
|
140
|
+
});
|
|
141
|
+
case "RegistryOperationFailed":
|
|
142
|
+
return new RegistryOperationFailed({
|
|
143
|
+
category: error.category,
|
|
144
|
+
detail: error.detail,
|
|
145
|
+
metadata,
|
|
146
|
+
...(error.suggestions === undefined ? {} : { suggestions: error.suggestions }),
|
|
147
|
+
...(error.cause === undefined ? {} : { cause: error.cause }),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
export const executeRegistryRequest = (effect, args) => {
|
|
152
|
+
const policy = args.policy ?? DEFAULT_REGISTRY_REQUEST_POLICY;
|
|
153
|
+
const maxAttempts = Math.max(1, policy.maxAttempts);
|
|
154
|
+
const attempt = effect.pipe(Effect.timeout(policy.requestTimeout));
|
|
155
|
+
return Effect.gen(function* () {
|
|
156
|
+
const attempts = yield* Ref.make(0);
|
|
157
|
+
const countedAttempt = Ref.update(attempts, (count) => count + 1).pipe(Effect.andThen(attempt));
|
|
158
|
+
const executed = isReplaySafe(args.replaySafety)
|
|
159
|
+
? countedAttempt.pipe(Effect.retry(retrySchedule(policy, args.operation)))
|
|
160
|
+
: countedAttempt;
|
|
161
|
+
return yield* executed.pipe(Effect.timeout(policy.totalDeadline), Effect.catch((error) => Ref.get(attempts).pipe(Effect.tap((attemptCount) => Effect.logDebug("Registry request finished with an error", {
|
|
162
|
+
service: "registry",
|
|
163
|
+
operation: args.operation,
|
|
164
|
+
attemptCount,
|
|
165
|
+
replaySafety: args.replaySafety.kind,
|
|
166
|
+
...(requestIdFromError(error) === undefined
|
|
167
|
+
? {}
|
|
168
|
+
: { requestId: requestIdFromError(error) }),
|
|
169
|
+
})), Effect.flatMap((attemptCount) => {
|
|
170
|
+
const deadlineExpired = Cause.isTimeoutError(error);
|
|
171
|
+
const mapped = deadlineExpired
|
|
172
|
+
? new RegistryRequestFailed({
|
|
173
|
+
category: "timeout",
|
|
174
|
+
detail: "Registry request did not complete within the configured deadline.",
|
|
175
|
+
metadata: { request: args.request },
|
|
176
|
+
cause: error,
|
|
177
|
+
})
|
|
178
|
+
: args.mapError(error);
|
|
179
|
+
return Effect.fail(withRequestPolicyMetadata(mapped, {
|
|
180
|
+
request: args.request,
|
|
181
|
+
replaySafety: args.replaySafety,
|
|
182
|
+
attemptCount,
|
|
183
|
+
maxAttempts,
|
|
184
|
+
retryable: deadlineExpired || isRetryableRegistryError(error),
|
|
185
|
+
deadlineExpired,
|
|
186
|
+
}));
|
|
187
|
+
}))));
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
//# sourceMappingURL=request-policy.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
2
|
+
import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
3
|
+
/**
|
|
4
|
+
* Capture non-success Registry response bodies before a generated or raw
|
|
5
|
+
* endpoint adapter decodes them. HttpClientResponse caches the consumed bytes,
|
|
6
|
+
* so the endpoint decoder still observes the same response.
|
|
7
|
+
*/
|
|
8
|
+
export declare const captureRegistryErrorResponseBodies: (client: HttpClient.HttpClient) => HttpClient.HttpClient;
|
|
9
|
+
export declare const retainedRegistryResponseBody: (response: HttpClientResponse.HttpClientResponse, fallback: unknown) => unknown;
|
|
10
|
+
export declare const recoverRegistryResponseBodyText: (text: string) => unknown;
|
|
11
|
+
//# sourceMappingURL=response-body.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
5
|
+
const decodeUnknownJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown));
|
|
6
|
+
const recoverBodyText = (text) => {
|
|
7
|
+
const decoded = decodeUnknownJson(text);
|
|
8
|
+
return Option.isSome(decoded) ? decoded.value : text;
|
|
9
|
+
};
|
|
10
|
+
const retainedResponseBodyKey = Symbol("@agentxm/registry/retained-response-body");
|
|
11
|
+
const retainResponseBody = (response) => response.text.pipe(Effect.flatMap((text) => Effect.sync(() => {
|
|
12
|
+
Object.defineProperty(response, retainedResponseBodyKey, {
|
|
13
|
+
configurable: false,
|
|
14
|
+
enumerable: false,
|
|
15
|
+
value: recoverBodyText(text),
|
|
16
|
+
writable: false,
|
|
17
|
+
});
|
|
18
|
+
return response;
|
|
19
|
+
})), Effect.catch(() => Effect.succeed(response)));
|
|
20
|
+
/**
|
|
21
|
+
* Capture non-success Registry response bodies before a generated or raw
|
|
22
|
+
* endpoint adapter decodes them. HttpClientResponse caches the consumed bytes,
|
|
23
|
+
* so the endpoint decoder still observes the same response.
|
|
24
|
+
*/
|
|
25
|
+
export const captureRegistryErrorResponseBodies = (client) => client.pipe(HttpClient.transformResponse(Effect.flatMap((response) => response.status < 200 || response.status >= 300
|
|
26
|
+
? retainResponseBody(response)
|
|
27
|
+
: Effect.succeed(response))));
|
|
28
|
+
export const retainedRegistryResponseBody = (response, fallback) => Object.hasOwn(response, retainedResponseBodyKey)
|
|
29
|
+
? Reflect.get(response, retainedResponseBodyKey)
|
|
30
|
+
: fallback;
|
|
31
|
+
export const recoverRegistryResponseBodyText = (text) => recoverBodyText(text);
|
|
32
|
+
//# sourceMappingURL=response-body.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
2
|
+
export declare const parseRetryAfterHeader: (value: string | undefined, nowMillis: number) => number | undefined;
|
|
3
|
+
export declare const registryRetryAfterSeconds: (args: {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly body: unknown;
|
|
6
|
+
readonly response: HttpClientResponse.HttpClientResponse;
|
|
7
|
+
readonly nowMillis: number;
|
|
8
|
+
}) => number | undefined;
|
|
9
|
+
//# sourceMappingURL=retry-after.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as DateTime from "effect/DateTime";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import { ProblemDetails as RegistryProblemDetailsSchema } from "./__generated__/registry-client.js";
|
|
5
|
+
const decodeRegistryProblemDetails = Schema.decodeUnknownSync(RegistryProblemDetailsSchema);
|
|
6
|
+
const retryAfterFromBody = (status, body) => {
|
|
7
|
+
if (status !== 429 && status !== 503)
|
|
8
|
+
return undefined;
|
|
9
|
+
try {
|
|
10
|
+
return decodeRegistryProblemDetails(body).details?.retryAfterSeconds;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export const parseRetryAfterHeader = (value, nowMillis) => {
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
const seconds = Number(value);
|
|
20
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
21
|
+
return Math.ceil(seconds);
|
|
22
|
+
const retryAt = DateTime.make(value);
|
|
23
|
+
if (Option.isNone(retryAt))
|
|
24
|
+
return undefined;
|
|
25
|
+
return Math.max(0, Math.ceil((DateTime.toEpochMillis(retryAt.value) - nowMillis) / 1_000));
|
|
26
|
+
};
|
|
27
|
+
export const registryRetryAfterSeconds = (args) => parseRetryAfterHeader(args.response.headers["retry-after"] ?? args.response.headers["Retry-After"], args.nowMillis) ?? retryAfterFromBody(args.status, args.body);
|
|
28
|
+
//# sourceMappingURL=retry-after.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
2
|
+
import type { SuggestedAction } from "@agentxm/registry-protocol/unstable/suggested-action";
|
|
3
|
+
import { type RegistryClientError } from "./__generated__/registry-client.js";
|
|
4
|
+
import { RegistryProblem, type RegistryErrorCategory } from "./errors.js";
|
|
5
|
+
export interface ProblemDetails {
|
|
6
|
+
readonly [key: string]: unknown;
|
|
7
|
+
readonly title?: string;
|
|
8
|
+
readonly status?: number;
|
|
9
|
+
readonly detail?: string;
|
|
10
|
+
readonly code?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const httpStatusToCategory: (status: number, code?: string) => RegistryErrorCategory;
|
|
13
|
+
export declare const registryErrorToProblem: (body: unknown, response: HttpClientResponse.HttpClientResponse, ctx?: {
|
|
14
|
+
readonly suggestions?: ReadonlyArray<SuggestedAction>;
|
|
15
|
+
readonly cause?: unknown;
|
|
16
|
+
}) => RegistryProblem;
|
|
17
|
+
export declare const registryClientErrorToProblem: (error: RegistryClientError<string, unknown>, ctx?: {
|
|
18
|
+
readonly suggestions?: ReadonlyArray<SuggestedAction>;
|
|
19
|
+
}) => RegistryProblem;
|
|
20
|
+
//# sourceMappingURL=translate.d.ts.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as DateTime from "effect/DateTime";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { ExtensionIdentityMismatchErrorEncoded, ExtensionLintFailedErrorEncoded, ForbiddenErrorEncoded, } from "./__generated__/registry-client.js";
|
|
4
|
+
import { RegistryProblem } from "./errors.js";
|
|
5
|
+
import { registryRetryAfterSeconds } from "./retry-after.js";
|
|
6
|
+
import { retainedRegistryResponseBody } from "./response-body.js";
|
|
7
|
+
const EmptyProblem = {};
|
|
8
|
+
const isProblemDetails = (value) => typeof value === "object" && value !== null;
|
|
9
|
+
const decodeForbiddenError = Schema.decodeUnknownSync(ForbiddenErrorEncoded);
|
|
10
|
+
const decodeExtensionLintFailedError = Schema.decodeUnknownSync(ExtensionLintFailedErrorEncoded);
|
|
11
|
+
const decodeExtensionIdentityMismatchError = Schema.decodeUnknownSync(ExtensionIdentityMismatchErrorEncoded);
|
|
12
|
+
const tryDecode = (decode, input) => {
|
|
13
|
+
try {
|
|
14
|
+
return decode(input);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export const httpStatusToCategory = (status, code) => {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case 400:
|
|
23
|
+
return "validation";
|
|
24
|
+
case 401:
|
|
25
|
+
return "auth";
|
|
26
|
+
case 403:
|
|
27
|
+
return code === "quota_exceeded" || code === "publish/quota-exceeded" ? "quota" : "forbidden";
|
|
28
|
+
case 404:
|
|
29
|
+
case 410:
|
|
30
|
+
return "not_found";
|
|
31
|
+
case 409:
|
|
32
|
+
case 412:
|
|
33
|
+
return "conflict";
|
|
34
|
+
case 413:
|
|
35
|
+
case 415:
|
|
36
|
+
case 422:
|
|
37
|
+
return "validation";
|
|
38
|
+
case 429:
|
|
39
|
+
return "rate_limit";
|
|
40
|
+
case 500:
|
|
41
|
+
case 501:
|
|
42
|
+
case 502:
|
|
43
|
+
return "internal";
|
|
44
|
+
case 503:
|
|
45
|
+
return "unavailable";
|
|
46
|
+
default:
|
|
47
|
+
return status >= 500 ? "internal" : "internal";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const retryAfterSuggestedAction = (status, body, response) => {
|
|
51
|
+
const retryAfterSeconds = registryRetryAfterSeconds({
|
|
52
|
+
status,
|
|
53
|
+
body,
|
|
54
|
+
response,
|
|
55
|
+
nowMillis: DateTime.toEpochMillis(DateTime.nowUnsafe()),
|
|
56
|
+
});
|
|
57
|
+
return retryAfterSeconds === undefined
|
|
58
|
+
? undefined
|
|
59
|
+
: { description: `Retry after ${String(retryAfterSeconds)}s.` };
|
|
60
|
+
};
|
|
61
|
+
const scopeDeniedSuggestedAction = (body) => {
|
|
62
|
+
const decoded = tryDecode(decodeForbiddenError, body);
|
|
63
|
+
const requiredScope = decoded?.details !== undefined && "requiredScope" in decoded.details
|
|
64
|
+
? decoded.details.requiredScope
|
|
65
|
+
: undefined;
|
|
66
|
+
return requiredScope === undefined
|
|
67
|
+
? undefined
|
|
68
|
+
: {
|
|
69
|
+
description: "Sign in with the required registry scope.",
|
|
70
|
+
cmd: `axm login --scope ${requiredScope}`,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const lintFailedSuggestions = (body) => {
|
|
74
|
+
const decoded = tryDecode(decodeExtensionLintFailedError, body);
|
|
75
|
+
if (decoded === undefined)
|
|
76
|
+
return [];
|
|
77
|
+
const findingCount = decoded.findings.length;
|
|
78
|
+
const findingLabel = findingCount === 1 ? "finding" : "findings";
|
|
79
|
+
const summary = {
|
|
80
|
+
description: `Publish lint failed with ${String(findingCount)} ${findingLabel}.`,
|
|
81
|
+
};
|
|
82
|
+
const findings = decoded.findings.slice(0, 5).map((finding) => ({
|
|
83
|
+
description: `${finding.severity}: ${finding.ruleId} - ${finding.message} (${finding.path})`,
|
|
84
|
+
}));
|
|
85
|
+
const suppressedCount = findingCount - findings.length;
|
|
86
|
+
const suppressed = suppressedCount === 0
|
|
87
|
+
? []
|
|
88
|
+
: [
|
|
89
|
+
{
|
|
90
|
+
description: `${String(suppressedCount)} additional ${suppressedCount === 1 ? "finding was" : "findings were"} suppressed.`,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
return [summary, ...findings, ...suppressed];
|
|
94
|
+
};
|
|
95
|
+
const identityMismatchSuggestions = (body) => {
|
|
96
|
+
const decoded = tryDecode(decodeExtensionIdentityMismatchError, body);
|
|
97
|
+
if (decoded === undefined)
|
|
98
|
+
return [];
|
|
99
|
+
const summary = {
|
|
100
|
+
description: `Publish identity mismatch on ${String(decoded.mismatches.length)} field${decoded.mismatches.length === 1 ? "" : "s"}.`,
|
|
101
|
+
};
|
|
102
|
+
const mismatches = decoded.mismatches.map((mismatch) => ({
|
|
103
|
+
description: `${mismatch.field}: URL has ${mismatch.urlPath ?? "<missing>"}, archive has ${mismatch.content ?? "<missing>"}.`,
|
|
104
|
+
}));
|
|
105
|
+
return [summary, ...mismatches];
|
|
106
|
+
};
|
|
107
|
+
const serverErrorSuggestedAction = (status) => status >= 500
|
|
108
|
+
? {
|
|
109
|
+
description: "The registry returned a server error. Retry shortly; if it persists, report it with the request ID.",
|
|
110
|
+
}
|
|
111
|
+
: undefined;
|
|
112
|
+
const getStringField = (value, field) => {
|
|
113
|
+
if (value === null || value === undefined || typeof value !== "object") {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const fieldValue = Reflect.get(value, field);
|
|
117
|
+
return typeof fieldValue === "string" ? fieldValue : undefined;
|
|
118
|
+
};
|
|
119
|
+
const problemSuggestions = (status, problem, response) => {
|
|
120
|
+
const body = problem;
|
|
121
|
+
const retry = retryAfterSuggestedAction(status, body, response);
|
|
122
|
+
const scope = status === 403 ? scopeDeniedSuggestedAction(body) : undefined;
|
|
123
|
+
const serverError = serverErrorSuggestedAction(status);
|
|
124
|
+
return [
|
|
125
|
+
...(retry === undefined ? [] : [retry]),
|
|
126
|
+
...(scope === undefined ? [] : [scope]),
|
|
127
|
+
...(serverError === undefined ? [] : [serverError]),
|
|
128
|
+
...(status === 422 && problem.code === "extension_lint_failed"
|
|
129
|
+
? lintFailedSuggestions(body)
|
|
130
|
+
: []),
|
|
131
|
+
...(status === 422 && problem.code === "extension_identity_mismatch"
|
|
132
|
+
? identityMismatchSuggestions(body)
|
|
133
|
+
: []),
|
|
134
|
+
];
|
|
135
|
+
};
|
|
136
|
+
export const registryErrorToProblem = (body, response, ctx) => {
|
|
137
|
+
const problem = isProblemDetails(body) ? body : EmptyProblem;
|
|
138
|
+
const status = response.status;
|
|
139
|
+
const category = httpStatusToCategory(status, problem.code);
|
|
140
|
+
const suggestions = [
|
|
141
|
+
...problemSuggestions(status, problem, response),
|
|
142
|
+
...(ctx?.suggestions ?? []),
|
|
143
|
+
];
|
|
144
|
+
const requestId = getStringField(problem, "requestId") ?? getStringField(problem, "request_id");
|
|
145
|
+
return new RegistryProblem({
|
|
146
|
+
category,
|
|
147
|
+
...(problem.title === undefined ? {} : { title: problem.title }),
|
|
148
|
+
...(problem.detail === undefined ? {} : { detail: problem.detail }),
|
|
149
|
+
metadata: {
|
|
150
|
+
request: {
|
|
151
|
+
service: "registry",
|
|
152
|
+
method: response.request.method,
|
|
153
|
+
url: response.request.url,
|
|
154
|
+
},
|
|
155
|
+
response: {
|
|
156
|
+
status,
|
|
157
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
158
|
+
...(problem.code === undefined ? {} : { problemCode: problem.code }),
|
|
159
|
+
body,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
...(suggestions.length > 0 ? { suggestions } : {}),
|
|
163
|
+
cause: ctx?.cause ?? body,
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
export const registryClientErrorToProblem = (error, ctx) => registryErrorToProblem(retainedRegistryResponseBody(error.response, error.cause), error.response, {
|
|
167
|
+
...(ctx?.suggestions === undefined ? {} : { suggestions: ctx.suggestions }),
|
|
168
|
+
cause: error,
|
|
169
|
+
});
|
|
170
|
+
//# sourceMappingURL=translate.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry utility functions extracted from sources/providers/registry.ts.
|
|
3
|
+
*
|
|
4
|
+
* Shared helpers for registry operations: version selection, integrity
|
|
5
|
+
* computation, zip extraction, type pluralization, and path building.
|
|
6
|
+
*
|
|
7
|
+
* @experimental This API is unstable and may change without notice.
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import * as FileSystem from "effect/FileSystem";
|
|
11
|
+
import * as Path from "effect/Path";
|
|
12
|
+
import * as Duration from "effect/Duration";
|
|
13
|
+
import * as Effect from "effect/Effect";
|
|
14
|
+
import * as Option from "effect/Option";
|
|
15
|
+
import { RegistryOperationFailed } from "./errors.js";
|
|
16
|
+
import type { Handle } from "@agentxm/extension-model/unstable/extensions/handle";
|
|
17
|
+
import { type ExtensionType } from "@agentxm/extension-model/unstable/extensions";
|
|
18
|
+
import type { ExtensionIndex, VersionEntry } from "@agentxm/registry-protocol/unstable/registry/schema";
|
|
19
|
+
import { type ReleaseAgeEvaluation, type ReleaseAgeEvidence, type ReleaseAgeExemption } from "@agentxm/extension-model/unstable/extensions/release-age";
|
|
20
|
+
/**
|
|
21
|
+
* Select the best matching version from a list of versions.
|
|
22
|
+
*
|
|
23
|
+
* Returns the maximum non-yanked version, independent of input order.
|
|
24
|
+
*/
|
|
25
|
+
export declare const selectVersion: (versions: ReadonlyArray<VersionEntry>) => Option.Option<VersionEntry>;
|
|
26
|
+
export declare const resolveVersionEntry: (versions: ReadonlyArray<VersionEntry>, versionRange: Option.Option<string>) => Option.Option<VersionEntry>;
|
|
27
|
+
export declare const extensionLifecycleWarnings: (index: ExtensionIndex, version: VersionEntry) => ReadonlyArray<string>;
|
|
28
|
+
export declare const resolveVersionEntryWithReleaseAge: (versions: ReadonlyArray<VersionEntry>, versionRange: Option.Option<string>, minimumReleaseAge: Option.Option<Duration.Duration>) => Effect.Effect<Option.Option<VersionEntry>>;
|
|
29
|
+
export type ReleaseAgeVersionResolution = {
|
|
30
|
+
readonly kind: "selected";
|
|
31
|
+
readonly version: VersionEntry;
|
|
32
|
+
readonly newerHeld?: ReleaseAgeEvidence;
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: "exempted";
|
|
35
|
+
readonly version: VersionEntry;
|
|
36
|
+
readonly bypassed: ReleaseAgeEvidence;
|
|
37
|
+
readonly exemption: ReleaseAgeExemption;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: "version_unsatisfied";
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: "policy_held";
|
|
42
|
+
readonly candidate: ReleaseAgeEvidence;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Resolve one visible Registry index under one caller-supplied release-age
|
|
46
|
+
* evaluation. The supplied timestamp makes a complete operation deterministic.
|
|
47
|
+
* An accepted version that satisfies the requested range is a lower bound for
|
|
48
|
+
* unattended selection, even while that version is itself under age.
|
|
49
|
+
*/
|
|
50
|
+
export declare const resolveVersionEntryForReleaseAge: (versions: ReadonlyArray<VersionEntry>, versionRange: Option.Option<string>, evaluation: ReleaseAgeEvaluation, exemption?: ReleaseAgeExemption, acceptedVersion?: string) => ReleaseAgeVersionResolution;
|
|
51
|
+
/** Pluralize extension type for directory segments. */
|
|
52
|
+
export declare const pluralizeType: (type: ExtensionType) => string;
|
|
53
|
+
/** Build the path to an extension's directory within a registry. */
|
|
54
|
+
export declare const extensionDir: (registryRoot: string, owner: Handle, type: ExtensionType, name: string, join: (...parts: readonly string[]) => string) => string;
|
|
55
|
+
/**
|
|
56
|
+
* Extract a zip archive to a target directory.
|
|
57
|
+
* Uses fflate for in-memory decompression (portable across platforms).
|
|
58
|
+
*/
|
|
59
|
+
export declare const extractZip: (archive: Uint8Array, targetDir: string) => Effect.Effect<void, RegistryOperationFailed, FileSystem.FileSystem | Path.Path>;
|
|
60
|
+
//# sourceMappingURL=utils.d.ts.map
|