@telorun/analyzer 0.68.0 → 0.69.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/dist/analysis-registry.d.ts +12 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +26 -0
- package/dist/call-graph.d.ts +80 -1
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +145 -12
- package/dist/import-resolution-diagnostics.d.ts.map +1 -1
- package/dist/import-resolution-diagnostics.js +22 -8
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/loaded-types.d.ts +4 -4
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-analysis.d.ts +19 -0
- package/dist/manifest-analysis.d.ts.map +1 -1
- package/dist/manifest-analysis.js +27 -0
- package/dist/manifest-loader.d.ts +3 -4
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +4 -5
- package/dist/manifest-schemas.d.ts +2 -0
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +4 -0
- package/dist/module-graph.d.ts +500 -0
- package/dist/module-graph.d.ts.map +1 -0
- package/dist/module-graph.js +1411 -0
- package/dist/reconcile-module-versions.d.ts.map +1 -1
- package/dist/reconcile-module-versions.js +10 -11
- package/dist/release/release-plan.d.ts +1 -1
- package/dist/resolve-zone-containment.d.ts +9 -1
- package/dist/resolve-zone-containment.d.ts.map +1 -1
- package/dist/resolve-zone-containment.js +34 -6
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +4 -2
- package/dist/sources/default-sources.d.ts +6 -6
- package/dist/sources/default-sources.d.ts.map +1 -1
- package/dist/sources/default-sources.js +7 -8
- package/dist/sources/integrity.d.ts +3 -2
- package/dist/sources/integrity.d.ts.map +1 -1
- package/dist/sources/integrity.js +26 -3
- package/dist/sources/versioned-ref.d.ts +17 -12
- package/dist/sources/versioned-ref.d.ts.map +1 -1
- package/dist/sources/versioned-ref.js +22 -24
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/package.json +1 -1
- package/src/analysis-registry.ts +37 -0
- package/src/call-graph.ts +207 -14
- package/src/import-resolution-diagnostics.ts +24 -7
- package/src/index.ts +38 -5
- package/src/loaded-types.ts +4 -4
- package/src/manifest-analysis.ts +39 -0
- package/src/manifest-loader.ts +4 -5
- package/src/manifest-schemas.ts +4 -0
- package/src/module-graph.ts +2005 -0
- package/src/reconcile-module-versions.ts +10 -11
- package/src/release/release-plan.ts +1 -1
- package/src/resolve-zone-containment.ts +49 -9
- package/src/resolve-zone-requirements.ts +7 -2
- package/src/sources/default-sources.ts +7 -8
- package/src/sources/integrity.ts +28 -3
- package/src/sources/versioned-ref.ts +26 -28
- package/src/telo-version.ts +1 -1
- package/dist/sources/module-ref.d.ts +0 -21
- package/dist/sources/module-ref.d.ts.map +0 -1
- package/dist/sources/module-ref.js +0 -36
- package/dist/sources/registry-source.d.ts +0 -14
- package/dist/sources/registry-source.d.ts.map +0 -1
- package/dist/sources/registry-source.js +0 -45
- package/src/sources/module-ref.ts +0 -49
- package/src/sources/registry-source.ts +0 -52
|
@@ -50,15 +50,14 @@ interface ModuleIdentity {
|
|
|
50
50
|
* **What this key cannot relate.** It compares ref *spellings*, so it groups by
|
|
51
51
|
* origin exactly and nothing else. Two consequences, both accepted:
|
|
52
52
|
*
|
|
53
|
-
* - A module imported once by
|
|
54
|
-
* groups, so a version skew between them is not hoisted. Keying on what
|
|
55
|
-
* module declares about itself would catch that case, but only by
|
|
56
|
-
* self-declared identity — which is what this change removes, and
|
|
57
|
-
* cannot tell two same-named modules from different origins apart.
|
|
58
|
-
* -
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* function does not have. */
|
|
53
|
+
* - A module imported once by an `oci://` ref and once by a relative path is
|
|
54
|
+
* two groups, so a version skew between them is not hoisted. Keying on what
|
|
55
|
+
* the module declares about itself would catch that case, but only by
|
|
56
|
+
* trusting a self-declared identity — which is what this change removes, and
|
|
57
|
+
* which cannot tell two same-named modules from different origins apart.
|
|
58
|
+
* - An `oci://` ref and a direct `https://…/telo.yaml` URL serving the same
|
|
59
|
+
* module are two groups. Relating them needs knowledge of the origin's
|
|
60
|
+
* layout, which this pure, browser-safe function does not have. */
|
|
62
61
|
function refIdentity(ref: string): string | null {
|
|
63
62
|
const base = ref.split("#")[0];
|
|
64
63
|
if (!base || base.startsWith(".") || base.startsWith("/") || base.startsWith("file:")) {
|
|
@@ -185,8 +184,8 @@ export function reconcileModuleVersions(
|
|
|
185
184
|
// source normally maps to exactly one identity; the entry module has no
|
|
186
185
|
// inbound edge and needs none (it is never reconciled against itself).
|
|
187
186
|
//
|
|
188
|
-
// One source CAN be reached by two spellings of the same location (a
|
|
189
|
-
//
|
|
187
|
+
// One source CAN be reached by two spellings of the same location (a ref and
|
|
188
|
+
// the direct URL it resolves to). Both name the same module
|
|
190
189
|
// at the same version, so either identity groups it correctly — but the choice
|
|
191
190
|
// must not depend on edge iteration order, or the same graph could reconcile
|
|
192
191
|
// differently across runs. First edge wins.
|
|
@@ -60,7 +60,7 @@ export interface ModuleEvidence {
|
|
|
60
60
|
readonly inlines: ReadonlyMap<ModuleKey, readonly string[]>;
|
|
61
61
|
/**
|
|
62
62
|
* Modules reached by an in-repo **relative** `imports:` source. A pinned
|
|
63
|
-
*
|
|
63
|
+
* remote ref is deliberately not an edge: pinning is the statement "I am not
|
|
64
64
|
* affected until I choose to be", and moving it is `telo upgrade`'s job.
|
|
65
65
|
*/
|
|
66
66
|
readonly imports: readonly ModuleKey[];
|
|
@@ -195,8 +195,16 @@ function collect(
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/** A region as the WALK finds it, before any attribute is asked for: the slot
|
|
199
|
+
* that opens it and everything it reaches. Every attribute query is a filter
|
|
200
|
+
* over these, so a consumer wanting regions regardless of what they declare —
|
|
201
|
+
* an editor drawing the enclosure, where a zone declaring no attribute is
|
|
202
|
+
* still a zone — reads them directly instead of enumerating the vocabulary and
|
|
203
|
+
* silently missing whatever is not in it. */
|
|
204
|
+
export type ZoneProvider = Omit<ZoneRegion, "attribute" | "reason">;
|
|
205
|
+
|
|
198
206
|
/**
|
|
199
|
-
* Every region in the graph
|
|
207
|
+
* Every region in the graph, keyed by the slot that opens it.
|
|
200
208
|
*
|
|
201
209
|
* The provider itself is NOT in `contents` — a zone constrains what runs inside
|
|
202
210
|
* its body, not the resource that establishes it. A transaction's own
|
|
@@ -204,12 +212,11 @@ function collect(
|
|
|
204
212
|
* treated the provider as contained would report the provider against its own
|
|
205
213
|
* constraint.
|
|
206
214
|
*/
|
|
207
|
-
export function
|
|
215
|
+
export function findZoneProviders(
|
|
208
216
|
graph: CallGraph,
|
|
209
217
|
resolveDef: DefinitionLookup,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const regions: ZoneRegion[] = [];
|
|
218
|
+
): ZoneProvider[] {
|
|
219
|
+
const regions: ZoneProvider[] = [];
|
|
213
220
|
|
|
214
221
|
for (const node of graph.nodes.values()) {
|
|
215
222
|
if (node.type !== "resource") continue;
|
|
@@ -221,8 +228,7 @@ export function findZoneRegions(
|
|
|
221
228
|
// properties rather than by knowing any kind's field names.
|
|
222
229
|
for (const [slot, slotSchema] of providingSlots(rootSchema)) {
|
|
223
230
|
const provides = readProvidesZone(slotSchema);
|
|
224
|
-
|
|
225
|
-
if (!provides || typeof reason !== "string") continue;
|
|
231
|
+
if (!provides) continue;
|
|
226
232
|
|
|
227
233
|
const contents = new Map<string, ContainedNode>();
|
|
228
234
|
const boundaries: RegionBoundary[] = [];
|
|
@@ -257,8 +263,6 @@ export function findZoneRegions(
|
|
|
257
263
|
}
|
|
258
264
|
|
|
259
265
|
regions.push({
|
|
260
|
-
attribute,
|
|
261
|
-
reason,
|
|
262
266
|
provider: node,
|
|
263
267
|
slot,
|
|
264
268
|
attributes: provides.attributes as Readonly<Record<string, string>>,
|
|
@@ -271,6 +275,42 @@ export function findZoneRegions(
|
|
|
271
275
|
return regions;
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Every region opened by a slot declaring `attribute`.
|
|
280
|
+
*
|
|
281
|
+
* A filter over {@link findZoneProviders}, so the walk is stated once: what an
|
|
282
|
+
* attribute query adds is which regions are in scope and the author's `reason`,
|
|
283
|
+
* which is the attribute's own value.
|
|
284
|
+
*
|
|
285
|
+
* **The provider list is memoized per graph**, because the filter made the walk
|
|
286
|
+
* unconditional: it used to skip the containment BFS for any provider not
|
|
287
|
+
* declaring the queried attribute, and six queries per analysis (four zone
|
|
288
|
+
* attributes plus two durable ones) became six full walks over every provider
|
|
289
|
+
* in the module. The cache is keyed on the CallGraph object, which is itself
|
|
290
|
+
* built once per analysis — so it lives exactly as long as the answer stays
|
|
291
|
+
* true, and a rebuilt graph gets a fresh one.
|
|
292
|
+
*/
|
|
293
|
+
const providersByGraph = new WeakMap<CallGraph, ZoneProvider[]>();
|
|
294
|
+
|
|
295
|
+
export function findZoneRegions(
|
|
296
|
+
graph: CallGraph,
|
|
297
|
+
resolveDef: DefinitionLookup,
|
|
298
|
+
attribute: string,
|
|
299
|
+
): ZoneRegion[] {
|
|
300
|
+
let providers = providersByGraph.get(graph);
|
|
301
|
+
if (!providers) {
|
|
302
|
+
providers = findZoneProviders(graph, resolveDef);
|
|
303
|
+
providersByGraph.set(graph, providers);
|
|
304
|
+
}
|
|
305
|
+
const out: ZoneRegion[] = [];
|
|
306
|
+
for (const region of providers) {
|
|
307
|
+
const reason = region.attributes[attribute];
|
|
308
|
+
if (typeof reason !== "string") continue;
|
|
309
|
+
out.push({ ...region, attribute, reason });
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
|
|
274
314
|
/** Field-map paths of every slot in a kind's schema carrying a provides-zone
|
|
275
315
|
* annotation, with the schema node at each. Walks properties, array items and
|
|
276
316
|
* `additionalProperties`, resolving local `$ref`s — the paths the call graph's
|
|
@@ -308,7 +308,12 @@ export function projectZoneRequirements(args: ProjectionArgs): ProjectionResult
|
|
|
308
308
|
const local = scopeLocal.get(`${from.scopeOwner}\0${from.scopeSite}`)?.get(name);
|
|
309
309
|
if (local) return local;
|
|
310
310
|
}
|
|
311
|
-
|
|
311
|
+
// In the scope that WROTE the name: a bare reference means the reader's own
|
|
312
|
+
// module, and two libraries may each declare one.
|
|
313
|
+
return graph.resourceByName(
|
|
314
|
+
name,
|
|
315
|
+
(from.manifest.metadata as { module?: string } | undefined)?.module,
|
|
316
|
+
);
|
|
312
317
|
};
|
|
313
318
|
|
|
314
319
|
const acceptedFor = (zone: string): ReadonlySet<string> => {
|
|
@@ -809,7 +814,7 @@ export function runZoneAnalysis(args: ZoneAnalysisArgs): AnalysisDiagnostic[] {
|
|
|
809
814
|
for (const lib of rootLibraries) {
|
|
810
815
|
for (const [exportName, specs] of openExports) {
|
|
811
816
|
if (!lib.exportedNames.has(exportName)) continue;
|
|
812
|
-
const node = graph.resourceByName(exportName);
|
|
817
|
+
const node = graph.resourceByName(exportName, lib.module);
|
|
813
818
|
if (!node || (node.manifest.metadata as { module?: string } | undefined)?.module !== lib.module) {
|
|
814
819
|
continue;
|
|
815
820
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { ManifestSource } from "../types.js";
|
|
2
2
|
import { HttpSource } from "./http-source.js";
|
|
3
|
-
import { RegistrySource } from "./registry-source.js";
|
|
4
3
|
|
|
5
|
-
/** The browser-safe built-in sources
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
export function defaultSources(
|
|
11
|
-
return [new HttpSource()
|
|
4
|
+
/** The browser-safe built-in sources. Node-specific sources (local filesystem)
|
|
5
|
+
* are supplied by the consuming package and passed alongside these into the
|
|
6
|
+
* `Loader` constructor. Callers that only want a subset (e.g. the editor, which
|
|
7
|
+
* brings its own manifest-cache adapter) construct the individual sources
|
|
8
|
+
* directly. */
|
|
9
|
+
export function defaultSources(): ManifestSource[] {
|
|
10
|
+
return [new HttpSource()];
|
|
12
11
|
}
|
package/src/sources/integrity.ts
CHANGED
|
@@ -110,11 +110,26 @@ export async function verifyIntegrity(
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/** Object-storage backends (Cloudflare R2, S3) surface auth and permission
|
|
114
|
+
* failures as a **200 with an XML error body**. Caught here rather than left to
|
|
115
|
+
* the loader, which would parse the XML as YAML and report a downstream
|
|
116
|
+
* `UNDEFINED_KIND` — naming the manifest's contents as the problem instead of
|
|
117
|
+
* the origin that refused to serve it. Returns the failure detail, or `null`
|
|
118
|
+
* when the body is not an XML error document. */
|
|
119
|
+
function objectStorageError(text: string): string | null {
|
|
120
|
+
const head = text.trimStart();
|
|
121
|
+
if (!head.startsWith("<?xml") && !head.startsWith("<Error")) return null;
|
|
122
|
+
const code = text.match(/<Code>([^<]+)<\/Code>/);
|
|
123
|
+
const message = text.match(/<Message>([^<]+)<\/Message>/);
|
|
124
|
+
return code && message ? `${code[1]}: ${message[1]}` : text.slice(0, 200);
|
|
125
|
+
}
|
|
126
|
+
|
|
113
127
|
/** The single verified network read for remote manifests: fetch `fetchUrl`,
|
|
114
128
|
* verify the raw bytes against `integrity` (when pinned), and return both the
|
|
115
129
|
* bytes and the decoded text. The one choke point every network `ManifestSource`
|
|
116
|
-
* routes through, so verification cannot drift between them
|
|
117
|
-
* the
|
|
130
|
+
* routes through, so verification cannot drift between them — which is also why
|
|
131
|
+
* the object-storage non-manifest check lives here rather than in each source.
|
|
132
|
+
* `describe` names the artifact in error messages. */
|
|
118
133
|
export async function verifiedFetch(
|
|
119
134
|
fetchUrl: string,
|
|
120
135
|
integrity: string | undefined,
|
|
@@ -127,6 +142,16 @@ export async function verifiedFetch(
|
|
|
127
142
|
);
|
|
128
143
|
}
|
|
129
144
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
145
|
+
const text = new TextDecoder().decode(bytes);
|
|
146
|
+
// Before the integrity check: a bucket's error page hashes to something, and
|
|
147
|
+
// reporting a pin mismatch would name the wrong cause just as the loader does.
|
|
148
|
+
const storageError = objectStorageError(text);
|
|
149
|
+
if (storageError) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`The origin returned a non-manifest response for ${describe} ` +
|
|
152
|
+
`(URL: ${fetchUrl}): ${storageError}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
130
155
|
if (integrity) await verifyIntegrity(bytes, integrity, describe);
|
|
131
|
-
return { bytes, text
|
|
156
|
+
return { bytes, text };
|
|
132
157
|
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { splitIntegrity } from "./integrity.js";
|
|
2
|
-
import { isRegistryRef } from "./module-ref.js";
|
|
3
2
|
import { OCI_SCHEME } from "./oci-ref.js";
|
|
4
3
|
|
|
5
4
|
/** A module ref split into the parts an upgrade needs: the version-independent
|
|
6
5
|
* ref, the version segment it currently names, and any inline pin. */
|
|
7
6
|
export interface ParsedVersionedRef {
|
|
8
7
|
/** The ref with its `@version` segment and integrity fragment removed —
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
8
|
+
* `oci://ghcr.io/telorun/timer`. This is the identity a version list is keyed
|
|
9
|
+
* by (the hub registers modules under exactly this form). */
|
|
11
10
|
baseRef: string;
|
|
12
|
-
/** The version segment, raw —
|
|
13
|
-
*
|
|
11
|
+
/** The version segment, raw — an OCI tag or an OCI digest reference
|
|
12
|
+
* (`sha256:…`). The caller applies its own SemVer check. */
|
|
14
13
|
version: string;
|
|
15
14
|
/** Telo's inline `sha256-<base64url>` pin, when the ref carried one. */
|
|
16
15
|
integrity?: string;
|
|
@@ -20,12 +19,17 @@ export interface ParsedVersionedRef {
|
|
|
20
19
|
* upgradeable version — a local path, a bare `https://` URL, or an OCI ref
|
|
21
20
|
* with no explicit reference (an implicit `latest` is not a pin).
|
|
22
21
|
*
|
|
23
|
-
* Browser-safe
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
22
|
+
* Browser-safe: this is the *grammar* half of an upgrade, shared by the kernel
|
|
23
|
+
* transports (whose `refVersion` / `withVersion` delegate here) and the editor,
|
|
24
|
+
* which cannot use a transport at all — the *network* half (enumerating
|
|
25
|
+
* versions) is scheme-specific and stays behind `Transport.listVersions` on
|
|
26
|
+
* Node and the hub's `/module/versions` in the browser.
|
|
27
|
+
*
|
|
28
|
+
* It is NOT transport-neutral: `oci://` is the only versionable grammar, named
|
|
29
|
+
* literally below. That was equally true when the registry ref was the second
|
|
30
|
+
* branch, and a third scheme still means editing this file — the versionable
|
|
31
|
+
* set would have to become data the transports contribute before that claim
|
|
32
|
+
* could be made honestly. */
|
|
29
33
|
export function parseVersionedRef(ref: string): ParsedVersionedRef | null {
|
|
30
34
|
const { base, integrity } = splitIntegrity(ref);
|
|
31
35
|
const at = versionSeparator(base);
|
|
@@ -41,37 +45,31 @@ export function parseVersionedRef(ref: string): ParsedVersionedRef | null {
|
|
|
41
45
|
*
|
|
42
46
|
* Throws when the ref's grammar has no version segment at all — a relative
|
|
43
47
|
* path, a bare `https://` URL. Producing `../lib@0.4.0` for those would write
|
|
44
|
-
* a ref nothing can resolve, so this fails where the transport-specific
|
|
45
|
-
*
|
|
48
|
+
* a ref nothing can resolve, so this fails where the transport-specific parser
|
|
49
|
+
* it replaced (`parseOciRef`) also failed. */
|
|
46
50
|
export function withRefVersion(ref: string, version: string): string {
|
|
47
51
|
const { base } = splitIntegrity(ref);
|
|
48
|
-
if (
|
|
52
|
+
if (!isVersionableRef(base)) {
|
|
49
53
|
throw new Error(
|
|
50
|
-
`Cannot set a version on '${ref}' — only
|
|
51
|
-
`and oci:// refs carry a version segment.`,
|
|
54
|
+
`Cannot set a version on '${ref}' — only oci:// refs carry a version segment.`,
|
|
52
55
|
);
|
|
53
56
|
}
|
|
54
57
|
const at = versionSeparator(base);
|
|
55
58
|
return `${at === null ? base : base.slice(0, at)}@${version}`;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
/**
|
|
59
|
-
*
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return base.indexOf("/", OCI_SCHEME.length) > OCI_SCHEME.length ? "oci" : null;
|
|
64
|
-
}
|
|
65
|
-
// `isRegistryRef` requires the `@`, so a version-less `std/console` is not a
|
|
66
|
-
// registry ref by this test — matching `parseModuleRef`, which throws on it.
|
|
67
|
-
return isRegistryRef(base) ? "registry" : null;
|
|
61
|
+
/** True when `base` is written in a grammar that carries a version segment — a
|
|
62
|
+
* relative/absolute path and a `file:`/`https://` URL are not. */
|
|
63
|
+
function isVersionableRef(base: string): boolean {
|
|
64
|
+
// A host alone is not addressable; the repo path is what carries a version.
|
|
65
|
+
return base.startsWith(OCI_SCHEME) && base.indexOf("/", OCI_SCHEME.length) > OCI_SCHEME.length;
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
/** Index of the `@` that separates the version, or `null` when the ref names
|
|
71
69
|
* none. Split on the LAST `@` so a digest reference (`repo@sha256:…`) keeps
|
|
72
70
|
* everything before it as the ref. */
|
|
73
71
|
function versionSeparator(base: string): number | null {
|
|
74
|
-
if (
|
|
72
|
+
if (!isVersionableRef(base)) return null;
|
|
75
73
|
const at = base.lastIndexOf("@");
|
|
76
|
-
return at >
|
|
74
|
+
return at > OCI_SCHEME.length ? at : null;
|
|
77
75
|
}
|
package/src/telo-version.ts
CHANGED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/** A parsed registry module reference. `modulePath` is `namespace/name`,
|
|
2
|
-
* `version` has any leading `v` stripped, and `integrity` carries the inline
|
|
3
|
-
* `sha256-<base64url>` hash when the ref was pinned. */
|
|
4
|
-
export interface ParsedModuleRef {
|
|
5
|
-
modulePath: string;
|
|
6
|
-
version: string;
|
|
7
|
-
integrity?: string;
|
|
8
|
-
}
|
|
9
|
-
/** True when `url` has the bare registry-ref shape `namespace/name@version`
|
|
10
|
-
* (no scheme of any kind, no leading `/` or `.`, contains both `@` and `/`).
|
|
11
|
-
* The integrity fragment, if any, does not affect the classification.
|
|
12
|
-
*
|
|
13
|
-
* A registry ref never carries a `scheme://` — that guard is what keeps an
|
|
14
|
-
* `oci://…@ver` (or future `s3://…`) ref from being misrouted here, so a
|
|
15
|
-
* scheme-owning transport claims it instead. */
|
|
16
|
-
export declare function isRegistryRef(url: string): boolean;
|
|
17
|
-
/** Canonical parser for `namespace/name@version[#sha256-...]` refs. The single
|
|
18
|
-
* source of truth shared by the registry source, the kernel manifest cache,
|
|
19
|
-
* and the CLI (install / upgrade / bundle). Throws on a malformed ref. */
|
|
20
|
-
export declare function parseModuleRef(ref: string): ParsedModuleRef;
|
|
21
|
-
//# sourceMappingURL=module-ref.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"module-ref.d.ts","sourceRoot":"","sources":["../../src/sources/module-ref.ts"],"names":[],"mappings":"AAEA;;yDAEyD;AACzD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;iDAMiD;AACjD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASlD;AAED;;2EAE2E;AAC3E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAgB3D"}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { splitIntegrity } from "./integrity.js";
|
|
2
|
-
/** True when `url` has the bare registry-ref shape `namespace/name@version`
|
|
3
|
-
* (no scheme of any kind, no leading `/` or `.`, contains both `@` and `/`).
|
|
4
|
-
* The integrity fragment, if any, does not affect the classification.
|
|
5
|
-
*
|
|
6
|
-
* A registry ref never carries a `scheme://` — that guard is what keeps an
|
|
7
|
-
* `oci://…@ver` (or future `s3://…`) ref from being misrouted here, so a
|
|
8
|
-
* scheme-owning transport claims it instead. */
|
|
9
|
-
export function isRegistryRef(url) {
|
|
10
|
-
const { base } = splitIntegrity(url);
|
|
11
|
-
return (!base.includes("://") &&
|
|
12
|
-
!base.startsWith("/") &&
|
|
13
|
-
!base.startsWith(".") &&
|
|
14
|
-
base.includes("@") &&
|
|
15
|
-
base.includes("/"));
|
|
16
|
-
}
|
|
17
|
-
/** Canonical parser for `namespace/name@version[#sha256-...]` refs. The single
|
|
18
|
-
* source of truth shared by the registry source, the kernel manifest cache,
|
|
19
|
-
* and the CLI (install / upgrade / bundle). Throws on a malformed ref. */
|
|
20
|
-
export function parseModuleRef(ref) {
|
|
21
|
-
const { base, integrity } = splitIntegrity(ref);
|
|
22
|
-
const atIdx = base.lastIndexOf("@");
|
|
23
|
-
if (atIdx <= 0 || atIdx === base.length - 1) {
|
|
24
|
-
throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
|
|
25
|
-
}
|
|
26
|
-
const modulePath = base.slice(0, atIdx);
|
|
27
|
-
if (!modulePath.includes("/")) {
|
|
28
|
-
throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
|
|
29
|
-
}
|
|
30
|
-
const rawVersion = base.slice(atIdx + 1);
|
|
31
|
-
const version = rawVersion.startsWith("v") ? rawVersion.slice(1) : rawVersion;
|
|
32
|
-
if (!version) {
|
|
33
|
-
throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
|
|
34
|
-
}
|
|
35
|
-
return { modulePath, version, integrity };
|
|
36
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type ManifestSource } from "../types.js";
|
|
2
|
-
export declare class RegistrySource implements ManifestSource {
|
|
3
|
-
private registryUrl;
|
|
4
|
-
constructor(registryUrl?: string);
|
|
5
|
-
supports(url: string): boolean;
|
|
6
|
-
read(moduleRef: string): Promise<{
|
|
7
|
-
text: string;
|
|
8
|
-
source: string;
|
|
9
|
-
}>;
|
|
10
|
-
resolveRelative(base: string, relative: string): string;
|
|
11
|
-
private toRegistryModuleBase;
|
|
12
|
-
private toRegistryUrl;
|
|
13
|
-
}
|
|
14
|
-
//# sourceMappingURL=registry-source.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"registry-source.d.ts","sourceRoot":"","sources":["../../src/sources/registry-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7E,qBAAa,cAAe,YAAW,cAAc;IACvC,OAAO,CAAC,WAAW;gBAAX,WAAW,SAAuB;IAEtD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIxB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAuBxE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAMvD,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,aAAa;CAGtB"}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_MANIFEST_FILENAME } from "../types.js";
|
|
2
|
-
import { splitIntegrity, verifiedFetch } from "./integrity.js";
|
|
3
|
-
import { isRegistryRef, parseModuleRef } from "./module-ref.js";
|
|
4
|
-
const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
|
|
5
|
-
export class RegistrySource {
|
|
6
|
-
registryUrl;
|
|
7
|
-
constructor(registryUrl = DEFAULT_REGISTRY_URL) {
|
|
8
|
-
this.registryUrl = registryUrl;
|
|
9
|
-
}
|
|
10
|
-
supports(url) {
|
|
11
|
-
return isRegistryRef(url);
|
|
12
|
-
}
|
|
13
|
-
async read(moduleRef) {
|
|
14
|
-
const { base, integrity } = splitIntegrity(moduleRef);
|
|
15
|
-
const fetchUrl = this.toRegistryUrl(base);
|
|
16
|
-
const { text } = await verifiedFetch(fetchUrl, integrity, base);
|
|
17
|
-
// Some object-storage backends (e.g. Cloudflare R2 / S3) surface auth or
|
|
18
|
-
// permission failures by returning a 200 status with an XML error body.
|
|
19
|
-
// Catch this here so the loader produces a precise error rather than
|
|
20
|
-
// silently parsing the XML as YAML and reporting a downstream UNDEFINED_KIND.
|
|
21
|
-
if (text.trimStart().startsWith("<?xml") || text.trimStart().startsWith("<Error")) {
|
|
22
|
-
const codeMatch = text.match(/<Code>([^<]+)<\/Code>/);
|
|
23
|
-
const messageMatch = text.match(/<Message>([^<]+)<\/Message>/);
|
|
24
|
-
const detail = codeMatch && messageMatch
|
|
25
|
-
? `${codeMatch[1]}: ${messageMatch[1]}`
|
|
26
|
-
: text.slice(0, 200);
|
|
27
|
-
throw new Error(`Registry returned a non-manifest response for ${base} ` +
|
|
28
|
-
`(URL: ${fetchUrl}): ${detail}`);
|
|
29
|
-
}
|
|
30
|
-
return { text, source: fetchUrl };
|
|
31
|
-
}
|
|
32
|
-
resolveRelative(base, relative) {
|
|
33
|
-
const baseUrl = this.supports(base) ? this.toRegistryModuleBase(base) : base;
|
|
34
|
-
const baseWithSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
35
|
-
return new URL(relative, baseWithSlash).href;
|
|
36
|
-
}
|
|
37
|
-
toRegistryModuleBase(moduleRef) {
|
|
38
|
-
const parsed = parseModuleRef(moduleRef);
|
|
39
|
-
const normalizedBase = this.registryUrl.replace(/\/+$/, "");
|
|
40
|
-
return `${normalizedBase}/${parsed.modulePath}/${parsed.version}`;
|
|
41
|
-
}
|
|
42
|
-
toRegistryUrl(moduleRef) {
|
|
43
|
-
return `${this.toRegistryModuleBase(moduleRef)}/${DEFAULT_MANIFEST_FILENAME}`;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { splitIntegrity } from "./integrity.js";
|
|
2
|
-
|
|
3
|
-
/** A parsed registry module reference. `modulePath` is `namespace/name`,
|
|
4
|
-
* `version` has any leading `v` stripped, and `integrity` carries the inline
|
|
5
|
-
* `sha256-<base64url>` hash when the ref was pinned. */
|
|
6
|
-
export interface ParsedModuleRef {
|
|
7
|
-
modulePath: string;
|
|
8
|
-
version: string;
|
|
9
|
-
integrity?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** True when `url` has the bare registry-ref shape `namespace/name@version`
|
|
13
|
-
* (no scheme of any kind, no leading `/` or `.`, contains both `@` and `/`).
|
|
14
|
-
* The integrity fragment, if any, does not affect the classification.
|
|
15
|
-
*
|
|
16
|
-
* A registry ref never carries a `scheme://` — that guard is what keeps an
|
|
17
|
-
* `oci://…@ver` (or future `s3://…`) ref from being misrouted here, so a
|
|
18
|
-
* scheme-owning transport claims it instead. */
|
|
19
|
-
export function isRegistryRef(url: string): boolean {
|
|
20
|
-
const { base } = splitIntegrity(url);
|
|
21
|
-
return (
|
|
22
|
-
!base.includes("://") &&
|
|
23
|
-
!base.startsWith("/") &&
|
|
24
|
-
!base.startsWith(".") &&
|
|
25
|
-
base.includes("@") &&
|
|
26
|
-
base.includes("/")
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Canonical parser for `namespace/name@version[#sha256-...]` refs. The single
|
|
31
|
-
* source of truth shared by the registry source, the kernel manifest cache,
|
|
32
|
-
* and the CLI (install / upgrade / bundle). Throws on a malformed ref. */
|
|
33
|
-
export function parseModuleRef(ref: string): ParsedModuleRef {
|
|
34
|
-
const { base, integrity } = splitIntegrity(ref);
|
|
35
|
-
const atIdx = base.lastIndexOf("@");
|
|
36
|
-
if (atIdx <= 0 || atIdx === base.length - 1) {
|
|
37
|
-
throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
|
|
38
|
-
}
|
|
39
|
-
const modulePath = base.slice(0, atIdx);
|
|
40
|
-
if (!modulePath.includes("/")) {
|
|
41
|
-
throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
|
|
42
|
-
}
|
|
43
|
-
const rawVersion = base.slice(atIdx + 1);
|
|
44
|
-
const version = rawVersion.startsWith("v") ? rawVersion.slice(1) : rawVersion;
|
|
45
|
-
if (!version) {
|
|
46
|
-
throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
|
|
47
|
-
}
|
|
48
|
-
return { modulePath, version, integrity };
|
|
49
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_MANIFEST_FILENAME, type ManifestSource } from "../types.js";
|
|
2
|
-
import { splitIntegrity, verifiedFetch } from "./integrity.js";
|
|
3
|
-
import { isRegistryRef, parseModuleRef } from "./module-ref.js";
|
|
4
|
-
|
|
5
|
-
const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
|
|
6
|
-
|
|
7
|
-
export class RegistrySource implements ManifestSource {
|
|
8
|
-
constructor(private registryUrl = DEFAULT_REGISTRY_URL) {}
|
|
9
|
-
|
|
10
|
-
supports(url: string): boolean {
|
|
11
|
-
return isRegistryRef(url);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async read(moduleRef: string): Promise<{ text: string; source: string }> {
|
|
15
|
-
const { base, integrity } = splitIntegrity(moduleRef);
|
|
16
|
-
const fetchUrl = this.toRegistryUrl(base);
|
|
17
|
-
const { text } = await verifiedFetch(fetchUrl, integrity, base);
|
|
18
|
-
// Some object-storage backends (e.g. Cloudflare R2 / S3) surface auth or
|
|
19
|
-
// permission failures by returning a 200 status with an XML error body.
|
|
20
|
-
// Catch this here so the loader produces a precise error rather than
|
|
21
|
-
// silently parsing the XML as YAML and reporting a downstream UNDEFINED_KIND.
|
|
22
|
-
if (text.trimStart().startsWith("<?xml") || text.trimStart().startsWith("<Error")) {
|
|
23
|
-
const codeMatch = text.match(/<Code>([^<]+)<\/Code>/);
|
|
24
|
-
const messageMatch = text.match(/<Message>([^<]+)<\/Message>/);
|
|
25
|
-
const detail =
|
|
26
|
-
codeMatch && messageMatch
|
|
27
|
-
? `${codeMatch[1]}: ${messageMatch[1]}`
|
|
28
|
-
: text.slice(0, 200);
|
|
29
|
-
throw new Error(
|
|
30
|
-
`Registry returned a non-manifest response for ${base} ` +
|
|
31
|
-
`(URL: ${fetchUrl}): ${detail}`,
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
return { text, source: fetchUrl };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
resolveRelative(base: string, relative: string): string {
|
|
38
|
-
const baseUrl = this.supports(base) ? this.toRegistryModuleBase(base) : base;
|
|
39
|
-
const baseWithSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
40
|
-
return new URL(relative, baseWithSlash).href;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private toRegistryModuleBase(moduleRef: string): string {
|
|
44
|
-
const parsed = parseModuleRef(moduleRef);
|
|
45
|
-
const normalizedBase = this.registryUrl.replace(/\/+$/, "");
|
|
46
|
-
return `${normalizedBase}/${parsed.modulePath}/${parsed.version}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private toRegistryUrl(moduleRef: string): string {
|
|
50
|
-
return `${this.toRegistryModuleBase(moduleRef)}/${DEFAULT_MANIFEST_FILENAME}`;
|
|
51
|
-
}
|
|
52
|
-
}
|