@intx/tool-packaging 0.2.2 → 0.4.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/README.md +17 -12
- package/dist/atomic-apply.d.ts +5 -0
- package/dist/atomic-apply.js +1 -0
- package/dist/cache.d.ts +1 -1
- package/dist/cache.js +14 -19
- package/dist/git-materialize.d.ts +19 -0
- package/dist/git-materialize.js +77 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/loader-internal.d.ts +47 -0
- package/dist/loader-internal.js +66 -0
- package/dist/loader.d.ts +71 -62
- package/dist/loader.js +135 -855
- package/dist/registry-fetch.d.ts +40 -0
- package/dist/registry-fetch.js +231 -0
- package/dist/resolver.d.ts +8 -5
- package/dist/resolver.js +23 -7
- package/dist/store-layout.d.ts +25 -0
- package/dist/store-layout.js +683 -0
- package/package.json +7 -4
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RegistryConfig } from "./resolver.js";
|
|
2
|
+
export declare function buildRegistryFetchOpts(registry: RegistryConfig): Record<string, unknown>;
|
|
3
|
+
export declare function defaultTarballUrl(registryUrl: string, name: string, version: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Read an HTTP-registry tarball response into a Uint8Array while enforcing
|
|
6
|
+
* `maxBytes`. Two guards:
|
|
7
|
+
*
|
|
8
|
+
* 1. If the upstream sent a `Content-Length` header, parse it (digit-
|
|
9
|
+
* only, per RFC 9110 §8.6) and reject up front when the declared
|
|
10
|
+
* length exceeds the cap. A header that fails the digit shape is
|
|
11
|
+
* also rejected so a header like `1e9` cannot read as 1e9 against
|
|
12
|
+
* `Number()` while a digit-only cap check would pass.
|
|
13
|
+
* 2. Stream the body chunk-by-chunk, tallying byte length, and abort
|
|
14
|
+
* the read when the running total crosses the cap. This catches
|
|
15
|
+
* the missing-or-lying header case.
|
|
16
|
+
*
|
|
17
|
+
* The body is read whether it is a web `ReadableStream` (the `getReader`
|
|
18
|
+
* shape a real `Response` exposes, exercised by the test seams) or a
|
|
19
|
+
* Node/Minipass async-iterable of `Buffer` chunks (what
|
|
20
|
+
* `npm-registry-fetch` actually returns in production). Both branches
|
|
21
|
+
* enforce the same running `maxBytes` cap while streaming, so the byte
|
|
22
|
+
* guard is never bypassed by buffering the whole body up front
|
|
23
|
+
* (`arrayBuffer()`).
|
|
24
|
+
*
|
|
25
|
+
* An optional `signal` adds a time guard: when it aborts (the caller's
|
|
26
|
+
* fetch deadline), the in-flight read is cancelled — the web reader via
|
|
27
|
+
* `cancel()`, the Node stream via `destroy()` — and the call rejects, so
|
|
28
|
+
* a registry that streams the body slowly or stalls mid-stream cannot
|
|
29
|
+
* outlast the deadline while staying under the byte cap.
|
|
30
|
+
*
|
|
31
|
+
* All rejections surface as `registry.fetch.failed` so the apply layer
|
|
32
|
+
* routes them the same as any other registry-side fetch defect.
|
|
33
|
+
*
|
|
34
|
+
* Exported for direct unit testing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readResponseWithLimit(res: Response, maxBytes: number, ctx: {
|
|
37
|
+
readonly registry: string;
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly version: string;
|
|
40
|
+
}, signal?: AbortSignal): Promise<Uint8Array>;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Registry HTTP fetch helpers for the tool-package loader: building the
|
|
2
|
+
// npm-registry-fetch options for a configured registry, deriving a default
|
|
3
|
+
// tarball URL, and reading a response body under a byte cap. Extracted from
|
|
4
|
+
// `loader.ts` so the fetch concern is isolated from author-code loading and
|
|
5
|
+
// store layout.
|
|
6
|
+
import { ToolLoaderError } from "./loader-internal.js";
|
|
7
|
+
export function buildRegistryFetchOpts(registry) {
|
|
8
|
+
const opts = { registry: registry.url };
|
|
9
|
+
if (registry.auth?.token !== undefined) {
|
|
10
|
+
opts.token = registry.auth.token;
|
|
11
|
+
}
|
|
12
|
+
if (registry.auth?.basic !== undefined) {
|
|
13
|
+
const { user, pass } = registry.auth.basic;
|
|
14
|
+
// `npm-registry-fetch` builds the `Authorization: Basic` header by
|
|
15
|
+
// base64-encoding `<username>:<password>` itself. Pre-encoding
|
|
16
|
+
// `pass` would double-encode the password component (the registry
|
|
17
|
+
// would see `base64(plaintext)` as the password, not `plaintext`).
|
|
18
|
+
opts.forceAuth = { username: user, password: pass };
|
|
19
|
+
}
|
|
20
|
+
return opts;
|
|
21
|
+
}
|
|
22
|
+
export function defaultTarballUrl(registryUrl, name, version) {
|
|
23
|
+
const base = registryUrl.endsWith("/") ? registryUrl : `${registryUrl}/`;
|
|
24
|
+
// Match npm's canonical tarball URL: {registry}/{name}/-/{basename}-{version}.tgz
|
|
25
|
+
const basename = name.startsWith("@") ? name.split("/")[1] : name;
|
|
26
|
+
if (basename === undefined) {
|
|
27
|
+
throw new Error(`internal: cannot derive tarball basename for ${name}`);
|
|
28
|
+
}
|
|
29
|
+
return `${base}${name}/-/${basename}-${version}.tgz`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* True when `body` is a web `ReadableStream`-shaped value the reader loop
|
|
33
|
+
* can pull through `getReader()`. Test seams that build a real `Response`
|
|
34
|
+
* hit this path; the production `npm-registry-fetch` body does not.
|
|
35
|
+
*/
|
|
36
|
+
function hasWebReadableBody(body) {
|
|
37
|
+
return (typeof body === "object" &&
|
|
38
|
+
body !== null &&
|
|
39
|
+
"getReader" in body &&
|
|
40
|
+
typeof body.getReader === "function");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* True when `body` is a Node-style byte stream: async-iterable, with an
|
|
44
|
+
* optional `destroy` the abort path uses to tear down a stalled read.
|
|
45
|
+
* This is the shape `npm-registry-fetch`'s Minipass response body has —
|
|
46
|
+
* chunks are validated as `Uint8Array` per-iteration, so the element type
|
|
47
|
+
* is left `unknown` here rather than asserted.
|
|
48
|
+
*/
|
|
49
|
+
function isByteStreamAsyncIterable(body) {
|
|
50
|
+
return (typeof body === "object" &&
|
|
51
|
+
body !== null &&
|
|
52
|
+
Symbol.asyncIterator in body &&
|
|
53
|
+
typeof body[Symbol.asyncIterator] === "function");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read an HTTP-registry tarball response into a Uint8Array while enforcing
|
|
57
|
+
* `maxBytes`. Two guards:
|
|
58
|
+
*
|
|
59
|
+
* 1. If the upstream sent a `Content-Length` header, parse it (digit-
|
|
60
|
+
* only, per RFC 9110 §8.6) and reject up front when the declared
|
|
61
|
+
* length exceeds the cap. A header that fails the digit shape is
|
|
62
|
+
* also rejected so a header like `1e9` cannot read as 1e9 against
|
|
63
|
+
* `Number()` while a digit-only cap check would pass.
|
|
64
|
+
* 2. Stream the body chunk-by-chunk, tallying byte length, and abort
|
|
65
|
+
* the read when the running total crosses the cap. This catches
|
|
66
|
+
* the missing-or-lying header case.
|
|
67
|
+
*
|
|
68
|
+
* The body is read whether it is a web `ReadableStream` (the `getReader`
|
|
69
|
+
* shape a real `Response` exposes, exercised by the test seams) or a
|
|
70
|
+
* Node/Minipass async-iterable of `Buffer` chunks (what
|
|
71
|
+
* `npm-registry-fetch` actually returns in production). Both branches
|
|
72
|
+
* enforce the same running `maxBytes` cap while streaming, so the byte
|
|
73
|
+
* guard is never bypassed by buffering the whole body up front
|
|
74
|
+
* (`arrayBuffer()`).
|
|
75
|
+
*
|
|
76
|
+
* An optional `signal` adds a time guard: when it aborts (the caller's
|
|
77
|
+
* fetch deadline), the in-flight read is cancelled — the web reader via
|
|
78
|
+
* `cancel()`, the Node stream via `destroy()` — and the call rejects, so
|
|
79
|
+
* a registry that streams the body slowly or stalls mid-stream cannot
|
|
80
|
+
* outlast the deadline while staying under the byte cap.
|
|
81
|
+
*
|
|
82
|
+
* All rejections surface as `registry.fetch.failed` so the apply layer
|
|
83
|
+
* routes them the same as any other registry-side fetch defect.
|
|
84
|
+
*
|
|
85
|
+
* Exported for direct unit testing.
|
|
86
|
+
*/
|
|
87
|
+
export async function readResponseWithLimit(res, maxBytes, ctx, signal) {
|
|
88
|
+
const declaredLengthRaw = res.headers.get("content-length");
|
|
89
|
+
if (declaredLengthRaw !== null) {
|
|
90
|
+
if (!/^\d+$/.test(declaredLengthRaw)) {
|
|
91
|
+
throw new ToolLoaderError({
|
|
92
|
+
category: "registry.fetch.failed",
|
|
93
|
+
message: `registry "${ctx.registry}" returned non-digit Content-Length ${JSON.stringify(declaredLengthRaw)} for ${ctx.name}@${ctx.version}`,
|
|
94
|
+
package: { name: ctx.name, version: ctx.version },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const declaredLength = Number(declaredLengthRaw);
|
|
98
|
+
if (!Number.isFinite(declaredLength) || declaredLength > maxBytes) {
|
|
99
|
+
throw new ToolLoaderError({
|
|
100
|
+
category: "registry.fetch.failed",
|
|
101
|
+
message: `tarball for ${ctx.name}@${ctx.version} declares Content-Length ${declaredLengthRaw} which exceeds the ${String(maxBytes)}-byte cap`,
|
|
102
|
+
package: { name: ctx.name, version: ctx.version },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const timeoutError = () => new ToolLoaderError({
|
|
107
|
+
category: "registry.fetch.failed",
|
|
108
|
+
message: `tarball read for ${ctx.name}@${ctx.version} exceeded the registry fetch timeout`,
|
|
109
|
+
package: { name: ctx.name, version: ctx.version },
|
|
110
|
+
});
|
|
111
|
+
const capOverflowError = () => new ToolLoaderError({
|
|
112
|
+
category: "registry.fetch.failed",
|
|
113
|
+
message: `tarball for ${ctx.name}@${ctx.version} streamed past the ${String(maxBytes)}-byte cap`,
|
|
114
|
+
package: { name: ctx.name, version: ctx.version },
|
|
115
|
+
});
|
|
116
|
+
// `res.body`'s declared web-`ReadableStream` type is a lie on the
|
|
117
|
+
// production path: `npm-registry-fetch` returns a Minipass (Node)
|
|
118
|
+
// stream that has no `getReader`, only async iteration. Treat the body
|
|
119
|
+
// as an unvalidated boundary value and dispatch on its actual runtime
|
|
120
|
+
// shape rather than trusting the declared type.
|
|
121
|
+
const body = res.body;
|
|
122
|
+
if (body === null || body === undefined) {
|
|
123
|
+
// No body and the upstream returned 2xx: treat as a zero-byte
|
|
124
|
+
// tarball. The cache and tar-extract layers will reject the
|
|
125
|
+
// resulting bytes as non-tar content, but the fetch itself didn't
|
|
126
|
+
// fail — keep this path simple rather than over-rejecting.
|
|
127
|
+
return new Uint8Array(0);
|
|
128
|
+
}
|
|
129
|
+
const chunks = [];
|
|
130
|
+
let total = 0;
|
|
131
|
+
if (hasWebReadableBody(body)) {
|
|
132
|
+
const reader = body.getReader();
|
|
133
|
+
// Cancelling the reader settles any pending read() as done, so the
|
|
134
|
+
// post-read check below surfaces the timeout even when the underlying
|
|
135
|
+
// body stream does not itself observe the abort signal.
|
|
136
|
+
let timedOut = false;
|
|
137
|
+
const onAbort = () => {
|
|
138
|
+
timedOut = true;
|
|
139
|
+
void reader.cancel();
|
|
140
|
+
};
|
|
141
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
142
|
+
if (signal?.aborted === true)
|
|
143
|
+
onAbort();
|
|
144
|
+
try {
|
|
145
|
+
for (;;) {
|
|
146
|
+
const { value, done } = await reader.read();
|
|
147
|
+
if (timedOut)
|
|
148
|
+
throw timeoutError();
|
|
149
|
+
if (done)
|
|
150
|
+
break;
|
|
151
|
+
if (value === undefined)
|
|
152
|
+
continue;
|
|
153
|
+
total += value.byteLength;
|
|
154
|
+
if (total > maxBytes) {
|
|
155
|
+
// Stop reading; we already have enough evidence the upstream
|
|
156
|
+
// is over the cap. The reader.cancel() call requests
|
|
157
|
+
// cancellation upstream; the runtime decides whether to drop
|
|
158
|
+
// the in-flight TCP frames or just unsubscribe our reader.
|
|
159
|
+
await reader.cancel();
|
|
160
|
+
throw capOverflowError();
|
|
161
|
+
}
|
|
162
|
+
chunks.push(value);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
signal?.removeEventListener("abort", onAbort);
|
|
167
|
+
reader.releaseLock();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (isByteStreamAsyncIterable(body)) {
|
|
171
|
+
// A `for await` parked awaiting the next chunk cannot observe the
|
|
172
|
+
// abort flag until a chunk (or the stream's end) arrives, so unlike
|
|
173
|
+
// the web reader's cancel() the flag alone cannot unblock a stalled
|
|
174
|
+
// read. destroy() forces the iteration to settle — it rejects with a
|
|
175
|
+
// stream-teardown error, which the catch below rewrites to the
|
|
176
|
+
// deadline error when the teardown was ours.
|
|
177
|
+
let timedOut = false;
|
|
178
|
+
const onAbort = () => {
|
|
179
|
+
timedOut = true;
|
|
180
|
+
if (typeof body.destroy === "function")
|
|
181
|
+
body.destroy();
|
|
182
|
+
};
|
|
183
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
184
|
+
if (signal?.aborted === true)
|
|
185
|
+
onAbort();
|
|
186
|
+
try {
|
|
187
|
+
for await (const chunk of body) {
|
|
188
|
+
if (timedOut)
|
|
189
|
+
throw timeoutError();
|
|
190
|
+
if (!(chunk instanceof Uint8Array)) {
|
|
191
|
+
throw new ToolLoaderError({
|
|
192
|
+
category: "registry.fetch.failed",
|
|
193
|
+
message: `registry "${ctx.registry}" streamed a non-binary chunk fetching ${ctx.name}@${ctx.version}`,
|
|
194
|
+
package: { name: ctx.name, version: ctx.version },
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
total += chunk.byteLength;
|
|
198
|
+
if (total > maxBytes)
|
|
199
|
+
throw capOverflowError();
|
|
200
|
+
chunks.push(chunk);
|
|
201
|
+
}
|
|
202
|
+
// An abort that lands after the final chunk destroys the stream
|
|
203
|
+
// without rejecting the iteration; surface the timeout here so that
|
|
204
|
+
// race does not slip through as a successful read.
|
|
205
|
+
if (timedOut)
|
|
206
|
+
throw timeoutError();
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
if (timedOut && !(err instanceof ToolLoaderError))
|
|
210
|
+
throw timeoutError();
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
signal?.removeEventListener("abort", onAbort);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
throw new ToolLoaderError({
|
|
219
|
+
category: "registry.fetch.failed",
|
|
220
|
+
message: `registry "${ctx.registry}" returned a response body of an unreadable shape for ${ctx.name}@${ctx.version}`,
|
|
221
|
+
package: { name: ctx.name, version: ctx.version },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const out = new Uint8Array(total);
|
|
225
|
+
let offset = 0;
|
|
226
|
+
for (const chunk of chunks) {
|
|
227
|
+
out.set(chunk, offset);
|
|
228
|
+
offset += chunk.byteLength;
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
package/dist/resolver.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolCredentialDeclaration } from "@intx/types/package-json";
|
|
1
2
|
import { type ToolPackageManifest, type ToolPackagePin, type ToolPackageSource } from "@intx/types/tool-packages";
|
|
2
3
|
/**
|
|
3
4
|
* One HTTP registry. `url` is the actual endpoint; `auth.token` and
|
|
@@ -54,6 +55,7 @@ export interface PackumentVersion {
|
|
|
54
55
|
}>;
|
|
55
56
|
os?: string[];
|
|
56
57
|
cpu?: string[];
|
|
58
|
+
credentials?: ToolCredentialDeclaration[];
|
|
57
59
|
}
|
|
58
60
|
/**
|
|
59
61
|
* The packument document a registry serves for one package. Shape
|
|
@@ -73,8 +75,9 @@ export type PackumentFetcher = (packageName: string, registry: RegistryConfig) =
|
|
|
73
75
|
/**
|
|
74
76
|
* The shape returned by `RegistrySource.materializeRefForEntry`. The
|
|
75
77
|
* walker copies these fields onto the `ToolPackageManifestEntry` it
|
|
76
|
-
* emits, alongside `name`, `version`,
|
|
77
|
-
*
|
|
78
|
+
* emits, alongside `name`, `version`, and the platform filter metadata
|
|
79
|
+
* it derives from the picked packument version. The content identity
|
|
80
|
+
* (`integrity`) rides on `source`, so the impl embeds it there.
|
|
78
81
|
*/
|
|
79
82
|
export interface MaterializedRef {
|
|
80
83
|
readonly source: ToolPackageSource;
|
|
@@ -96,7 +99,7 @@ export interface MaterializedRef {
|
|
|
96
99
|
export interface RegistrySource {
|
|
97
100
|
readonly name: string;
|
|
98
101
|
fetchPackument(name: string): Promise<Packument>;
|
|
99
|
-
materializeRefForEntry(name: string, version: string, picked: PackumentVersion): MaterializedRef;
|
|
102
|
+
materializeRefForEntry(name: string, version: string, picked: PackumentVersion, integrity: string): MaterializedRef;
|
|
100
103
|
}
|
|
101
104
|
/**
|
|
102
105
|
* Configuration for `createClosureResolver`.
|
|
@@ -181,7 +184,7 @@ export declare class HttpRegistrySource implements RegistrySource {
|
|
|
181
184
|
readonly fetchPackument?: PackumentFetcher;
|
|
182
185
|
});
|
|
183
186
|
fetchPackument(name: string): Promise<Packument>;
|
|
184
|
-
materializeRefForEntry(_name: string, _version: string, picked: PackumentVersion): MaterializedRef;
|
|
187
|
+
materializeRefForEntry(_name: string, _version: string, picked: PackumentVersion, integrity: string): MaterializedRef;
|
|
185
188
|
}
|
|
186
189
|
/**
|
|
187
190
|
* Asset-backed registry source. Reads tarballs from a
|
|
@@ -216,7 +219,7 @@ export declare class AssetRegistrySource implements RegistrySource {
|
|
|
216
219
|
* callers must call `fetchPackument` first or accept the structured
|
|
217
220
|
* error this method throws when the index lookup misses.
|
|
218
221
|
*/
|
|
219
|
-
materializeRefForEntry(name: string, version: string, _picked: PackumentVersion): MaterializedRef;
|
|
222
|
+
materializeRefForEntry(name: string, version: string, _picked: PackumentVersion, integrity: string): MaterializedRef;
|
|
220
223
|
}
|
|
221
224
|
export declare function createClosureResolver(config: ClosureResolverConfig): ClosureResolver;
|
|
222
225
|
/**
|
package/dist/resolver.js
CHANGED
|
@@ -98,9 +98,9 @@ export class HttpRegistrySource {
|
|
|
98
98
|
async fetchPackument(name) {
|
|
99
99
|
return this.#fetchPackument(name, this.#config);
|
|
100
100
|
}
|
|
101
|
-
materializeRefForEntry(_name, _version, picked) {
|
|
101
|
+
materializeRefForEntry(_name, _version, picked, integrity) {
|
|
102
102
|
return {
|
|
103
|
-
source: { kind: "registry", registry: this.name },
|
|
103
|
+
source: { kind: "registry", registry: this.name, integrity },
|
|
104
104
|
tarballUrl: picked.dist.tarball,
|
|
105
105
|
};
|
|
106
106
|
}
|
|
@@ -156,7 +156,7 @@ export class AssetRegistrySource {
|
|
|
156
156
|
* callers must call `fetchPackument` first or accept the structured
|
|
157
157
|
* error this method throws when the index lookup misses.
|
|
158
158
|
*/
|
|
159
|
-
materializeRefForEntry(name, version, _picked) {
|
|
159
|
+
materializeRefForEntry(name, version, _picked, integrity) {
|
|
160
160
|
const key = `${name}@${version}`;
|
|
161
161
|
const path = this.#pathByNameVersion.get(key);
|
|
162
162
|
if (path === undefined) {
|
|
@@ -173,7 +173,7 @@ export class AssetRegistrySource {
|
|
|
173
173
|
source: {
|
|
174
174
|
kind: "asset",
|
|
175
175
|
assetId: this.#assetId,
|
|
176
|
-
path,
|
|
176
|
+
package: { format: "tarball", path, integrity },
|
|
177
177
|
},
|
|
178
178
|
};
|
|
179
179
|
}
|
|
@@ -201,6 +201,11 @@ export class AssetRegistrySource {
|
|
|
201
201
|
version: validated.version,
|
|
202
202
|
dist: { tarball: repoPath, integrity },
|
|
203
203
|
...readDependencyFields(extracted.raw),
|
|
204
|
+
// Read from the arktype-validated `parsed` (credentials is in-schema),
|
|
205
|
+
// not `raw`. Absent stays absent -- never defaulted to an empty array.
|
|
206
|
+
...(validated.interchange?.credentials !== undefined
|
|
207
|
+
? { credentials: validated.interchange.credentials }
|
|
208
|
+
: {}),
|
|
204
209
|
};
|
|
205
210
|
if (packument === undefined) {
|
|
206
211
|
byName.set(validated.name, {
|
|
@@ -400,6 +405,10 @@ export function createClosureResolver(config) {
|
|
|
400
405
|
// top-level resolution.
|
|
401
406
|
const topLevelNames = new Set(pins.map((p) => p.name));
|
|
402
407
|
const topLevelResolved = new Map();
|
|
408
|
+
// Credential declarations harvested from each top-level package,
|
|
409
|
+
// captured alongside the resolved version. Only top-level pins
|
|
410
|
+
// contribute; a transitive dependency's declaration is never recorded.
|
|
411
|
+
const topLevelDeclarations = new Map();
|
|
403
412
|
// Optional-subtree bookkeeping. Subtree-tagged entries and peer
|
|
404
413
|
// declarations stay in their per-subtree stash until the whole
|
|
405
414
|
// subtree finishes successfully, at which point they merge into
|
|
@@ -517,6 +526,9 @@ export function createClosureResolver(config) {
|
|
|
517
526
|
}
|
|
518
527
|
if (topLevelNames.has(next.name) && !topLevelResolved.has(next.name)) {
|
|
519
528
|
topLevelResolved.set(next.name, picked.version);
|
|
529
|
+
if (picked.credentials !== undefined) {
|
|
530
|
+
topLevelDeclarations.set(next.name, picked.credentials);
|
|
531
|
+
}
|
|
520
532
|
}
|
|
521
533
|
const key = `${picked.name}@${picked.version}`;
|
|
522
534
|
const alreadyInHardClosure = entries.has(key);
|
|
@@ -543,7 +555,7 @@ export function createClosureResolver(config) {
|
|
|
543
555
|
// rather than aborting the whole closure walk.
|
|
544
556
|
let ref;
|
|
545
557
|
try {
|
|
546
|
-
ref = source.materializeRefForEntry(picked.name, picked.version, picked);
|
|
558
|
+
ref = source.materializeRefForEntry(picked.name, picked.version, picked, picked.dist.integrity);
|
|
547
559
|
}
|
|
548
560
|
catch (err) {
|
|
549
561
|
if (next.subtreeId !== null) {
|
|
@@ -556,7 +568,6 @@ export function createClosureResolver(config) {
|
|
|
556
568
|
const entry = {
|
|
557
569
|
name: picked.name,
|
|
558
570
|
version: picked.version,
|
|
559
|
-
integrity: picked.dist.integrity,
|
|
560
571
|
source: ref.source,
|
|
561
572
|
...(ref.tarballUrl !== undefined
|
|
562
573
|
? { tarballUrl: ref.tarballUrl }
|
|
@@ -638,7 +649,12 @@ export function createClosureResolver(config) {
|
|
|
638
649
|
// in the walker, not in the input.
|
|
639
650
|
throw new Error(`resolver internal error: top-level pin ${p.name}@${p.version} did not resolve to a concrete version`);
|
|
640
651
|
}
|
|
641
|
-
|
|
652
|
+
const credentials = topLevelDeclarations.get(p.name);
|
|
653
|
+
return {
|
|
654
|
+
name: p.name,
|
|
655
|
+
version: resolved,
|
|
656
|
+
...(credentials !== undefined ? { credentials } : {}),
|
|
657
|
+
};
|
|
642
658
|
}),
|
|
643
659
|
entries: Array.from(entries.values()),
|
|
644
660
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MaterializeClosureArgs, MaterializeClosureResult } from "./loader.js";
|
|
2
|
+
/**
|
|
3
|
+
* Lay out a resolved manifest closure into the per-instance store
|
|
4
|
+
* WITHOUT importing any author code. This is phases 1-2 of the loader:
|
|
5
|
+
*
|
|
6
|
+
* 1. Fetch + SRI-verify + extract each platform-matching entry into
|
|
7
|
+
* the content-addressable cache.
|
|
8
|
+
* 2. Resolve the closure's dependency ranges by first arrival, then
|
|
9
|
+
* copy each entry into `<instanceScratchDir>/store/<name>/<version>/`
|
|
10
|
+
* and symlink each direct dep into that entry's `node_modules/` so
|
|
11
|
+
* Node's ancestor walk resolves bare-specifier imports.
|
|
12
|
+
*
|
|
13
|
+
* Returns the `storeDir` the closure was laid out under. The first real
|
|
14
|
+
* `import()` of author code is NOT here — it belongs to `loadManifest`'s
|
|
15
|
+
* phase-3 loop — so a caller (e.g. an install-time probe) can stage the
|
|
16
|
+
* frozen closure into a package directory the loader consumes without
|
|
17
|
+
* executing author code on its host.
|
|
18
|
+
*
|
|
19
|
+
* Only entries whose `os`/`cpu` match `host` are materialized; the rest
|
|
20
|
+
* are skipped with a `platform.mismatch.skipped` debug log. Errors
|
|
21
|
+
* surface as `ToolLoaderError` with a `category` matching the
|
|
22
|
+
* corresponding `DeployApplyErrorCategory`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function materializeClosure(args: MaterializeClosureArgs): Promise<MaterializeClosureResult>;
|
|
25
|
+
export declare function storeEntryDir(storeDir: string, name: string, version: string): string;
|