@telorun/analyzer 0.31.0 → 0.33.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.
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAmB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAwjB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAkyBvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CACP,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,EAI1B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,GACtC,gBAAgB,EAAE;IAmBrB,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAmB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA2wB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IA0yBvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CACP,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,EAI1B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,GACtC,gBAAgB,EAAE;IAmBrB,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
package/dist/analyzer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { canonicalTypeSchemaId } from "@telorun/sdk";
2
- import { defaultRegistry, isTaggedSentinel } from "@telorun/templating";
2
+ import { defaultRegistry, isRefSentinel, isTaggedSentinel } from "@telorun/templating";
3
3
  import { AliasResolver, scopeResolverForModule } from "./alias-resolver.js";
4
4
  import { buildCelEnvironment, buildImportInputCelEnvironment, buildTypedCelEnvironment, } from "./cel-environment.js";
5
5
  import { DefinitionRegistry } from "./definition-registry.js";
@@ -20,6 +20,7 @@ import { extractCelRegionScopes, extractContextsFromSchema, getManifestItem, pat
20
20
  import { buildEvalPaths, evalPathsCover } from "./eval-paths.js";
21
21
  import { validateExtends } from "./validate-extends.js";
22
22
  import { validateNestedInlineResources } from "./validate-nested-inline.js";
23
+ import { validateKindDescriptions } from "./validate-kind-descriptions.js";
23
24
  import { validateProviderCoherence } from "./validate-provider-coherence.js";
24
25
  import { validateReferences } from "./validate-references.js";
25
26
  import { validateReferenceForms } from "./validate-reference-forms.js";
@@ -186,6 +187,59 @@ function gatherPropertySchemas(schema) {
186
187
  }
187
188
  return out;
188
189
  }
