@telorun/kernel 0.54.0 → 0.56.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 (37) 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.d.ts +12 -0
  9. package/dist/manifest-schemas.d.ts.map +1 -1
  10. package/dist/manifest-schemas.js +6 -1
  11. package/dist/manifest-schemas.js.map +1 -1
  12. package/dist/manifest-sources/local-manifest-cache-source.d.ts.map +1 -1
  13. package/dist/manifest-sources/local-manifest-cache-source.js +10 -6
  14. package/dist/manifest-sources/local-manifest-cache-source.js.map +1 -1
  15. package/dist/transports/oci/oci-transport.d.ts +8 -4
  16. package/dist/transports/oci/oci-transport.d.ts.map +1 -1
  17. package/dist/transports/oci/oci-transport.js +13 -4
  18. package/dist/transports/oci/oci-transport.js.map +1 -1
  19. package/dist/transports/registry-transport.d.ts +6 -4
  20. package/dist/transports/registry-transport.d.ts.map +1 -1
  21. package/dist/transports/registry-transport.js +81 -18
  22. package/dist/transports/registry-transport.js.map +1 -1
  23. package/dist/transports/transport-registry.d.ts +4 -4
  24. package/dist/transports/transport-registry.d.ts.map +1 -1
  25. package/dist/transports/transport-registry.js +4 -4
  26. package/dist/transports/transport-registry.js.map +1 -1
  27. package/dist/transports/transport.d.ts +27 -19
  28. package/dist/transports/transport.d.ts.map +1 -1
  29. package/package.json +3 -3
  30. package/src/bundle/module-manifest.ts +2 -4
  31. package/src/kernel.ts +5 -3
  32. package/src/manifest-schemas.ts +6 -1
  33. package/src/manifest-sources/local-manifest-cache-source.ts +15 -6
  34. package/src/transports/oci/oci-transport.ts +14 -5
  35. package/src/transports/registry-transport.ts +80 -19
  36. package/src/transports/transport-registry.ts +5 -5
  37. package/src/transports/transport.ts +27 -20
@@ -7,6 +7,7 @@ import {
7
7
  parseModuleRef,
8
8
  sha256Base64Url,
9
9
  splitIntegrity,
10
+ type ManifestCacheCoords,
10
11
  type ManifestSource,
11
12
  } from "@telorun/analyzer";
12
13
  import { fetchOrThrow } from "@telorun/sdk";
@@ -21,12 +22,23 @@ import type {
21
22
  PublishBundle,
22
23
  PublishOptions,
23
24
  PublishResult,
24
- SiblingIdentity,
25
25
  Transport,
26
26
  } from "./transport.js";
27
27
 
28
28
  const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
