@telorun/kernel 0.55.0 → 0.57.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.
Files changed (39) hide show
  1. package/dist/bundle/module-manifest.d.ts +2 -3
  2. package/dist/bundle/module-manifest.d.ts.map +1 -1
  3. package/dist/bundle/module-manifest.js +2 -3
  4. package/dist/bundle/module-manifest.js.map +1 -1
  5. package/dist/kernel.d.ts.map +1 -1
  6. package/dist/kernel.js +5 -3
  7. package/dist/kernel.js.map +1 -1
  8. package/dist/manifest-schemas.js +1 -1
  9. package/dist/manifest-sources/local-manifest-cache-source.d.ts.map +1 -1
  10. package/dist/manifest-sources/local-manifest-cache-source.js +10 -6
  11. package/dist/manifest-sources/local-manifest-cache-source.js.map +1 -1
  12. package/dist/transports/oci/oci-ref.d.ts +1 -1
  13. package/dist/transports/oci/oci-ref.d.ts.map +1 -1
  14. package/dist/transports/oci/oci-ref.js +1 -1
  15. package/dist/transports/oci/oci-ref.js.map +1 -1
  16. package/dist/transports/oci/oci-transport.d.ts +8 -4
  17. package/dist/transports/oci/oci-transport.d.ts.map +1 -1
  18. package/dist/transports/oci/oci-transport.js +20 -14
  19. package/dist/transports/oci/oci-transport.js.map +1 -1
  20. package/dist/transports/registry-transport.d.ts +6 -4
  21. package/dist/transports/registry-transport.d.ts.map +1 -1
  22. package/dist/transports/registry-transport.js +84 -26
  23. package/dist/transports/registry-transport.js.map +1 -1
  24. package/dist/transports/transport-registry.d.ts +4 -4
  25. package/dist/transports/transport-registry.d.ts.map +1 -1
  26. package/dist/transports/transport-registry.js +4 -4
  27. package/dist/transports/transport-registry.js.map +1 -1
  28. package/dist/transports/transport.d.ts +27 -19
  29. package/dist/transports/transport.d.ts.map +1 -1
  30. package/package.json +3 -3
  31. package/src/bundle/module-manifest.ts +2 -4
  32. package/src/kernel.ts +5 -3
  33. package/src/manifest-schemas.ts +1 -1
  34. package/src/manifest-sources/local-manifest-cache-source.ts +15 -6
  35. package/src/transports/oci/oci-ref.ts +8 -1
  36. package/src/transports/oci/oci-transport.ts +26 -14
  37. package/src/transports/registry-transport.ts +84 -25
  38. package/src/transports/transport-registry.ts +5 -5
  39. package/src/transports/transport.ts +27 -20
@@ -5,8 +5,11 @@ import {
5
5
  RegistrySource,
6
6
  isRegistryRef,
7
7
  parseModuleRef,
8
+ parseVersionedRef,
8
9
  sha256Base64Url,
10
+ withRefVersion,
9
11
  splitIntegrity,
12
+ type ManifestCacheCoords,
10
13
  type ManifestSource,
11
14
  } from "@telorun/analyzer";
12
15
  import { fetchOrThrow } from "@telorun/sdk";
@@ -21,12 +24,23 @@ import type {
21
24
  PublishBundle,
22
25
  PublishOptions,
23
26
  PublishResult,
24
- SiblingIdentity,
25
27
  Transport,
26
28
  } from "./transport.js";
27
29
 
28
30
  const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
