@truefoundry/tfy-infra-engine 0.1.0 → 0.1.2-canary.e8cd23d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,7 +53,8 @@ console.log('Aggregate Hash:', result.manifest.aggregateHash);
53
53
  - **Pure transformer**: Accepts pre-resolved `Template` objects -- no I/O for fetching
54
54
  - **JSON Schema validation**: Validate inputs against JSON Schema Draft-07 using AJV with `useDefaults`, `allErrors`, and `discriminator` support
55
55
  - **Handlebars templating**: 7 built-in helpers for HCL generation
56
- - **HCL formatting**: Automatic formatting with `tofu fmt`
56
+ - **HCL formatting**: Optional formatting with `tofu fmt` for output readability
57
+ - **Format-independent canonical hashing**: Deterministic SHA-256 hashes via `canonical_hash.sh` regardless of formatting, comments, or whitespace. The script is available for external callers (e.g., bash scripts using Terraform directly)
57
58
  - **Deterministic output**: Byte-identical output for identical inputs
58
59
  - **Integrity verification**: Zone-based file ownership, `@tfy-status` headers, JSON Manifest, and drift detection
59
60
  - **Four-operation API**: `install`, `verify`, `upgrade`, and `hashOnly`
package/dist/index.d.mts CHANGED
@@ -529,6 +529,47 @@ declare const DEFAULT_PREFIX = "tfy_";
529
529
  */
530
530
  declare function classifyFile(filePath: string, prefix?: string): Zone;
531
531
 
532
+ /**
533
+ * Format-independent canonical hashing via external shell script.
534
+ *
535
+ * Delegates to `scripts/canonical_hash.sh` which canonicalizes HCL content
536
+ * (stripping headers, comments, whitespace, and normalizing trailing commas)
537
+ * then computes a SHA-256 hash. Uses spawnSync to keep the entire call graph
538
+ * synchronous.
539
+ *
540
+ * Reference: R-003, R-010
541
+ */
542
+ /**
543
+ * Get the absolute path to the canonical_hash.sh script.
544
+ *
545
+ * Resolves by walking up from the current module's directory to find the
546
+ * package root, then appending `scripts/canonical_hash.sh`. The result is
547
+ * cached after first resolution.
548
+ *
549
+ * Works in all environments:
550
+ * - Dev/test: __dirname = src/integrity -> walk up 2 levels
551
+ * - tsup CJS: __dirname = dist -> walk up 1 level
552
+ * - tsup ESM: __dirname shimmed by esbuild -> walk up 1 level
553
+ */
554
+ declare function getCanonicalHashScriptPath(): string;
555
+ /**
556
+ * Compute a format-independent canonical hash of HCL content.
557
+ *
558
+ * The shell script handles the full pipeline: strip @tfy-status header,
559
+ * strip carriage returns, canonicalize via AWK 4-state machine, then SHA-256.
560
+ *
561
+ * If the script fails for any reason (not found, timeout, bad exit code,
562
+ * malformed output), falls back to a raw SHA-256 of the header-stripped
563
+ * content and logs a warning.
564
+ *
565
+ * @param content - Raw file content (may include @tfy-status header)
566
+ * @param options - Optional timeout in milliseconds (default: 10000)
567
+ * @returns Hash string in format "sha256:<64-hex-chars>"
568
+ */
569
+ declare function canonicalHash(content: string, options?: {
570
+ timeout?: number;
571
+ }): string;
572
+
532
573
  /**
533
574
  * @tfy-status header processing: parse, strip, inject.
534
575
  *
@@ -586,4 +627,4 @@ declare function parseHeader(content: string): TfyStatusHeader | undefined;
586
627
  */
587
628
  declare function createEngine(): HclEngine;
588
629
 
589
- export { DEFAULT_PREFIX, type DriftReport, type DriftType, EngineError, EngineErrorCode, type Envelope, type FileEntry, type FileMap, type HashOnlyResult, type InstallResult, type JSONSchema7, type Manifest, type Resolver, type ResolverOptions, type Template, type UpgradeResult, type VerifyResult, type VersionInfo, classifyFile, createEngine, isTofuAvailable, parseHeader, templateJsonSchema, validateJsonSchemaStructure, validateTemplateJson };
630
+ export { DEFAULT_PREFIX, type DriftReport, type DriftType, EngineError, EngineErrorCode, type Envelope, type FileEntry, type FileMap, type HashOnlyResult, type InstallResult, type JSONSchema7, type Manifest, type Resolver, type ResolverOptions, type Template, type UpgradeResult, type VerifyResult, type VersionInfo, canonicalHash, classifyFile, createEngine, getCanonicalHashScriptPath, isTofuAvailable, parseHeader, templateJsonSchema, validateJsonSchemaStructure, validateTemplateJson };
package/dist/index.d.ts CHANGED
@@ -529,6 +529,47 @@ declare const DEFAULT_PREFIX = "tfy_";
529
529
  */