29
- const HTTP_NAMESPACE = "__http";
29
+ /** Throwaway origin that lets a bare registry ref use URL path resolution. */
30
+ const JOIN_ORIGIN = "https://ref.invalid";
31
+
32
+ /** True for an HTTP(S) destination that names a registry root rather than a
33
+ * module within it — no path segments to resolve a sibling beside. */
34
+ function isRegistryBase(base: string): boolean {
35
+ if (!base.startsWith("http://") && !base.startsWith("https://")) return false;
36
+ try {
37
+ return new URL(base).pathname.replace(/\/+/g, "/").replace(/^\/|\/$/g, "") === "";
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
30
42
  const QUERY_HASH_LENGTH = 12;
31
43
 
32
44
  /** Mirror `HttpSource.read`'s `fetchUrl` derivation: when the URL does not
@@ -95,19 +107,28 @@ export class RegistryTransport implements Transport {
95
107
  return base.startsWith("http://") || base.startsWith("https://") || isRegistryRef(ref);
96
108
  }
97
109
 
98
- cacheLocation(ref: string): string[] | null {
110
+ cacheCoords(ref: string): ManifestCacheCoords | null {
99
111
  const url = splitIntegrity(ref).base;
100
112
  const trimmedRegistry = this.registryUrl.replace(/\/+$/, "");
113
+ const registryHost = this.registryHost();
101
114
 
102
- // 1. Registry ref form: namespace/name@version
115
+ // 1. Registry ref form: <path>@<version>. Keyed under the registry's host —
116
+ // a ref says nothing about which registry serves it, so without the host
117
+ // two registries' copies of the same path/version share one cache entry.
103
118
  if (isRegistryRef(url)) {
119
+ if (!registryHost) return null;
104
120
  let parsed: ReturnType<typeof parseModuleRef>;
105
121
  try {
106
122
  parsed = parseModuleRef(url);
107
123
  } catch {
108
124
  return null;
109
125
  }
110
- return [parsed.modulePath, parsed.version, DEFAULT_MANIFEST_FILENAME];
126
+ return {
127
+ transport: "registry",
128
+ host: registryHost,
129
+ path: parsed.modulePath,
130
+ version: parsed.version,
131
+ };
111
132
  }
112
133
 
113
134
  // 2. HTTP(S) URL — a direct registry URL or arbitrary external.
@@ -124,24 +145,49 @@ export class RegistryTransport implements Transport {
124
145
  // registry layout so a ref and a direct URL land on the same file.
125
146
  const normalizedUrl = `${parsed.protocol}//${parsed.host}${pathname}`;
126
147
  if (
148
+ registryHost &&
127
149
  !parsed.search &&
128
150
  !parsed.hash &&
129
- (normalizedUrl === trimmedRegistry || normalizedUrl.startsWith(`${trimmedRegistry}/`))
151
+ normalizedUrl.startsWith(`${trimmedRegistry}/`)
130
152
  ) {
131
- const rel = normalizedUrl.slice(trimmedRegistry.length + 1);
132
- if (!rel) return null;
133
- return rel.split("/");
153
+ // The registry serves `<path…>/<version>/<file>`, where `<file>` is the
154
+ // module manifest or one of its `include:` partials.
155
+ const segments = normalizedUrl.slice(trimmedRegistry.length + 1).split("/");
156
+ const file = segments.pop();
157
+ const version = segments.pop();
158
+ if (file && version && segments.length > 0) {
159
+ return {
160
+ transport: "registry",
161
+ host: registryHost,
162
+ path: segments.join("/"),
163
+ version,
164
+ file,
165
+ };
166
+ }
134
167
  }
135
168
 
136
- // 2b. Arbitrary HTTP(S) → __http subtree, query-hash suffix on collision.
169
+ // 2b. Arbitrary HTTP(S) → `url` subtree, query-hash suffix on collision.
170
+ // No version segment: a URL addresses exactly one file, and the
171
+ // version it declares lives inside bytes the cache maps paths without.
137
172
  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("/")];
173
+ const segments = disambiguatePath(cleanPath, parsed.search, parsed.hash).split("/");
174
+ const file = segments.pop();
175
+ if (!file) return null;
176
+ return { transport: "url", host: parsed.host, path: segments.join("/"), file };
140
177
  }
141
178
 
142
179
  return null;
143
180
  }
144
181
 
182
+ /** Host of the configured registry, or `null` when the URL is unparseable. */
183
+ private registryHost(): string | null {
184
+ try {
185
+ return new URL(this.registryUrl).host || null;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+
145
191
  async listVersions(ref: string): Promise<string[] | null> {
146
192
  // Only bare registry refs are version-enumerable — a direct `https://` URL
147
193
  // has no version-list endpoint.
@@ -279,16 +325,31 @@ export class RegistryTransport implements Transport {
279
325
  }
280
326
 
281
327
  canonicalizeSiblingRef(
282
- _destination: string,
283
- _relativeSource: string,
284
- sibling: SiblingIdentity,
328
+ destination: string,
329
+ relativeSource: string,
330
+ version: string,
285
331
  ): string {
286
- // An HTTP registry path defaults to the sibling's own `<namespace>/<name>`.
287
- if (!sibling.namespace || !sibling.name) {
332
+ // The sibling sits beside the destination module: resolve the relative path
333
+ // against the destination's own path, then pin the sibling's version. A bare
334
+ // registry ref (`std/foo`) is a path, not a URL, so it borrows a throwaway
335
+ // origin for the join and drops it again.
336
+ const base = splitIntegrity(destination).base.replace(/@[^/@]*$/, "");
337
+ // A registry *base* (`https://registry.telo.run`) is not a module location,
338
+ // so there is nothing for `../lib` to resolve beside — joining anyway would
339
+ // silently produce a ref one path segment short.
340
+ if (isRegistryBase(base)) {
288
341
  throw new Error(
289
- "a relative import canonicalized to an HTTP registry needs the sibling's metadata.namespace and metadata.name.",
342
+ `cannot canonicalize the relative import '${relativeSource}': publish destination ` +
343
+ `'${destination}' is a registry base, not this module's own location. Pass the ` +
344
+ `module's full destination (e.g. '${base.replace(/\/+$/, "")}/<namespace>/<name>').`,
290
345
  );
291
346
  }
292
- return `${sibling.namespace}/${sibling.name}@${sibling.version}`;
347
+ const isUrl = base.startsWith("http://") || base.startsWith("https://");
348
+ const origin = isUrl ? base : `${JOIN_ORIGIN}/${base.replace(/^\/+/, "")}`;
349
+ const resolved = new URL(relativeSource, `${origin.replace(/\/+$/, "")}/`);
350
+ const joined = isUrl
351
+ ? `${resolved.protocol}//${resolved.host}${resolved.pathname}`
352
+ : resolved.pathname.replace(/^\/+/, "");
353
+ return `${joined.replace(/\/+$/, "")}@${version}`;
293
354
  }
294
355
  }
@@ -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
  }