29
- const HTTP_NAMESPACE = "__http";
31
+ /** Throwaway origin that lets a bare registry ref use URL path resolution. */
32
+ const JOIN_ORIGIN = "https://ref.invalid";
33
+
34
+ /** True for an HTTP(S) destination that names a registry root rather than a
35
+ * module within it — no path segments to resolve a sibling beside. */
36
+ function isRegistryBase(base: string): boolean {
37
+ if (!base.startsWith("http://") && !base.startsWith("https://")) return false;
38
+ try {
39
+ return new URL(base).pathname.replace(/\/+/g, "/").replace(/^\/|\/$/g, "") === "";
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
30
44
  const QUERY_HASH_LENGTH = 12;
31
45
 
32
46
  /** Mirror `HttpSource.read`'s `fetchUrl` derivation: when the URL does not
@@ -95,19 +109,28 @@ export class RegistryTransport implements Transport {
95
109
  return base.startsWith("http://") || base.startsWith("https://") || isRegistryRef(ref);
96
110
  }
97
111
 
98
- cacheLocation(ref: string): string[] | null {
112
+ cacheCoords(ref: string): ManifestCacheCoords | null {
99
113
  const url = splitIntegrity(ref).base;
100
114
  const trimmedRegistry = this.registryUrl.replace(/\/+$/, "");
115
+ const registryHost = this.registryHost();
101
116
 
102
- // 1. Registry ref form: namespace/name@version
117
+ // 1. Registry ref form: <path>@<version>. Keyed under the registry's host —
118
+ // a ref says nothing about which registry serves it, so without the host
119
+ // two registries' copies of the same path/version share one cache entry.
103
120
  if (isRegistryRef(url)) {
121
+ if (!registryHost) return null;
104
122
  let parsed: ReturnType<typeof parseModuleRef>;
105
123
  try {
106
124
  parsed = parseModuleRef(url);
107
125
  } catch {
108
126
  return null;
109
127
  }
110
- return [parsed.modulePath, parsed.version, DEFAULT_MANIFEST_FILENAME];
128
+ return {
129
+ transport: "registry",
130
+ host: registryHost,
131
+ path: parsed.modulePath,
132
+ version: parsed.version,
133
+ };
111
134
  }
112
135
 
113
136
  // 2. HTTP(S) URL — a direct registry URL or arbitrary external.
@@ -124,24 +147,49 @@ export class RegistryTransport implements Transport {
124
147
  // registry layout so a ref and a direct URL land on the same file.
125
148
  const normalizedUrl = `${parsed.protocol}//${parsed.host}${pathname}`;
126
149
  if (
150
+ registryHost &&
127
151
  !parsed.search &&
128
152
  !parsed.hash &&
129
- (normalizedUrl === trimmedRegistry || normalizedUrl.startsWith(`${trimmedRegistry}/`))
153
+ normalizedUrl.startsWith(`${trimmedRegistry}/`)
130
154
  ) {
131
- const rel = normalizedUrl.slice(trimmedRegistry.length + 1);
132
- if (!rel) return null;
133
- return rel.split("/");
155
+ // The registry serves `<path…>/<version>/<file>`, where `<file>` is the
156
+ // module manifest or one of its `include:` partials.
157
+ const segments = normalizedUrl.slice(trimmedRegistry.length + 1).split("/");
158
+ const file = segments.pop();
159
+ const version = segments.pop();
160
+ if (file && version && segments.length > 0) {
161
+ return {
162
+ transport: "registry",
163
+ host: registryHost,
164
+ path: segments.join("/"),
165
+ version,
166
+ file,
167
+ };
168
+ }
134
169
  }
135
170
 
136
- // 2b. Arbitrary HTTP(S) → __http subtree, query-hash suffix on collision.
171
+ // 2b. Arbitrary HTTP(S) → `url` subtree, query-hash suffix on collision.
172
+ // No version segment: a URL addresses exactly one file, and the
173
+ // version it declares lives inside bytes the cache maps paths without.
137
174
  const cleanPath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
138
- const disambiguated = disambiguatePath(cleanPath, parsed.search, parsed.hash);
139
- return [HTTP_NAMESPACE, parsed.host, ...disambiguated.split("/")];
175
+ const segments = disambiguatePath(cleanPath, parsed.search, parsed.hash).split("/");
176
+ const file = segments.pop();
177
+ if (!file) return null;
178
+ return { transport: "url", host: parsed.host, path: segments.join("/"), file };
140
179
  }
141
180
 
142
181
  return null;
143
182
  }
144
183
 
184
+ /** Host of the configured registry, or `null` when the URL is unparseable. */
185
+ private registryHost(): string | null {
186
+ try {
187
+ return new URL(this.registryUrl).host || null;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
145
193
  async listVersions(ref: string): Promise<string[] | null> {
146
194
  // Only bare registry refs are version-enumerable — a direct `https://` URL
147
195
  // has no version-list endpoint.
@@ -165,15 +213,11 @@ export class RegistryTransport implements Transport {
165
213
  refVersion(ref: string): string | null {
166
214
  // Only bare `namespace/name@version` refs carry an upgradeable version — a
167
215
  // direct `https://` URL has no version segment to bump.
168
- if (!isRegistryRef(ref)) return null;
169
- const { base } = splitIntegrity(ref);
170
- const at = base.lastIndexOf("@");
171
- return at > 0 ? base.slice(at + 1) : null;
216
+ return isRegistryRef(ref) ? (parseVersionedRef(ref)?.version ?? null) : null;
172
217
  }
173
218
 
174
219
  withVersion(ref: string, version: string): string {
175
- const { modulePath } = parseModuleRef(ref);
176
- return `${modulePath}@${version}`;
220
+ return withRefVersion(ref, version);
177
221
  }
178
222
 
179
223
  /** Mirrors the sources' fetch-URL derivation: a direct URL points at (or
@@ -279,16 +323,31 @@ export class RegistryTransport implements Transport {
279
323
  }
280
324
 
281
325
  canonicalizeSiblingRef(
282
- _destination: string,
283
- _relativeSource: string,
284
- sibling: SiblingIdentity,
326
+ destination: string,
327
+ relativeSource: string,
328
+ version: string,
285
329
  ): string {
286
- // An HTTP registry path defaults to the sibling's own `<namespace>/<name>`.
287
- if (!sibling.namespace || !sibling.name) {
330
+ // The sibling sits beside the destination module: resolve the relative path
331
+ // against the destination's own path, then pin the sibling's version. A bare
332
+ // registry ref (`std/foo`) is a path, not a URL, so it borrows a throwaway
333
+ // origin for the join and drops it again.
334
+ const base = splitIntegrity(destination).base.replace(/@[^/@]*$/, "");
335
+ // A registry *base* (`https://registry.telo.run`) is not a module location,
336
+ // so there is nothing for `../lib` to resolve beside — joining anyway would
337
+ // silently produce a ref one path segment short.
338
+ if (isRegistryBase(base)) {
288
339
  throw new Error(
289
- "a relative import canonicalized to an HTTP registry needs the sibling's metadata.namespace and metadata.name.",
340
+ `cannot canonicalize the relative import '${relativeSource}': publish destination ` +
341
+ `'${destination}' is a registry base, not this module's own location. Pass the ` +
342
+ `module's full destination (e.g. '${base.replace(/\/+$/, "")}/<namespace>/<name>').`,
290
343
  );
291
344
  }
292
- return `${sibling.namespace}/${sibling.name}@${sibling.version}`;
345
+ const isUrl = base.startsWith("http://") || base.startsWith("https://");
346
+ const origin = isUrl ? base : `${JOIN_ORIGIN}/${base.replace(/^\/+/, "")}`;
347
+ const resolved = new URL(relativeSource, `${origin.replace(/\/+$/, "")}/`);
348
+ const joined = isUrl
349
+ ? `${resolved.protocol}//${resolved.host}${resolved.pathname}`
350
+ : resolved.pathname.replace(/^\/+/, "");
351
+ return `${joined.replace(/\/+$/, "")}@${version}`;
293
352
  }
294
353
  }
@@ -1,4 +1,4 @@
1
- import type { ManifestSource } from "@telorun/analyzer";
1
+ import type { ManifestCacheCoords, ManifestSource } from "@telorun/analyzer";
2
2
 
3
3
  import { OciTransport } from "./oci/oci-transport.js";
4
4
  import { RegistryTransport } from "./registry-transport.js";
@@ -29,10 +29,10 @@ export class TransportRegistry {
29
29
  return this.transports.map((t) => t.source);
30
30
  }
31
31
 
32
- /** Cache-path segments for `ref`, from its owning transport; `null` when no
33
- * transport owns it or the ref is not cacheable. */
34
- cacheLocation(ref: string): string[] | null {
35
- return this.forRef(ref)?.cacheLocation(ref) ?? null;
32
+ /** Manifest-cache coordinates for `ref`, from its owning transport; `null`
33
+ * when no transport owns it or the ref is not cacheable. */
34
+ cacheCoords(ref: string): ManifestCacheCoords | null {
35
+ return this.forRef(ref)?.cacheCoords(ref) ?? null;
36
36
  }
37
37
 
38
38
  /** Published versions for `ref` via its owning transport; `null` when the
@@ -1,4 +1,4 @@
1
- import type { ManifestSource } from "@telorun/analyzer";
1
+ import type { ManifestCacheCoords, ManifestSource } from "@telorun/analyzer";
2
2
 
3
3
  import type { PayloadFile } from "../bundle/files-integrity.js";
4
4
 
@@ -28,15 +28,6 @@ export interface PublishResult {
28
28
  url: string;
29
29
  }
30
30
 
31
- /** Identity of a sibling library, read from its own manifest, that a relative
32
- * import canonicalizes to. `version` is always required; `namespace`/`name` are
33
- * used only by transports whose location is metadata-derived (HTTP registry). */
34
- export interface SiblingIdentity {
35
- namespace?: string;
36
- name?: string;
37
- version: string;
38
- }
39
-
40
31
  export interface PublishOptions {
41
32
  /** Bearer token for registries that require auth. */
42
33
  token?: string;
@@ -61,7 +52,7 @@ export interface PublishOptions {
61
52
  * `ManifestSource` is the browser-safe resolution primitive (also implemented
62
53
  * by the cache / local / memory sources, which have no versions and nothing to
63
54
  * publish), so it stays in `analyzer`, while the Node-only management methods
64
- * (`cacheLocation` and, in later phases, `listVersions` / `fetchArtifact` /
55
+ * (`cacheCoords` and, in later phases, `listVersions` / `fetchArtifact` /
65
56
  * `publish`) live on the Transport here in `kernel`. */
66
57
  export interface Transport {
67
58
  /** True when this transport owns the given ref (or publish destination). */
@@ -72,10 +63,21 @@ export interface Transport {
72
63
  * live in `analyzer`; a Node-only transport has no browser-safe source. */
73
64
  readonly source: ManifestSource;
74
65
 
75
- /** Deterministic cache-path segments for a ref, joined under the cache root by
76
- * the cache source. Returns `null` when the ref is not cacheable here
77
- * (unsupported scheme, malformed ref, or path-traversal in the ref). */
78
- cacheLocation(ref: string): string[] | null;
66
+ /** Where a ref's `telo.yaml` is cached, as the transport-neutral
67
+ * `{ transport, host, path, version, file }` coordinates the analyzer's
68
+ * `manifestCacheKey` renders into a path. One grammar serves the local
69
+ * install cache, the hub's static manifest bucket, and the editor's read
70
+ * path, so the three cannot drift on the *shape* of a cache key.
71
+ *
72
+ * They do differ on coordinates, deliberately, for a `url` ref: the hub reads
73
+ * the version out of the fetched manifest and keys by it, while this cache
74
+ * maps a ref to a path before any fetch and so has no version to supply —
75
+ * it names the file instead, since it also stores each `include:` partial.
76
+ * Same grammar, different coordinates; not a drift to reconcile.
77
+ *
78
+ * Returns `null` when the ref is not cacheable here (unsupported scheme,
79
+ * malformed ref, or path-traversal in the ref). */
80
+ cacheCoords(ref: string): ManifestCacheCoords | null;
79
81
 
80
82
  /** The versions published for the module `ref` names, newest-first order not
81
83
  * guaranteed (the caller sorts). Returns `null` when the module is not
@@ -147,13 +149,18 @@ export interface Transport {
147
149
 
148
150
  /** Canonicalize a relative sibling import (`../lib`) declared in a module
149
151
  * being published to `destination` into the absolute ref it will resolve to
150
- * once published. Owns the scheme-specific "where does a sibling land" rule —
151
- * OCI derives the repo from the destination, HTTP from the sibling's
152
- * `<namespace>/<name>` so `telo publish` delegates instead of branching on
153
- * transport shape. */
152
+ * once published.
153
+ *
154
+ * The sibling lands beside the destination: the destination's last segment is
155
+ * the module's own directory, so the relative path resolves against it
156
+ * exactly as it does on the publisher's disk — publishing `…/telorun/foo`
157
+ * with an import of `../bar` yields `…/telorun/bar`. Only the version comes
158
+ * from the sibling's own manifest; nothing is read from its metadata to
159
+ * decide where it lives. Each transport owns the join for its ref grammar, so
160
+ * `telo publish` delegates instead of branching on transport shape. */
154
161
  canonicalizeSiblingRef(
155
162
  destination: string,
156
163
  relativeSource: string,
157
- sibling: SiblingIdentity,
164
+ version: string,
158
165
  ): string;
159
166
  }