@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,181 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
3
|
+
import { type RegistryClientFailure } from "./errors.js";
|
|
4
|
+
import { RegistryUrl } from "./registry-url.js";
|
|
5
|
+
import { type RegistryRequestPolicy } from "./request-policy.js";
|
|
6
|
+
import * as GeneratedRegistryClient from "./__generated__/registry-client.js";
|
|
7
|
+
import type { DeprecationReplacementIntent } from "@agentxm/registry-protocol/unstable/registry/schema";
|
|
8
|
+
import type { DeprecationView } from "@agentxm/extension-model/unstable/extensions/deprecation";
|
|
9
|
+
export interface RegistryExtensionReference {
|
|
10
|
+
readonly owner: string;
|
|
11
|
+
readonly type: string;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
}
|
|
14
|
+
export interface RegistryExtensionVersionReference extends RegistryExtensionReference {
|
|
15
|
+
readonly version: string;
|
|
16
|
+
}
|
|
17
|
+
export type YankCategory = "broken" | "security" | "accidental" | "other";
|
|
18
|
+
export interface RegistryLifecycleCallOptions {
|
|
19
|
+
readonly stepUpRequestId?: string;
|
|
20
|
+
readonly requestPolicy?: RegistryRequestPolicy;
|
|
21
|
+
}
|
|
22
|
+
export interface PutExtensionDeprecationInput {
|
|
23
|
+
readonly revision: string;
|
|
24
|
+
readonly message: string | null;
|
|
25
|
+
readonly replacement: DeprecationReplacementIntent;
|
|
26
|
+
}
|
|
27
|
+
export declare const normalizeRegistryDeprecation: (value: GeneratedRegistryClient.DeprecationView | null) => Effect.Effect<DeprecationView | null, RegistryClientFailure>;
|
|
28
|
+
export declare const yankExtensionVersion: (ref: RegistryExtensionVersionReference, input: {
|
|
29
|
+
readonly category?: YankCategory;
|
|
30
|
+
readonly notice?: string;
|
|
31
|
+
}, options?: RegistryLifecycleCallOptions) => Effect.Effect<{
|
|
32
|
+
readonly owner: string;
|
|
33
|
+
readonly type: "skill" | "mcp-server" | "subagent" | "rule" | "hook" | "knowledge" | "pack";
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly version: string;
|
|
36
|
+
readonly yankedAt: import("effect/DateTime").Utc | null;
|
|
37
|
+
readonly yankCategory: string | null;
|
|
38
|
+
readonly yankNotice: string | null;
|
|
39
|
+
readonly links: {
|
|
40
|
+
readonly html: string;
|
|
41
|
+
};
|
|
42
|
+
}, RegistryClientFailure, RegistryUrl | HttpClient.HttpClient>;
|
|
43
|
+
export declare const yankAvailableExtensionVersions: (ref: RegistryExtensionReference, input: {
|
|
44
|
+
readonly category?: YankCategory;
|
|
45
|
+
readonly notice?: string;
|
|
46
|
+
}, options?: RegistryLifecycleCallOptions) => Effect.Effect<{
|
|
47
|
+
readonly selection: "all-available";
|
|
48
|
+
readonly affectedVersions: readonly string[];
|
|
49
|
+
readonly futureVersionsAffected: false;
|
|
50
|
+
}, RegistryClientFailure, RegistryUrl | HttpClient.HttpClient>;
|
|
51
|
+
export declare const unyankExtensionVersion: (ref: RegistryExtensionVersionReference, options?: RegistryLifecycleCallOptions) => Effect.Effect<{
|
|
52
|
+
readonly owner: string;
|
|
53
|
+
readonly type: "skill" | "mcp-server" | "subagent" | "rule" | "hook" | "knowledge" | "pack";
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly version: string;
|
|
56
|
+
readonly yankedAt: null;
|
|
57
|
+
readonly yankCategory: null;
|
|
58
|
+
readonly yankNotice: null;
|
|
59
|
+
readonly links: {
|
|
60
|
+
readonly html: string;
|
|
61
|
+
};
|
|
62
|
+
}, RegistryClientFailure, RegistryUrl | HttpClient.HttpClient>;
|
|
63
|
+
export declare const getExtensionDeprecation: (ref: RegistryExtensionReference) => Effect.Effect<{
|
|
64
|
+
deprecation: {
|
|
65
|
+
readonly message: string;
|
|
66
|
+
readonly deprecatedAt: import("effect/DateTime").Utc;
|
|
67
|
+
readonly replacement?: {
|
|
68
|
+
readonly status: "available";
|
|
69
|
+
readonly fqn: string;
|
|
70
|
+
} | {
|
|
71
|
+
readonly status: "unavailable";
|
|
72
|
+
readonly fqn?: string | undefined;
|
|
73
|
+
} | undefined;
|
|
74
|
+
} | {
|
|
75
|
+
readonly replacement: {
|
|
76
|
+
readonly status: "available";
|
|
77
|
+
readonly fqn: string;
|
|
78
|
+
} | {
|
|
79
|
+
readonly status: "unavailable";
|
|
80
|
+
readonly fqn?: string | undefined;
|
|
81
|
+
};
|
|
82
|
+
readonly deprecatedAt: import("effect/DateTime").Utc;
|
|
83
|
+
readonly message?: string | undefined;
|
|
84
|
+
} | null;
|
|
85
|
+
revision: string;
|
|
86
|
+
}, RegistryClientFailure, RegistryUrl | HttpClient.HttpClient>;
|
|
87
|
+
export declare const deprecateExtension: (ref: RegistryExtensionReference, input: PutExtensionDeprecationInput) => Effect.Effect<{
|
|
88
|
+
target: string;
|
|
89
|
+
before: {
|
|
90
|
+
readonly message: string;
|
|
91
|
+
readonly deprecatedAt: import("effect/DateTime").Utc;
|
|
92
|
+
readonly replacement?: {
|
|
93
|
+
readonly status: "available";
|
|
94
|
+
readonly fqn: string;
|
|
95
|
+
} | {
|
|
96
|
+
readonly status: "unavailable";
|
|
97
|
+
readonly fqn?: string | undefined;
|
|
98
|
+
} | undefined;
|
|
99
|
+
} | {
|
|
100
|
+
readonly replacement: {
|
|
101
|
+
readonly status: "available";
|
|
102
|
+
readonly fqn: string;
|
|
103
|
+
} | {
|
|
104
|
+
readonly status: "unavailable";
|
|
105
|
+
readonly fqn?: string | undefined;
|
|
106
|
+
};
|
|
107
|
+
readonly deprecatedAt: import("effect/DateTime").Utc;
|
|
108
|
+
readonly message?: string | undefined;
|
|
109
|
+
} | null;
|
|
110
|
+
after: {
|
|
111
|
+
readonly message: string;
|
|
112
|
+
readonly deprecatedAt: import("effect/DateTime").Utc;
|
|
113
|
+
readonly replacement?: {
|
|
114
|
+
readonly status: "available";
|
|
115
|
+
readonly fqn: string;
|
|
116
|
+
} | {
|
|
117
|
+
readonly status: "unavailable";
|
|
118
|
+
readonly fqn?: string | undefined;
|
|
119
|
+
} | undefined;
|
|
120
|
+
} | {
|
|
121
|
+
readonly replacement: {
|
|
122
|
+
readonly status: "available";
|
|
123
|
+
readonly fqn: string;
|
|
124
|
+
} | {
|
|
125
|
+
readonly status: "unavailable";
|
|
126
|
+
readonly fqn?: string | undefined;
|
|
127
|
+
};
|
|
128
|
+
readonly deprecatedAt: import("effect/DateTime").Utc;
|
|
129
|
+
readonly message?: string | undefined;
|
|
130
|
+
} | null;
|
|
131
|
+
disposition: "created" | "edited" | "restored" | "unchanged";
|
|
132
|
+
revision: string;
|
|
133
|
+
}, RegistryClientFailure, RegistryUrl | HttpClient.HttpClient>;
|
|
134
|
+
export declare const undeprecateExtension: (ref: RegistryExtensionReference, revision: string) => Effect.Effect<{
|
|
135
|
+
target: string;
|
|
136
|
+
before: {
|
|
137
|
+
readonly message: string;
|
|
138
|
+
readonly deprecatedAt: import("effect/DateTime").Utc;
|
|
139
|
+
readonly replacement?: {
|
|
140
|
+
readonly status: "available";
|
|
141
|
+
readonly fqn: string;
|
|
142
|
+
} | {
|
|
143
|
+
readonly status: "unavailable";
|
|
144
|
+
readonly fqn?: string | undefined;
|
|
145
|
+
} | undefined;
|
|
146
|
+
} | {
|
|
147
|
+
readonly replacement: {
|
|
148
|
+
readonly status: "available";
|
|
149
|
+
readonly fqn: string;
|
|
150
|
+
} | {
|
|
151
|
+
readonly status: "unavailable";
|
|
152
|
+
readonly fqn?: string | undefined;
|
|
153
|
+
};
|
|
154
|
+
readonly deprecatedAt: import("effect/DateTime").Utc;
|
|
155
|
+
readonly message?: string | undefined;
|
|
156
|
+
} | null;
|
|
157
|
+
after: {
|
|
158
|
+
readonly message: string;
|
|
159
|
+
readonly deprecatedAt: import("effect/DateTime").Utc;
|
|
160
|
+
readonly replacement?: {
|
|
161
|
+
readonly status: "available";
|
|
162
|
+
readonly fqn: string;
|
|
163
|
+
} | {
|
|
164
|
+
readonly status: "unavailable";
|
|
165
|
+
readonly fqn?: string | undefined;
|
|
166
|
+
} | undefined;
|
|
167
|
+
} | {
|
|
168
|
+
readonly replacement: {
|
|
169
|
+
readonly status: "available";
|
|
170
|
+
readonly fqn: string;
|
|
171
|
+
} | {
|
|
172
|
+
readonly status: "unavailable";
|
|
173
|
+
readonly fqn?: string | undefined;
|
|
174
|
+
};
|
|
175
|
+
readonly deprecatedAt: import("effect/DateTime").Utc;
|
|
176
|
+
readonly message?: string | undefined;
|
|
177
|
+
} | null;
|
|
178
|
+
disposition: "created" | "edited" | "restored" | "unchanged";
|
|
179
|
+
revision: string;
|
|
180
|
+
}, RegistryClientFailure, RegistryUrl | HttpClient.HttpClient>;
|
|
181
|
+
//# sourceMappingURL=admin-client.d.ts.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// @effect-diagnostics anyUnknownInErrorContext:off — generated HTTP response errors are normalized by this registry adapter
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
4
|
+
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
5
|
+
import { RegistryRequestFailed } from "./errors.js";
|
|
6
|
+
import { RegistryUrl } from "./registry-url.js";
|
|
7
|
+
import { captureRegistryErrorResponseBodies, mapRegistryFailure } from "./failure-mapping.js";
|
|
8
|
+
import { executeRegistryRequest, } from "./request-policy.js";
|
|
9
|
+
import * as GeneratedRegistryClient from "./__generated__/registry-client.js";
|
|
10
|
+
export const normalizeRegistryDeprecation = (value) => {
|
|
11
|
+
if (value === null)
|
|
12
|
+
return Effect.succeed(null);
|
|
13
|
+
const replacement = value.replacement === undefined || value.replacement === null
|
|
14
|
+
? undefined
|
|
15
|
+
: value.replacement.status === "available"
|
|
16
|
+
? value.replacement
|
|
17
|
+
: {
|
|
18
|
+
status: "unavailable",
|
|
19
|
+
...(value.replacement.fqn === undefined || value.replacement.fqn === null
|
|
20
|
+
? {}
|
|
21
|
+
: { fqn: value.replacement.fqn }),
|
|
22
|
+
};
|
|
23
|
+
if (value.message !== undefined && value.message !== null) {
|
|
24
|
+
return Effect.succeed({
|
|
25
|
+
deprecatedAt: value.deprecatedAt,
|
|
26
|
+
message: value.message,
|
|
27
|
+
...(replacement === undefined ? {} : { replacement }),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (replacement !== undefined) {
|
|
31
|
+
return Effect.succeed({ deprecatedAt: value.deprecatedAt, replacement });
|
|
32
|
+
}
|
|
33
|
+
return Effect.fail(new RegistryRequestFailed({
|
|
34
|
+
category: "internal",
|
|
35
|
+
detail: "Registry response did not contain required deprecation guidance.",
|
|
36
|
+
}));
|
|
37
|
+
};
|
|
38
|
+
const normalizeManagementView = (value) => Effect.map(normalizeRegistryDeprecation(value.deprecation), (deprecation) => ({
|
|
39
|
+
deprecation,
|
|
40
|
+
revision: value.revision,
|
|
41
|
+
}));
|
|
42
|
+
const normalizeTransition = (value) => Effect.all({
|
|
43
|
+
before: normalizeRegistryDeprecation(value.before),
|
|
44
|
+
after: normalizeRegistryDeprecation(value.after),
|
|
45
|
+
}).pipe(Effect.map(({ before, after }) => ({
|
|
46
|
+
target: value.target,
|
|
47
|
+
before,
|
|
48
|
+
after,
|
|
49
|
+
disposition: value.disposition,
|
|
50
|
+
revision: value.revision,
|
|
51
|
+
})));
|
|
52
|
+
const mapAdminClientError = (registryUrl) => (error) => mapRegistryFailure(error, {
|
|
53
|
+
baseUrl: registryUrl,
|
|
54
|
+
networkDetail: "Registry request failed.",
|
|
55
|
+
incompatibleDetail: "Registry response did not match the expected schema.",
|
|
56
|
+
requestConstructionDetail: "Could not construct the Registry request.",
|
|
57
|
+
fallbackDetail: "Unexpected registry client failure.",
|
|
58
|
+
});
|
|
59
|
+
const makeLifecycleClient = (options) => Effect.gen(function* () {
|
|
60
|
+
const registryUrl = yield* RegistryUrl;
|
|
61
|
+
const httpClient = yield* HttpClient.HttpClient;
|
|
62
|
+
const remoteHttpClient = captureRegistryErrorResponseBodies(httpClient.pipe(HttpClient.mapRequest((request) => {
|
|
63
|
+
const withUrl = HttpClientRequest.prependUrl(request, registryUrl);
|
|
64
|
+
return options?.stepUpRequestId === undefined
|
|
65
|
+
? withUrl
|
|
66
|
+
: HttpClientRequest.setHeaders(withUrl, {
|
|
67
|
+
"x-axm-step-up-request": options.stepUpRequestId,
|
|
68
|
+
});
|
|
69
|
+
})));
|
|
70
|
+
return {
|
|
71
|
+
registryUrl,
|
|
72
|
+
client: GeneratedRegistryClient.make(remoteHttpClient),
|
|
73
|
+
requestPolicy: options?.requestPolicy,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
const runAdminCall = (registryUrl, effect, args) => executeRegistryRequest(effect, {
|
|
77
|
+
operation: args.operation,
|
|
78
|
+
request: {
|
|
79
|
+
service: "registry",
|
|
80
|
+
method: args.method,
|
|
81
|
+
url: new URL(args.path, registryUrl).href,
|
|
82
|
+
},
|
|
83
|
+
replaySafety: args.replaySafety,
|
|
84
|
+
mapError: mapAdminClientError(registryUrl),
|
|
85
|
+
...(args.requestPolicy === undefined ? {} : { policy: args.requestPolicy }),
|
|
86
|
+
});
|
|
87
|
+
const safe = { kind: "safe" };
|
|
88
|
+
const mutation = { kind: "mutation" };
|
|
89
|
+
export const yankExtensionVersion = (ref, input, options) => Effect.gen(function* () {
|
|
90
|
+
const { client, registryUrl, requestPolicy } = yield* makeLifecycleClient(options);
|
|
91
|
+
return yield* runAdminCall(registryUrl, client.ExtensionsYankVersion(ref.owner, ref.type, ref.name, ref.version, {
|
|
92
|
+
payload: {
|
|
93
|
+
...(input.category === undefined ? {} : { category: input.category }),
|
|
94
|
+
...(input.notice === undefined ? {} : { notice: input.notice }),
|
|
95
|
+
},
|
|
96
|
+
}), {
|
|
97
|
+
operation: "yank extension version",
|
|
98
|
+
method: "POST",
|
|
99
|
+
path: `/v1/extensions/${ref.owner}/${ref.type}/${ref.name}/${ref.version}/yank`,
|
|
100
|
+
replaySafety: mutation,
|
|
101
|
+
...(requestPolicy === undefined ? {} : { requestPolicy }),
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
export const yankAvailableExtensionVersions = (ref, input, options) => Effect.gen(function* () {
|
|
105
|
+
const { client, registryUrl, requestPolicy } = yield* makeLifecycleClient(options);
|
|
106
|
+
return yield* runAdminCall(registryUrl, client.ExtensionsYankAvailableVersions(ref.owner, ref.type, ref.name, {
|
|
107
|
+
payload: {
|
|
108
|
+
selection: "all-available",
|
|
109
|
+
...(input.category === undefined ? {} : { category: input.category }),
|
|
110
|
+
...(input.notice === undefined ? {} : { notice: input.notice }),
|
|
111
|
+
},
|
|
112
|
+
}), {
|
|
113
|
+
operation: "yank available extension versions",
|
|
114
|
+
method: "POST",
|
|
115
|
+
path: `/v1/extensions/${ref.owner}/${ref.type}/${ref.name}/versions/yank`,
|
|
116
|
+
replaySafety: mutation,
|
|
117
|
+
...(requestPolicy === undefined ? {} : { requestPolicy }),
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
export const unyankExtensionVersion = (ref, options) => Effect.gen(function* () {
|
|
121
|
+
const { client, registryUrl, requestPolicy } = yield* makeLifecycleClient(options);
|
|
122
|
+
return yield* runAdminCall(registryUrl, client.ExtensionsUnyankVersion(ref.owner, ref.type, ref.name, ref.version, undefined), {
|
|
123
|
+
operation: "unyank extension version",
|
|
124
|
+
method: "DELETE",
|
|
125
|
+
path: `/v1/extensions/${ref.owner}/${ref.type}/${ref.name}/${ref.version}/yank`,
|
|
126
|
+
replaySafety: mutation,
|
|
127
|
+
...(requestPolicy === undefined ? {} : { requestPolicy }),
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
export const getExtensionDeprecation = (ref) => Effect.gen(function* () {
|
|
131
|
+
const { client, registryUrl } = yield* makeLifecycleClient();
|
|
132
|
+
const result = yield* runAdminCall(registryUrl, client.ExtensionsGetDeprecation(ref.owner, ref.type, ref.name, undefined), {
|
|
133
|
+
operation: "get extension deprecation",
|
|
134
|
+
method: "GET",
|
|
135
|
+
path: `/v1/extensions/${ref.owner}/${ref.type}/${ref.name}/deprecation`,
|
|
136
|
+
replaySafety: safe,
|
|
137
|
+
});
|
|
138
|
+
return yield* normalizeManagementView(result);
|
|
139
|
+
});
|
|
140
|
+
export const deprecateExtension = (ref, input) => Effect.gen(function* () {
|
|
141
|
+
const { client, registryUrl } = yield* makeLifecycleClient();
|
|
142
|
+
const result = yield* runAdminCall(registryUrl, client.ExtensionsPutDeprecation(ref.owner, ref.type, ref.name, {
|
|
143
|
+
params: { "if-match": input.revision },
|
|
144
|
+
payload: { message: input.message, replacement: input.replacement },
|
|
145
|
+
}), {
|
|
146
|
+
operation: "deprecate extension",
|
|
147
|
+
method: "PUT",
|
|
148
|
+
path: `/v1/extensions/${ref.owner}/${ref.type}/${ref.name}/deprecation`,
|
|
149
|
+
replaySafety: mutation,
|
|
150
|
+
});
|
|
151
|
+
return yield* normalizeTransition(result);
|
|
152
|
+
});
|
|
153
|
+
export const undeprecateExtension = (ref, revision) => Effect.gen(function* () {
|
|
154
|
+
const { client, registryUrl } = yield* makeLifecycleClient();
|
|
155
|
+
const result = yield* runAdminCall(registryUrl, client.ExtensionsDeleteDeprecation(ref.owner, ref.type, ref.name, {
|
|
156
|
+
params: { "if-match": revision },
|
|
157
|
+
}), {
|
|
158
|
+
operation: "undeprecate extension",
|
|
159
|
+
method: "DELETE",
|
|
160
|
+
path: `/v1/extensions/${ref.owner}/${ref.type}/${ref.name}/deprecation`,
|
|
161
|
+
replaySafety: mutation,
|
|
162
|
+
});
|
|
163
|
+
return yield* normalizeTransition(result);
|
|
164
|
+
});
|
|
165
|
+
//# sourceMappingURL=admin-client.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verified, content-addressed cache for registry ZIP archives.
|
|
3
|
+
*
|
|
4
|
+
* @experimental This API is unstable and may change without notice.
|
|
5
|
+
*/
|
|
6
|
+
import * as Duration from "effect/Duration";
|
|
7
|
+
import * as FileSystem from "effect/FileSystem";
|
|
8
|
+
import * as Path from "effect/Path";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Option from "effect/Option";
|
|
11
|
+
import { type RegistryClientFailure } from "./errors.js";
|
|
12
|
+
export declare const ARCHIVE_CACHE_MAX_BYTES: number;
|
|
13
|
+
export declare const ARCHIVE_CACHE_MAX_AGE: Duration.Duration;
|
|
14
|
+
export interface ArchiveCacheOptions {
|
|
15
|
+
readonly maxBytes?: number;
|
|
16
|
+
readonly maxAge?: Duration.Duration;
|
|
17
|
+
}
|
|
18
|
+
export interface ArchiveCacheStatus {
|
|
19
|
+
readonly entries: number;
|
|
20
|
+
readonly bytes: number;
|
|
21
|
+
readonly maxBytes: number;
|
|
22
|
+
readonly maxAgeDays: number;
|
|
23
|
+
}
|
|
24
|
+
export interface ArchiveCacheVerifyResult {
|
|
25
|
+
readonly checked: number;
|
|
26
|
+
readonly valid: number;
|
|
27
|
+
readonly corruptRemoved: number;
|
|
28
|
+
}
|
|
29
|
+
export interface ArchiveCachePruneResult {
|
|
30
|
+
readonly removed: number;
|
|
31
|
+
readonly bytesFreed: number;
|
|
32
|
+
readonly remaining: number;
|
|
33
|
+
readonly remainingBytes: number;
|
|
34
|
+
}
|
|
35
|
+
export interface ArchiveCache {
|
|
36
|
+
readonly read: (integrity: string) => Effect.Effect<Option.Option<Uint8Array>, RegistryClientFailure>;
|
|
37
|
+
readonly write: (integrity: string, archive: Uint8Array, options?: {
|
|
38
|
+
readonly prune?: boolean;
|
|
39
|
+
}) => Effect.Effect<void, RegistryClientFailure>;
|
|
40
|
+
readonly status: () => Effect.Effect<ArchiveCacheStatus, RegistryClientFailure>;
|
|
41
|
+
readonly verify: () => Effect.Effect<ArchiveCacheVerifyResult, RegistryClientFailure>;
|
|
42
|
+
readonly prune: () => Effect.Effect<ArchiveCachePruneResult, RegistryClientFailure>;
|
|
43
|
+
}
|
|
44
|
+
export declare const makeArchiveCache: (fs: FileSystem.FileSystem, path: Path.Path, root: string, options?: ArchiveCacheOptions) => ArchiveCache;
|
|
45
|
+
export declare const makeUserArchiveCache: () => Effect.Effect<ArchiveCache, never, FileSystem.FileSystem | Path.Path>;
|
|
46
|
+
//# sourceMappingURL=archive-cache.d.ts.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verified, content-addressed cache for registry ZIP archives.
|
|
3
|
+
*
|
|
4
|
+
* @experimental This API is unstable and may change without notice.
|
|
5
|
+
*/
|
|
6
|
+
import * as DateTime from "effect/DateTime";
|
|
7
|
+
import * as Duration from "effect/Duration";
|
|
8
|
+
import * as FileSystem from "effect/FileSystem";
|
|
9
|
+
import * as Path from "effect/Path";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Option from "effect/Option";
|
|
12
|
+
import { RegistryOperationFailed } from "./errors.js";
|
|
13
|
+
import { computeIntegrity } from "./integrity.js";
|
|
14
|
+
import { writeFileAtomic } from "./atomic-write.js";
|
|
15
|
+
import { resolveAxmCacheRoot } from "./cache-root.js";
|
|
16
|
+
export const ARCHIVE_CACHE_MAX_BYTES = 2 * 1024 * 1024 * 1024;
|
|
17
|
+
export const ARCHIVE_CACHE_MAX_AGE = Duration.days(90);
|
|
18
|
+
const cacheError = (detail, cause) => new RegistryOperationFailed({
|
|
19
|
+
category: "internal",
|
|
20
|
+
detail,
|
|
21
|
+
...(cause === undefined ? {} : { cause }),
|
|
22
|
+
});
|
|
23
|
+
const cacheFileName = (integrity) => {
|
|
24
|
+
if (!integrity.startsWith("sha512-") || integrity.length === "sha512-".length)
|
|
25
|
+
return undefined;
|
|
26
|
+
const base64Url = integrity
|
|
27
|
+
.slice("sha512-".length)
|
|
28
|
+
.replaceAll("+", "-")
|
|
29
|
+
.replaceAll("/", "_")
|
|
30
|
+
.replace(/=+$/, "");
|
|
31
|
+
return `${base64Url}.zip`;
|
|
32
|
+
};
|
|
33
|
+
const integrityFromFileName = (fileName) => {
|
|
34
|
+
if (!fileName.endsWith(".zip"))
|
|
35
|
+
return undefined;
|
|
36
|
+
const base64Url = fileName.slice(0, -".zip".length);
|
|
37
|
+
if (base64Url.length === 0 || !/^[A-Za-z0-9_-]+$/.test(base64Url))
|
|
38
|
+
return undefined;
|
|
39
|
+
const remainder = base64Url.length % 4;
|
|
40
|
+
const padding = remainder === 0 ? "" : "=".repeat(4 - remainder);
|
|
41
|
+
return `sha512-${base64Url.replaceAll("-", "+").replaceAll("_", "/")}${padding}`;
|
|
42
|
+
};
|
|
43
|
+
export const makeArchiveCache = (fs, path, root, options = {}) => {
|
|
44
|
+
const maxBytes = options.maxBytes ?? ARCHIVE_CACHE_MAX_BYTES;
|
|
45
|
+
const maxAge = options.maxAge ?? ARCHIVE_CACHE_MAX_AGE;
|
|
46
|
+
const pathForIntegrity = (integrity) => {
|
|
47
|
+
const fileName = cacheFileName(integrity);
|
|
48
|
+
return fileName === undefined
|
|
49
|
+
? Effect.fail(cacheError(`Unsupported archive integrity value: ${integrity}`))
|
|
50
|
+
: Effect.succeed(path.join(root, fileName));
|
|
51
|
+
};
|
|
52
|
+
const listEntries = () => Effect.gen(function* () {
|
|
53
|
+
const exists = yield* fs.exists(root).pipe(Effect.orElseSucceed(() => false));
|
|
54
|
+
if (!exists)
|
|
55
|
+
return [];
|
|
56
|
+
const names = yield* fs
|
|
57
|
+
.readDirectory(root)
|
|
58
|
+
.pipe(Effect.mapError((cause) => cacheError(`Failed to inspect archive cache: ${root}`, cause)));
|
|
59
|
+
return yield* Effect.forEach(names.filter((name) => name.endsWith(".zip")), (name) => Effect.gen(function* () {
|
|
60
|
+
const entryPath = path.join(root, name);
|
|
61
|
+
const info = yield* fs
|
|
62
|
+
.stat(entryPath)
|
|
63
|
+
.pipe(Effect.mapError((cause) => cacheError(`Failed to inspect cache entry: ${entryPath}`, cause)));
|
|
64
|
+
return {
|
|
65
|
+
path: entryPath,
|
|
66
|
+
integrity: integrityFromFileName(name),
|
|
67
|
+
size: Number(info.size),
|
|
68
|
+
accessedAt: Option.match(info.mtime, {
|
|
69
|
+
onNone: () => DateTime.makeUnsafe(0),
|
|
70
|
+
onSome: (mtime) => DateTime.makeUnsafe(mtime),
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
}), { concurrency: 16 });
|
|
74
|
+
});
|
|
75
|
+
const removeEntry = (entryPath) => fs
|
|
76
|
+
.remove(entryPath)
|
|
77
|
+
.pipe(Effect.mapError((cause) => cacheError(`Failed to remove cache entry: ${entryPath}`, cause)));
|
|
78
|
+
const prune = () => Effect.gen(function* () {
|
|
79
|
+
const now = yield* DateTime.now;
|
|
80
|
+
const entries = [...(yield* listEntries())].sort((a, b) => DateTime.Order(a.accessedAt, b.accessedAt));
|
|
81
|
+
const expired = entries.filter((entry) => Duration.isGreaterThan(DateTime.distance(entry.accessedAt, now), maxAge));
|
|
82
|
+
yield* Effect.forEach(expired, (entry) => removeEntry(entry.path), { concurrency: 8 });
|
|
83
|
+
const expiredPaths = new Set(expired.map((entry) => entry.path));
|
|
84
|
+
const retained = entries.filter((entry) => !expiredPaths.has(entry.path));
|
|
85
|
+
let retainedBytes = retained.reduce((total, entry) => total + entry.size, 0);
|
|
86
|
+
const removedForSize = [];
|
|
87
|
+
for (const entry of retained) {
|
|
88
|
+
if (retainedBytes <= maxBytes)
|
|
89
|
+
break;
|
|
90
|
+
yield* removeEntry(entry.path);
|
|
91
|
+
removedForSize.push(entry);
|
|
92
|
+
retainedBytes -= entry.size;
|
|
93
|
+
}
|
|
94
|
+
const removed = [...expired, ...removedForSize];
|
|
95
|
+
return {
|
|
96
|
+
removed: removed.length,
|
|
97
|
+
bytesFreed: removed.reduce((total, entry) => total + entry.size, 0),
|
|
98
|
+
remaining: entries.length - removed.length,
|
|
99
|
+
remainingBytes: retainedBytes,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
const read = (integrity) => Effect.gen(function* () {
|
|
103
|
+
const entryPath = yield* pathForIntegrity(integrity);
|
|
104
|
+
const exists = yield* fs.exists(entryPath).pipe(Effect.orElseSucceed(() => false));
|
|
105
|
+
if (!exists)
|
|
106
|
+
return Option.none();
|
|
107
|
+
const archive = yield* fs
|
|
108
|
+
.readFile(entryPath)
|
|
109
|
+
.pipe(Effect.mapError((cause) => cacheError(`Failed to read cache entry: ${entryPath}`, cause)));
|
|
110
|
+
const actualIntegrity = yield* computeIntegrity(archive);
|
|
111
|
+
if (actualIntegrity !== integrity) {
|
|
112
|
+
yield* removeEntry(entryPath);
|
|
113
|
+
return Option.none();
|
|
114
|
+
}
|
|
115
|
+
const now = DateTime.toDateUtc(yield* DateTime.now);
|
|
116
|
+
yield* fs.utimes(entryPath, now, now).pipe(Effect.ignore);
|
|
117
|
+
return Option.some(archive);
|
|
118
|
+
});
|
|
119
|
+
const write = (integrity, archive, writeOptions = {}) => Effect.gen(function* () {
|
|
120
|
+
const actualIntegrity = yield* computeIntegrity(archive);
|
|
121
|
+
if (actualIntegrity !== integrity) {
|
|
122
|
+
return yield* new RegistryOperationFailed({
|
|
123
|
+
category: "validation",
|
|
124
|
+
detail: "Downloaded registry archive did not match its published integrity.",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const entryPath = yield* pathForIntegrity(integrity);
|
|
128
|
+
yield* fs
|
|
129
|
+
.makeDirectory(root, { recursive: true })
|
|
130
|
+
.pipe(Effect.mapError((cause) => cacheError(`Failed to create archive cache: ${root}`, cause)));
|
|
131
|
+
yield* writeFileAtomic(fs, {
|
|
132
|
+
targetPath: entryPath,
|
|
133
|
+
content: archive,
|
|
134
|
+
removeTargetBeforeRename: true,
|
|
135
|
+
mapError: (failure) => failure.step === "rename"
|
|
136
|
+
? cacheError(`Failed to commit cache entry: ${entryPath}`, failure.cause)
|
|
137
|
+
: cacheError("Failed to write archive cache temp file", failure.cause),
|
|
138
|
+
});
|
|
139
|
+
if (writeOptions.prune !== false)
|
|
140
|
+
yield* prune();
|
|
141
|
+
});
|
|
142
|
+
const status = () => Effect.map(listEntries(), (entries) => ({
|
|
143
|
+
entries: entries.length,
|
|
144
|
+
bytes: entries.reduce((total, entry) => total + entry.size, 0),
|
|
145
|
+
maxBytes,
|
|
146
|
+
maxAgeDays: Duration.toDays(maxAge),
|
|
147
|
+
}));
|
|
148
|
+
const verify = () => Effect.gen(function* () {
|
|
149
|
+
const entries = yield* listEntries();
|
|
150
|
+
const results = yield* Effect.forEach(entries, (entry) => Effect.gen(function* () {
|
|
151
|
+
if (entry.integrity === undefined) {
|
|
152
|
+
yield* removeEntry(entry.path);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const archive = yield* fs
|
|
156
|
+
.readFile(entry.path)
|
|
157
|
+
.pipe(Effect.mapError((cause) => cacheError(`Failed to read cache entry: ${entry.path}`, cause)));
|
|
158
|
+
const actualIntegrity = yield* computeIntegrity(archive);
|
|
159
|
+
if (actualIntegrity === entry.integrity)
|
|
160
|
+
return true;
|
|
161
|
+
yield* removeEntry(entry.path);
|
|
162
|
+
return false;
|
|
163
|
+
}), { concurrency: 8 });
|
|
164
|
+
const valid = results.filter(Boolean).length;
|
|
165
|
+
return {
|
|
166
|
+
checked: results.length,
|
|
167
|
+
valid,
|
|
168
|
+
corruptRemoved: results.length - valid,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
return { read, write, status, verify, prune };
|
|
172
|
+
};
|
|
173
|
+
export const makeUserArchiveCache = () => Effect.gen(function* () {
|
|
174
|
+
const fs = yield* FileSystem.FileSystem;
|
|
175
|
+
const path = yield* Path.Path;
|
|
176
|
+
const root = path.join(yield* resolveAxmCacheRoot(), "archives");
|
|
177
|
+
return makeArchiveCache(fs, path, root);
|
|
178
|
+
});
|
|
179
|
+
//# sourceMappingURL=archive-cache.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared atomic single-file replacement: write a uniquely named temp file in
|
|
3
|
+
* the target's directory, then rename it over the target.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately duplicated from the workspace-state kernel: an integration
|
|
6
|
+
* may not depend on a kernel, and this helper is within the sanctioned
|
|
7
|
+
* duplication budget for small pure functions. The registry consumers never
|
|
8
|
+
* sweep stale temps, so that half of the original stays behind.
|
|
9
|
+
*
|
|
10
|
+
* @experimental This API is unstable and may change without notice.
|
|
11
|
+
*/
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import type * as FileSystem from "effect/FileSystem";
|
|
14
|
+
import type { PlatformError } from "effect/PlatformError";
|
|
15
|
+
/**
|
|
16
|
+
* Step at which an atomic write failed. `check-target` and `read-target` can
|
|
17
|
+
* only occur with `skipIfUnchanged: "fail-on-read-error"`.
|
|
18
|
+
*/
|
|
19
|
+
export type AtomicWriteStep = "check-target" | "read-target" | "write-temp" | "rename";
|
|
20
|
+
/** Failure passed to `mapError` so each call site keeps its own error shape. */
|
|
21
|
+
export interface AtomicWriteFailure {
|
|
22
|
+
readonly step: AtomicWriteStep;
|
|
23
|
+
readonly targetPath: string;
|
|
24
|
+
readonly tempPath: string;
|
|
25
|
+
readonly cause: PlatformError;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Content-equality short-circuit mode: `fail-on-read-error` surfaces
|
|
29
|
+
* check/read failures via `mapError`; `ignore-read-errors` treats an
|
|
30
|
+
* unreadable target as changed and proceeds to write.
|
|
31
|
+
*/
|
|
32
|
+
export type SkipIfUnchanged = "fail-on-read-error" | "ignore-read-errors";
|
|
33
|
+
/** Options for `writeFileAtomic`. */
|
|
34
|
+
export interface WriteFileAtomicOptions<E> {
|
|
35
|
+
readonly targetPath: string;
|
|
36
|
+
readonly content: string | Uint8Array;
|
|
37
|
+
/** When set, leave the target untouched if it already has this content. */
|
|
38
|
+
readonly skipIfUnchanged?: SkipIfUnchanged;
|
|
39
|
+
/** Best-effort remove of the target before rename (Windows cannot always rename over an existing file). */
|
|
40
|
+
readonly removeTargetBeforeRename?: boolean;
|
|
41
|
+
readonly mapError: (failure: AtomicWriteFailure) => E;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Atomically replace `targetPath` with `content` via a same-directory temp
|
|
45
|
+
* file and rename. Failures at each step are mapped by the caller, so error
|
|
46
|
+
* categories, details, and suggestions stay call-site specific.
|
|
47
|
+
*/
|
|
48
|
+
export declare const writeFileAtomic: <E>(fs: FileSystem.FileSystem, options: WriteFileAtomicOptions<E>) => Effect.Effect<"written" | "skipped", E>;
|
|
49
|
+
//# sourceMappingURL=atomic-write.d.ts.map
|