@telorun/analyzer 0.30.1 → 0.32.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":"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, } 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,GAChB,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, } 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 } : {}),
@@ -80,6 +80,11 @@ export interface LoadedGraph {
80
80
  * error for a major mismatch). Surfaced alongside `analyze()` diagnostics by
81
81
  * every consumer (CLI, editor, VS Code). */
82
82
  versionDiagnostics: AnalysisDiagnostic[];
83
+ /** YAML parse failures aggregated from every file's `parseErrors`. A file
84
+ * that fails to parse yields a mangled `toJSON()` projection, so these are
85
+ * fatal Error diagnostics — surfaced alongside `analyze()` output by every
86
+ * consumer (CLI, editor, VS Code) and treated as fatal by the kernel. */
87
+ parseDiagnostics: AnalysisDiagnostic[];
83
88
  /** Surface-level errors that did not abort the graph load (e.g. an import
84
89
  * whose target failed to fetch). */
85
90
  errors: GraphLoadError[];
@@ -1 +1 @@
1
- {"version":3,"file":"loaded-types.d.ts","sourceRoot":"","sources":["../src/loaded-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;4EAI4E;AAC5E,MAAM,WAAW,UAAU;IACzB;+DAC2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf;gEAC4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,6EAA6E;IAC7E,SAAS,EAAE,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1C,0EAA0E;IAC1E,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED;mCACmC;AACnC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,UAAU,CAAC;IAClB;oEACgE;IAChE,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED;;;;;2EAK2E;AAC3E,MAAM,WAAW,UAAU;IACzB,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB;6EACyE;IACzE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,oEAAoE;IACpE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;0BAC0B;AAC1B,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;IACpB;;wBAEoB;IACpB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACnC;;;;2BAIuB;IACvB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAClD;;;iFAG6E;IAC7E,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;;;iDAG6C;IAC7C,kBAAkB,EAAE,kBAAkB,EAAE,CAAC;IACzC;yCACqC;IACrC,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,KAAK,CAAC;CACd"}
1
+ {"version":3,"file":"loaded-types.d.ts","sourceRoot":"","sources":["../src/loaded-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;4EAI4E;AAC5E,MAAM,WAAW,UAAU;IACzB;+DAC2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf;gEAC4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,6EAA6E;IAC7E,SAAS,EAAE,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1C,0EAA0E;IAC1E,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED;mCACmC;AACnC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,UAAU,CAAC;IAClB;oEACgE;IAChE,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED;;;;;2EAK2E;AAC3E,MAAM,WAAW,UAAU;IACzB,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB;6EACyE;IACzE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,oEAAoE;IACpE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;0BAC0B;AAC1B,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;IACpB;;wBAEoB;IACpB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACnC;;;;2BAIuB;IACvB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAClD;;;iFAG6E;IAC7E,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;;;iDAG6C;IAC7C,kBAAkB,EAAE,kBAAkB,EAAE,CAAC;IACzC;;;8EAG0E;IAC1E,gBAAgB,EAAE,kBAAkB,EAAE,CAAC;IACvC;yCACqC;IACrC,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,KAAK,CAAC;CACd"}
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAiBpB,qBAAa,MAAM;IACjB;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAE3D;;;;;8BAK0B;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC;;;;mBAIe;gBACH,OAAO,GAAE,cAAc,EAAO,EAAE,OAAO,GAAE,iBAAsB;IAK3E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUrD;;;;2CAIuC;IACvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAM7C;;;+BAG2B;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAyCvE;;;;gEAI4D;IAC5D,OAAO,CAAC,oBAAoB;IAa5B;gFAC4E;IAC5E,OAAO,CAAC,cAAc;IAQtB;;wEAEoE;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAsB3E;;;qCAGiC;IAC3B,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAqH9E;;;;;;;;sBAQkB;IAClB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAOlE,OAAO,CAAC,6BAA6B;IAarC,OAAO,CAAC,mCAAmC;IAc3C,OAAO,CAAC,2BAA2B;YAkCrB,eAAe;IAmB7B;;;0CAGsC;IAChC,gBAAgB,CACpB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CA0B5D"}
1
+ {"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAwCpB,qBAAa,MAAM;IACjB;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAE3D;;;;;8BAK0B;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC;;;;mBAIe;gBACH,OAAO,GAAE,cAAc,EAAO,EAAE,OAAO,GAAE,iBAAsB;IAK3E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUrD;;;;2CAIuC;IACvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAM7C;;;+BAG2B;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAyCvE;;;;gEAI4D;IAC5D,OAAO,CAAC,oBAAoB;IAa5B;gFAC4E;IAC5E,OAAO,CAAC,cAAc;IAQtB;;wEAEoE;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAsB3E;;;qCAGiC;IAC3B,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAsH9E;;;;;;;;sBAQkB;IAClB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAOlE,OAAO,CAAC,6BAA6B;IAarC,OAAO,CAAC,mCAAmC;IAc3C,OAAO,CAAC,2BAA2B;YAkCrB,eAAe;IAmB7B;;;0CAGsC;IAChC,gBAAgB,CACpB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CA0B5D"}
@@ -3,7 +3,27 @@ import { desugarLoadedFile } from "./inline-imports.js";
3
3
  import { isModuleKind } from "./module-kinds.js";
4
4
  import { parseLoadedFile } from "./parse-loaded-file.js";
5
5
  import { reconcileModuleVersions } from "./reconcile-module-versions.js";
6
- import { DEFAULT_MANIFEST_FILENAME, } from "./types.js";
6
+ import { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity, } from "./types.js";
7
+ /** Project every file's YAML `parseErrors` into fatal Error diagnostics. Each
8
+ * carries `data.filePath` so `findPositions` routes it to the failing file. */
9
+ function collectParseDiagnostics(modules) {
10
+ const diagnostics = [];
11
+ for (const mod of modules.values()) {
12
+ for (const file of [mod.owner, ...mod.partials]) {
13
+ for (const err of file.parseErrors) {
14
+ diagnostics.push({
15
+ severity: DiagnosticSeverity.Error,
16
+ code: "MANIFEST_PARSE_FAILED",
17
+ source: "telo-analyzer",
18
+ message: err.message,
19
+ range: err.range,
20
+ data: { filePath: file.source },
21
+ });
22
+ }
23
+ }
24
+ }
25
+ return diagnostics;
26
+ }
7
27
  const SYSTEM_KINDS = new Set([
8
28
  "Telo.Application",
9
29
  "Telo.Library",
@@ -268,6 +288,7 @@ export class Loader {
268
288
  importEdges,
269
289
  overrides,
270
290
  versionDiagnostics: diagnostics,
291
+ parseDiagnostics: collectParseDiagnostics(modules),
271
292
  errors,
272
293
  };
273
294
  }
@@ -1 +1 @@
1
- {"version":3,"file":"parse-loaded-file.d.ts","sourceRoot":"","sources":["../src/parse-loaded-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAKxD,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,mBAAmB,CAAC;AAIhE,MAAM,WAAW,YAAY;IAC3B;4EACwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,oEAAoE;AACpE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,YAAY,GACrB,UAAU,CA8CZ"}
1
+ {"version":3,"file":"parse-loaded-file.d.ts","sourceRoot":"","sources":["../src/parse-loaded-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAKxD,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,mBAAmB,CAAC;AAIhE,MAAM,WAAW,YAAY;IAC3B;4EACwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AA4BD,oEAAoE;AACpE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,YAAY,GACrB,UAAU,CA+CZ"}
@@ -3,6 +3,29 @@ import { parseAllDocuments } from "yaml";
3
3
  import { buildCelEnvironment } from "./cel-environment.js";
4
4
  import { buildDocumentPositions } from "./position-metadata.js";
5
5
  import { precompileDoc } from "./precompile.js";
6
+ /** Append an actionable hint to raw yaml-parser messages that are otherwise
7
+ * cryptic. The parser reports `BLOCK_AS_IMPLICIT_KEY` ("Nested mappings are
8
+ * not allowed in compact mappings") when a plain (unquoted) scalar contains
9
+ * `: ` (colon-space) — the parser reads the colon as a nested key. Telling the
10
+ * author to quote the value turns an opaque error into a one-step fix. */
11
+ function augmentParseMessage(err) {
12
+ if (err.code === "BLOCK_AS_IMPLICIT_KEY") {
13
+ return `${err.message}\n\nHint: a plain (unquoted) value cannot contain ': ' (colon followed by a space) — the parser reads it as a nested mapping. Wrap the value in quotes, e.g. \`description: "… \`encoding: base64\` …"\`.`;
14
+ }
15
+ return err.message;
16
+ }
17
+ /** The yaml lib reports 1-based `{line, col}` pairs; convert to a 0-based
18
+ * analyzer `Range`. Returns undefined when the error carried no position. */
19
+ function rangeFromLinePos(linePos) {
20
+ const start = linePos?.[0];
21
+ if (!start)
22
+ return undefined;
23
+ const end = linePos?.[1] ?? start;
24
+ return {
25
+ start: { line: start.line - 1, character: start.col - 1 },
26
+ end: { line: end.line - 1, character: end.col - 1 },
27
+ };
28
+ }
6
29
  /** Pure: text in, structured load result out. No I/O, no caches. */
7
30
  export function parseLoadedFile(source, requestedUrl, text, options) {
8
31
  const documents = parseAllDocuments(text, { customTags: defaultCustomTags() });
@@ -12,7 +35,8 @@ export function parseLoadedFile(source, requestedUrl, text, options) {
12
35
  for (const err of doc.errors) {
13
36
  parseErrors.push({
14
37
  documentIndex,
15
- message: err.message,
38
+ message: augmentParseMessage(err),
39
+ range: rangeFromLinePos(err.linePos),
16
40
  });
17
41
  }
18
42
  });
@@ -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,35 @@
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
+ /** Split a trailing integrity fragment off a ref/URL. Returns the bare ref in
9
+ * `base` (safe to build fetch URLs and cache paths from) and the fragment in
10
+ * `integrity` (e.g. `sha256-<base64url>`), or `undefined` when absent. */
11
+ export declare function splitIntegrity(ref: string): {
12
+ base: string;
13
+ integrity?: string;
14
+ };
15
+ /** Attach an integrity hash to a ref as a `#<alg>-...` fragment. No-op when the
16
+ * hash is absent/non-string or the ref already carries a fragment (an
17
+ * author-authored pin is never overwritten). Inverse of `splitIntegrity`;
18
+ * used to fold the object form's `integrity:` sibling into the source string. */
19
+ export declare function foldIntegrity(source: string, integrity: unknown): string;
20
+ /** SHA-256 of `bytes` as unpadded base64url — the canonical inline-hash form. */
21
+ export declare function sha256Base64Url(bytes: Uint8Array): Promise<string>;
22
+ /** Hash `bytes` and compare against `integrity` (`<alg>-<digest>`). Throws a
23
+ * terminal error on mismatch or an unsupported algorithm. `describe` names the
24
+ * artifact in the error (e.g. the module ref) so the failure is actionable. */
25
+ export declare function verifyIntegrity(bytes: Uint8Array, integrity: string, describe: string): Promise<void>;
26
+ /** The single verified network read for remote manifests: fetch `fetchUrl`,
27
+ * verify the raw bytes against `integrity` (when pinned), and return both the
28
+ * bytes and the decoded text. The one choke point every network `ManifestSource`
29
+ * routes through, so verification cannot drift between them. `describe` names
30
+ * the artifact in error messages. */
31
+ export declare function verifiedFetch(fetchUrl: string, integrity: string | undefined, describe: string): Promise<{
32
+ bytes: Uint8Array;
33
+ text: string;
34
+ }>;
35
+ //# 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;;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"}
@@ -0,0 +1,82 @@
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
+ /** Only a `#<alg>-<base64url>` suffix is treated as integrity; other `#`
9
+ * fragments (rare in module refs) pass through untouched. `sha256` is the
10
+ * only algorithm accepted today; the prefix leaves room to migrate. */
11
+ const INTEGRITY_FRAGMENT = /#(sha256-[A-Za-z0-9_+/=-]+)$/;
12
+ /** Split a trailing integrity fragment off a ref/URL. Returns the bare ref in
13
+ * `base` (safe to build fetch URLs and cache paths from) and the fragment in
14
+ * `integrity` (e.g. `sha256-<base64url>`), or `undefined` when absent. */
15
+ export function splitIntegrity(ref) {
16
+ const match = ref.match(INTEGRITY_FRAGMENT);
17
+ if (!match)
18
+ return { base: ref };
19
+ return { base: ref.slice(0, match.index), integrity: match[1] };
20
+ }
21
+ /** Attach an integrity hash to a ref as a `#<alg>-...` fragment. No-op when the
22
+ * hash is absent/non-string or the ref already carries a fragment (an
23
+ * author-authored pin is never overwritten). Inverse of `splitIntegrity`;
24
+ * used to fold the object form's `integrity:` sibling into the source string. */
25
+ export function foldIntegrity(source, integrity) {
26
+ return typeof integrity === "string" && !source.includes("#")
27
+ ? `${source}#${integrity}`
28
+ : source;
29
+ }
30
+ function toBase64Url(bytes) {
31
+ let binary = "";
32
+ for (let i = 0; i < bytes.length; i++)
33
+ binary += String.fromCharCode(bytes[i]);
34
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
35
+ }
36
+ /** Normalize an encoded digest to unpadded base64url so a standard-base64 or
37
+ * padded input still compares equal to our canonical form. */
38
+ function normalizeDigest(value) {
39
+ return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
40
+ }
41
+ /** SHA-256 of `bytes` as unpadded base64url — the canonical inline-hash form. */
42
+ export async function sha256Base64Url(bytes) {
43
+ // Copy into a plain ArrayBuffer: a Uint8Array may be backed by a
44
+ // SharedArrayBuffer, which `crypto.subtle.digest` does not accept.
45
+ const buffer = new ArrayBuffer(bytes.byteLength);
46
+ new Uint8Array(buffer).set(bytes);
47
+ const digest = await crypto.subtle.digest("SHA-256", buffer);
48
+ return toBase64Url(new Uint8Array(digest));
49
+ }
50
+ /** Hash `bytes` and compare against `integrity` (`<alg>-<digest>`). Throws a
51
+ * terminal error on mismatch or an unsupported algorithm. `describe` names the
52
+ * artifact in the error (e.g. the module ref) so the failure is actionable. */
53
+ export async function verifyIntegrity(bytes, integrity, describe) {
54
+ const dash = integrity.indexOf("-");
55
+ const algorithm = dash > 0 ? integrity.slice(0, dash) : "";
56
+ const expected = dash > 0 ? integrity.slice(dash + 1) : "";
57
+ if (algorithm !== "sha256") {
58
+ throw new Error(`Unsupported integrity algorithm '${algorithm || integrity}' for ${describe}. ` +
59
+ `Only sha256 is supported (sha256-<base64url>).`);
60
+ }
61
+ const actual = await sha256Base64Url(bytes);
62
+ if (actual !== normalizeDigest(expected)) {
63
+ throw new Error(`Integrity check failed for ${describe}: expected sha256-${normalizeDigest(expected)}, ` +
64
+ `got sha256-${actual}. The fetched bytes do not match the recorded hash — ` +
65
+ `the module may have been tampered with or republished.`);
66
+ }
67
+ }
68
+ /** The single verified network read for remote manifests: fetch `fetchUrl`,
69
+ * verify the raw bytes against `integrity` (when pinned), and return both the
70
+ * bytes and the decoded text. The one choke point every network `ManifestSource`
71
+ * routes through, so verification cannot drift between them. `describe` names
72
+ * the artifact in error messages. */
73
+ export async function verifiedFetch(fetchUrl, integrity, describe) {
74
+ const response = await fetch(fetchUrl);
75
+ if (!response.ok) {
76
+ throw new Error(`Failed to fetch manifest ${describe}: ${response.status} ${response.statusText} (${fetchUrl})`);
77
+ }
78
+ const bytes = new Uint8Array(await response.arrayBuffer());
79
+ if (integrity)
80
+ await verifyIntegrity(bytes, integrity, describe);
81
+ return { bytes, text: new TextDecoder().decode(bytes) };
82
+ }
@@ -0,0 +1,17 @@
1
+ /** A parsed registry module reference. `modulePath` is `namespace/name`,
2
+ * `version` has any leading `v` stripped, and `integrity` carries the inline
3
+ * `sha256-<base64url>` hash when the ref was pinned. */
4
+ export interface ParsedModuleRef {
5
+ modulePath: string;
6
+ version: string;
7
+ integrity?: string;
8
+ }
9
+ /** True when `url` has the bare registry-ref shape `namespace/name@version`
10
+ * (no scheme, no leading `/` or `.`, contains both `@` and `/`). The integrity
11
+ * fragment, if any, does not affect the classification. */
12
+ export declare function isRegistryRef(url: string): boolean;
13
+ /** Canonical parser for `namespace/name@version[#sha256-...]` refs. The single
14
+ * source of truth shared by the registry source, the kernel manifest cache,
15
+ * and the CLI (install / upgrade / bundle). Throws on a malformed ref. */
16
+ export declare function parseModuleRef(ref: string): ParsedModuleRef;
17
+ //# sourceMappingURL=module-ref.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module-ref.d.ts","sourceRoot":"","sources":["../../src/sources/module-ref.ts"],"names":[],"mappings":"AAEA;;yDAEyD;AACzD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;4DAE4D;AAC5D,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAYlD;AAED;;2EAE2E;AAC3E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAgB3D"}
@@ -0,0 +1,35 @@
1
+ import { splitIntegrity } from "./integrity.js";
2
+ /** True when `url` has the bare registry-ref shape `namespace/name@version`
3
+ * (no scheme, no leading `/` or `.`, contains both `@` and `/`). The integrity
4
+ * fragment, if any, does not affect the classification. */
5
+ export function isRegistryRef(url) {
6
+ const { base } = splitIntegrity(url);
7
+ return (!base.startsWith("http://") &&
8
+ !base.startsWith("https://") &&
9
+ !base.startsWith("/") &&
10
+ !base.startsWith(".") &&
11
+ !base.startsWith("file://") &&
12
+ !base.startsWith("memory://") &&
13
+ base.includes("@") &&
14
+ base.includes("/"));
15
+ }
16
+ /** Canonical parser for `namespace/name@version[#sha256-...]` refs. The single
17
+ * source of truth shared by the registry source, the kernel manifest cache,
18
+ * and the CLI (install / upgrade / bundle). Throws on a malformed ref. */
19
+ export function parseModuleRef(ref) {
20
+ const { base, integrity } = splitIntegrity(ref);
21
+ const atIdx = base.lastIndexOf("@");
22
+ if (atIdx <= 0 || atIdx === base.length - 1) {
23
+ throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
24
+ }
25
+ const modulePath = base.slice(0, atIdx);
26
+ if (!modulePath.includes("/")) {
27
+ throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
28
+ }
29
+ const rawVersion = base.slice(atIdx + 1);
30
+ const version = rawVersion.startsWith("v") ? rawVersion.slice(1) : rawVersion;
31
+ if (!version) {
32
+ throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
33
+ }
34
+ return { modulePath, version, integrity };
35
+ }
@@ -10,6 +10,5 @@ export declare class RegistrySource implements ManifestSource {
10
10
  resolveRelative(base: string, relative: string): string;
11
11
  private toRegistryModuleBase;
12
12
  private toRegistryUrl;
13
- private parseModuleRef;
14
13
  }
15
14
  //# sourceMappingURL=registry-source.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry-source.d.ts","sourceRoot":"","sources":["../../src/sources/registry-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAI7E,qBAAa,cAAe,YAAW,cAAc;IACvC,OAAO,CAAC,WAAW;gBAAX,WAAW,SAAuB;IAEtD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAWxB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IA4BxE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAMvD,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,cAAc;CAmBvB"}
1
+ {"version":3,"file":"registry-source.d.ts","sourceRoot":"","sources":["../../src/sources/registry-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7E,qBAAa,cAAe,YAAW,cAAc;IACvC,OAAO,CAAC,WAAW;gBAAX,WAAW,SAAuB;IAEtD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIxB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAuBxE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAMvD,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,aAAa;CAGtB"}
@@ -1,4 +1,6 @@
1
1
  import { DEFAULT_MANIFEST_FILENAME } from "../types.js";
2
+ import { splitIntegrity, verifiedFetch } from "./integrity.js";
3
+ import { isRegistryRef, parseModuleRef } from "./module-ref.js";
2
4
  const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
3
5
  export class RegistrySource {
4
6
  registryUrl;
@@ -6,20 +8,12 @@ export class RegistrySource {
6
8
  this.registryUrl = registryUrl;
7
9
  }
8
10
  supports(url) {
9
- return (!url.startsWith("http://") &&
10
- !url.startsWith("https://") &&
11
- !url.startsWith("/") &&
12
- !url.startsWith(".") &&
13
- url.includes("@") &&
14
- url.includes("/"));
11
+ return isRegistryRef(url);
15
12
  }
16
13
  async read(moduleRef) {
17
- const fetchUrl = this.toRegistryUrl(moduleRef);
18
- const response = await fetch(fetchUrl);
19
- if (!response.ok) {
20
- throw new Error(`Failed to fetch manifest ${moduleRef}: ${response.status} ${response.statusText}`);
21
- }
22
- const text = await response.text();
14
+ const { base, integrity } = splitIntegrity(moduleRef);
15
+ const fetchUrl = this.toRegistryUrl(base);
16
+ const { text } = await verifiedFetch(fetchUrl, integrity, base);
23
17
  // Some object-storage backends (e.g. Cloudflare R2 / S3) surface auth or
24
18
  // permission failures by returning a 200 status with an XML error body.
25
19
  // Catch this here so the loader produces a precise error rather than
@@ -30,7 +24,7 @@ export class RegistrySource {
30
24
  const detail = codeMatch && messageMatch
31
25
  ? `${codeMatch[1]}: ${messageMatch[1]}`
32
26
  : text.slice(0, 200);
33
- throw new Error(`Registry returned a non-manifest response for ${moduleRef} ` +
27
+ throw new Error(`Registry returned a non-manifest response for ${base} ` +
34
28
  `(URL: ${fetchUrl}): ${detail}`);
35
29
  }
36
30
  return { text, source: fetchUrl };
@@ -41,27 +35,11 @@ export class RegistrySource {
41
35
  return new URL(relative, baseWithSlash).href;
42
36
  }
43
37
  toRegistryModuleBase(moduleRef) {
44
- const parsed = this.parseModuleRef(moduleRef);
38
+ const parsed = parseModuleRef(moduleRef);
45
39
  const normalizedBase = this.registryUrl.replace(/\/+$/, "");
46
40
  return `${normalizedBase}/${parsed.modulePath}/${parsed.version}`;
47
41
  }
48
42
  toRegistryUrl(moduleRef) {
49
43
  return `${this.toRegistryModuleBase(moduleRef)}/${DEFAULT_MANIFEST_FILENAME}`;
50
44
  }
51
- parseModuleRef(moduleRef) {
52
- const atIdx = moduleRef.lastIndexOf("@");
53
- if (atIdx <= 0 || atIdx === moduleRef.length - 1) {
54
- throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
55
- }
56
- const modulePath = moduleRef.slice(0, atIdx);
57
- if (!modulePath.includes("/")) {
58
- throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
59
- }
60
- const rawVersion = moduleRef.slice(atIdx + 1);
61
- const version = rawVersion.startsWith("v") ? rawVersion.substring(1) : rawVersion;
62
- if (!version) {
63
- throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
64
- }
65
- return { modulePath, version };
66
- }
67
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.30.1",
3
+ "version": "0.32.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -48,7 +48,7 @@
48
48
  "@types/node": "^20.0.0",
49
49
  "typescript": "^5.0.0",
50
50
  "vitest": "^2.1.8",
51
- "@telorun/sdk": "0.38.0"
51
+ "@telorun/sdk": "0.41.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@telorun/sdk": "*"
package/src/builtins.ts CHANGED
@@ -227,6 +227,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
227
227
  additionalProperties: true,
228
228
  },
229
229
  source: { type: "string" },
230
+ integrity: { type: "string" },
230
231
  variables: { type: "object" },
231
232
  secrets: { type: "object" },
232
233
  runtime: {
@@ -361,6 +362,11 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
361
362
  type: "array",
362
363
  items: { type: "string" },
363
364
  },
365
+ // Integrity hash of the decompressed payload tar (`module.tar.gz`,
366
+ // telo.yaml excluded), written by `telo publish`. Pinned transitively
367
+ // by the importer's `#sha256-...` hash over this telo.yaml; verified at
368
+ // extract time. See plans/federated-registries.md.
369
+ filesIntegrity: { type: "string" },
364
370
  // Inline imports — name-keyed map sugar for separate `Telo.Import`
365
371
  // documents. The key is the PascalCase alias (the import's
366
372
  // `metadata.name`). Each value is either a bare source string
@@ -378,6 +384,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
378
384
  required: ["source"],
379
385
  properties: {
380
386
  source: { type: "string" },
387
+ integrity: { type: "string" },
381
388
  variables: { type: "object" },
382
389
  secrets: { type: "object" },
383
390
  runtime: {
@@ -492,6 +499,11 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
492
499
  type: "array",
493
500
  items: { type: "string" },
494
501
  },
502
+ // Integrity hash of the decompressed payload tar (`module.tar.gz`,
503
+ // telo.yaml excluded), written by `telo publish`. Pinned transitively
504
+ // by the importer's `#sha256-...` hash over this telo.yaml; verified at
505
+ // extract time. See plans/federated-registries.md.
506
+ filesIntegrity: { type: "string" },
495
507
  // Inline imports — same name-keyed map sugar as Telo.Application; the
496
508
  // loader desugars each entry into a synthetic Telo.Import. See the
497
509
  // Application schema above and analyzer/nodejs/src/inline-imports.ts.
@@ -505,6 +517,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
505
517
  required: ["source"],
506
518
  properties: {
507
519
  source: { type: "string" },
520
+ integrity: { type: "string" },
508
521
  variables: { type: "object" },
509
522
  secrets: { type: "object" },
510
523
  runtime: {
package/src/index.ts CHANGED
@@ -53,6 +53,15 @@ export type { DocumentPosition } from "./position-metadata.js";
53
53
  export { HttpSource } from "./sources/http-source.js";
54
54
  export { RegistrySource } from "./sources/registry-source.js";
55
55
  export { defaultSources } from "./sources/default-sources.js";
56
+ export {
57
+ splitIntegrity,
58
+ foldIntegrity,
59
+ verifyIntegrity,
60
+ verifiedFetch,
61
+ sha256Base64Url,
62
+ } from "./sources/integrity.js";
63
+ export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
64
+ export type { ParsedModuleRef } from "./sources/module-ref.js";
56
65
  export { withSyntheticPositions } from "./with-synthetic-positions.js";
57
66
  export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
58
67
  export type {
@@ -1,5 +1,6 @@
1
1
  import type { ResourceManifest } from "@telorun/sdk";
2
2
  import type { LoadedFile } from "./loaded-types.js";
3
+ import { foldIntegrity } from "./sources/integrity.js";
3
4
  import { isModuleKind } from "./module-kinds.js";
4
5
  import type { DocumentPosition } from "./position-metadata.js";
5
6
  import type { PositionIndex } from "./types.js";
@@ -42,10 +43,15 @@ export function inlineImportManifests(
42
43
  : undefined;
43
44
  if (!entry || typeof entry.source !== "string") continue;
44
45
 
46
+ // The object form carries integrity as a sibling `integrity:` field; fold it
47
+ // into the source string as a `#sha256-...` fragment so every downstream
48
+ // consumer sees a single representation (the scalar form already inlines it).
49
+ const source = foldIntegrity(entry.source, entry.integrity);
50
+
45
51
  const manifest = {
46
52
  kind: "Telo.Import",
47
53
  metadata: { name: alias },
48
- source: entry.source,
54
+ source,
49
55
  ...(entry.variables !== undefined ? { variables: entry.variables } : {}),
50
56
  ...(entry.secrets !== undefined ? { secrets: entry.secrets } : {}),
51
57
  ...(entry.runtime !== undefined ? { runtime: entry.runtime } : {}),
@@ -85,6 +85,11 @@ export interface LoadedGraph {
85
85
  * error for a major mismatch). Surfaced alongside `analyze()` diagnostics by
86
86
  * every consumer (CLI, editor, VS Code). */
87
87
  versionDiagnostics: AnalysisDiagnostic[];
88
+ /** YAML parse failures aggregated from every file's `parseErrors`. A file
89
+ * that fails to parse yields a mangled `toJSON()` projection, so these are
90
+ * fatal Error diagnostics — surfaced alongside `analyze()` output by every
91
+ * consumer (CLI, editor, VS Code) and treated as fatal by the kernel. */
92
+ parseDiagnostics: AnalysisDiagnostic[];
88
93
  /** Surface-level errors that did not abort the graph load (e.g. an import
89
94
  * whose target failed to fetch). */
90
95
  errors: GraphLoadError[];
@@ -13,12 +13,37 @@ import { isModuleKind } from "./module-kinds.js";
13
13
  import { parseLoadedFile } from "./parse-loaded-file.js";
14
14
  import { reconcileModuleVersions } from "./reconcile-module-versions.js";
15
15
  import {
16
+ type AnalysisDiagnostic,
16
17
  DEFAULT_MANIFEST_FILENAME,
18
+ DiagnosticSeverity,
17
19
  type LoadOptions,
18
20
  type LoaderInitOptions,
19
21
  type ManifestSource,
20
22
  } from "./types.js";
21
23
 
24
+ /** Project every file's YAML `parseErrors` into fatal Error diagnostics. Each
25
+ * carries `data.filePath` so `findPositions` routes it to the failing file. */
26
+ function collectParseDiagnostics(
27
+ modules: Map<string, LoadedModule>,
28
+ ): AnalysisDiagnostic[] {
29
+ const diagnostics: AnalysisDiagnostic[] = [];
30
+ for (const mod of modules.values()) {
31
+ for (const file of [mod.owner, ...mod.partials]) {
32
+ for (const err of file.parseErrors) {
33
+ diagnostics.push({
34
+ severity: DiagnosticSeverity.Error,
35
+ code: "MANIFEST_PARSE_FAILED",
36
+ source: "telo-analyzer",
37
+ message: err.message,
38
+ range: err.range,
39
+ data: { filePath: file.source },
40
+ });
41
+ }
42
+ }
43
+ }
44
+ return diagnostics;
45
+ }
46
+
22
47
  const SYSTEM_KINDS = new Set([
23
48
  "Telo.Application",
24
49
  "Telo.Library",
@@ -309,6 +334,7 @@ export class Loader {
309
334
  importEdges,
310
335
  overrides,
311
336
  versionDiagnostics: diagnostics,
337
+ parseDiagnostics: collectParseDiagnostics(modules),
312
338
  errors,
313
339
  };
314
340
  }
@@ -15,6 +15,32 @@ export interface ParseOptions {
15
15
  celEnv?: Environment;
16
16
  }
17
17
 
18
+ /** Append an actionable hint to raw yaml-parser messages that are otherwise
19
+ * cryptic. The parser reports `BLOCK_AS_IMPLICIT_KEY` ("Nested mappings are
20
+ * not allowed in compact mappings") when a plain (unquoted) scalar contains
21
+ * `: ` (colon-space) — the parser reads the colon as a nested key. Telling the
22
+ * author to quote the value turns an opaque error into a one-step fix. */
23
+ function augmentParseMessage(err: import("yaml").YAMLError): string {
24
+ if (err.code === "BLOCK_AS_IMPLICIT_KEY") {
25
+ return `${err.message}\n\nHint: a plain (unquoted) value cannot contain ': ' (colon followed by a space) — the parser reads it as a nested mapping. Wrap the value in quotes, e.g. \`description: "… \`encoding: base64\` …"\`.`;
26
+ }
27
+ return err.message;
28
+ }
29
+
30
+ /** The yaml lib reports 1-based `{line, col}` pairs; convert to a 0-based
31
+ * analyzer `Range`. Returns undefined when the error carried no position. */
32
+ function rangeFromLinePos(
33
+ linePos: import("yaml").YAMLError["linePos"],
34
+ ): import("./types.js").Range | undefined {
35
+ const start = linePos?.[0];
36
+ if (!start) return undefined;
37
+ const end = linePos?.[1] ?? start;
38
+ return {
39
+ start: { line: start.line - 1, character: start.col - 1 },
40
+ end: { line: end.line - 1, character: end.col - 1 },
41
+ };
42
+ }
43
+
18
44
  /** Pure: text in, structured load result out. No I/O, no caches. */
19
45
  export function parseLoadedFile(
20
46
  source: string,
@@ -30,7 +56,8 @@ export function parseLoadedFile(
30
56
  for (const err of doc.errors) {
31
57
  parseErrors.push({
32
58
  documentIndex,
33
- message: err.message,
59
+ message: augmentParseMessage(err),
60
+ range: rangeFromLinePos(err.linePos),
34
61
  });
35
62
  }
36
63
  });
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_MANIFEST_FILENAME, type ManifestSource } from "../types.js";
2
+ import { splitIntegrity, verifiedFetch } from "./integrity.js";
2
3
 
3
4
  export class HttpSource implements ManifestSource {
4
5
  supports(url: string): boolean {
@@ -6,14 +7,10 @@ export class HttpSource implements ManifestSource {
6
7
  }
7
8
 
8
9
  async read(url: string): Promise<{ text: string; source: string }> {
9
- const fetchUrl = url.includes(".yaml") ? url : `${url}/${DEFAULT_MANIFEST_FILENAME}`;
10
- const response = await fetch(fetchUrl);
11
- if (!response.ok) {
12
- throw new Error(
13
- `Failed to fetch manifest from ${fetchUrl}: ${response.status} ${response.statusText}`,
14
- );
15
- }
16
- return { text: await response.text(), source: fetchUrl };
10
+ const { base, integrity } = splitIntegrity(url);
11
+ const fetchUrl = base.includes(".yaml") ? base : `${base}/${DEFAULT_MANIFEST_FILENAME}`;
12
+ const { text } = await verifiedFetch(fetchUrl, integrity, fetchUrl);
13
+ return { text, source: fetchUrl };
17
14
  }
18
15
 
19
16
  resolveRelative(base: string, relative: string): string {
@@ -0,0 +1,101 @@
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
+
9
+ /** Only a `#<alg>-<base64url>` suffix is treated as integrity; other `#`
10
+ * fragments (rare in module refs) pass through untouched. `sha256` is the
11
+ * only algorithm accepted today; the prefix leaves room to migrate. */
12
+ const INTEGRITY_FRAGMENT = /#(sha256-[A-Za-z0-9_+/=-]+)$/;
13
+
14
+ /** Split a trailing integrity fragment off a ref/URL. Returns the bare ref in
15
+ * `base` (safe to build fetch URLs and cache paths from) and the fragment in
16
+ * `integrity` (e.g. `sha256-<base64url>`), or `undefined` when absent. */
17
+ export function splitIntegrity(ref: string): { base: string; integrity?: string } {
18
+ const match = ref.match(INTEGRITY_FRAGMENT);
19
+ if (!match) return { base: ref };
20
+ return { base: ref.slice(0, match.index), integrity: match[1] };
21
+ }
22
+
23
+ /** Attach an integrity hash to a ref as a `#<alg>-...` fragment. No-op when the
24
+ * hash is absent/non-string or the ref already carries a fragment (an
25
+ * author-authored pin is never overwritten). Inverse of `splitIntegrity`;
26
+ * used to fold the object form's `integrity:` sibling into the source string. */
27
+ export function foldIntegrity(source: string, integrity: unknown): string {
28
+ return typeof integrity === "string" && !source.includes("#")
29
+ ? `${source}#${integrity}`
30
+ : source;
31
+ }
32
+
33
+ function toBase64Url(bytes: Uint8Array): string {
34
+ let binary = "";
35
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
36
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
37
+ }
38
+
39
+ /** Normalize an encoded digest to unpadded base64url so a standard-base64 or
40
+ * padded input still compares equal to our canonical form. */
41
+ function normalizeDigest(value: string): string {
42
+ return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
43
+ }
44
+
45
+ /** SHA-256 of `bytes` as unpadded base64url — the canonical inline-hash form. */
46
+ export async function sha256Base64Url(bytes: Uint8Array): Promise<string> {
47
+ // Copy into a plain ArrayBuffer: a Uint8Array may be backed by a
48
+ // SharedArrayBuffer, which `crypto.subtle.digest` does not accept.
49
+ const buffer = new ArrayBuffer(bytes.byteLength);
50
+ new Uint8Array(buffer).set(bytes);
51
+ const digest = await crypto.subtle.digest("SHA-256", buffer);
52
+ return toBase64Url(new Uint8Array(digest));
53
+ }
54
+
55
+ /** Hash `bytes` and compare against `integrity` (`<alg>-<digest>`). Throws a
56
+ * terminal error on mismatch or an unsupported algorithm. `describe` names the
57
+ * artifact in the error (e.g. the module ref) so the failure is actionable. */
58
+ export async function verifyIntegrity(
59
+ bytes: Uint8Array,
60
+ integrity: string,
61
+ describe: string,
62
+ ): Promise<void> {
63
+ const dash = integrity.indexOf("-");
64
+ const algorithm = dash > 0 ? integrity.slice(0, dash) : "";
65
+ const expected = dash > 0 ? integrity.slice(dash + 1) : "";
66
+ if (algorithm !== "sha256") {
67
+ throw new Error(
68
+ `Unsupported integrity algorithm '${algorithm || integrity}' for ${describe}. ` +
69
+ `Only sha256 is supported (sha256-<base64url>).`,
70
+ );
71
+ }
72
+ const actual = await sha256Base64Url(bytes);
73
+ if (actual !== normalizeDigest(expected)) {
74
+ throw new Error(
75
+ `Integrity check failed for ${describe}: expected sha256-${normalizeDigest(expected)}, ` +
76
+ `got sha256-${actual}. The fetched bytes do not match the recorded hash — ` +
77
+ `the module may have been tampered with or republished.`,
78
+ );
79
+ }
80
+ }
81
+
82
+ /** The single verified network read for remote manifests: fetch `fetchUrl`,
83
+ * verify the raw bytes against `integrity` (when pinned), and return both the
84
+ * bytes and the decoded text. The one choke point every network `ManifestSource`
85
+ * routes through, so verification cannot drift between them. `describe` names
86
+ * the artifact in error messages. */
87
+ export async function verifiedFetch(
88
+ fetchUrl: string,
89
+ integrity: string | undefined,
90
+ describe: string,
91
+ ): Promise<{ bytes: Uint8Array; text: string }> {
92
+ const response = await fetch(fetchUrl);
93
+ if (!response.ok) {
94
+ throw new Error(
95
+ `Failed to fetch manifest ${describe}: ${response.status} ${response.statusText} (${fetchUrl})`,
96
+ );
97
+ }
98
+ const bytes = new Uint8Array(await response.arrayBuffer());
99
+ if (integrity) await verifyIntegrity(bytes, integrity, describe);
100
+ return { bytes, text: new TextDecoder().decode(bytes) };
101
+ }
@@ -0,0 +1,48 @@
1
+ import { splitIntegrity } from "./integrity.js";
2
+
3
+ /** A parsed registry module reference. `modulePath` is `namespace/name`,
4
+ * `version` has any leading `v` stripped, and `integrity` carries the inline
5
+ * `sha256-<base64url>` hash when the ref was pinned. */
6
+ export interface ParsedModuleRef {
7
+ modulePath: string;
8
+ version: string;
9
+ integrity?: string;
10
+ }
11
+
12
+ /** True when `url` has the bare registry-ref shape `namespace/name@version`
13
+ * (no scheme, no leading `/` or `.`, contains both `@` and `/`). The integrity
14
+ * fragment, if any, does not affect the classification. */
15
+ export function isRegistryRef(url: string): boolean {
16
+ const { base } = splitIntegrity(url);
17
+ return (
18
+ !base.startsWith("http://") &&
19
+ !base.startsWith("https://") &&
20
+ !base.startsWith("/") &&
21
+ !base.startsWith(".") &&
22
+ !base.startsWith("file://") &&
23
+ !base.startsWith("memory://") &&
24
+ base.includes("@") &&
25
+ base.includes("/")
26
+ );
27
+ }
28
+
29
+ /** Canonical parser for `namespace/name@version[#sha256-...]` refs. The single
30
+ * source of truth shared by the registry source, the kernel manifest cache,
31
+ * and the CLI (install / upgrade / bundle). Throws on a malformed ref. */
32
+ export function parseModuleRef(ref: string): ParsedModuleRef {
33
+ const { base, integrity } = splitIntegrity(ref);
34
+ const atIdx = base.lastIndexOf("@");
35
+ if (atIdx <= 0 || atIdx === base.length - 1) {
36
+ throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
37
+ }
38
+ const modulePath = base.slice(0, atIdx);
39
+ if (!modulePath.includes("/")) {
40
+ throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
41
+ }
42
+ const rawVersion = base.slice(atIdx + 1);
43
+ const version = rawVersion.startsWith("v") ? rawVersion.slice(1) : rawVersion;
44
+ if (!version) {
45
+ throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
46
+ }
47
+ return { modulePath, version, integrity };
48
+ }
@@ -1,4 +1,6 @@
1
1
  import { DEFAULT_MANIFEST_FILENAME, type ManifestSource } from "../types.js";
2
+ import { splitIntegrity, verifiedFetch } from "./integrity.js";
3
+ import { isRegistryRef, parseModuleRef } from "./module-ref.js";
2
4
 
3
5
  const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
4
6
 
@@ -6,25 +8,13 @@ export class RegistrySource implements ManifestSource {
6
8
  constructor(private registryUrl = DEFAULT_REGISTRY_URL) {}
7
9
 
8
10
  supports(url: string): boolean {
9
- return (
10
- !url.startsWith("http://") &&
11
- !url.startsWith("https://") &&
12
- !url.startsWith("/") &&
13
- !url.startsWith(".") &&
14
- url.includes("@") &&
15
- url.includes("/")
16
- );
11
+ return isRegistryRef(url);
17
12
  }
18
13
 
19
14
  async read(moduleRef: string): Promise<{ text: string; source: string }> {
20
- const fetchUrl = this.toRegistryUrl(moduleRef);
21
- const response = await fetch(fetchUrl);
22
- if (!response.ok) {
23
- throw new Error(
24
- `Failed to fetch manifest ${moduleRef}: ${response.status} ${response.statusText}`,
25
- );
26
- }
27
- const text = await response.text();
15
+ const { base, integrity } = splitIntegrity(moduleRef);
16
+ const fetchUrl = this.toRegistryUrl(base);
17
+ const { text } = await verifiedFetch(fetchUrl, integrity, base);
28
18
  // Some object-storage backends (e.g. Cloudflare R2 / S3) surface auth or
29
19
  // permission failures by returning a 200 status with an XML error body.
30
20
  // Catch this here so the loader produces a precise error rather than
@@ -37,7 +27,7 @@ export class RegistrySource implements ManifestSource {
37
27
  ? `${codeMatch[1]}: ${messageMatch[1]}`
38
28
  : text.slice(0, 200);
39
29
  throw new Error(
40
- `Registry returned a non-manifest response for ${moduleRef} ` +
30
+ `Registry returned a non-manifest response for ${base} ` +
41
31
  `(URL: ${fetchUrl}): ${detail}`,
42
32
  );
43
33
  }
@@ -51,7 +41,7 @@ export class RegistrySource implements ManifestSource {
51
41
  }
52
42
 
53
43
  private toRegistryModuleBase(moduleRef: string): string {
54
- const parsed = this.parseModuleRef(moduleRef);
44
+ const parsed = parseModuleRef(moduleRef);
55
45
  const normalizedBase = this.registryUrl.replace(/\/+$/, "");
56
46
  return `${normalizedBase}/${parsed.modulePath}/${parsed.version}`;
57
47
  }
@@ -59,24 +49,4 @@ export class RegistrySource implements ManifestSource {
59
49
  private toRegistryUrl(moduleRef: string): string {
60
50
  return `${this.toRegistryModuleBase(moduleRef)}/${DEFAULT_MANIFEST_FILENAME}`;
61
51
  }
62
-
63
- private parseModuleRef(moduleRef: string): { modulePath: string; version: string } {
64
- const atIdx = moduleRef.lastIndexOf("@");
65
- if (atIdx <= 0 || atIdx === moduleRef.length - 1) {
66
- throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
67
- }
68
-
69
- const modulePath = moduleRef.slice(0, atIdx);
70
- if (!modulePath.includes("/")) {
71
- throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
72
- }
73
-
74
- const rawVersion = moduleRef.slice(atIdx + 1);
75
- const version = rawVersion.startsWith("v") ? rawVersion.substring(1) : rawVersion;
76
- if (!version) {
77
- throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
78
- }
79
-
80
- return { modulePath, version };
81
- }
82
52
  }