@telorun/kernel 0.60.0 → 0.62.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/module-artifact.d.ts +28 -2
- package/dist/bundle/module-artifact.d.ts.map +1 -1
- package/dist/bundle/module-artifact.js +36 -9
- package/dist/bundle/module-artifact.js.map +1 -1
- package/dist/controller-loader.d.ts +7 -4
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-loaders/bundle-loader.d.ts.map +1 -1
- package/dist/controller-loaders/bundle-loader.js +9 -4
- package/dist/controller-loaders/bundle-loader.js.map +1 -1
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +62 -10
- package/dist/controller-loaders/npm-loader.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 +57 -10
- package/dist/controllers/resource-definition/resource-inherited-controller.js.map +1 -1
- package/dist/controllers/type/json-schema-controller.d.ts +8 -0
- package/dist/controllers/type/json-schema-controller.d.ts.map +1 -0
- package/dist/controllers/type/json-schema-controller.js +91 -0
- package/dist/controllers/type/json-schema-controller.js.map +1 -0
- package/dist/evaluation-context.d.ts +5 -0
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +63 -33
- package/dist/evaluation-context.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init-failure-diagnostics.d.ts +61 -0
- package/dist/init-failure-diagnostics.d.ts.map +1 -0
- package/dist/init-failure-diagnostics.js +141 -0
- package/dist/init-failure-diagnostics.js.map +1 -0
- package/dist/invocation-contract-binding.d.ts +105 -0
- package/dist/invocation-contract-binding.d.ts.map +1 -0
- package/dist/invocation-contract-binding.js +296 -0
- package/dist/invocation-contract-binding.js.map +1 -0
- package/dist/kernel.d.ts +17 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +76 -25
- package/dist/kernel.js.map +1 -1
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +21 -0
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +45 -0
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +79 -0
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-compiled-values.d.ts +9 -1
- package/dist/schema-compiled-values.d.ts.map +1 -1
- package/dist/schema-compiled-values.js +55 -16
- package/dist/schema-compiled-values.js.map +1 -1
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +15 -1
- package/dist/schema-validator.js.map +1 -1
- package/dist/static-analysis-diagnostics.d.ts +19 -0
- package/dist/static-analysis-diagnostics.d.ts.map +1 -0
- package/dist/static-analysis-diagnostics.js +52 -0
- package/dist/static-analysis-diagnostics.js.map +1 -0
- package/dist/transports/oci/oci-transport.d.ts +17 -0
- package/dist/transports/oci/oci-transport.d.ts.map +1 -1
- package/dist/transports/oci/oci-transport.js +33 -8
- package/dist/transports/oci/oci-transport.js.map +1 -1
- package/dist/transports/transport-registry.d.ts +6 -4
- package/dist/transports/transport-registry.d.ts.map +1 -1
- package/dist/transports/transport-registry.js +6 -4
- package/dist/transports/transport-registry.js.map +1 -1
- package/package.json +4 -4
- package/src/bundle/module-artifact.ts +54 -12
- package/src/controller-loader.ts +7 -4
- package/src/controller-loaders/bundle-loader.ts +9 -4
- package/src/controller-loaders/npm-loader.ts +75 -13
- package/src/controllers/resource-definition/resource-inherited-controller.ts +69 -10
- package/src/controllers/type/json-schema-controller.ts +114 -0
- package/src/evaluation-context.ts +81 -32
- package/src/index.ts +3 -1
- package/src/init-failure-diagnostics.ts +169 -0
- package/src/invocation-contract-binding.ts +392 -0
- package/src/kernel.ts +95 -23
- package/src/module-context.ts +27 -0
- package/src/resource-context.ts +81 -0
- package/src/schema-compiled-values.ts +55 -15
- package/src/schema-validator.ts +15 -1
- package/src/static-analysis-diagnostics.ts +51 -0
- package/src/transports/oci/oci-transport.ts +34 -8
- package/src/transports/transport-registry.ts +6 -4
package/src/resource-context.ts
CHANGED
|
@@ -174,6 +174,87 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
174
174
|
return this.validator.getTypeRules(name);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/** The JSON Schema behind a type field, resolved the same four ways
|
|
178
|
+
* {@link createTypeValidator} resolves it (named ref, `{kind, name}` ref
|
|
179
|
+
* object, inline `{kind, schema}`, raw schema), and then followed through a
|
|
180
|
+
* bare `telo://<module>/<Type>` `$ref` to the schema that type registered.
|
|
181
|
+
*
|
|
182
|
+
* Following the `$ref` matters because AJV resolves cross-schema references
|
|
183
|
+
* against its own registry at compile time, while the kernel's registry is
|
|
184
|
+
* what actually holds these — a `Type.JsonSchema` registers under the
|
|
185
|
+
* canonical URI as a *key*, which AJV does not treat as a resolvable id. A
|
|
186
|
+
* definition whose whole contract is `{ $ref: "telo://Self/TokenSet" }` (the
|
|
187
|
+
* sanctioned way to declare a shape once and reference it from several kinds)
|
|
188
|
+
* would otherwise be uncompilable at dispatch. Resolving here means AJV is
|
|
189
|
+
* handed the real schema and never has to resolve the reference at all.
|
|
190
|
+
*
|
|
191
|
+
* Contract binding needs the schema rather than just a compiled validator
|
|
192
|
+
* anyway, for the decisions a validator cannot answer: which properties carry
|
|
193
|
+
* `x-telo-stream` and must be exempt from the walk, and which paths a
|
|
194
|
+
* `default:` can be written to. Returns undefined when the reference resolves
|
|
195
|
+
* to nothing. */
|
|
196
|
+
resolveTypeSchema(typeRef: unknown): Record<string, any> | undefined {
|
|
197
|
+
return this.followTypeAlias(this.readTypeSchema(typeRef), new Set());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private readTypeSchema(typeRef: unknown): Record<string, any> | undefined {
|
|
201
|
+
if (!typeRef) return undefined;
|
|
202
|
+
if (typeof typeRef === "string") return this.validator.getSchema(typeRef) as any;
|
|
203
|
+
if (typeof typeRef !== "object") return undefined;
|
|
204
|
+
const ref = typeRef as Record<string, any>;
|
|
205
|
+
if (ref.schema && typeof ref.schema === "object") return ref.schema;
|
|
206
|
+
if (typeof ref.name === "string") return this.validator.getSchema(ref.name) as any;
|
|
207
|
+
if (ref.type || ref.properties || ref.$ref) return ref;
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Follow a schema that is nothing but a `$ref` to a registered type, so the
|
|
213
|
+
* schema-level questions (which properties are streams, which paths carry a
|
|
214
|
+
* default) are asked of the real shape rather than of an alias.
|
|
215
|
+
*
|
|
216
|
+
* Only the whole-document alias form is followed, and only to READ it — the
|
|
217
|
+
* schema handed to AJV keeps its `$ref`s intact, because AJV resolves them
|
|
218
|
+
* itself against the registered ids and each type stays its own document with
|
|
219
|
+
* its own `$defs`. Inlining instead would move a `$ref: "#/$defs/X"` out of the
|
|
220
|
+
* document that defines `$defs.X`.
|
|
221
|
+
*
|
|
222
|
+
* `seen` guards a cycle two mutually-referencing types would otherwise spin on.
|
|
223
|
+
* A `$ref` alongside other keywords is left alone: that is a composition, not
|
|
224
|
+
* an alias.
|
|
225
|
+
*/
|
|
226
|
+
private followTypeAlias(
|
|
227
|
+
schema: Record<string, any> | undefined,
|
|
228
|
+
seen: Set<string>,
|
|
229
|
+
): Record<string, any> | undefined {
|
|
230
|
+
let current = schema;
|
|
231
|
+
while (
|
|
232
|
+
current &&
|
|
233
|
+
typeof current.$ref === "string" &&
|
|
234
|
+
Object.keys(current).length === 1 &&
|
|
235
|
+
!seen.has(current.$ref)
|
|
236
|
+
) {
|
|
237
|
+
seen.add(current.$ref);
|
|
238
|
+
const target = this.validator.getSchema(current.$ref) as Record<string, any> | undefined;
|
|
239
|
+
if (!target) return current;
|
|
240
|
+
current = target;
|
|
241
|
+
}
|
|
242
|
+
return current;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Compile `schema` but compose the CEL `rules:` registered under `name`.
|
|
246
|
+
*
|
|
247
|
+
* A named type's rules are its business invariants, and they are reachable
|
|
248
|
+
* only through the name. {@link createTypeValidator} composes them when it is
|
|
249
|
+
* handed a bare name, but a caller that must adjust the schema first — the
|
|
250
|
+
* contract binding, which strips `x-telo-stream` properties before validating
|
|
251
|
+
* — would otherwise have to choose between the adjustment and the rules. */
|
|
252
|
+
createTypeValidatorWithRules(name: string | undefined, schema: Record<string, any>) {
|
|
253
|
+
const base = this.validator.compile(schema);
|
|
254
|
+
const rules = name ? this.validator.getTypeRules(name) : undefined;
|
|
255
|
+
return rules && rules.length > 0 ? this.validator.composeWithRules(base, name!, rules) : base;
|
|
256
|
+
}
|
|
257
|
+
|
|
177
258
|
createTypeValidator(typeRef: string | Record<string, any> | undefined) {
|
|
178
259
|
if (!typeRef) return new NoopValidator();
|
|
179
260
|
|
|
@@ -65,27 +65,67 @@ function collectSchemaProperties(
|
|
|
65
65
|
* Template strings were compiled from YAML at load time; this restores a shape
|
|
66
66
|
* that AJV can validate without evaluating expressions. When no schema is
|
|
67
67
|
* supplied every compiled value collapses to `""` (the `default` branch of
|
|
68
|
-
* `placeholderForSchema`), matching the schema-unaware strip.
|
|
68
|
+
* `placeholderForSchema`), matching the schema-unaware strip.
|
|
69
|
+
*
|
|
70
|
+
* The walk stops short of anything that is not plain config, mirroring
|
|
71
|
+
* `buildResolvedProperties`: by the time a resource reaches validation its ref
|
|
72
|
+
* slots may already hold LIVE resource instances (a template passing the
|
|
73
|
+
* caller's client down with `client: !cel "self.client"` hands the child the
|
|
74
|
+
* injected instance, not a ref), and a controller's object graph is
|
|
75
|
+
* arbitrarily deep and routinely cyclic — walking one overflows the stack
|
|
76
|
+
* instead of producing a diagnostic, and there is nothing inside it to strip. */
|
|
69
77
|
export function stripCompiledValues(
|
|
70
78
|
v: unknown,
|
|
71
79
|
schema: Record<string, unknown> = {},
|
|
72
80
|
rootSchema?: Record<string, unknown>,
|
|
73
81
|
): unknown {
|
|
74
82
|
const root = rootSchema ?? schema;
|
|
75
|
-
|
|
83
|
+
// Ancestors on the current path, so a genuine cycle stops while a sub-object
|
|
84
|
+
// that merely appears twice is still stripped both times.
|
|
85
|
+
const ancestors = new Set<object>();
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
const walk = (value: unknown, nodeSchema: Record<string, unknown>): unknown => {
|
|
88
|
+
const resolved = resolveSchemaRef(nodeSchema, root);
|
|
89
|
+
|
|
90
|
+
if (isCompiledValue(value)) return placeholderForSchema(resolved);
|
|
91
|
+
// A slot the schema declares as a reference is never config: it holds a
|
|
92
|
+
// `{kind, name}` ref or the live instance Phase 5 replaced it with, and the
|
|
93
|
+
// schema declares no shape to validate against either way.
|
|
94
|
+
if (resolved["x-telo-ref"] !== undefined) return value;
|
|
95
|
+
|
|
96
|
+
if (Array.isArray(value)) {
|
|
97
|
+
const itemSchema = resolveSchemaRef((resolved.items ?? {}) as Record<string, unknown>, root);
|
|
98
|
+
return walkGuarded(value, () => value.map((item) => walk(item, itemSchema)));
|
|
87
99
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
if (value !== null && typeof value === "object") {
|
|
101
|
+
// A class instance (a client, a pool, a stream) carries no CompiledValues
|
|
102
|
+
// and is not described by the schema — copying it is pure risk.
|
|
103
|
+
const proto = Object.getPrototypeOf(value);
|
|
104
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
105
|
+
|
|
106
|
+
const props = collectSchemaProperties(resolved);
|
|
107
|
+
return walkGuarded(value, () => {
|
|
108
|
+
const out: Record<string, unknown> = {};
|
|
109
|
+
for (const [k, val] of Object.entries(value as Record<string, unknown>)) {
|
|
110
|
+
out[k] = walk(val, props[k] ?? {});
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Runs `fn` with `node` marked as an ancestor; a node already on the path is
|
|
119
|
+
* a cycle and is returned as-is rather than recursed into. */
|
|
120
|
+
const walkGuarded = (node: object, fn: () => unknown): unknown => {
|
|
121
|
+
if (ancestors.has(node)) return node;
|
|
122
|
+
ancestors.add(node);
|
|
123
|
+
try {
|
|
124
|
+
return fn();
|
|
125
|
+
} finally {
|
|
126
|
+
ancestors.delete(node);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return walk(v, schema);
|
|
91
131
|
}
|
package/src/schema-validator.ts
CHANGED
|
@@ -8,6 +8,20 @@ import * as fs from "node:fs";
|
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { formatAjvErrors } from "./manifest-schemas.js";
|
|
11
|
+
|
|
12
|
+
/** Render a value for an error message without ever throwing.
|
|
13
|
+
*
|
|
14
|
+
* `JSON.stringify` refuses BigInt, and CEL evaluates an integer literal to one —
|
|
15
|
+
* so serializing the offending data threw from inside the message template and
|
|
16
|
+
* the thrown stringify error REPLACED the validation failure. The author was
|
|
17
|
+
* told "cannot serialize BigInt" instead of which field was wrong. */
|
|
18
|
+
function describeValue(data: unknown): string {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(data, (_k, v) => (typeof v === "bigint" ? `${v}` : v)) ?? String(data);
|
|
21
|
+
} catch {
|
|
22
|
+
return String(data);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
11
25
|
import {
|
|
12
26
|
EXACT_TEMPLATE_REGEX,
|
|
13
27
|
isTaggedSentinel,
|
|
@@ -297,7 +311,7 @@ export class SchemaValidator {
|
|
|
297
311
|
if (!isValid) {
|
|
298
312
|
throw new RuntimeError(
|
|
299
313
|
"ERR_RESOURCE_SCHEMA_VALIDATION_FAILED",
|
|
300
|
-
`Invalid value passed: ${
|
|
314
|
+
`Invalid value passed: ${describeValue(data)}. Error: ${formatAjvErrors(validate.errors)}`,
|
|
301
315
|
);
|
|
302
316
|
}
|
|
303
317
|
},
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DiagnosticSeverity, type AnalysisDiagnostic } from "@telorun/analyzer";
|
|
2
|
+
import type { DiagnosticOrigin, RuntimeDiagnostic } from "@telorun/sdk";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Carry a static-analysis diagnostic into the runtime failure set.
|
|
6
|
+
*
|
|
7
|
+
* The analyzer's `data` — the file, the field path within it, and the owning
|
|
8
|
+
* resource — travels as `origin`, alongside the diagnostic's own `range` for
|
|
9
|
+
* the failures that have no field path to look up (a YAML parse error knows
|
|
10
|
+
* where the syntax broke but has no parsed tree to index). That is what lets a
|
|
11
|
+
* renderer resolve the same `file:line:col` `telo check` prints; flattening it
|
|
12
|
+
* into `message` leaves `telo run` pointing at nothing, which is the whole
|
|
13
|
+
* reason the two commands used to disagree about one error.
|
|
14
|
+
*
|
|
15
|
+
* The sibling of `init-failure-diagnostics.ts`: both turn a kernel failure set
|
|
16
|
+
* into `RuntimeDiagnostic[]`, one for what static analysis rejected and one for
|
|
17
|
+
* what failed to initialize.
|
|
18
|
+
*/
|
|
19
|
+
export function staticDiagnosticToRuntime(d: AnalysisDiagnostic): RuntimeDiagnostic {
|
|
20
|
+
const data = d.data as DiagnosticOrigin | undefined;
|
|
21
|
+
const origin: DiagnosticOrigin = {};
|
|
22
|
+
if (data?.filePath !== undefined) origin.filePath = data.filePath;
|
|
23
|
+
if (data?.path !== undefined) origin.path = data.path;
|
|
24
|
+
if (data?.resource !== undefined) origin.resource = data.resource;
|
|
25
|
+
if (d.range !== undefined) origin.range = d.range;
|
|
26
|
+
return {
|
|
27
|
+
// Mapped, not assumed: every current caller passes a pre-filtered error set,
|
|
28
|
+
// but a warning routed through here must not be rendered in red and counted
|
|
29
|
+
// toward the exit code. `AnalysisDiagnostic.severity` is optional and its
|
|
30
|
+
// scale runs Error(1) → Hint(4), so anything looser than Error is a warning.
|
|
31
|
+
severity: (d.severity ?? DiagnosticSeverity.Warning) <= DiagnosticSeverity.Error
|
|
32
|
+
? "error"
|
|
33
|
+
: "warning",
|
|
34
|
+
message: d.message,
|
|
35
|
+
code: d.code !== undefined ? String(d.code) : undefined,
|
|
36
|
+
resource: describeResource(data?.resource),
|
|
37
|
+
// Only when something is actually set, so `origin` stays usable as the
|
|
38
|
+
// "this came from static analysis" predicate its contract promises.
|
|
39
|
+
...(Object.keys(origin).length > 0 ? { origin } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `Kind.name`, or whichever half is present — a diagnostic carrying only one
|
|
44
|
+
* of them used to render as `undefined.foo`. */
|
|
45
|
+
function describeResource(
|
|
46
|
+
resource: { kind?: string; name?: string } | undefined,
|
|
47
|
+
): string | undefined {
|
|
48
|
+
if (!resource) return undefined;
|
|
49
|
+
const parts = [resource.kind, resource.name].filter((p): p is string => p !== undefined);
|
|
50
|
+
return parts.length > 0 ? parts.join(".") : undefined;
|
|
51
|
+
}
|
|
@@ -62,9 +62,8 @@ import {
|
|
|
62
62
|
* payload gets a clear "republish" failure at the controller instead, while the
|
|
63
63
|
* npm-backed majority, which ships none, is unaffected.
|
|
64
64
|
*/
|
|
65
|
-
async function pullManifestLayer(ref: string): Promise<string> {
|
|
66
|
-
const {
|
|
67
|
-
const client = new OciClient(host, repo);
|
|
65
|
+
async function pullManifestLayer(ref: string, client: OciClient): Promise<string> {
|
|
66
|
+
const { reference, integrity } = parseOciRef(ref);
|
|
68
67
|
const manifest = await client.pullManifest(reference);
|
|
69
68
|
const layer =
|
|
70
69
|
manifest.layers.find((l) => l.mediaType === TELO_MANIFEST_LAYER_MEDIA_TYPE) ??
|
|
@@ -108,18 +107,44 @@ async function pullManifestLayer(ref: string): Promise<string> {
|
|
|
108
107
|
export class OciTransport implements Transport {
|
|
109
108
|
readonly source: ManifestSource;
|
|
110
109
|
|
|
110
|
+
/** One read-side `OciClient` per `(host, repo)`, for this transport's lifetime.
|
|
111
|
+
*
|
|
112
|
+
* The client caches bearer tokens per scope, but a client built per operation
|
|
113
|
+
* discards that cache immediately — so every manifest and every blob paid its
|
|
114
|
+
* own 401→challenge→token round trip, and with it a `~/.docker/config.json`
|
|
115
|
+
* read and possibly a credential-helper subprocess. Pooling collapses those
|
|
116
|
+
* to one handshake per repository. An expired token still self-heals:
|
|
117
|
+
* `authedFetch` re-runs the challenge on a 401 and replaces the entry.
|
|
118
|
+
*
|
|
119
|
+
* Owned by the instance rather than the module, so a second transport — a
|
|
120
|
+
* test, or a second in-process kernel — never inherits another's credentials.
|
|
121
|
+
* `defaultTransportRegistry` memoizes per registry URL, so the production
|
|
122
|
+
* lifetime is unchanged. Publishing keeps its own client: it already reuses
|
|
123
|
+
* one across the whole push, and a push-scoped token has no reason to
|
|
124
|
+
* outlive the command. */
|
|
125
|
+
private readonly readClients = new Map<string, OciClient>();
|
|
126
|
+
|
|
111
127
|
constructor() {
|
|
112
128
|
this.source = {
|
|
113
129
|
supports: (url) => this.supports(url),
|
|
114
130
|
read: async (url) => {
|
|
115
|
-
const manifest = await pullManifestLayer(url);
|
|
116
131
|
const { host, repo, reference } = parseOciRef(url);
|
|
132
|
+
const manifest = await pullManifestLayer(url, this.readClient(host, repo));
|
|
117
133
|
return { text: manifest, source: `${OCI_SCHEME}${host}/${repo}@${reference}` };
|
|
118
134
|
},
|
|
119
135
|
resolveRelative: (base, relative) => this.resolveRelative(base, relative),
|
|
120
136
|
};
|
|
121
137
|
}
|
|
122
138
|
|
|
139
|
+
private readClient(host: string, repo: string): OciClient {
|
|
140
|
+
const key = `${host}/${repo}`;
|
|
141
|
+
const existing = this.readClients.get(key);
|
|
142
|
+
if (existing) return existing;
|
|
143
|
+
const client = new OciClient(host, repo);
|
|
144
|
+
this.readClients.set(key, client);
|
|
145
|
+
return client;
|
|
146
|
+
}
|
|
147
|
+
|
|
123
148
|
supports(ref: string): boolean {
|
|
124
149
|
return isOciRef(ref);
|
|
125
150
|
}
|
|
@@ -159,7 +184,7 @@ export class OciTransport implements Transport {
|
|
|
159
184
|
|
|
160
185
|
async listVersions(ref: string): Promise<string[] | null> {
|
|
161
186
|
const { host, repo } = parseOciRef(ref);
|
|
162
|
-
const tags = await
|
|
187
|
+
const tags = await this.readClient(host, repo).listTags();
|
|
163
188
|
return tags;
|
|
164
189
|
}
|
|
165
190
|
|
|
@@ -178,7 +203,7 @@ export class OciTransport implements Transport {
|
|
|
178
203
|
|
|
179
204
|
async digest(ref: string): Promise<string | null> {
|
|
180
205
|
const { host, repo, reference } = parseOciRef(ref);
|
|
181
|
-
return
|
|
206
|
+
return this.readClient(host, repo).headManifest(reference);
|
|
182
207
|
}
|
|
183
208
|
|
|
184
209
|
/** Pull one payload layer by the `blob` digest the pinned index supplies. The
|
|
@@ -188,7 +213,7 @@ export class OciTransport implements Transport {
|
|
|
188
213
|
* expected `integrity`. */
|
|
189
214
|
async fetchLayer(ref: string, blobDigest: string): Promise<PayloadFile[]> {
|
|
190
215
|
const { host, repo } = parseOciRef(ref);
|
|
191
|
-
const tar = await
|
|
216
|
+
const tar = await this.readClient(host, repo).pullBlob(blobDigest);
|
|
192
217
|
// Verify the transfer against the digest that addressed it. A registry is
|
|
193
218
|
// not trusted to return the blob that was asked for, and this is the only
|
|
194
219
|
// place the pushed bytes exist — the content digest checked after extraction
|
|
@@ -212,7 +237,8 @@ export class OciTransport implements Transport {
|
|
|
212
237
|
* blob per import rather than a full artifact pull, and a corrupt payload
|
|
213
238
|
* upstream no longer surfaces here as a pinning failure. */
|
|
214
239
|
async manifestHash(ref: string): Promise<string> {
|
|
215
|
-
const
|
|
240
|
+
const { host, repo } = parseOciRef(ref);
|
|
241
|
+
const manifest = await pullManifestLayer(ref, this.readClient(host, repo));
|
|
216
242
|
return `sha256-${await sha256Base64Url(new TextEncoder().encode(manifest))}`;
|
|
217
243
|
}
|
|
218
244
|
|
|
@@ -82,10 +82,12 @@ export function defaultTransports(registryUrl?: string): Transport[] {
|
|
|
82
82
|
const defaultRegistryCache = new Map<string, TransportRegistry>();
|
|
83
83
|
|
|
84
84
|
/** A `TransportRegistry` seeded with {@link defaultTransports}, memoized per
|
|
85
|
-
* `registryUrl`. The default transports
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
85
|
+
* `registryUrl`. The default transports hold no per-call state, so one shared
|
|
86
|
+
* instance per registry URL is safe — and avoids re-instantiating the whole set
|
|
87
|
+
* on hot paths like `cachePathForCanonical`. It is also what gives
|
|
88
|
+
* `OciTransport`'s per-instance read-client pool a process-wide lifetime here,
|
|
89
|
+
* so the bearer-token cache survives across operations without the pool having
|
|
90
|
+
* to be global. */
|
|
89
91
|
export function defaultTransportRegistry(registryUrl?: string): TransportRegistry {
|
|
90
92
|
const key = registryUrl ?? "";
|
|
91
93
|
let cached = defaultRegistryCache.get(key);
|