530
530
  declare function classifyFile(filePath: string, prefix?: string): Zone;
531
531
 
532
+ /**
533
+ * Format-independent canonical hashing via external shell script.
534
+ *
535
+ * Delegates to `scripts/canonical_hash.sh` which canonicalizes HCL content
536
+ * (stripping headers, comments, whitespace, and normalizing trailing commas)
537
+ * then computes a SHA-256 hash. Uses spawnSync to keep the entire call graph
538
+ * synchronous.
539
+ *
540
+ * Reference: R-003, R-010
541
+ */
542
+ /**
543
+ * Get the absolute path to the canonical_hash.sh script.
544
+ *
545
+ * Resolves by walking up from the current module's directory to find the
546
+ * package root, then appending `scripts/canonical_hash.sh`. The result is
547
+ * cached after first resolution.
548
+ *
549
+ * Works in all environments:
550
+ * - Dev/test: __dirname = src/integrity -> walk up 2 levels
551
+ * - tsup CJS: __dirname = dist -> walk up 1 level
552
+ * - tsup ESM: __dirname shimmed by esbuild -> walk up 1 level
553
+ */
554
+ declare function getCanonicalHashScriptPath(): string;
555
+ /**
556
+ * Compute a format-independent canonical hash of HCL content.
557
+ *
558
+ * The shell script handles the full pipeline: strip @tfy-status header,
559
+ * strip carriage returns, canonicalize via AWK 4-state machine, then SHA-256.
560
+ *
561
+ * If the script fails for any reason (not found, timeout, bad exit code,
562
+ * malformed output), falls back to a raw SHA-256 of the header-stripped
563
+ * content and logs a warning.
564
+ *
565
+ * @param content - Raw file content (may include @tfy-status header)
566
+ * @param options - Optional timeout in milliseconds (default: 10000)
567
+ * @returns Hash string in format "sha256:<64-hex-chars>"
568
+ */
569
+ declare function canonicalHash(content: string, options?: {
570
+ timeout?: number;
571
+ }): string;
572
+
532
573
  /**
533
574
  * @tfy-status header processing: parse, strip, inject.
534
575
  *
@@ -586,4 +627,4 @@ declare function parseHeader(content: string): TfyStatusHeader | undefined;
586
627
  */
587
628
  declare function createEngine(): HclEngine;
588
629
 
