@telorun/kernel 0.41.0 → 0.43.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 (74) hide show
  1. package/dist/bundle/files-integrity.d.ts +25 -0
  2. package/dist/bundle/files-integrity.d.ts.map +1 -0
  3. package/dist/bundle/files-integrity.js +40 -0
  4. package/dist/bundle/files-integrity.js.map +1 -0
  5. package/dist/bundle/module-manifest.d.ts +22 -0
  6. package/dist/bundle/module-manifest.d.ts.map +1 -0
  7. package/dist/bundle/module-manifest.js +33 -0
  8. package/dist/bundle/module-manifest.js.map +1 -0
  9. package/dist/bundle/tar.d.ts +20 -0
  10. package/dist/bundle/tar.d.ts.map +1 -0
  11. package/dist/bundle/tar.js +63 -0
  12. package/dist/bundle/tar.js.map +1 -0
  13. package/dist/controller-loaders/bundle-builder.d.ts.map +1 -1
  14. package/dist/controller-loaders/bundle-builder.js +13 -2
  15. package/dist/controller-loaders/bundle-builder.js.map +1 -1
  16. package/dist/controllers/module/import-controller.d.ts +3 -0
  17. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  18. package/dist/controllers/module/import-controller.js +7 -2
  19. package/dist/controllers/module/import-controller.js.map +1 -1
  20. package/dist/index.d.ts +9 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +7 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/kernel.d.ts.map +1 -1
  25. package/dist/kernel.js +8 -2
  26. package/dist/kernel.js.map +1 -1
  27. package/dist/manifest-sources/local-manifest-cache-source.d.ts +2 -2
  28. package/dist/manifest-sources/local-manifest-cache-source.d.ts.map +1 -1
  29. package/dist/manifest-sources/local-manifest-cache-source.js +24 -90
  30. package/dist/manifest-sources/local-manifest-cache-source.js.map +1 -1
  31. package/dist/transports/oci/docker-credentials.d.ts +10 -0
  32. package/dist/transports/oci/docker-credentials.d.ts.map +1 -0
  33. package/dist/transports/oci/docker-credentials.js +75 -0
  34. package/dist/transports/oci/docker-credentials.js.map +1 -0
  35. package/dist/transports/oci/oci-client.d.ts +51 -0
  36. package/dist/transports/oci/oci-client.d.ts.map +1 -0
  37. package/dist/transports/oci/oci-client.js +170 -0
  38. package/dist/transports/oci/oci-client.js.map +1 -0
  39. package/dist/transports/oci/oci-ref.d.ts +21 -0
  40. package/dist/transports/oci/oci-ref.d.ts.map +1 -0
  41. package/dist/transports/oci/oci-ref.js +36 -0
  42. package/dist/transports/oci/oci-ref.js.map +1 -0
  43. package/dist/transports/oci/oci-transport.d.ts +29 -0
  44. package/dist/transports/oci/oci-transport.d.ts.map +1 -0
  45. package/dist/transports/oci/oci-transport.js +142 -0
  46. package/dist/transports/oci/oci-transport.js.map +1 -0
  47. package/dist/transports/registry-transport.d.ts +23 -0
  48. package/dist/transports/registry-transport.d.ts.map +1 -0
  49. package/dist/transports/registry-transport.js +225 -0
  50. package/dist/transports/registry-transport.js.map +1 -0
  51. package/dist/transports/transport-registry.d.ts +40 -0
  52. package/dist/transports/transport-registry.d.ts.map +1 -0
  53. package/dist/transports/transport-registry.js +70 -0
  54. package/dist/transports/transport-registry.js.map +1 -0
  55. package/dist/transports/transport.d.ts +92 -0
  56. package/dist/transports/transport.d.ts.map +1 -0
  57. package/dist/transports/transport.js +2 -0
  58. package/dist/transports/transport.js.map +1 -0
  59. package/package.json +6 -3
  60. package/src/bundle/files-integrity.ts +46 -0
  61. package/src/bundle/module-manifest.ts +49 -0
  62. package/src/bundle/tar.ts +78 -0
  63. package/src/controller-loaders/bundle-builder.ts +14 -2
  64. package/src/controllers/module/import-controller.ts +7 -2
  65. package/src/index.ts +22 -0
  66. package/src/kernel.ts +7 -2
  67. package/src/manifest-sources/local-manifest-cache-source.ts +25 -98
  68. package/src/transports/oci/docker-credentials.ts +85 -0
  69. package/src/transports/oci/oci-client.ts +237 -0
  70. package/src/transports/oci/oci-ref.ts +53 -0
  71. package/src/transports/oci/oci-transport.ts +192 -0
  72. package/src/transports/registry-transport.ts +281 -0
  73. package/src/transports/transport-registry.ts +91 -0
  74. package/src/transports/transport.ts +112 -0
