@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 +2 -1
- package/dist/index.d.mts +42 -1
- package/dist/index.d.ts +42 -1
- package/dist/index.js +66 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/scripts/canonical_hash.sh +278 -0
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**:
|
|
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/
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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,
|