@telorun/kernel 0.47.0 → 0.49.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/application-env.d.ts +26 -11
- package/dist/application-env.d.ts.map +1 -1
- package/dist/application-env.js +36 -12
- package/dist/application-env.js.map +1 -1
- package/dist/bundle/module-manifest.d.ts +6 -0
- package/dist/bundle/module-manifest.d.ts.map +1 -1
- package/dist/bundle/module-manifest.js +4 -0
- package/dist/bundle/module-manifest.js.map +1 -1
- package/dist/controller-loaders/npm-loader.d.ts +2 -16
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +42 -1
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controller-registry.d.ts +6 -6
- package/dist/controller-registry.d.ts.map +1 -1
- package/dist/controller-registry.js +14 -10
- package/dist/controller-registry.js.map +1 -1
- package/dist/controllers/module/import-controller.d.ts +0 -46
- package/dist/controllers/module/import-controller.d.ts.map +1 -1
- package/dist/controllers/module/import-controller.js +0 -27
- package/dist/controllers/module/import-controller.js.map +1 -1
- package/dist/controllers/resource-definition/abstract-controller.d.ts +0 -4
- package/dist/controllers/resource-definition/abstract-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/abstract-controller.js +0 -4
- package/dist/controllers/resource-definition/abstract-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts +0 -4
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +0 -4
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-inherited-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-inherited-controller.js +0 -1
- package/dist/controllers/resource-definition/resource-inherited-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.js +0 -1
- package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +18 -11
- package/dist/kernel.js.map +1 -1
- package/dist/transports/oci/oci-client.d.ts +5 -0
- package/dist/transports/oci/oci-client.d.ts.map +1 -1
- package/dist/transports/oci/oci-client.js +13 -3
- package/dist/transports/oci/oci-client.js.map +1 -1
- package/dist/transports/oci/oci-transport.d.ts +18 -0
- package/dist/transports/oci/oci-transport.d.ts.map +1 -1
- package/dist/transports/oci/oci-transport.js +41 -8
- package/dist/transports/oci/oci-transport.js.map +1 -1
- package/dist/transports/registry-transport.d.ts +7 -0
- package/dist/transports/registry-transport.d.ts.map +1 -1
- package/dist/transports/registry-transport.js +43 -14
- package/dist/transports/registry-transport.js.map +1 -1
- package/dist/transports/transport.d.ts +19 -0
- package/dist/transports/transport.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/application-env.ts +36 -11
- package/src/bundle/module-manifest.ts +10 -0
- package/src/controller-loaders/npm-loader.ts +47 -1
- package/src/controller-registry.ts +14 -10
- package/src/controllers/module/import-controller.ts +0 -27
- package/src/controllers/resource-definition/abstract-controller.ts +0 -4
- package/src/controllers/resource-definition/resource-definition-controller.ts +0 -4
- package/src/controllers/resource-definition/resource-inherited-controller.ts +0 -2
- package/src/controllers/resource-definition/resource-template-controller.ts +0 -2
- package/src/kernel.ts +20 -11
- package/src/transports/oci/oci-client.ts +18 -3
- package/src/transports/oci/oci-transport.ts +46 -10
- package/src/transports/registry-transport.ts +54 -14
- package/src/transports/transport.ts +20 -0
|
@@ -61,8 +61,6 @@ export function createTemplateController(definition: {
|
|
|
61
61
|
result?: Record<string, any>;
|
|
62
62
|
}, definingContext: EvaluationContext): ControllerInstance {
|
|
63
63
|
return {
|
|
64
|
-
schema: definition.schema ?? { type: "object", additionalProperties: true },
|
|
65
|
-
|
|
66
64
|
create: async (resource: any, ctx: ResourceContext): Promise<ResourceInstance> => {
|
|
67
65
|
// `self` is read lazily: Phase 5 injection mutates `resource`'s ref slots
|
|
68
66
|
// (e.g. `connection: !ref Db` → the live instance) AFTER create() but before
|
package/src/kernel.ts
CHANGED
|
@@ -483,13 +483,17 @@ export class Kernel implements IKernel {
|
|
|
483
483
|
);
|
|
484
484
|
}
|
|
485
485
|
// Bake the per-kind resource-config validators too. `_createInstance`
|
|
486
|
-
// compiles `
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
486
|
+
// compiles the definition's `schema` for every resource at runtime — work
|
|
487
|
+
// this analyze-only pass otherwise skips because it stops before
|
|
488
|
+
// instantiation, leaving the runtime to recompile and fail to persist
|
|
489
|
+
// them on a read-only image.
|
|
490
490
|
// Pre-compiling every `Telo.Definition` schema here writes them into the
|
|
491
|
-
// same content-addressed `__validators/` cache the runtime reads.
|
|
492
|
-
|
|
491
|
+
// same content-addressed `__validators/` cache the runtime reads. The
|
|
492
|
+
// resolver lets it also bake each `extends` child's inheritance-resolved
|
|
493
|
+
// schema — the form the runtime actually validates against.
|
|
494
|
+
precompileDefinitionSchemas(staticManifests, this.sharedSchemaValidator, (def) =>
|
|
495
|
+
this.registry.resolverForDefinition(def),
|
|
496
|
+
);
|
|
493
497
|
// Framework/builtin controller schemas (`Telo.Import`, `Telo.Definition`,
|
|
494
498
|
// the module controller, …) aren't in the static manifests but are
|
|
495
499
|
// validated per-resource at runtime just the same. They're registered by
|
|
@@ -1040,12 +1044,17 @@ export class Kernel implements IKernel {
|
|
|
1040
1044
|
`Controller for ${kind} does not implement create method`,
|
|
1041
1045
|
);
|
|
1042
1046
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1047
|
+
// The DEFINITION is the sole resource-config contract — a controller module
|
|
1048
|
+
// cannot supply or override one. Keeping the schema on the manifest is what
|
|
1049
|
+
// lets `telo check` see it, the validator warm bake it, and the editor render
|
|
1050
|
+
// it; a code-side schema was invisible to all three and free to drift.
|
|
1051
|
+
const definition = this.controllers.getDefinition(resolvedKind);
|
|
1052
|
+
const configSchema = definition?.schema as Record<string, unknown> | undefined;
|
|
1053
|
+
if (!configSchema?.type) {
|
|
1054
|
+
throw new Error(`No schema defined for kind ${kind}`);
|
|
1045
1055
|
}
|
|
1046
1056
|
|
|
1047
1057
|
// Resolve eval paths from x-telo-eval annotations in the parent and own schema
|
|
1048
|
-
const definition = this.controllers.getDefinition(resolvedKind);
|
|
1049
1058
|
const parentDef = definition?.capability
|
|
1050
1059
|
? this.controllers.getDefinition(definition.capability)
|
|
1051
1060
|
: undefined;
|
|
@@ -1063,8 +1072,8 @@ export class Kernel implements IKernel {
|
|
|
1063
1072
|
// restoring the pre-CEL string view that the schema expects.
|
|
1064
1073
|
try {
|
|
1065
1074
|
this.sharedSchemaValidator
|
|
1066
|
-
.compile(
|
|
1067
|
-
.validate(stripCompiledValues(resource,
|
|
1075
|
+
.compile(configSchema)
|
|
1076
|
+
.validate(stripCompiledValues(resource, configSchema));
|
|
1068
1077
|
} catch (error) {
|
|
1069
1078
|
throw new RuntimeError(
|
|
1070
1079
|
"ERR_RESOURCE_SCHEMA_VALIDATION_FAILED",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fetchOrThrow } from "@telorun/sdk";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
|
|
3
4
|
import { assertPublicEgress } from "../egress-guard.js";
|
|
@@ -32,6 +33,11 @@ export interface OciManifest {
|
|
|
32
33
|
artifactType?: string;
|
|
33
34
|
config: OciDescriptor;
|
|
34
35
|
layers: OciDescriptor[];
|
|
36
|
+
/** Descriptive key/value metadata on the manifest. Telo projects the module's
|
|
37
|
+
* declared provenance into the standard `org.opencontainers.image.*` keys —
|
|
38
|
+
* the only metadata channel GHCR exposes, since it does not serve the
|
|
39
|
+
* referrers API. */
|
|
40
|
+
annotations?: Record<string, string>;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
function sha256Hex(bytes: Uint8Array): string {
|
|
@@ -104,7 +110,10 @@ export class OciClient {
|
|
|
104
110
|
};
|
|
105
111
|
|
|
106
112
|
const cached = this.tokenByScope.get(scope);
|
|
107
|
-
let res = await
|
|
113
|
+
let res = await fetchOrThrow(url, withToken(cached), {
|
|
114
|
+
operation: "OCI registry request",
|
|
115
|
+
setting: "the oci:// ref host",
|
|
116
|
+
});
|
|
108
117
|
if (res.status !== 401) return res;
|
|
109
118
|
|
|
110
119
|
const challenge = res.headers.get("www-authenticate");
|
|
@@ -114,7 +123,10 @@ export class OciClient {
|
|
|
114
123
|
this.tokenByScope.set(scope, token);
|
|
115
124
|
// Drain the 401 body so the connection can be reused.
|
|
116
125
|
await res.text().catch(() => {});
|
|
117
|
-
return
|
|
126
|
+
return fetchOrThrow(url, withToken(token), {
|
|
127
|
+
operation: "OCI registry request",
|
|
128
|
+
setting: "the oci:// ref host",
|
|
129
|
+
});
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
/** Exchange a bearer challenge for a token, authenticating to the token
|
|
@@ -136,7 +148,10 @@ export class OciClient {
|
|
|
136
148
|
const basic = Buffer.from(`${cred.username}:${cred.password}`).toString("base64");
|
|
137
149
|
headers.set("authorization", `Basic ${basic}`);
|
|
138
150
|
}
|
|
139
|
-
const res = await
|
|
151
|
+
const res = await fetchOrThrow(tokenUrl.href, { headers }, {
|
|
152
|
+
operation: "OCI registry auth",
|
|
153
|
+
setting: "the oci:// ref host",
|
|
154
|
+
});
|
|
140
155
|
if (!res.ok) return null;
|
|
141
156
|
const body = (await res.json()) as { token?: string; access_token?: string };
|
|
142
157
|
return body.token ?? body.access_token ?? null;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_MANIFEST_FILENAME,
|
|
3
3
|
IntegrityError,
|
|
4
|
+
sha256Base64Url,
|
|
4
5
|
verifyIntegrity,
|
|
5
6
|
type ManifestSource,
|
|
6
7
|
} from "@telorun/analyzer";
|
|
7
8
|
|
|
8
9
|
import { computeFilesIntegrity, injectFilesIntegrity } from "../../bundle/files-integrity.js";
|
|
9
|
-
import { readOwnerManifest } from "../../bundle/module-manifest.js";
|
|
10
|
+
import { readOwnerManifest, type OwnerManifest } from "../../bundle/module-manifest.js";
|
|
10
11
|
import { makeTarGz, readTarGz, toPayloadFiles } from "../../bundle/tar.js";
|
|
11
12
|
import type {
|
|
12
13
|
FetchedArtifact,
|
|
@@ -134,6 +135,39 @@ export class OciTransport implements Transport {
|
|
|
134
135
|
return pullVerified(ref);
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
/** Hashes the **UTF-8 encoding of the extracted `telo.yaml`**, which is what
|
|
139
|
+
* `pullVerified` checks an inline `#sha256-…` pin against on the read path.
|
|
140
|
+
*
|
|
141
|
+
* Cost note: a module is one tar blob, so there is no way to read `telo.yaml`
|
|
142
|
+
* without pulling the whole artifact — including any `files:` payload — and
|
|
143
|
+
* this path is deliberately uncached (a pin must hash what is published
|
|
144
|
+
* *now*, not a cached copy). Two consequences for callers: pinning N imports
|
|
145
|
+
* costs N full artifact pulls, and `pullVerified` also re-checks the
|
|
146
|
+
* dependency's `filesIntegrity`, so a corrupt *payload* upstream surfaces
|
|
147
|
+
* here as a pinning failure rather than a payload error. Both are acceptable
|
|
148
|
+
* for publish-time pinning, where correctness beats latency and refusing to
|
|
149
|
+
* pin against a corrupt dependency is the right outcome. */
|
|
150
|
+
async manifestHash(ref: string): Promise<string> {
|
|
151
|
+
const { manifest } = await pullVerified(ref);
|
|
152
|
+
return `sha256-${await sha256Base64Url(new TextEncoder().encode(manifest))}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Project a module's declared provenance onto the standard
|
|
156
|
+
* `org.opencontainers.image.*` annotation keys. Descriptive only — nothing
|
|
157
|
+
* addresses the artifact by these. Absent fields are omitted rather than
|
|
158
|
+
* written empty, so the manifest carries only what the module declared. */
|
|
159
|
+
private static annotationsFor(identity: OwnerManifest): Record<string, string> {
|
|
160
|
+
const mapped: Array<[string, string | undefined]> = [
|
|
161
|
+
["org.opencontainers.image.title", identity.name],
|
|
162
|
+
["org.opencontainers.image.version", identity.version],
|
|
163
|
+
["org.opencontainers.image.description", identity.description],
|
|
164
|
+
["org.opencontainers.image.source", identity.repository],
|
|
165
|
+
["org.opencontainers.image.licenses", identity.license],
|
|
166
|
+
["org.opencontainers.image.documentation", identity.documentation],
|
|
167
|
+
];
|
|
168
|
+
return Object.fromEntries(mapped.filter((e): e is [string, string] => Boolean(e[1])));
|
|
169
|
+
}
|
|
170
|
+
|
|
137
171
|
async publish(
|
|
138
172
|
destination: string,
|
|
139
173
|
bundle: PublishBundle,
|
|
@@ -144,19 +178,19 @@ export class OciTransport implements Transport {
|
|
|
144
178
|
throw new Error("OCI publish requires metadata.version (used as the tag).");
|
|
145
179
|
}
|
|
146
180
|
|
|
147
|
-
// Destination
|
|
148
|
-
//
|
|
181
|
+
// Destination must be a full repo (`oci://host/repo`). Identity is the ref,
|
|
182
|
+
// so the repo is never derived from `metadata.namespace`/`name` — a
|
|
183
|
+
// metadata-derived path is wrong whenever the repo differs from the name,
|
|
184
|
+
// and would silently push to a namespace the publisher may not own.
|
|
149
185
|
const afterScheme = destination.replace(/^oci:\/\//, "").replace(/\/+$/, "");
|
|
150
186
|
const slash = afterScheme.indexOf("/");
|
|
151
187
|
const host = slash > 0 ? afterScheme.slice(0, slash) : afterScheme;
|
|
152
|
-
|
|
188
|
+
const repo = slash > 0 ? afterScheme.slice(slash + 1) : "";
|
|
153
189
|
if (!repo) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
`
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
repo = `${identity.namespace}/${identity.name}`;
|
|
190
|
+
throw new Error(
|
|
191
|
+
`OCI publish destination '${destination}' is host-only — it must name a full repository, ` +
|
|
192
|
+
`e.g. 'oci://${host || "ghcr.io"}/<org>/<name>'.`,
|
|
193
|
+
);
|
|
160
194
|
}
|
|
161
195
|
const tag = identity.version;
|
|
162
196
|
|
|
@@ -173,12 +207,14 @@ export class OciTransport implements Transport {
|
|
|
173
207
|
const client = new OciClient(host, repo);
|
|
174
208
|
const layerDigest = await client.pushBlob(tar);
|
|
175
209
|
const config = await client.pushEmptyConfig();
|
|
210
|
+
const annotations = OciTransport.annotationsFor(identity);
|
|
176
211
|
const manifest: OciManifest = {
|
|
177
212
|
schemaVersion: 2,
|
|
178
213
|
mediaType: OCI_MANIFEST_MEDIA_TYPE,
|
|
179
214
|
artifactType: TELO_LAYER_MEDIA_TYPE,
|
|
180
215
|
config,
|
|
181
216
|
layers: [{ mediaType: TELO_LAYER_MEDIA_TYPE, digest: layerDigest, size: tar.length }],
|
|
217
|
+
...(Object.keys(annotations).length > 0 ? { annotations } : {}),
|
|
182
218
|
};
|
|
183
219
|
await client.pushManifest(tag, manifest);
|
|
184
220
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
splitIntegrity,
|
|
10
10
|
type ManifestSource,
|
|
11
11
|
} from "@telorun/analyzer";
|
|
12
|
+
import { fetchOrThrow } from "@telorun/sdk";
|
|
12
13
|
import { createHash } from "crypto";
|
|
13
14
|
|
|
14
15
|
import { computeFilesIntegrity, injectFilesIntegrity } from "../bundle/files-integrity.js";
|
|
@@ -159,7 +160,11 @@ export class RegistryTransport implements Transport {
|
|
|
159
160
|
const { modulePath } = parseModuleRef(ref);
|
|
160
161
|
const url = `${this.registryUrl.replace(/\/+$/, "")}/${modulePath}`;
|
|
161
162
|
await assertPublicEgress(url);
|
|
162
|
-
const res = await
|
|
163
|
+
const res = await fetchOrThrow(
|
|
164
|
+
url,
|
|
165
|
+
{ headers: { accept: "application/json" } },
|
|
166
|
+
{ operation: "Registry version list", setting: "--registry / TELO_REGISTRY" },
|
|
167
|
+
);
|
|
163
168
|
if (res.status === 404) return null;
|
|
164
169
|
if (!res.ok) {
|
|
165
170
|
throw new Error(`Registry returned ${res.status} ${res.statusText} for ${modulePath}`);
|
|
@@ -168,22 +173,31 @@ export class RegistryTransport implements Transport {
|
|
|
168
173
|
return Array.isArray(body.versions) ? body.versions : [];
|
|
169
174
|
}
|
|
170
175
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
/** Mirrors the sources' fetch-URL derivation: a direct URL points at (or
|
|
177
|
+
* contains) the YAML file; a bare registry ref folds into the registry
|
|
178
|
+
* layout. `null` when this transport does not own the ref's shape. */
|
|
179
|
+
private manifestUrl(ref: string): string | null {
|
|
175
180
|
const { base } = splitIntegrity(ref);
|
|
176
|
-
let fetchUrl: string;
|
|
177
181
|
if (base.startsWith("http://") || base.startsWith("https://")) {
|
|
178
|
-
|
|
179
|
-
}
|
|
182
|
+
return base.includes(".yaml") ? base : `${base}/${DEFAULT_MANIFEST_FILENAME}`;
|
|
183
|
+
}
|
|
184
|
+
if (isRegistryRef(ref)) {
|
|
180
185
|
const { modulePath, version } = parseModuleRef(ref);
|
|
181
|
-
|
|
182
|
-
} else {
|
|
183
|
-
return null;
|
|
186
|
+
return `${this.registryUrl.replace(/\/+$/, "")}/${modulePath}/${version}/${DEFAULT_MANIFEST_FILENAME}`;
|
|
184
187
|
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async digest(ref: string): Promise<string | null> {
|
|
192
|
+
// The digest is Telo's canonical hash over the `telo.yaml` bytes — the same
|
|
193
|
+
// value `manifestHash` returns, but absent-is-null rather than a throw.
|
|
194
|
+
const fetchUrl = this.manifestUrl(ref);
|
|
195
|
+
if (!fetchUrl) return null;
|
|
185
196
|
await assertPublicEgress(fetchUrl);
|
|
186
|
-
const res = await
|
|
197
|
+
const res = await fetchOrThrow(fetchUrl, undefined, {
|
|
198
|
+
operation: "Registry manifest read",
|
|
199
|
+
setting: "--registry / TELO_REGISTRY",
|
|
200
|
+
});
|
|
187
201
|
if (res.status === 404) return null;
|
|
188
202
|
if (!res.ok) {
|
|
189
203
|
throw new Error(`Registry returned ${res.status} ${res.statusText} for ${fetchUrl}`);
|
|
@@ -192,6 +206,25 @@ export class RegistryTransport implements Transport {
|
|
|
192
206
|
return `sha256-${await sha256Base64Url(bytes)}`;
|
|
193
207
|
}
|
|
194
208
|
|
|
209
|
+
/** Hashes the **raw response bytes**, which is exactly what `verifiedFetch`
|
|
210
|
+
* checks an inline `#sha256-…` pin against on the read path. */
|
|
211
|
+
async manifestHash(ref: string): Promise<string> {
|
|
212
|
+
const fetchUrl = this.manifestUrl(ref);
|
|
213
|
+
if (!fetchUrl) {
|
|
214
|
+
throw new Error(`cannot hash non-remote import '${ref}'`);
|
|
215
|
+
}
|
|
216
|
+
await assertPublicEgress(fetchUrl);
|
|
217
|
+
const res = await fetchOrThrow(fetchUrl, undefined, {
|
|
218
|
+
operation: "Registry manifest hash",
|
|
219
|
+
setting: "--registry / TELO_REGISTRY",
|
|
220
|
+
});
|
|
221
|
+
if (!res.ok) {
|
|
222
|
+
throw new Error(`fetch ${fetchUrl}: ${res.status} ${res.statusText}`);
|
|
223
|
+
}
|
|
224
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
225
|
+
return `sha256-${await sha256Base64Url(bytes)}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
195
228
|
async fetchArtifact(ref: string): Promise<FetchedArtifact> {
|
|
196
229
|
// `read` verifies the manifest bytes against the inline `#sha256-...` hash.
|
|
197
230
|
const { text: manifest, source } = await this.source.read(ref);
|
|
@@ -201,7 +234,10 @@ export class RegistryTransport implements Transport {
|
|
|
201
234
|
// The payload rides beside the manifest as `module.tar.gz`.
|
|
202
235
|
const tarUrl = source.replace(/\/telo\.yaml$/, "/module.tar.gz");
|
|
203
236
|
await assertPublicEgress(tarUrl);
|
|
204
|
-
const res = await
|
|
237
|
+
const res = await fetchOrThrow(tarUrl, undefined, {
|
|
238
|
+
operation: "Module payload download",
|
|
239
|
+
setting: "--registry / TELO_REGISTRY",
|
|
240
|
+
});
|
|
205
241
|
if (!res.ok) {
|
|
206
242
|
throw new Error(`could not fetch bundle ${tarUrl}: ${res.status} ${res.statusText}`);
|
|
207
243
|
}
|
|
@@ -263,7 +299,11 @@ export class RegistryTransport implements Transport {
|
|
|
263
299
|
for (let attempt = 1; attempt <= MAX_PUSH_ATTEMPTS; attempt++) {
|
|
264
300
|
networkErr = null;
|
|
265
301
|
try {
|
|
266
|
-
res = await
|
|
302
|
+
res = await fetchOrThrow(
|
|
303
|
+
url,
|
|
304
|
+
{ method: "PUT", headers, body },
|
|
305
|
+
{ operation: "Registry publish", setting: "--registry / TELO_REGISTRY" },
|
|
306
|
+
);
|
|
267
307
|
} catch (err) {
|
|
268
308
|
networkErr = err;
|
|
269
309
|
res = null;
|
|
@@ -99,6 +99,26 @@ export interface Transport {
|
|
|
99
99
|
* digest per version and re-checks it on every track. */
|
|
100
100
|
digest(ref: string): Promise<string | null>;
|
|
101
101
|
|
|
102
|
+
/** Telo's inline integrity hash (`sha256-<base64url>`) for the `telo.yaml`
|
|
103
|
+
* `ref` resolves to — the value written as a `#sha256-…` pin by `telo
|
|
104
|
+
* publish` and re-pinned by `telo upgrade`. Throws when the ref does not
|
|
105
|
+
* resolve; callers decide whether that is fatal (`--frozen`) or best-effort.
|
|
106
|
+
*
|
|
107
|
+
* This is on the interface, not computed by the caller, because *what gets
|
|
108
|
+
* hashed* is transport-specific and must match exactly what that transport's
|
|
109
|
+
* own `source.read()` verifies — otherwise a pin written at publish fails
|
|
110
|
+
* verification at import. HTTP/registry hash the raw response bytes;
|
|
111
|
+
* OCI hashes the UTF-8 encoding of the `telo.yaml` extracted from the tar
|
|
112
|
+
* layer. A caller cannot know which, so a caller-side scheme branch silently
|
|
113
|
+
* degrades the moment a transport is added — which is exactly how `oci://`
|
|
114
|
+
* refs came to be published unpinned.
|
|
115
|
+
*
|
|
116
|
+
* Distinct from `digest()`: that is an opaque transport-native content id for
|
|
117
|
+
* change detection, never written into a manifest or compared across
|
|
118
|
+
* transports. This is the portable, cross-transport hash Telo itself
|
|
119
|
+
* verifies. */
|
|
120
|
+
manifestHash(ref: string): Promise<string>;
|
|
121
|
+
|
|
102
122
|
/** Push `bundle` to `destination` (a base ref / repo whose scheme this
|
|
103
123
|
* transport owns), pinning the payload and writing the transport-native
|
|
104
124
|
* artifact shape. Throws on failure. Used by `telo publish`. */
|