@telorun/kernel 0.76.0 → 0.78.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/bundle/tar.d.ts +3 -0
- package/dist/bundle/tar.d.ts.map +1 -1
- package/dist/bundle/tar.js +27 -1
- package/dist/bundle/tar.js.map +1 -1
- package/dist/host-versions.d.ts +23 -0
- package/dist/host-versions.d.ts.map +1 -0
- package/dist/host-versions.js +25 -0
- package/dist/host-versions.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +6 -1
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-schemas.d.ts +1 -1
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +5 -3
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/runtime-seam.d.ts.map +1 -1
- package/dist/runtime-seam.js +2 -0
- package/dist/runtime-seam.js.map +1 -1
- package/dist/transports/oci/oci-client.d.ts +4 -0
- package/dist/transports/oci/oci-client.d.ts.map +1 -1
- package/dist/transports/oci/oci-client.js +7 -1
- package/dist/transports/oci/oci-client.js.map +1 -1
- package/dist/transports/oci/oci-transport.d.ts +8 -3
- package/dist/transports/oci/oci-transport.d.ts.map +1 -1
- package/dist/transports/oci/oci-transport.js +67 -21
- package/dist/transports/oci/oci-transport.js.map +1 -1
- package/dist/transports/registry-transport.d.ts +3 -2
- package/dist/transports/registry-transport.d.ts.map +1 -1
- package/dist/transports/registry-transport.js +10 -0
- package/dist/transports/registry-transport.js.map +1 -1
- package/dist/transports/transport.d.ts +25 -6
- package/dist/transports/transport.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/bundle/tar.ts +28 -1
- package/src/host-versions.ts +26 -0
- package/src/index.ts +1 -0
- package/src/kernel.ts +6 -1
- package/src/manifest-schemas.ts +8 -2
- package/src/runtime-seam.ts +2 -0
- package/src/transports/oci/oci-client.ts +8 -1
- package/src/transports/oci/oci-transport.ts +76 -22
- package/src/transports/registry-transport.ts +14 -0
- package/src/transports/transport.ts +26 -5
|
@@ -71,6 +71,13 @@ export interface OciManifest {
|
|
|
71
71
|
annotations?: Record<string, string>;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** A blob's OCI digest, as `pushBlob` addresses it. Exported because the
|
|
75
|
+
* `layers:` index has to name a layer's digest BEFORE it is pushed — a second
|
|
76
|
+
* spelling of this would be a second answer to what a blob is called. */
|
|
77
|
+
export function blobDigest(bytes: Uint8Array): string {
|
|
78
|
+
return `sha256:${sha256Hex(bytes)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
function sha256Hex(bytes: Uint8Array): string {
|
|
75
82
|
return createHash("sha256").update(bytes).digest("hex");
|
|
76
83
|
}
|
|
@@ -270,7 +277,7 @@ export class OciClient {
|
|
|
270
277
|
* `sha256:<hex>` digest. Two-step upload: obtain a session, then PUT with
|
|
271
278
|
* the digest. */
|
|
272
279
|
async pushBlob(bytes: Uint8Array): Promise<string> {
|
|
273
|
-
const digest =
|
|
280
|
+
const digest = blobDigest(bytes);
|
|
274
281
|
|
|
275
282
|
const head = await this.authedFetch(
|
|
276
283
|
`${this.base()}/blobs/${digest}`,
|
|
@@ -5,25 +5,25 @@ import {
|
|
|
5
5
|
sha256Base64Url,
|
|
6
6
|
verifyIntegrity,
|
|
7
7
|
type ArtifactLayer,
|
|
8
|
+
type ArtifactSelector,
|
|
8
9
|
type ManifestCacheCoords,
|
|
9
10
|
type ManifestSource,
|
|
10
11
|
} from "@telorun/analyzer";
|
|
11
|
-
import { createHash } from "node:crypto";
|
|
12
|
-
|
|
13
12
|
import {
|
|
14
13
|
computeFilesIntegrity,
|
|
15
|
-
injectLayerIndex,
|
|
16
14
|
type PayloadFile,
|
|
17
15
|
} from "../../bundle/files-integrity.js";
|
|
18
16
|
import { readOwnerManifest, type OwnerManifest } from "../../bundle/module-manifest.js";
|
|
19
17
|
import { makeTarGz, readTarGz, toPayloadFiles } from "../../bundle/tar.js";
|
|
20
18
|
import type {
|
|
19
|
+
PayloadLayer,
|
|
21
20
|
PublishBundle,
|
|
22
21
|
PublishOptions,
|
|
23
22
|
PublishResult,
|
|
24
23
|
Transport,
|
|
25
24
|
} from "../transport.js";
|
|
26
25
|
import {
|
|
26
|
+
blobDigest,
|
|
27
27
|
OciClient,
|
|
28
28
|
OCI_MANIFEST_MEDIA_TYPE,
|
|
29
29
|
TELO_LAYER_ROLE_ANNOTATION,
|
|
@@ -42,6 +42,21 @@ import {
|
|
|
42
42
|
withRefVersion,
|
|
43
43
|
} from "./oci-ref.js";
|
|
44
44
|
|
|
45
|
+
/** A layer's identity within one artifact: role plus, for a controller layer,
|
|
46
|
+
* its selector. Role alone is not it — there is one controller layer per
|
|
47
|
+
* selector. Same shape the release ledger keys on. */
|
|
48
|
+
function layerKey(role: string, selector?: ArtifactSelector): string {
|
|
49
|
+
return selector ? `${role}/${selectorKey(selector)}` : role;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The `layers:` index the bundle's own manifest declares, keyed for lookup.
|
|
53
|
+
* Read back out of the text rather than passed alongside it, because the text
|
|
54
|
+
* is what ships — anything else would be a second copy to keep in agreement. */
|
|
55
|
+
function declaredLayerIndex(bundle: PublishBundle): Map<string, ArtifactLayer> {
|
|
56
|
+
const declared = readOwnerManifest(bundle.manifest).layers ?? [];
|
|
57
|
+
return new Map(declared.map((layer) => [layerKey(layer.role, layer.selector), layer]));
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
/**
|
|
46
61
|
* Pull only the **manifest layer** and return its verified `telo.yaml` text.
|
|
47
62
|
*
|
|
@@ -211,17 +226,17 @@ export class OciTransport implements Transport {
|
|
|
211
226
|
* republish that reorders layers is simply invisible here rather than a
|
|
212
227
|
* failure. Content verification is the artifact handle's, which holds the
|
|
213
228
|
* expected `integrity`. */
|
|
214
|
-
async fetchLayer(ref: string,
|
|
229
|
+
async fetchLayer(ref: string, digest: string): Promise<PayloadFile[]> {
|
|
215
230
|
const { host, repo } = parseOciRef(ref);
|
|
216
|
-
const tar = await this.readClient(host, repo).pullBlob(
|
|
231
|
+
const tar = await this.readClient(host, repo).pullBlob(digest);
|
|
217
232
|
// Verify the transfer against the digest that addressed it. A registry is
|
|
218
233
|
// not trusted to return the blob that was asked for, and this is the only
|
|
219
234
|
// place the pushed bytes exist — the content digest checked after extraction
|
|
220
235
|
// covers the file set, not the archive that carried it.
|
|
221
|
-
const actual =
|
|
222
|
-
if (actual !==
|
|
236
|
+
const actual = blobDigest(tar);
|
|
237
|
+
if (actual !== digest) {
|
|
223
238
|
throw new IntegrityError(
|
|
224
|
-
`Blob digest mismatch fetching a layer of ${ref}: requested ${
|
|
239
|
+
`Blob digest mismatch fetching a layer of ${ref}: requested ${digest}, ` +
|
|
225
240
|
`received ${actual}. The registry returned different bytes than were addressed.`,
|
|
226
241
|
);
|
|
227
242
|
}
|
|
@@ -258,6 +273,27 @@ export class OciTransport implements Transport {
|
|
|
258
273
|
return Object.fromEntries(mapped.filter((e): e is [string, string] => Boolean(e[1])));
|
|
259
274
|
}
|
|
260
275
|
|
|
276
|
+
/** The index as it will be published: `blob` over the gzipped tar this
|
|
277
|
+
* transport pushes, `integrity` over the layer's file contents. Both are
|
|
278
|
+
* computed from the files alone, which is what lets the payload builder
|
|
279
|
+
* write the index into `telo.yaml` before a single byte is pushed. */
|
|
280
|
+
async layerIndex(layers: readonly PayloadLayer[]): Promise<ArtifactLayer[]> {
|
|
281
|
+
const index: ArtifactLayer[] = [];
|
|
282
|
+
for (const layer of layers) {
|
|
283
|
+
if (layer.files.length === 0) continue;
|
|
284
|
+
const tar = await makeTarGz(
|
|
285
|
+
layer.files.map((f) => ({ name: f.name, content: Buffer.from(f.content) })),
|
|
286
|
+
);
|
|
287
|
+
index.push({
|
|
288
|
+
role: layer.role,
|
|
289
|
+
...(layer.selector ? { selector: layer.selector } : {}),
|
|
290
|
+
blob: blobDigest(tar),
|
|
291
|
+
integrity: await computeFilesIntegrity(layer.files),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return index;
|
|
295
|
+
}
|
|
296
|
+
|
|
261
297
|
async publish(
|
|
262
298
|
destination: string,
|
|
263
299
|
bundle: PublishBundle,
|
|
@@ -285,17 +321,42 @@ export class OciTransport implements Transport {
|
|
|
285
321
|
const tag = identity.version;
|
|
286
322
|
const client = new OciClient(host, repo);
|
|
287
323
|
|
|
288
|
-
// Push every payload layer first
|
|
289
|
-
//
|
|
290
|
-
// pushed
|
|
324
|
+
// Push every payload layer first. This ordering is what keeps the index
|
|
325
|
+
// non-circular: the manifest layer is pushed last and names only the layers
|
|
326
|
+
// pushed before it, never itself.
|
|
327
|
+
//
|
|
328
|
+
// The index is NOT written here — `bundle.manifest` already carries it, and
|
|
329
|
+
// rewriting the manifest at this point is precisely the bug this replaced:
|
|
330
|
+
// the bytes a dependent hashed to derive its pin would then never be the
|
|
331
|
+
// bytes pushed. So each pushed blob is CHECKED against what the manifest
|
|
332
|
+
// already claims, which is also the standing test that framing stayed
|
|
333
|
+
// deterministic — a claim nobody can satisfy is a hard failure, not a
|
|
334
|
+
// silently corrected index.
|
|
335
|
+
const declared = declaredLayerIndex(bundle);
|
|
291
336
|
const payloadDescriptors: OciDescriptor[] = [];
|
|
292
|
-
const index: ArtifactLayer[] = [];
|
|
293
337
|
for (const layer of bundle.layers) {
|
|
294
338
|
if (layer.files.length === 0) continue;
|
|
339
|
+
const key = layerKey(layer.role, layer.selector);
|
|
295
340
|
const tar = await makeTarGz(
|
|
296
341
|
layer.files.map((f) => ({ name: f.name, content: Buffer.from(f.content) })),
|
|
297
342
|
);
|
|
298
343
|
const blob = await client.pushBlob(tar);
|
|
344
|
+
const claim = declared.get(key);
|
|
345
|
+
if (!claim) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`layer '${key}' is being pushed but the manifest's 'layers:' index does not name it. ` +
|
|
348
|
+
`The index is written by the payload builder before publish, so a layer missing ` +
|
|
349
|
+
`from it would be unaddressable by any importer.`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (claim.blob !== blob) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`layer '${key}' framed to ${blob}, but the manifest's 'layers:' index claims ` +
|
|
355
|
+
`${claim.blob}. The published telo.yaml is hashed into every dependent's import ` +
|
|
356
|
+
`pin, so it cannot be corrected here — the archive framing must be a pure ` +
|
|
357
|
+
`function of the layer's files.`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
299
360
|
payloadDescriptors.push({
|
|
300
361
|
mediaType: TELO_PAYLOAD_LAYER_MEDIA_TYPE,
|
|
301
362
|
digest: blob,
|
|
@@ -307,18 +368,11 @@ export class OciTransport implements Transport {
|
|
|
307
368
|
: {}),
|
|
308
369
|
},
|
|
309
370
|
});
|
|
310
|
-
index.push({
|
|
311
|
-
role: layer.role,
|
|
312
|
-
...(layer.selector ? { selector: layer.selector } : {}),
|
|
313
|
-
blob,
|
|
314
|
-
integrity: await computeFilesIntegrity(layer.files),
|
|
315
|
-
});
|
|
316
371
|
}
|
|
317
372
|
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
const manifestText =
|
|
321
|
-
index.length > 0 ? injectLayerIndex(bundle.manifest, index) : bundle.manifest;
|
|
373
|
+
// Push telo.yaml as its own layer so a manifest read never has to pull a
|
|
374
|
+
// payload. Verbatim — these are the bytes importers pin.
|
|
375
|
+
const manifestText = bundle.manifest;
|
|
322
376
|
const manifestTar = await makeTarGz([
|
|
323
377
|
{ name: DEFAULT_MANIFEST_FILENAME, content: manifestText },
|
|
324
378
|
]);
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
sha256Base64Url,
|
|
9
9
|
withRefVersion,
|
|
10
10
|
splitIntegrity,
|
|
11
|
+
type ArtifactLayer,
|
|
11
12
|
type ManifestCacheCoords,
|
|
12
13
|
type ManifestSource,
|
|
13
14
|
} from "@telorun/analyzer";
|
|
@@ -17,6 +18,7 @@ import { createHash } from "crypto";
|
|
|
17
18
|
import type { PayloadFile } from "../bundle/files-integrity.js";
|
|
18
19
|
import { assertPublicEgress } from "./egress-guard.js";
|
|
19
20
|
import type {
|
|
21
|
+
PayloadLayer,
|
|
20
22
|
PublishBundle,
|
|
21
23
|
PublishOptions,
|
|
22
24
|
PublishResult,
|
|
@@ -279,6 +281,18 @@ export class RegistryTransport implements Transport {
|
|
|
279
281
|
);
|
|
280
282
|
}
|
|
281
283
|
|
|
284
|
+
async layerIndex(layers: readonly PayloadLayer[]): Promise<ArtifactLayer[]> {
|
|
285
|
+
// Same boundary `fetchLayer` and `publish` draw: the HTTP registry serves
|
|
286
|
+
// manifests only, so it frames no layer and can name no blob. An empty set
|
|
287
|
+
// is not a payload, so it answers rather than throws — a manifest-only
|
|
288
|
+
// module builds its payload through this transport during analysis.
|
|
289
|
+
if (layers.every((layer) => layer.files.length === 0)) return [];
|
|
290
|
+
throw new Error(
|
|
291
|
+
"The Telo registry serves manifests only, so it cannot index payload layers. " +
|
|
292
|
+
"A module with a bundled payload is published as an OCI artifact (oci://host/repo).",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
282
296
|
async publish(
|
|
283
297
|
destination: string,
|
|
284
298
|
bundle: PublishBundle,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ArtifactLayer,
|
|
2
3
|
ArtifactSelector,
|
|
3
4
|
LayerRole,
|
|
4
5
|
ManifestCacheCoords,
|
|
@@ -19,10 +20,10 @@ export interface PayloadLayer {
|
|
|
19
20
|
|
|
20
21
|
/** The module bundle handed to a transport for publishing: the final,
|
|
21
22
|
* already-analyzed / pinned / canonicalized `telo.yaml` bytes plus the payload
|
|
22
|
-
* partitioned into layers (empty for a manifest-only module)
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* the index non-circular. */
|
|
23
|
+
* partitioned into layers (empty for a manifest-only module), whose `layers:`
|
|
24
|
+
* index the manifest already carries. The transport pushes each layer as its
|
|
25
|
+
* own blob, checks the digest against what the index claims, and only then
|
|
26
|
+
* pushes the manifest layer — the order that keeps the index non-circular. */
|
|
26
27
|
export interface PublishBundle {
|
|
27
28
|
manifest: string;
|
|
28
29
|
layers: PayloadLayer[];
|
|
@@ -151,9 +152,29 @@ export interface Transport {
|
|
|
151
152
|
* verifies. */
|
|
152
153
|
manifestHash(ref: string): Promise<string>;
|
|
153
154
|
|
|
155
|
+
/** The `layers:` index `layers` will be published under: each entry's
|
|
156
|
+
* `integrity` over the layer's file contents and its `blob` over the exact
|
|
157
|
+
* bytes this transport will push.
|
|
158
|
+
*
|
|
159
|
+
* On the interface, and called BEFORE anything is pushed, because the index
|
|
160
|
+
* goes *inside* `telo.yaml` and a dependent's import pin is a hash of that
|
|
161
|
+
* file. Injecting the index at push time — which is what this replaced —
|
|
162
|
+
* meant the payload builder's manifest was never the published one, so every
|
|
163
|
+
* sibling pin derived from it named bytes the registry does not have. The
|
|
164
|
+
* builder therefore needs the index up front, and only the transport knows
|
|
165
|
+
* the framing a `blob` digest covers.
|
|
166
|
+
*
|
|
167
|
+
* Requires the framing to be a pure function of the files (see `makeTarGz`),
|
|
168
|
+
* since `publish` re-frames the same layers and hard-fails if a digest moved.
|
|
169
|
+
* Returns `[]` for an empty layer set; throws when this transport cannot
|
|
170
|
+
* publish payload layers at all. */
|
|
171
|
+
layerIndex(layers: readonly PayloadLayer[]): Promise<ArtifactLayer[]>;
|
|
172
|
+
|
|
154
173
|
/** Push `bundle` to `destination` (a base ref / repo whose scheme this
|
|
155
174
|
* transport owns), pinning the payload and writing the transport-native
|
|
156
|
-
* artifact shape.
|
|
175
|
+
* artifact shape. `bundle.manifest` already carries the `layers:` index this
|
|
176
|
+
* transport returned from {@link layerIndex}; publish verifies rather than
|
|
177
|
+
* rewrites it. Throws on failure. Used by `telo publish`. */
|
|
157
178
|
publish(
|
|
158
179
|
destination: string,
|
|
159
180
|
bundle: PublishBundle,
|