190
+ /**
191
+ * Generic, role-driven walk over an `x-telo-step-context` step array. Calls
192
+ * `visit(step, stepPath)` for every step — top-level and nested through the
193
+ * `x-telo-topology-role` forms (`branch`, `branch-list`, `case-map`). This is
194
+ * the single definition of how steps nest, shared by `buildStepContextSchema`
195
+ * (which types `steps.<name>.result`) and `validateStepInvokeReferences` (which
196
+ * checks invoke refs), so the topology contract lives in one place — adding a
197
+ * role or nesting form updates both consumers at once. No resource kind is
198
+ * hardcoded; recursion is driven entirely by the schema annotations.
199
+ */
200
+ function walkStepArray(steps, stepItemSchema, rootSchema, basePath, visit) {
201
+ const dispatchRole = (data, role, itemsSchema, path) => {
202
+ if (role === "branch" && Array.isArray(data)) {
203
+ walkStepArray(data, stepItemSchema, rootSchema, path, visit);
204
+ }
205
+ else if (role === "case-map" && data && typeof data === "object" && !Array.isArray(data)) {
206
+ for (const [caseKey, arr] of Object.entries(data)) {
207
+ if (Array.isArray(arr))
208
+ walkStepArray(arr, stepItemSchema, rootSchema, `${path}.${caseKey}`, visit);
209
+ }
210
+ }
211
+ else if (role === "branch-list" && Array.isArray(data)) {
212
+ const entrySchema = resolveLocalRef(itemsSchema, rootSchema);
213
+ if (!entrySchema)
214
+ return;
215
+ data.forEach((entry, i) => {
216
+ if (!entry || typeof entry !== "object")
217
+ return;
218
+ for (const [subKey, subSchema] of gatherPropertySchemas(entrySchema)) {
219
+ const subRole = subSchema["x-telo-topology-role"];
220
+ if (typeof subRole !== "string")
221
+ continue;
222
+ dispatchRole(entry[subKey], subRole, subSchema.items, `${path}[${i}].${subKey}`);
223
+ }
224
+ });
225
+ }
226
+ };
227
+ steps.forEach((step, i) => {
228
+ if (!step || typeof step !== "object")
229
+ return;
230
+ const s = step;
231
+ const stepPath = `${basePath}[${i}]`;
232
+ visit(s, stepPath);
233
+ if (!stepItemSchema)
234
+ return;
235
+ for (const [propKey, propSchema] of gatherPropertySchemas(stepItemSchema)) {
236
+ const role = propSchema["x-telo-topology-role"];
237
+ if (typeof role !== "string")
238
+ continue;
239
+ dispatchRole(s[propKey], role, propSchema.items, `${stepPath}.${propKey}`);
240
+ }
241
+ });
242
+ }
189
243
  /**
190
244
  * Build a `steps` context schema from `x-telo-step-context` annotation.
191
245
  * Walks each step in the manifest array, resolves the invoked resource's outputType,
@@ -229,80 +283,40 @@ function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases
229
283
  continue;
230
284
  const stepItemSchema = resolveLocalRef(fieldSchema.items, defSchema);
231
285
  const stepProperties = {};
232
- const dispatchRole = (data, role, itemsSchema) => {
233
- if (role === "branch" && Array.isArray(data)) {
234
- collectSteps(data);
235
- }
236
- else if (role === "case-map" && data && typeof data === "object" && !Array.isArray(data)) {
237
- for (const arr of Object.values(data)) {
238
- if (Array.isArray(arr))
239
- collectSteps(arr);
286
+ walkStepArray(steps, stepItemSchema, defSchema, fieldName, (s) => {
287
+ const name = s.name;
288
+ const invoke = s[invokeField];
289
+ // Only invoke steps register a `steps.<name>.result` entry — control-flow
290
+ // wrappers (try/if/while/switch/throw) don't produce a result and must
291
+ // not shadow real entries with a permissive `additionalProperties: true`,
292
+ // or unknown step references slip through chain validation.
293
+ if (typeof name !== "string" || !invoke || typeof invoke !== "object")
294
+ return;
295
+ let outputSchema;
296
+ const invokedKind = invoke.kind;
297
+ const invokedName = invoke.name;
298
+ if (invokedName) {
299
+ const invokedManifest = allManifests.find((m) => m.metadata?.name === invokedName &&
300
+ (!invokedKind || m.kind === invokedKind));
301
+ if (invokedManifest) {
302
+ outputSchema = resolveTypeFieldToSchema(invokedManifest[outputTypeField], allManifests);
240
303
  }
241
304
  }
242
- else if (role === "branch-list" && Array.isArray(data)) {
243
- const entrySchema = resolveLocalRef(itemsSchema, defSchema);
244
- if (!entrySchema)
245
- return;
246
- for (const entry of data) {
247
- if (!entry || typeof entry !== "object")
248
- continue;
249
- for (const [subKey, subSchema] of gatherPropertySchemas(entrySchema)) {
250
- const subRole = subSchema["x-telo-topology-role"];
251
- if (typeof subRole !== "string")
252
- continue;
253
- dispatchRole(entry[subKey], subRole, subSchema.items);
254
- }
255
- }
305
+ else {
306
+ outputSchema = resolveTypeFieldToSchema(invoke[outputTypeField], allManifests);
256
307
  }
257
- };
258
- function collectSteps(items) {
259
- for (const step of items) {
260
- if (!step || typeof step !== "object")
261
- continue;
262
- const s = step;
263
- const name = s.name;
264
- const invoke = s[invokeField];
265
- // Only invoke steps register a `steps.<name>.result` entry — control-flow
266
- // wrappers (try/if/while/switch/throw) don't produce a result and must
267
- // not shadow real entries with a permissive `additionalProperties: true`,
268
- // or unknown step references slip through chain validation.
269
- if (typeof name === "string" && invoke && typeof invoke === "object") {
270
- let outputSchema;
271
- const invokedKind = invoke.kind;
272
- const invokedName = invoke.name;
273
- if (invokedName) {
274
- const invokedManifest = allManifests.find((m) => m.metadata?.name === invokedName &&
275
- (!invokedKind || m.kind === invokedKind));
276
- if (invokedManifest) {
277
- outputSchema = resolveTypeFieldToSchema(invokedManifest[outputTypeField], allManifests);
278
- }
279
- }
280
- else {
281
- outputSchema = resolveTypeFieldToSchema(invoke[outputTypeField], allManifests);
282
- }
283
- // Fallback: pull outputType from the kind's Telo.Definition. The
284
- // resource manifest typically doesn't carry outputType; the def does.
285
- if (!outputSchema && invokedKind) {
286
- outputSchema = lookupDefinitionTypeField(invokedKind, outputTypeField, defs, aliases, allManifests);
287
- }
288
- stepProperties[name] = {
289
- type: "object",
290
- properties: {
291
- result: outputSchema ?? { type: "object", additionalProperties: true },
292
- },
293
- };
294
- }
295
- if (stepItemSchema) {
296
- for (const [propKey, propSchema] of gatherPropertySchemas(stepItemSchema)) {
297
- const role = propSchema["x-telo-topology-role"];
298
- if (typeof role !== "string")
299
- continue;
300
- dispatchRole(s[propKey], role, propSchema.items);
301
- }
302
- }
308
+ // Fallback: pull outputType from the kind's Telo.Definition. The
309
+ // resource manifest typically doesn't carry outputType; the def does.
310
+ if (!outputSchema && invokedKind) {
311
+ outputSchema = lookupDefinitionTypeField(invokedKind, outputTypeField, defs, aliases, allManifests);
303
312
  }
304
- }
305
- collectSteps(steps);
313
+ stepProperties[name] = {
314
+ type: "object",
315
+ properties: {
316
+ result: outputSchema ?? { type: "object", additionalProperties: true },
317
+ },
318
+ };
319
+ });
306
320
  if (Object.keys(stepProperties).length > 0) {
307
321
  return {
308
322
  type: "object",
@@ -312,6 +326,178 @@ function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases
312
326
  }
313
327
  return undefined;
314
328
  }
329
+ /**
330
+ * Capabilities whose instances structurally expose no `invoke`/`run` method, so
331
+ * a step `invoke` of one always fails at runtime with ERR_RESOURCE_NOT_INVOKABLE
332
+ * (kernel dispatch checks method presence, not capability — evaluation-context.ts).
333
+ * `Telo.Service` is intentionally absent: some services are invocable (a function
334
+ * handler dispatched directly, e.g. `Lambda.Function`), so it can't be rejected
335
+ * statically without false positives. This is the sound subset of the runtime rule.
336
+ */
337
+ const NON_INVOKABLE_CAPABILITIES = new Set([
338
+ "Telo.Provider",
339
+ "Telo.Mount",
340
+ "Telo.Type",
341
+ "Telo.Template",
342
+ ]);
343
+ /**
344
+ * Validate `x-telo-step-context` step `invoke` references (e.g. `Run.Sequence`
345
+ * steps).
346
+ *
347
+ * The reference field map deliberately does NOT descend into step `invoke`
348
+ * slots — they sit behind a local `$ref` to the shared step definition, and
349
+ * turning the descent on would make Phase 5 inject live instances there,
350
+ * breaking the invoke dispatch path (see `reference-field-map.ts`). A
351
+ * consequence is that `validateReferences` never sees these slots, so a bad
352
+ * step invoke passes `telo check` and only fails at runtime. This pass covers
353
+ * exactly those slots, in two dimensions:
354
+ * - Existence: an `invoke: !ref <name>` that names a missing instance — or a
355
+ * *kind* instead of an exported instance (`!ref Stream.Of`) — is a still-a-
356
+ * sentinel after Phase 2.5 resolution → `UNRESOLVED_REFERENCE` (runtime
357
+ * `ERR_RESOURCE_NOT_FOUND`).
358
+ * - Invokability: a resolved instance whose capability structurally has no
359
+ * invoke/run method (`NON_INVOKABLE_CAPABILITIES`) → `REFERENCE_KIND_MISMATCH`
360
+ * (runtime `ERR_RESOURCE_NOT_INVOKABLE`).
361
+ *
362
+ * Generic and topology-driven — it walks steps via the same `x-telo-step-context`
363
+ * / `x-telo-topology-role` annotations `buildStepContextSchema` uses (through the
364
+ * shared `walkStepArray`), so nested branches (then/else/do/catch/cases) are
365
+ * covered and no `Run.Sequence` field name is hardcoded. The cross-module
366
+ * partial-analysis guard mirrors `validateReferences`, so a reference into an
367
+ * unloaded import is skipped rather than false-flagged.
368
+ */
369
+ function validateStepInvokeReferences(allManifests, defs, aliases) {
370
+ const diagnostics = [];
371
+ // Local instance names + loaded-module set — same construction as
372
+ // validateReferences, so the cross-module guard behaves identically.
373
+ const localNames = new Set();
374
+ const loadedModules = new Set();
375
+ // Also collect names of resources nested inside a manifest tree — notably
376
+ // `with:`-scoped resources (an `x-telo-scope` region the field map does not
377
+ // extract to a top-level manifest). A step can invoke one by bare name, so
378
+ // omitting them would false-flag a valid `!ref`. Conservative: any nested
379
+ // object carrying both a `kind` and a `metadata.name` is a resource
380
+ // definition; scope visibility is left to the runtime.
381
+ const collectNestedNames = (value) => {
382
+ if (!value || typeof value !== "object" || isTaggedSentinel(value))
383
+ return;
384
+ if (Array.isArray(value)) {
385
+ for (const item of value)
386
+ collectNestedNames(item);
387
+ return;
388
+ }
389
+ const obj = value;
390
+ const name = obj.metadata?.name;
391
+ if (typeof obj.kind === "string" && typeof name === "string")
392
+ localNames.add(name);
393
+ for (const v of Object.values(obj))
394
+ collectNestedNames(v);
395
+ };
396
+ for (const r of allManifests) {
397
+ if (r.kind === "Telo.Import") {
398
+ const m = r.metadata?.resolvedModuleName;
399
+ if (typeof m === "string")
400
+ loadedModules.add(m);
401
+ continue;
402
+ }
403
+ const meta = r.metadata;
404
+ if (typeof meta?.name !== "string" || REF_VALIDATION_SKIP_KINDS.has(r.kind))
405
+ continue;
406
+ if (meta.forwardedExport === true) {
407
+ if (typeof meta.module === "string")
408
+ loadedModules.add(meta.module);
409
+ continue;
410
+ }
411
+ localNames.add(meta.name);
412
+ collectNestedNames(r);
413
+ }
414
+ const validateInvoke = (value, resource, filePath, path) => {
415
+ if (isRefSentinel(value)) {
416
+ // An unresolved `!ref` is a miss: a real instance would have resolved to
417
+ // `{kind, name}` in Phase 2.5.
418
+ const refName = value.source;
419
+ const dot = refName.indexOf(".");
420
+ const aliasPrefix = dot > 0 ? refName.slice(0, dot) : undefined;
421
+ if (aliasPrefix && aliasPrefix !== "Self" && aliases.hasAlias(aliasPrefix)) {
422
+ const module = aliases.moduleForAlias(aliasPrefix);
423
+ // Partial single-file analysis (import not loaded) — skip to avoid a false miss.
424
+ if (module && !loadedModules.has(module))
425
+ return;
426
+ diagnostics.push({
427
+ severity: DiagnosticSeverity.Error,
428
+ code: "UNRESOLVED_REFERENCE",
429
+ source: SOURCE,
430
+ message: `${resource.kind}/${resource.name}: step invoke at '${path}' → '${refName}' is not an exported instance of module '${module ?? aliasPrefix}' (reference a declared instance, not a kind)`,
431
+ data: { resource, filePath, path },
432
+ });
433
+ return;
434
+ }
435
+ const localName = aliasPrefix === "Self" ? refName.slice(dot + 1) : refName;
436
+ if (localNames.has(localName))
437
+ return;
438
+ diagnostics.push({
439
+ severity: DiagnosticSeverity.Error,
440
+ code: "UNRESOLVED_REFERENCE",
441
+ source: SOURCE,
442
+ message: `${resource.kind}/${resource.name}: step invoke at '${path}' → resource '${localName}' not found`,
443
+ data: { resource, filePath, path },
444
+ });
445
+ return;
446
+ }
447
+ // Resolved `{kind, name}` (or an inline `{kind, …}` definition) — the
448
+ // instance exists. Mirror the kernel's ERR_RESOURCE_NOT_INVOKABLE, which
449
+ // fires when the instance has neither an `invoke` nor a `run` method
450
+ // (evaluation-context.ts). That is a per-instance property, not a pure
451
+ // capability, so only capabilities that STRUCTURALLY expose no such method
452
+ // are rejected statically — Service is intentionally excluded, since some
453
+ // services are invocable (e.g. a function handler dispatched directly).
454
+ if (!value || typeof value !== "object" || Array.isArray(value))
455
+ return;
456
+ const kind = value.kind;
457
+ if (typeof kind !== "string")
458
+ return;
459
+ const capability = defs.resolve(aliases.resolveKind(kind) ?? kind)?.capability;
460
+ if (typeof capability === "string" && NON_INVOKABLE_CAPABILITIES.has(capability)) {
461
+ diagnostics.push({
462
+ severity: DiagnosticSeverity.Error,
463
+ code: "REFERENCE_KIND_MISMATCH",
464
+ source: SOURCE,
465
+ message: `${resource.kind}/${resource.name}: step invoke at '${path}' → '${kind}' is a ${capability} and cannot be invoked in a step — it has no invoke or run method (runtime ERR_RESOURCE_NOT_INVOKABLE)`,
466
+ data: { resource, filePath, path },
467
+ });
468
+ }
469
+ };
470
+ for (const m of allManifests) {
471
+ const meta = m.metadata;
472
+ if (typeof meta?.name !== "string" ||
473
+ REF_VALIDATION_SKIP_KINDS.has(m.kind) ||
474
+ meta.forwardedExport === true)
475
+ continue;
476
+ const def = defs.resolve(aliases.resolveKind(m.kind) ?? m.kind);
477
+ const defSchema = def?.schema;
478
+ if (!defSchema?.properties)
479
+ continue;
480
+ const resource = { kind: m.kind, name: meta.name };
481
+ const filePath = typeof meta.source === "string" ? meta.source : undefined;
482
+ for (const [fieldName, fieldSchema] of Object.entries(defSchema.properties)) {
483
+ const stepCtx = fieldSchema["x-telo-step-context"];
484
+ const invokeField = stepCtx?.invoke;
485
+ if (!invokeField)
486
+ continue;
487
+ const steps = m[fieldName];
488
+ if (!Array.isArray(steps))
489
+ continue;
490
+ const stepItemSchema = resolveLocalRef(fieldSchema.items, defSchema);
491
+ walkStepArray(steps, stepItemSchema, defSchema, fieldName, (s, stepPath) => {
492
+ const invoke = s[invokeField];
493
+ if (invoke === undefined || invoke === null)
494
+ return;
495
+ validateInvoke(invoke, resource, filePath, `${stepPath}.${invokeField}`);
496
+ });
497
+ }
498
+ }
499
+ return diagnostics;
500
+ }
315
501
  /**
316
502
  * Collect every field annotated with `x-telo-error-context` anywhere in a
317
503
  * definition schema (resolving local `$ref`s into `$defs`, cycle-safe), mapping
@@ -1177,10 +1363,16 @@ export class StaticAnalyzer {
1177
1363
  }, { aliases });
1178
1364
  // Validate resource references (Phase 3)
1179
1365
  diagnostics.push(...validateReferences(allManifests, { aliases, definitions: defs, aliasesByModule }));
1366
+ // Validate step `invoke` references — the slots the reference field map
1367
+ // deliberately skips (behind the step `$ref`), so a missing instance or a
1368
+ // kind-instead-of-instance ref there is caught statically, not at runtime.
1369
+ diagnostics.push(...validateStepInvokeReferences(allManifests, defs, aliases));
1180
1370
  // Validate `extends` fields and flag legacy `capability: <UserAbstract>` overload.
1181
1371
  diagnostics.push(...validateExtends(allManifests, defs, aliases));
1182
1372
  // Validate provider coherence rules for `provide:` template-target definitions.
1183
1373
  diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
1374
+ // Warn about exported kinds lacking a metadata.description (semantic-search input).
1375
+ diagnostics.push(...validateKindDescriptions(allManifests));
1184
1376
  // Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
1185
1377
  diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv, aliasesByModule, rootModules));
1186
1378
  // Warn about declared variables / secrets / ports that no CEL references.
@@ -1 +1 @@
1
- {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EA2hB/C,CAAC"}
1
+ {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EAwiB/C,CAAC"}
package/dist/builtins.js CHANGED
@@ -225,6 +225,7 @@ export const KERNEL_BUILTINS = [
225
225
  additionalProperties: true,
226
226
  },
227
227
  source: { type: "string" },
228
+ integrity: { type: "string" },
228
229
  variables: { type: "object" },
229
230
  secrets: { type: "object" },
230
231
  runtime: {
@@ -359,6 +360,11 @@ export const KERNEL_BUILTINS = [
359
360
  type: "array",
360
361
  items: { type: "string" },
361
362
  },
363
+ // Integrity hash of the decompressed payload tar (`module.tar.gz`,
364
+ // telo.yaml excluded), written by `telo publish`. Pinned transitively
365
+ // by the importer's `#sha256-...` hash over this telo.yaml; verified at
366
+ // extract time. See plans/federated-registries.md.
367
+ filesIntegrity: { type: "string" },
362
368
  // Inline imports — name-keyed map sugar for separate `Telo.Import`
363
369
  // documents. The key is the PascalCase alias (the import's
364
370
  // `metadata.name`). Each value is either a bare source string
@@ -376,6 +382,7 @@ export const KERNEL_BUILTINS = [
376
382
  required: ["source"],
377
383
  properties: {
378
384
  source: { type: "string" },
385
+ integrity: { type: "string" },
379
386
  variables: { type: "object" },
380
387
  secrets: { type: "object" },
381
388
  runtime: {
@@ -490,6 +497,11 @@ export const KERNEL_BUILTINS = [
490
497
  type: "array",
491
498
  items: { type: "string" },
492
499
  },
500
+ // Integrity hash of the decompressed payload tar (`module.tar.gz`,
501
+ // telo.yaml excluded), written by `telo publish`. Pinned transitively
502
+ // by the importer's `#sha256-...` hash over this telo.yaml; verified at
503
+ // extract time. See plans/federated-registries.md.
504
+ filesIntegrity: { type: "string" },
493
505
  // Inline imports — same name-keyed map sugar as Telo.Application; the
494
506
  // loader desugars each entry into a synthetic Telo.Import. See the
495
507
  // Application schema above and analyzer/nodejs/src/inline-imports.ts.
@@ -503,6 +515,7 @@ export const KERNEL_BUILTINS = [
503
515
  required: ["source"],
504
516
  properties: {
505
517
  source: { type: "string" },
518
+ integrity: { type: "string" },
506
519
  variables: { type: "object" },
507
520
  secrets: { type: "object" },
508
521
  runtime: {
package/dist/index.d.ts CHANGED
@@ -21,6 +21,9 @@ export type { DocumentPosition } from "./position-metadata.js";
21
21
  export { HttpSource } from "./sources/http-source.js";
22
22
  export { RegistrySource } from "./sources/registry-source.js";
23
23
  export { defaultSources } from "./sources/default-sources.js";
24
+ export { splitIntegrity, foldIntegrity, verifyIntegrity, verifiedFetch, sha256Base64Url, IntegrityError, } from "./sources/integrity.js";
25
+ export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
26
+ export type { ParsedModuleRef } from "./sources/module-ref.js";
24
27
  export { withSyntheticPositions } from "./with-synthetic-positions.js";
25
28
  export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
26
29
  export type { AnalysisDiagnostic, AnalysisOptions, LoaderInitOptions, LoadOptions, ManifestSource, Position, PositionIndex, Range } from "./types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,cAAc,EACd,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACZ,UAAU,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACH,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,gCAAgC,EAChC,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,GACpB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EACR,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,GACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,YAAY,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EACH,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EACR,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,aAAa,EACb,KAAK,EACR,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,cAAc,EACd,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACZ,UAAU,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACH,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,gCAAgC,EAChC,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,GACpB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EACR,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,GACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,YAAY,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EACH,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EACL,cAAc,EACd,aAAa,EACb,eAAe,EACf,aAAa,EACb,eAAe,EACf,cAAc,GACf,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxE,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EACR,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,aAAa,EACb,KAAK,EACR,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -13,5 +13,7 @@ export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentL
13
13
  export { HttpSource } from "./sources/http-source.js";
14
14
  export { RegistrySource } from "./sources/registry-source.js";
15
15
  export { defaultSources } from "./sources/default-sources.js";
16
+ export { splitIntegrity, foldIntegrity, verifyIntegrity, verifiedFetch, sha256Base64Url, IntegrityError, } from "./sources/integrity.js";
17
+ export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
16
18
  export { withSyntheticPositions } from "./with-synthetic-positions.js";
17
19
  export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"inline-imports.d.ts","sourceRoot":"","sources":["../src/inline-imports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D;;6CAE6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,gBAAgB,EAChC,cAAc,EAAE,gBAAgB,GAAG,SAAS,GAC3C,eAAe,EAAE,CA0BnB;AAED;;;;;;;iFAOiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAmB9D"}
1
+ {"version":3,"file":"inline-imports.d.ts","sourceRoot":"","sources":["../src/inline-imports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D;;6CAE6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,gBAAgB,EAChC,cAAc,EAAE,gBAAgB,GAAG,SAAS,GAC3C,eAAe,EAAE,CA+BnB;AAED;;;;;;;iFAOiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAmB9D"}
@@ -1,3 +1,4 @@
1
+ import { foldIntegrity } from "./sources/integrity.js";
1
2
  import { isModuleKind } from "./module-kinds.js";
2
3
  /**
3
4
  * Desugar a module document's inline `imports:` map into synthetic
@@ -26,10 +27,14 @@ export function inlineImportManifests(moduleManifest, modulePosition) {
26
27
  : undefined;
27
28
  if (!entry || typeof entry.source !== "string")
28
29
  continue;
30
+ // The object form carries integrity as a sibling `integrity:` field; fold it
31
+ // into the source string as a `#sha256-...` fragment so every downstream
32
+ // consumer sees a single representation (the scalar form already inlines it).
33
+ const source = foldIntegrity(entry.source, entry.integrity);
29
34
  const manifest = {
30
35
  kind: "Telo.Import",
31
36
  metadata: { name: alias },
32
- source: entry.source,
37
+ source,
33
38
  ...(entry.variables !== undefined ? { variables: entry.variables } : {}),
34
39
  ...(entry.secrets !== undefined ? { secrets: entry.secrets } : {}),
35
40
  ...(entry.runtime !== undefined ? { runtime: entry.runtime } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"http-source.d.ts","sourceRoot":"","sources":["../../src/sources/http-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7E,qBAAa,UAAW,YAAW,cAAc;IAC/C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAWlE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;CAIxD"}
1
+ {"version":3,"file":"http-source.d.ts","sourceRoot":"","sources":["../../src/sources/http-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7E,qBAAa,UAAW,YAAW,cAAc;IAC/C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAOlE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;CAIxD"}
@@ -1,15 +1,14 @@
1
1
  import { DEFAULT_MANIFEST_FILENAME } from "../types.js";
2
+ import { splitIntegrity, verifiedFetch } from "./integrity.js";
2
3
  export class HttpSource {
3
4
  supports(url) {
4
5
  return url.startsWith("http://") || url.startsWith("https://");
5
6
  }
6
7
  async read(url) {
7
- const fetchUrl = url.includes(".yaml") ? url : `${url}/${DEFAULT_MANIFEST_FILENAME}`;
8
- const response = await fetch(fetchUrl);
9
- if (!response.ok) {
10
- throw new Error(`Failed to fetch manifest from ${fetchUrl}: ${response.status} ${response.statusText}`);
11
- }
12
- return { text: await response.text(), source: fetchUrl };
8
+ const { base, integrity } = splitIntegrity(url);
9
+ const fetchUrl = base.includes(".yaml") ? base : `${base}/${DEFAULT_MANIFEST_FILENAME}`;
10
+ const { text } = await verifiedFetch(fetchUrl, integrity, fetchUrl);
11
+ return { text, source: fetchUrl };
13
12
  }
14
13
  resolveRelative(base, relative) {
15
14
  const baseDir = base.endsWith("/") ? base : base.slice(0, base.lastIndexOf("/") + 1);
@@ -0,0 +1,42 @@
1
+ /** Inline module integrity — the `#sha256-<base64url>` fragment carried on a
2
+ * remote import ref. Browser-safe: uses Web Crypto (`crypto.subtle`) and
3
+ * `btoa`, both globals in Node and the browser. No Node built-ins.
4
+ *
5
+ * The fragment is authoritative across every transport: a source's `read()`
6
+ * hashes the fetched bytes and compares against it before the manifest is
7
+ * parsed or cached. A mismatch is a terminal error — never a cache miss. */
8
+ /** A failed integrity/tamper check — always terminal, never best-effort. A
9
+ * distinct type so a caller doing best-effort network handling (e.g. the
10
+ * bundle extractor warning-and-skipping on a fetch blip) can still let a
11
+ * tamper error propagate rather than swallow it. */
12
+ export declare class IntegrityError extends Error {
13
+ constructor(message: string);
14
+ }
15
+ /** Split a trailing integrity fragment off a ref/URL. Returns the bare ref in
16
+ * `base` (safe to build fetch URLs and cache paths from) and the fragment in
17
+ * `integrity` (e.g. `sha256-<base64url>`), or `undefined` when absent. */
18
+ export declare function splitIntegrity(ref: string): {
19
+ base: string;
20
+ integrity?: string;
21
+ };
22
+ /** Attach an integrity hash to a ref as a `#<alg>-...` fragment. No-op when the
23
+ * hash is absent/non-string or the ref already carries a fragment (an
24
+ * author-authored pin is never overwritten). Inverse of `splitIntegrity`;
25
+ * used to fold the object form's `integrity:` sibling into the source string. */
26
+ export declare function foldIntegrity(source: string, integrity: unknown): string;
27
+ /** SHA-256 of `bytes` as unpadded base64url — the canonical inline-hash form. */
28
+ export declare function sha256Base64Url(bytes: Uint8Array): Promise<string>;
29
+ /** Hash `bytes` and compare against `integrity` (`<alg>-<digest>`). Throws a
30
+ * terminal error on mismatch or an unsupported algorithm. `describe` names the
31
+ * artifact in the error (e.g. the module ref) so the failure is actionable. */
32
+ export declare function verifyIntegrity(bytes: Uint8Array, integrity: string, describe: string): Promise<void>;
33
+ /** The single verified network read for remote manifests: fetch `fetchUrl`,
34
+ * verify the raw bytes against `integrity` (when pinned), and return both the
35
+ * bytes and the decoded text. The one choke point every network `ManifestSource`
36
+ * routes through, so verification cannot drift between them. `describe` names
37
+ * the artifact in error messages. */
38
+ export declare function verifiedFetch(fetchUrl: string, integrity: string | undefined, describe: string): Promise<{
39
+ bytes: Uint8Array;
40
+ text: string;
41
+ }>;
42
+ //# sourceMappingURL=integrity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integrity.d.ts","sourceRoot":"","sources":["../../src/sources/integrity.ts"],"names":[],"mappings":"AAAA;;;;;;6EAM6E;AAO7E;;;qDAGqD;AACrD,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAED;;2EAE2E;AAC3E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAIhF;AAED;;;kFAGkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,CAIxE;AAcD,iFAAiF;AACjF,wBAAsB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAOxE;AAED;;gFAEgF;AAChF,wBAAsB,eAAe,CACnC,KAAK,EAAE,UAAU,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED;;;;sCAIsC;AACtC,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAU9C"}