589
- export { DEFAULT_PREFIX, type DriftReport, type DriftType, EngineError, EngineErrorCode, type Envelope, type FileEntry, type FileMap, type HashOnlyResult, type InstallResult, type JSONSchema7, type Manifest, type Resolver, type ResolverOptions, type Template, type UpgradeResult, type VerifyResult, type VersionInfo, classifyFile, createEngine, isTofuAvailable, parseHeader, templateJsonSchema, validateJsonSchemaStructure, validateTemplateJson };
630
+ export { DEFAULT_PREFIX, type DriftReport, type DriftType, EngineError, EngineErrorCode, type Envelope, type FileEntry, type FileMap, type HashOnlyResult, type InstallResult, type JSONSchema7, type Manifest, type Resolver, type ResolverOptions, type Template, type UpgradeResult, type VerifyResult, type VersionInfo, canonicalHash, classifyFile, createEngine, getCanonicalHashScriptPath, isTofuAvailable, parseHeader, templateJsonSchema, validateJsonSchemaStructure, validateTemplateJson };
package/dist/index.js CHANGED
@@ -33,8 +33,10 @@ __export(index_exports, {
33
33
  DEFAULT_PREFIX: () => DEFAULT_PREFIX,
34
34
  EngineError: () => EngineError,
35
35
  EngineErrorCode: () => EngineErrorCode,
36
+ canonicalHash: () => canonicalHash,
36
37
  classifyFile: () => classifyFile,
37
38
  createEngine: () => createEngine,
39
+ getCanonicalHashScriptPath: () => getCanonicalHashScriptPath,
38
40
  isTofuAvailable: () => isTofuAvailable,
39
41
  parseHeader: () => parseHeader,
40
42
  templateJsonSchema: () => templateJsonSchema,
@@ -533,6 +535,12 @@ async function isTofuAvailable() {
533
535
  }
534
536
 
535
537
  // src/integrity/hasher.ts
538
+ var import_node_crypto2 = require("crypto");
539
+
540
+ // src/integrity/canonical-hash.ts
541
+ var import_node_child_process2 = require("child_process");
542
+ var import_node_path = require("path");
543
+ var import_node_fs = require("fs");
536
544
  var import_node_crypto = require("crypto");
537
545
 
538
546
  // src/integrity/header.ts
@@ -594,20 +602,65 @@ ${HEADER_END}
594
602
  ${content}`;
595
603
  }
596
604
 
597
- // src/integrity/normalizer.ts
598
- function normalizeForHashing(content) {
599
- let normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
600
- normalized = normalized.split("\n").map((line) => line.replace(/[\t ]+$/, "")).join("\n");
601
- normalized = normalized.replace(/\n*$/, "\n");
602
- return normalized;
605
+ // src/integrity/canonical-hash.ts
606
+ var HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
607
+ var DEFAULT_TIMEOUT_MS = 3e3;
608
+ var _cachedScriptPath;
609
+ function findPackageRoot(startDir) {
610
+ let dir = startDir;
611
+ for (; ; ) {
612
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"))) return dir;
613
+ const parent = (0, import_node_path.dirname)(dir);
614
+ if (parent === dir) break;
615
+ dir = parent;
616
+ }
617
+ throw new Error(`Cannot find package root (no package.json found above ${startDir})`);
618
+ }
619
+ function getCanonicalHashScriptPath() {
620
+ if (_cachedScriptPath !== void 0) return _cachedScriptPath;
621
+ const pkgRoot = findPackageRoot(__dirname);
622
+ _cachedScriptPath = (0, import_node_path.join)(pkgRoot, "scripts", "canonical_hash.sh");
623
+ return _cachedScriptPath;
624
+ }
625
+ function canonicalHash(content, options) {
626
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
627
+ try {
628
+ const scriptPath = getCanonicalHashScriptPath();
629
+ const result = (0, import_node_child_process2.spawnSync)("sh", [scriptPath], {
630
+ input: content,
631
+ encoding: "utf-8",
632
+ timeout,
633
+ stdio: ["pipe", "pipe", "pipe"]
634
+ });
635
+ if (result.error) {
636
+ throw result.error;
637
+ }
638
+ if (result.status !== 0) {
639
+ throw new Error(`canonical_hash.sh exited with code ${result.status}: ${result.stderr}`);
640
+ }
641
+ const hash = result.stdout.trim();
642
+ if (!HASH_PATTERN.test(hash)) {
643
+ throw new Error(`canonical_hash.sh returned malformed output: ${hash}`);
644
+ }
645
+ return hash;
646
+ } catch (err) {
647
+ const message = err instanceof Error ? err.message : String(err);
648
+ process.stderr.write(
649
+ `[tfy-infra-engine] canonical hash failed, using raw hash fallback: ${message}
650
+ `
651
+ );
652
+ const stripped = stripHeader(content);
653
+ const hex = (0, import_node_crypto.createHash)("sha256").update(stripped, "utf-8").digest("hex");
654
+ return `sha256:${hex}`;
655
+ }
603
656
  }
604
657
 
605
658
  // src/integrity/zones.ts
606
- var import_node_path = require("path");
659
+ var import_node_path2 = require("path");
607
660
  var DEFAULT_PREFIX = "tfy_";
608
661
  var MANIFEST_FILENAME = "manifest.json";
609
662
  function classifyFile(filePath, prefix = DEFAULT_PREFIX) {
610
- const name = (0, import_node_path.basename)(filePath.replace(/\\/g, "/"));
663
+ const name = (0, import_node_path2.basename)(filePath.replace(/\\/g, "/"));
611
664
  if (name === MANIFEST_FILENAME) {
612
665
  return "platform";
613
666
  }
@@ -626,13 +679,11 @@ function countPlatformFiles(files) {
626
679
 
627
680
  // src/integrity/hasher.ts
628
681
  function sha256(content) {
629
- const hex = (0, import_node_crypto.createHash)("sha256").update(content, "utf-8").digest("hex");
682
+ const hex = (0, import_node_crypto2.createHash)("sha256").update(content, "utf-8").digest("hex");
630
683
  return `sha256:${hex}`;
631
684
  }
632
685
  function hashFileContent(content) {
633
- const stripped = stripHeader(content);
634
- const normalized = normalizeForHashing(stripped);
635
- return sha256(normalized);
686
+ return canonicalHash(content);
636
687
  }
637
688
  function computeAggregateHash(files) {
638
689
  const entries = [];
@@ -650,7 +701,7 @@ function computeAggregateHash(files) {
650
701
  }
651
702
 
652
703
  // src/render/manifest.ts
653
- var ENGINE_VERSION = "0.1.0";
704
+ var ENGINE_VERSION = "0.1.2-canary.e8cd23d";
654
705
  function generateManifest(opts) {
655
706
  const { files, template, inputs, formatted, intentId, platformPrefix = "tfy_" } = opts;
656
707
  const manifestFiles = [];
@@ -1144,8 +1195,10 @@ function createEngine() {
1144
1195
  DEFAULT_PREFIX,
1145
1196
  EngineError,
1146
1197
  EngineErrorCode,
1198
+ canonicalHash,
1147
1199
  classifyFile,
1148
1200
  createEngine,
1201
+ getCanonicalHashScriptPath,
1149
1202
  isTofuAvailable,
1150
1203
  parseHeader,
1151
1204
  templateJsonSchema,