@@ -0,0 +1,281 @@
1
+ import {
2
+ DEFAULT_MANIFEST_FILENAME,
3
+ HttpSource,
4
+ IntegrityError,
5
+ RegistrySource,
6
+ isRegistryRef,
7
+ parseModuleRef,
8
+ splitIntegrity,
9
+ type ManifestSource,
10
+ } from "@telorun/analyzer";
11
+ import { createHash } from "crypto";
12
+
13
+ import { computeFilesIntegrity, injectFilesIntegrity } from "../bundle/files-integrity.js";
14
+ import { readOwnerManifest } from "../bundle/module-manifest.js";
15
+ import { makeTarGz, readTarGz, toPayloadFiles } from "../bundle/tar.js";
16
+ import type {
17
+ FetchedArtifact,
18
+ PublishBundle,
19
+ PublishOptions,
20
+ PublishResult,
21
+ SiblingIdentity,
22
+ Transport,
23
+ } from "./transport.js";
24
+
25
+ const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
26
+ const HTTP_NAMESPACE = "__http";
27
+ const QUERY_HASH_LENGTH = 12;
28
+ const MAX_PUSH_ATTEMPTS = 4;
29
+ const PUSH_BASE_DELAY_MS = 1000;
30
+
31
+ /** Registry / object-storage backends treat 408/425/429/5xx as transient. */
32
+ function isRetryableStatus(status: number): boolean {
33
+ return status === 408 || status === 425 || status === 429 || status >= 500;
34
+ }
35
+
36
+ function sleep(ms: number): Promise<void> {
37
+ return new Promise((resolve) => setTimeout(resolve, ms));
38
+ }
39
+
40
+ /** Mirror `HttpSource.read`'s `fetchUrl` derivation: when the URL does not
41
+ * already point at a YAML file, append `/telo.yaml`, so a raw import URL and
42
+ * the canonical source it resolves to map to the same cache path. */
43
+ function normalizePathname(rawUrl: string, parsed: URL): string {
44
+ let pathname = parsed.pathname;
45
+ if (!rawUrl.includes(".yaml")) {
46
+ pathname = pathname.endsWith("/")
47
+ ? `${pathname}${DEFAULT_MANIFEST_FILENAME}`
48
+ : `${pathname}/${DEFAULT_MANIFEST_FILENAME}`;
49
+ }
50
+ return pathname;
51
+ }
52
+
53
+ /** Short hash of `search + hash` so two URLs that differ only in query /
54
+ * fragment do not collide at the same cache path. */
55
+ function disambiguatePath(pathname: string, search: string, hash: string): string {
56
+ if (!search && !hash) return pathname;
57
+ const digest = createHash("sha256")
58
+ .update(search + hash)
59
+ .digest("hex")
60
+ .slice(0, QUERY_HASH_LENGTH);
61
+ const dotIdx = pathname.lastIndexOf(".");
62
+ const slashIdx = pathname.lastIndexOf("/");
63
+ const ext = dotIdx > slashIdx ? pathname.slice(dotIdx) : "";
64
+ const base = pathname.slice(0, pathname.length - ext.length);
65
+ return `${base}.${digest}${ext}`;
66
+ }
67
+
68
+ interface VersionsResponse {
69
+ versions?: string[];
70
+ }
71
+
72
+ /** The default HTTP transport: bare `namespace/name@version` registry refs and
73
+ * direct `https://…` URLs, resolving against `registry.telo.run` (or a
74
+ * configured registry). Its resolution `source` composes the browser-safe
75
+ * `RegistrySource` / `HttpSource` from `analyzer`; the Node-only management
76
+ * methods live here. This is the fallback transport for any ref that carries
77
+ * no owning scheme, so `oci://` (or a future `s3://`) never falls through to
78
+ * it — those refs are claimed by their own transport's `supports()`. */
79
+ export class RegistryTransport implements Transport {
80
+ private readonly registrySource: RegistrySource;
81
+ private readonly httpSource: HttpSource;
82
+ readonly source: ManifestSource;
83
+
84
+ constructor(private readonly registryUrl: string = DEFAULT_REGISTRY_URL) {
85
+ this.registrySource = new RegistrySource(registryUrl);
86
+ this.httpSource = new HttpSource();
87
+ const pick = (ref: string): ManifestSource =>
88
+ this.httpSource.supports(ref) ? this.httpSource : this.registrySource;
89
+ this.source = {
90
+ supports: (url) => this.supports(url),
91
+ read: (url) => pick(url).read(url),
92
+ resolveRelative: (base, relative) => pick(base).resolveRelative(base, relative),
93
+ };
94
+ }
95
+
96
+ supports(ref: string): boolean {
97
+ const { base } = splitIntegrity(ref);
98
+ return base.startsWith("http://") || base.startsWith("https://") || isRegistryRef(ref);
99
+ }
100
+
101
+ cacheLocation(ref: string): string[] | null {
102
+ const url = splitIntegrity(ref).base;
103
+ const trimmedRegistry = this.registryUrl.replace(/\/+$/, "");
104
+
105
+ // 1. Registry ref form: namespace/name@version
106
+ if (isRegistryRef(url)) {
107
+ let parsed: ReturnType<typeof parseModuleRef>;
108
+ try {
109
+ parsed = parseModuleRef(url);
110
+ } catch {
111
+ return null;
112
+ }
113
+ return [parsed.modulePath, parsed.version, DEFAULT_MANIFEST_FILENAME];
114
+ }
115
+
116
+ // 2. HTTP(S) URL — a direct registry URL or arbitrary external.
117
+ if (url.startsWith("http://") || url.startsWith("https://")) {
118
+ let parsed: URL;
119
+ try {
120
+ parsed = new URL(url);
121
+ } catch {
122
+ return null;
123
+ }
124
+ const pathname = normalizePathname(url, parsed);
125
+
126
+ // 2a. On the configured registry, no query/fragment: fold into the
127
+ // registry layout so a ref and a direct URL land on the same file.
128
+ const normalizedUrl = `${parsed.protocol}//${parsed.host}${pathname}`;
129
+ if (
130
+ !parsed.search &&
131
+ !parsed.hash &&
132
+ (normalizedUrl === trimmedRegistry || normalizedUrl.startsWith(`${trimmedRegistry}/`))
133
+ ) {
134
+ const rel = normalizedUrl.slice(trimmedRegistry.length + 1);
135
+ if (!rel) return null;
136
+ return rel.split("/");
137
+ }
138
+
139
+ // 2b. Arbitrary HTTP(S) → __http subtree, query-hash suffix on collision.
140
+ const cleanPath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
141
+ const disambiguated = disambiguatePath(cleanPath, parsed.search, parsed.hash);
142
+ return [HTTP_NAMESPACE, parsed.host, ...disambiguated.split("/")];
143
+ }
144
+
145
+ return null;
146
+ }
147
+
148
+ async listVersions(ref: string): Promise<string[] | null> {
149
+ // Only bare registry refs are version-enumerable — a direct `https://` URL
150
+ // has no version-list endpoint.
151
+ if (!isRegistryRef(ref)) return null;
152
+ const { modulePath } = parseModuleRef(ref);
153
+ const url = `${this.registryUrl.replace(/\/+$/, "")}/${modulePath}`;
154
+ const res = await fetch(url, { headers: { accept: "application/json" } });
155
+ if (res.status === 404) return null;
156
+ if (!res.ok) {
157
+ throw new Error(`Registry returned ${res.status} ${res.statusText} for ${modulePath}`);
158
+ }
159
+ const body = (await res.json()) as VersionsResponse;
160
+ return Array.isArray(body.versions) ? body.versions : [];
161
+ }
162
+
163
+ async fetchArtifact(ref: string): Promise<FetchedArtifact> {
164
+ // `read` verifies the manifest bytes against the inline `#sha256-...` hash.
165
+ const { text: manifest, source } = await this.source.read(ref);
166
+ const meta = readOwnerManifest(manifest);
167
+ if (!meta.declaresFiles) return { manifest, files: [] };
168
+
169
+ // The payload rides beside the manifest as `module.tar.gz`.
170
+ const tarUrl = source.replace(/\/telo\.yaml$/, "/module.tar.gz");
171
+ const res = await fetch(tarUrl);
172
+ if (!res.ok) {
173
+ throw new Error(`could not fetch bundle ${tarUrl}: ${res.status} ${res.statusText}`);
174
+ }
175
+ const files = toPayloadFiles(await readTarGz(Buffer.from(await res.arrayBuffer())));
176
+
177
+ // Verify the payload against the manifest's `filesIntegrity` before handing
178
+ // it back — a mismatch is terminal (a tampered bundle must never be used).
179
+ // The manifest that carries the hash is itself pinned by the inline hash.
180
+ if (meta.filesIntegrity) {
181
+ const actual = await computeFilesIntegrity(files);
182
+ if (actual !== meta.filesIntegrity) {
183
+ throw new IntegrityError(
184
+ `Integrity check failed for bundle ${tarUrl}: filesIntegrity expected ` +
185
+ `${meta.filesIntegrity}, got ${actual}. The payload does not match the recorded ` +
186
+ `hash — the module may have been tampered with or republished.`,
187
+ );
188
+ }
189
+ }
190
+
191
+ return { manifest, files };
192
+ }
193
+
194
+ async publish(
195
+ destination: string,
196
+ bundle: PublishBundle,
197
+ opts: PublishOptions = {},
198
+ ): Promise<PublishResult> {
199
+ // Pin the payload in the manifest before it enters the tarball, then choose
200
+ // the artifact body: a `module.tar.gz` when there are files, else raw YAML.
201
+ let manifest = bundle.manifest;
202
+ let body: string | Uint8Array = manifest;
203
+ let contentType = "text/yaml";
204
+ let urlSuffix = "";
205
+ if (bundle.files.length > 0) {
206
+ manifest = injectFilesIntegrity(manifest, await computeFilesIntegrity(bundle.files));
207
+ const entries = [
208
+ { name: DEFAULT_MANIFEST_FILENAME, content: manifest },
209
+ ...bundle.files.map((f) => ({ name: f.name, content: Buffer.from(f.content) })),
210
+ ];
211
+ body = await makeTarGz(entries);
212
+ contentType = "application/gzip";
213
+ urlSuffix = "/module.tar.gz";
214
+ }
215
+
216
+ const { namespace, name, version } = readOwnerManifest(manifest);
217
+ if (!namespace || !name || !version) {
218
+ throw new Error("metadata must include namespace, name, and version.");
219
+ }
220
+ const identity = { namespace, name, version };
221
+ const base = `${destination.replace(/\/+$/, "")}/${identity.namespace}/${identity.name}/${identity.version}`;
222
+ const url = `${base}${urlSuffix}`;
223
+ const label = `${identity.namespace}/${identity.name}@${identity.version}`;
224
+
225
+ const headers: Record<string, string> = { "content-type": contentType };
226
+ if (opts.token) headers.authorization = `Bearer ${opts.token}`;
227
+
228
+ let res: Response | null = null;
229
+ let networkErr: unknown = null;
230
+ for (let attempt = 1; attempt <= MAX_PUSH_ATTEMPTS; attempt++) {
231
+ networkErr = null;
232
+ try {
233
+ res = await fetch(url, { method: "PUT", headers, body });
234
+ } catch (err) {
235
+ networkErr = err;
236
+ res = null;
237
+ }
238
+
239
+ const transient = networkErr != null || (res != null && isRetryableStatus(res.status));
240
+ if (!transient) break;
241
+ if (attempt === MAX_PUSH_ATTEMPTS) break;
242
+
243
+ const reason = networkErr
244
+ ? `network error: ${networkErr instanceof Error ? networkErr.message : String(networkErr)}`
245
+ : `HTTP ${res!.status}`;
246
+ // Drain the body so the underlying connection can be reused for the retry.
247
+ if (res) await res.text().catch(() => {});
248
+ const delayMs = PUSH_BASE_DELAY_MS * 2 ** (attempt - 1) + Math.floor(Math.random() * 250);
249
+ opts.onRetry?.({ reason, attempt, maxAttempts: MAX_PUSH_ATTEMPTS, delayMs });
250
+ await sleep(delayMs);
251
+ }
252
+
253
+ if (networkErr) {
254
+ throw new Error(
255
+ `Network error: ${networkErr instanceof Error ? networkErr.message : String(networkErr)} ` +
256
+ `(after ${MAX_PUSH_ATTEMPTS} attempts)`,
257
+ );
258
+ }
259
+ if (!res!.ok) {
260
+ const ct = res!.headers.get("content-type") ?? "";
261
+ const errBody = ct.includes("application/json") ? await res!.json() : await res!.text();
262
+ throw new Error(`Push failed (${res!.status}): ${JSON.stringify(errBody)}`);
263
+ }
264
+
265
+ return { label, url };
266
+ }
267
+
268
+ canonicalizeSiblingRef(
269
+ _destination: string,
270
+ _relativeSource: string,
271
+ sibling: SiblingIdentity,
272
+ ): string {
273
+ // An HTTP registry path defaults to the sibling's own `<namespace>/<name>`.
274
+ if (!sibling.namespace || !sibling.name) {
275
+ throw new Error(
276
+ "a relative import canonicalized to an HTTP registry needs the sibling's metadata.namespace and metadata.name.",
277
+ );
278
+ }
279
+ return `${sibling.namespace}/${sibling.name}@${sibling.version}`;
280
+ }
281
+ }
@@ -0,0 +1,91 @@
1
+ import type { ManifestSource } from "@telorun/analyzer";
2
+
3
+ import { OciTransport } from "./oci/oci-transport.js";
4
+ import { RegistryTransport } from "./registry-transport.js";
5
+ import type {
6
+ FetchedArtifact,
7
+ PublishBundle,
8
+ PublishOptions,
9
+ PublishResult,
10
+ Transport,
11
+ } from "./transport.js";
12
+
13
+ /** Dispatches ref-scheme-specific operations to the transport that owns a ref.
14
+ * The loader, cache source, `upgrade`, and `publish` consult this instead of
15
+ * branching on ref shape. `RegistryTransport` is always last so it is the
16
+ * fallback for bare / `https` refs, and a scheme-owning transport (OCI, later
17
+ * S3) claims its refs via `supports()` before the fallback is reached. */
18
+ export class TransportRegistry {
19
+ constructor(private readonly transports: Transport[]) {}
20
+
21
+ /** The transport that owns `ref`, or `undefined` if none does. */
22
+ forRef(ref: string): Transport | undefined {
23
+ return this.transports.find((t) => t.supports(ref));
24
+ }
25
+
26
+ /** The resolution `ManifestSource`s of every registered transport, in order —
27
+ * the browser-safe subset the loader appends to its source chain. */
28
+ sources(): ManifestSource[] {
29
+ return this.transports.map((t) => t.source);
30
+ }
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;
36
+ }
37
+
38
+ /** Published versions for `ref` via its owning transport; `null` when the
39
+ * module is unpublished. Throws when no transport owns the ref. */
40
+ listVersions(ref: string): Promise<string[] | null> {
41
+ return this.require(ref).listVersions(ref);
42
+ }
43
+
44
+ /** Full artifact for `ref` via its owning transport. Throws when no transport
45
+ * owns the ref. */
46
+ fetchArtifact(ref: string): Promise<FetchedArtifact> {
47
+ return this.require(ref).fetchArtifact(ref);
48
+ }
49
+
50
+ /** Publish `bundle` to `destination` via the transport its scheme selects.
51
+ * Throws when no transport owns the destination. */
52
+ publish(
53
+ destination: string,
54
+ bundle: PublishBundle,
55
+ opts?: PublishOptions,
56
+ ): Promise<PublishResult> {
57
+ return this.require(destination).publish(destination, bundle, opts);
58
+ }
59
+
60
+ private require(ref: string): Transport {
61
+ const transport = this.forRef(ref);
62
+ if (!transport) {
63
+ throw new Error(`no transport owns ref '${ref}'`);
64
+ }
65
+ return transport;
66
+ }
67
+ }
68
+
69
+ /** The default transport set. Scheme-owning transports come first; the
70
+ * `RegistryTransport` is last, the fallback for bare / `https` refs. OCI (and
71
+ * later S3) claim their `oci://` / `s3://` refs before the fallback is reached. */
72
+ export function defaultTransports(registryUrl?: string): Transport[] {
73
+ return [new OciTransport(), new RegistryTransport(registryUrl)];
74
+ }
75
+
76
+ const defaultRegistryCache = new Map<string, TransportRegistry>();
77
+
78
+ /** A `TransportRegistry` seeded with {@link defaultTransports}, memoized per
79
+ * `registryUrl`. The default transports are stateless config (a fresh
80
+ * `OciClient` with its own token cache is created per OCI operation), so one
81
+ * shared instance per registry URL is safe — and avoids re-instantiating the
82
+ * whole set on hot paths like `cachePathForCanonical`. */
83
+ export function defaultTransportRegistry(registryUrl?: string): TransportRegistry {
84
+ const key = registryUrl ?? "";
85
+ let cached = defaultRegistryCache.get(key);
86
+ if (!cached) {
87
+ cached = new TransportRegistry(defaultTransports(registryUrl));
88
+ defaultRegistryCache.set(key, cached);
89
+ }
90
+ return cached;
91
+ }
@@ -0,0 +1,112 @@
1
+ import type { ManifestSource } from "@telorun/analyzer";
2
+
3
+ import type { PayloadFile } from "../bundle/files-integrity.js";
4
+
5
+ /** The full module artifact a transport delivers: the `telo.yaml` bytes plus
6
+ * the decompressed `files:` payload (empty for a manifest-only module). The
7
+ * manifest is already verified against the import's inline hash; the payload
8
+ * against the manifest-embedded `filesIntegrity`. */
9
+ export interface FetchedArtifact {
10
+ manifest: string;
11
+ files: PayloadFile[];
12
+ }
13
+
14
+ /** The module bundle handed to a transport for publishing: the final,
15
+ * already-analyzed / pinned / canonicalized `telo.yaml` bytes plus the `files:`
16
+ * payload (empty for a manifest-only module). The transport is responsible for
17
+ * pinning the payload (`filesIntegrity`) and for the artifact shape it writes
18
+ * (HTTP: `telo.yaml` + `module.tar.gz`; OCI: a single blob). */
19
+ export interface PublishBundle {
20
+ manifest: string;
21
+ files: PayloadFile[];
22
+ }
23
+
24
+ export interface PublishResult {
25
+ /** Human-readable `<ns>/<name>@<version>` label of what was pushed. */
26
+ label: string;
27
+ /** The location the artifact was written to. */
28
+ url: string;
29
+ }
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
+ export interface PublishOptions {
41
+ /** Bearer token for registries that require auth. */
42
+ token?: string;
43
+ /** Notified before each backoff sleep on a transient push failure, so the
44
+ * caller can surface retry progress. */
45
+ onRetry?: (info: {
46
+ reason: string;
47
+ attempt: number;
48
+ maxAttempts: number;
49
+ delayMs: number;
50
+ }) => void;
51
+ }
52
+
53
+ /** A Transport owns everything ref-scheme-specific about a module's lifecycle:
54
+ * resolution (through its `ManifestSource`), cache placement, version
55
+ * enumeration, full-artifact fetch, and publish. Registering a transport is
56
+ * the only thing needed to add a backend — the loader, cache, `upgrade`, and
57
+ * `publish` never branch on scheme again; they ask the {@link TransportRegistry}
58
+ * which transport owns a ref and delegate.
59
+ *
60
+ * A Transport *composes* a resolution `ManifestSource`, it does not extend it:
61
+ * `ManifestSource` is the browser-safe resolution primitive (also implemented
62
+ * by the cache / local / memory sources, which have no versions and nothing to
63
+ * publish), so it stays in `analyzer`, while the Node-only management methods
64
+ * (`cacheLocation` and, in later phases, `listVersions` / `fetchArtifact` /
65
+ * `publish`) live on the Transport here in `kernel`. */
66
+ export interface Transport {
67
+ /** True when this transport owns the given ref (or publish destination). */
68
+ supports(ref: string): boolean;
69
+
70
+ /** The resolution primitive: fetch + verify `telo.yaml`, resolve relatives.
71
+ * Browser-safe for a browser-reachable transport (HTTP/registry), so it can
72
+ * live in `analyzer`; a Node-only transport has no browser-safe source. */
73
+ readonly source: ManifestSource;
74
+
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;
79
+
80
+ /** The versions published for the module `ref` names, newest-first order not
81
+ * guaranteed (the caller sorts). Returns `null` when the module is not
82
+ * published (e.g. a 404), distinct from `[]` (published, no versions). Used
83
+ * by `telo upgrade`. */
84
+ listVersions(ref: string): Promise<string[] | null>;
85
+
86
+ /** Retrieve the full artifact for `ref` — the `telo.yaml` and its `files:`
87
+ * payload — verifying the manifest against the inline hash and the payload
88
+ * against the manifest's `filesIntegrity`. Used by `telo install`; subsumes
89
+ * the out-of-band bundle fetch that used to sit outside the source chain. */
90
+ fetchArtifact(ref: string): Promise<FetchedArtifact>;
91
+
92
+ /** Push `bundle` to `destination` (a base ref / repo whose scheme this
93
+ * transport owns), pinning the payload and writing the transport-native
94
+ * artifact shape. Throws on failure. Used by `telo publish`. */
95
+ publish(
96
+ destination: string,
97
+ bundle: PublishBundle,
98
+ opts?: PublishOptions,
99
+ ): Promise<PublishResult>;
100
+
101
+ /** Canonicalize a relative sibling import (`../lib`) declared in a module
102
+ * being published to `destination` into the absolute ref it will resolve to
103
+ * once published. Owns the scheme-specific "where does a sibling land" rule —
104
+ * OCI derives the repo from the destination, HTTP from the sibling's
105
+ * `<namespace>/<name>` — so `telo publish` delegates instead of branching on
106
+ * transport shape. */
107
+ canonicalizeSiblingRef(
108
+ destination: string,
109
+ relativeSource: string,
110
+ sibling: SiblingIdentity,
111
+ ): string;
112
+ }