puls-dev 0.2.8 → 0.3.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.
Files changed (66) hide show
  1. package/dist/core/checker.js +71 -0
  2. package/dist/core/config.d.ts +5 -0
  3. package/dist/core/config.js +12 -1
  4. package/dist/core/context.d.ts +14 -0
  5. package/dist/core/context.js +2 -0
  6. package/dist/core/decorators.d.ts +2 -0
  7. package/dist/core/decorators.js +8 -14
  8. package/dist/core/group.test.d.ts +1 -0
  9. package/dist/core/group.test.js +94 -0
  10. package/dist/core/parallel.test.d.ts +1 -0
  11. package/dist/core/parallel.test.js +215 -0
  12. package/dist/core/production.test.d.ts +1 -0
  13. package/dist/core/production.test.js +189 -0
  14. package/dist/core/provisioner.js +29 -11
  15. package/dist/core/resource.d.ts +8 -0
  16. package/dist/core/resource.js +45 -0
  17. package/dist/core/retry.d.ts +9 -0
  18. package/dist/core/retry.js +28 -0
  19. package/dist/core/retry.test.d.ts +1 -0
  20. package/dist/core/retry.test.js +66 -0
  21. package/dist/core/secret.d.ts +2 -1
  22. package/dist/core/secret.js +12 -2
  23. package/dist/core/stack.js +381 -75
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/providers/aws/api.js +97 -17
  27. package/dist/providers/aws/ec2.d.ts +3 -0
  28. package/dist/providers/aws/ec2.js +37 -3
  29. package/dist/providers/aws/ec2.test.js +5 -3
  30. package/dist/providers/aws/index.d.ts +2 -0
  31. package/dist/providers/aws/index.js +2 -0
  32. package/dist/providers/aws/secrets.js +20 -3
  33. package/dist/providers/aws/template.d.ts +34 -0
  34. package/dist/providers/aws/template.js +252 -0
  35. package/dist/providers/aws/template.test.d.ts +1 -0
  36. package/dist/providers/aws/template.test.js +208 -0
  37. package/dist/providers/do/api.d.ts +2 -0
  38. package/dist/providers/do/api.js +124 -26
  39. package/dist/providers/do/droplet.js +14 -0
  40. package/dist/providers/firebase/api.js +92 -29
  41. package/dist/providers/firebase/list.d.ts +2 -0
  42. package/dist/providers/firebase/list.js +25 -0
  43. package/dist/providers/gcp/api.js +88 -14
  44. package/dist/providers/gcp/index.d.ts +3 -1
  45. package/dist/providers/gcp/index.js +3 -1
  46. package/dist/providers/gcp/list.d.ts +2 -0
  47. package/dist/providers/gcp/list.js +55 -0
  48. package/dist/providers/gcp/secrets.js +21 -4
  49. package/dist/providers/gcp/template.d.ts +32 -0
  50. package/dist/providers/gcp/template.js +252 -0
  51. package/dist/providers/gcp/template.test.d.ts +1 -0
  52. package/dist/providers/gcp/template.test.js +227 -0
  53. package/dist/providers/gcp/vm.d.ts +3 -0
  54. package/dist/providers/gcp/vm.js +46 -3
  55. package/dist/providers/proxmox/api.d.ts +1 -0
  56. package/dist/providers/proxmox/api.js +72 -16
  57. package/dist/providers/proxmox/index.d.ts +3 -1
  58. package/dist/providers/proxmox/index.js +14 -1
  59. package/dist/providers/proxmox/template.d.ts +44 -0
  60. package/dist/providers/proxmox/template.js +350 -0
  61. package/dist/providers/proxmox/template.test.d.ts +1 -0
  62. package/dist/providers/proxmox/template.test.js +215 -0
  63. package/dist/providers/proxmox/vm.d.ts +3 -0
  64. package/dist/providers/proxmox/vm.js +43 -11
  65. package/dist/types/inventory.d.ts +44 -1
  66. package/package.json +2 -2
@@ -0,0 +1,189 @@
1
+ import { test, describe, beforeEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { Stack } from "./stack.js";
4
+ import { Config } from "./config.js";
5
+ import { Output } from "./output.js";
6
+ import { BaseBuilder } from "./resource.js";
7
+ import { Secret } from "./secret.js";
8
+ import { resourceContextStorage } from "./context.js";
9
+ import { runAnsible } from "./provisioner.js";
10
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+ class DelayResource extends BaseBuilder {
12
+ delayMs;
13
+ executionLogs;
14
+ out = { val: new Output() };
15
+ constructor(name, delayMs, executionLogs) {
16
+ super(name);
17
+ this.delayMs = delayMs;
18
+ this.executionLogs = executionLogs;
19
+ }
20
+ async deploy() {
21
+ this.executionLogs.push(`start:${this.name}`);
22
+ await delay(this.delayMs);
23
+ this.executionLogs.push(`end:${this.name}`);
24
+ return { name: this.name };
25
+ }
26
+ }
27
+ class FailResource extends BaseBuilder {
28
+ async deploy() {
29
+ throw new Error("Failure in deploy!");
30
+ }
31
+ }
32
+ class AbortResource extends BaseBuilder {
33
+ signalLogs;
34
+ constructor(name, signalLogs) {
35
+ super(name);
36
+ this.signalLogs = signalLogs;
37
+ }
38
+ async deploy() {
39
+ const signal = resourceContextStorage.getStore()?.abortSignal;
40
+ if (signal) {
41
+ if (signal.aborted) {
42
+ this.signalLogs.push(`aborted-before:${this.name}`);
43
+ }
44
+ else {
45
+ signal.addEventListener("abort", () => {
46
+ this.signalLogs.push(`aborted-during:${this.name}`);
47
+ });
48
+ }
49
+ }
50
+ await delay(50);
51
+ return { name: this.name };
52
+ }
53
+ }
54
+ describe("Production Features Unit Tests", () => {
55
+ beforeEach(() => {
56
+ Config.set({
57
+ dryRun: false,
58
+ parallel: false,
59
+ offline: false,
60
+ providers: {}
61
+ });
62
+ });
63
+ test("Secret Redaction in console.log during Stack execution", async () => {
64
+ // Register secret
65
+ const mySecret = new Secret("my-api-key", async () => "super-secret-12345");
66
+ await mySecret.get(); // resolves and registers it
67
+ const logs = [];
68
+ const originalLog = console.log;
69
+ // Create a stack that logs the secret
70
+ class SecretStack extends Stack {
71
+ r1 = new DelayResource("r1", 10, []);
72
+ async beforeDeploy() {
73
+ console.log("Starting deployment with my-api-key super-secret-12345");
74
+ }
75
+ }
76
+ const stack = new SecretStack();
77
+ // Intercept console.log temporarily just to capture it
78
+ console.log = (...args) => {
79
+ logs.push(args.join(" "));
80
+ originalLog(...args);
81
+ };
82
+ try {
83
+ await stack.deploy();
84
+ }
85
+ finally {
86
+ console.log = originalLog;
87
+ }
88
+ const deploymentLog = logs.join("\n");
89
+ assert.ok(!deploymentLog.includes("super-secret-12345"), "Should NOT contain raw secret");
90
+ assert.ok(deploymentLog.includes("my-api-key ********"), "Should contain redacted secret replacement");
91
+ });
92
+ test("Secret Redaction in formatEntry table matching patterns", async () => {
93
+ class CredentialsStack extends Stack {
94
+ creds_resource = new class extends BaseBuilder {
95
+ async deploy() {
96
+ return {
97
+ password: "my-cleartext-password",
98
+ api_token: "my-cleartext-token",
99
+ safe_field: "public-info"
100
+ };
101
+ }
102
+ }("creds");
103
+ db_password = new class extends BaseBuilder {
104
+ async deploy() {
105
+ return "my-cleartext-db-pass";
106
+ }
107
+ }("db-pass");
108
+ }
109
+ const logs = [];
110
+ const originalLog = console.log;
111
+ console.log = (...args) => {
112
+ logs.push(args.join(" "));
113
+ originalLog(...args);
114
+ };
115
+ try {
116
+ await new CredentialsStack().deploy();
117
+ }
118
+ finally {
119
+ console.log = originalLog;
120
+ }
121
+ const tableOutput = logs.join("\n");
122
+ assert.ok(!tableOutput.includes("my-cleartext-password"), "Should redact password key values in table");
123
+ assert.ok(!tableOutput.includes("my-cleartext-token"), "Should redact token key values in table");
124
+ assert.ok(!tableOutput.includes("my-cleartext-db-pass"), "Should redact sensitive parent key values in table");
125
+ assert.ok(tableOutput.includes("********"), "Should replace with asterisks");
126
+ assert.ok(tableOutput.includes("public-info"), "Should preserve non-sensitive fields");
127
+ });
128
+ test("Parallel Scheduler Cancellation (Fail-Fast)", async () => {
129
+ Config.set({ parallel: true });
130
+ const signalLogs = [];
131
+ class CancelStack extends Stack {
132
+ r1 = new AbortResource("r1", signalLogs);
133
+ r2 = new FailResource("r2");
134
+ }
135
+ await assert.rejects(async () => {
136
+ await new CancelStack().deploy();
137
+ }, /Failure in deploy!/);
138
+ // Wait a bit for abort listener
139
+ await delay(30);
140
+ assert.ok(signalLogs.includes("aborted-during:r1") || signalLogs.includes("aborted-before:r1"), "Aborted resource should have abort signal triggered");
141
+ });
142
+ test("Credential-Free Offline Dry-Run Mode", async () => {
143
+ Config.set({ offline: true, dryRun: true });
144
+ class OfflineStack extends Stack {
145
+ aws_vm = new class extends BaseBuilder {
146
+ async deploy() {
147
+ const { getEC2Client } = await import("../providers/aws/api.js");
148
+ const client = getEC2Client();
149
+ const res = await client.send({ constructor: { name: "RunInstancesCommand" } });
150
+ return res;
151
+ }
152
+ }("aws");
153
+ gcp_vm = new class extends BaseBuilder {
154
+ async deploy() {
155
+ const { gcpFetch } = await import("../providers/gcp/api.js");
156
+ const res = await gcpFetch("compute", "/instances");
157
+ return res;
158
+ }
159
+ }("gcp");
160
+ }
161
+ const outputs = await new OfflineStack().deploy();
162
+ assert.ok(outputs.aws_vm.Instances[0].InstanceId.startsWith("i-mock"), "AWS VM should resolve to mock");
163
+ assert.ok(outputs.gcp_vm.id.startsWith("mock-gcp-instance"), "GCP VM should resolve to mock");
164
+ });
165
+ test("Ansible Provisioner Stack-Wide Dynamic Inventory Generation", async () => {
166
+ const context = {
167
+ stackName: "my-test-stack",
168
+ hosts: [
169
+ { name: "web1", ip: "1.2.3.4", user: "root", sshKey: "/path/to/key", provider: "do" },
170
+ { name: "db1", ip: "5.6.7.8", user: "ubuntu", sshKey: "/path/to/other-key", provider: "aws" }
171
+ ]
172
+ };
173
+ await resourceContextStorage.run(context, async () => {
174
+ try {
175
+ await runAnsible("1.2.3.4", "root", "/path/to/key", "playbook.yml");
176
+ }
177
+ catch (err) {
178
+ // We expect it might fail if ansible-playbook binary is missing, but it should still attempt write!
179
+ assert.ok(err.message.includes("ansible-playbook") || err.message.includes("ENOENT"), "Should attempt run");
180
+ }
181
+ const fs = await import("node:fs");
182
+ const exists = fs.existsSync("/tmp/puls-inventory-my-test-stack.ini");
183
+ assert.ok(exists, "Dynamic inventory file should be generated in /tmp");
184
+ const contents = fs.readFileSync("/tmp/puls-inventory-my-test-stack.ini", "utf8");
185
+ assert.ok(contents.includes("web1 ansible_host=1.2.3.4"), "Should contain web1 host entry");
186
+ assert.ok(contents.includes("db1 ansible_host=5.6.7.8"), "Should contain db1 host entry");
187
+ });
188
+ });
189
+ });
@@ -1,6 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import net from "node:net";
3
3
  import { homedir } from "node:os";
4
+ import { writeFileSync } from "node:fs";
5
+ import { resourceContextStorage } from "./context.js";
4
6
  export function checkPort(ip, port) {
5
7
  return new Promise((resolve) => {
6
8
  const socket = new net.Socket();
@@ -31,18 +33,34 @@ function resolveSshKeyPath(sshKeys) {
31
33
  export function runAnsible(ip, user, sshKeys, playbook) {
32
34
  console.log(` 🔧 Running Ansible: ${playbook} → ${ip}`);
33
35
  const keyPath = resolveSshKeyPath(sshKeys);
36
+ const context = resourceContextStorage.getStore();
37
+ const stackName = context?.stackName ?? "default";
38
+ const hosts = context?.hosts ?? [];
34
39
  return new Promise((resolve, reject) => {
35
- const proc = spawn("ansible-playbook", [
36
- playbook,
37
- "-i",
38
- `${ip},`,
39
- "-u",
40
- user,
41
- "--private-key",
42
- keyPath,
43
- "--ssh-extra-args",
44
- "-o StrictHostKeyChecking=no -o ConnectTimeout=30",
45
- ], { stdio: "inherit" });
40
+ const args = [playbook];
41
+ if (hosts.length > 0) {
42
+ const inventoryPath = `/tmp/puls-inventory-${stackName}.ini`;
43
+ let inventoryContent = "[all]\n";
44
+ for (const h of hosts) {
45
+ const hKeyPath = resolveSshKeyPath(h.sshKey);
46
+ inventoryContent += `${h.name} ansible_host=${h.ip} ansible_user=${h.user} ansible_ssh_private_key_file=${hKeyPath}\n`;
47
+ }
48
+ try {
49
+ writeFileSync(inventoryPath, inventoryContent, "utf8");
50
+ }
51
+ catch (err) {
52
+ reject(new Error(`Failed to write Ansible inventory: ${err.message}`));
53
+ return;
54
+ }
55
+ const currentHost = hosts.find(h => h.ip === ip);
56
+ const hostLimit = currentHost ? currentHost.name : ip;
57
+ args.push("-i", inventoryPath, "--limit", hostLimit);
58
+ }
59
+ else {
60
+ args.push("-i", `${ip},`, "-u", user, "--private-key", keyPath);
61
+ }
62
+ args.push("--ssh-extra-args", "-o StrictHostKeyChecking=no -o ConnectTimeout=30");
63
+ const proc = spawn("ansible-playbook", args, { stdio: "inherit" });
46
64
  proc.on("close", (code) => {
47
65
  if (code === 0) {
48
66
  console.log(` ✅ Provisioning complete`);
@@ -4,11 +4,18 @@ export declare abstract class BaseBuilder {
4
4
  protected localDryRun: boolean | null;
5
5
  protected discoveryPromise: Promise<any>;
6
6
  protected sidecars: BaseBuilder[];
7
+ /** @internal */
8
+ _deployPromise: Promise<any>;
9
+ /** @internal */
10
+ _destroyPromise?: Promise<any>;
11
+ /** @internal */
12
+ _dependencies: BaseBuilder[];
7
13
  private _beforeDeployHooks;
8
14
  private _afterDeployHooks;
9
15
  private _beforeDestroyHooks;
10
16
  private _afterDestroyHooks;
11
17
  constructor(name: string);
18
+ dependsOn(resource: BaseBuilder): this;
12
19
  protect(): this;
13
20
  dryRun(enabled?: boolean): this;
14
21
  beforeDeploy(callback: () => Promise<void> | void): this;
@@ -34,3 +41,4 @@ export declare abstract class BaseBuilder {
34
41
  destroy(): Promise<any>;
35
42
  abstract deploy(): Promise<any>;
36
43
  }
44
+ export declare function createBuilderArray<T extends BaseBuilder>(builders: T[]): T[] & T;
@@ -5,6 +5,12 @@ export class BaseBuilder {
5
5
  localDryRun = null;
6
6
  discoveryPromise;
7
7
  sidecars = [];
8
+ /** @internal */
9
+ _deployPromise;
10
+ /** @internal */
11
+ _destroyPromise;
12
+ /** @internal */
13
+ _dependencies = [];
8
14
  _beforeDeployHooks = [];
9
15
  _afterDeployHooks = [];
10
16
  _beforeDestroyHooks = [];
@@ -12,6 +18,10 @@ export class BaseBuilder {
12
18
  constructor(name) {
13
19
  this.name = name;
14
20
  }
21
+ dependsOn(resource) {
22
+ this._dependencies.push(resource);
23
+ return this;
24
+ }
15
25
  protect() {
16
26
  this.isProtected = true;
17
27
  return this;
@@ -119,3 +129,38 @@ export class BaseBuilder {
119
129
  return { destroyed: this.name };
120
130
  }
121
131
  }
132
+ export function createBuilderArray(builders) {
133
+ return new Proxy(builders, {
134
+ get(target, prop, receiver) {
135
+ if (prop in target) {
136
+ const val = Reflect.get(target, prop, receiver);
137
+ if (typeof val === "function") {
138
+ return val.bind(target);
139
+ }
140
+ return val;
141
+ }
142
+ if (builders.length > 0) {
143
+ const first = builders[0];
144
+ const val = Reflect.get(first, prop);
145
+ if (typeof val === "function") {
146
+ return function (...args) {
147
+ builders.forEach((b, idx) => {
148
+ const method = Reflect.get(b, prop);
149
+ if (typeof method === "function") {
150
+ const mappedArgs = args.map((arg) => {
151
+ if (Array.isArray(arg) && arg.length === builders.length) {
152
+ return arg[idx];
153
+ }
154
+ return arg;
155
+ });
156
+ method.apply(b, mappedArgs);
157
+ }
158
+ });
159
+ return receiver;
160
+ };
161
+ }
162
+ }
163
+ return undefined;
164
+ },
165
+ });
166
+ }
@@ -0,0 +1,9 @@
1
+ export interface RetryOptions {
2
+ maxAttempts?: number;
3
+ initialDelayMs?: number;
4
+ backoffFactor?: number;
5
+ jitter?: boolean;
6
+ retryable?: (error: any) => boolean;
7
+ delayFn?: (ms: number) => Promise<void>;
8
+ }
9
+ export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
@@ -0,0 +1,28 @@
1
+ export async function withRetry(fn, options = {}) {
2
+ const maxAttempts = options.maxAttempts ?? 5;
3
+ const initialDelayMs = options.initialDelayMs ?? 1000;
4
+ const backoffFactor = options.backoffFactor ?? 2;
5
+ const useJitter = options.jitter ?? true;
6
+ const retryable = options.retryable ?? (() => true);
7
+ const delayFn = options.delayFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
8
+ let attempt = 0;
9
+ while (true) {
10
+ try {
11
+ return await fn();
12
+ }
13
+ catch (error) {
14
+ attempt++;
15
+ if (attempt >= maxAttempts || !retryable(error)) {
16
+ throw error;
17
+ }
18
+ let delay = initialDelayMs * Math.pow(backoffFactor, attempt - 1);
19
+ if (useJitter) {
20
+ // Add random jitter offset of +/- 10%
21
+ const jitterAmount = (Math.random() * 0.2 - 0.1) * delay;
22
+ delay += jitterAmount;
23
+ }
24
+ console.warn(` ⚠️ Transient error encountered: ${error.message || error}. Retrying in ${Math.round(delay)}ms (attempt ${attempt}/${maxAttempts})...`);
25
+ await delayFn(delay);
26
+ }
27
+ }
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ import { test, describe } from "node:test";
2
+ import assert from "node:assert";
3
+ import { withRetry } from "./retry.js";
4
+ describe("Retry & Backoff Engine Unit Tests", () => {
5
+ test("resolves immediately if target function succeeds on the first try", async () => {
6
+ let calls = 0;
7
+ const res = await withRetry(async () => {
8
+ calls++;
9
+ return "success";
10
+ });
11
+ assert.strictEqual(res, "success");
12
+ assert.strictEqual(calls, 1);
13
+ });
14
+ test("retries up to max attempts and then throws the final error", async () => {
15
+ let calls = 0;
16
+ const delayTimes = [];
17
+ await assert.rejects(withRetry(async () => {
18
+ calls++;
19
+ throw new Error(`fail-${calls}`);
20
+ }, {
21
+ maxAttempts: 3,
22
+ jitter: false,
23
+ delayFn: async (ms) => {
24
+ delayTimes.push(ms);
25
+ },
26
+ }), /fail-3/);
27
+ assert.strictEqual(calls, 3);
28
+ // Exponential backoff verification:
29
+ // Delay 1: 1000ms * 2^0 = 1000ms
30
+ // Delay 2: 1000ms * 2^1 = 2000ms
31
+ assert.deepStrictEqual(delayTimes, [1000, 2000]);
32
+ });
33
+ test("immediately stops retrying and throws on non-retryable errors", async () => {
34
+ let calls = 0;
35
+ await assert.rejects(withRetry(async () => {
36
+ calls++;
37
+ if (calls === 1)
38
+ throw new Error("transient");
39
+ throw new Error("fatal");
40
+ }, {
41
+ maxAttempts: 5,
42
+ retryable: (err) => err.message === "transient",
43
+ delayFn: async () => { },
44
+ }), /fatal/);
45
+ assert.strictEqual(calls, 2);
46
+ });
47
+ test("includes randomized jitter when options.jitter is enabled", async () => {
48
+ const delayTimes = [];
49
+ await assert.rejects(withRetry(async () => {
50
+ throw new Error("fail");
51
+ }, {
52
+ maxAttempts: 3,
53
+ initialDelayMs: 1000,
54
+ backoffFactor: 2,
55
+ jitter: true,
56
+ delayFn: async (ms) => {
57
+ delayTimes.push(ms);
58
+ },
59
+ }), /fail/);
60
+ assert.strictEqual(delayTimes.length, 2);
61
+ // With 10% jitter, first delay (1000) should be between 900 and 1100
62
+ assert.ok(delayTimes[0] >= 900 && delayTimes[0] <= 1100);
63
+ // Second delay (2000) should be between 1800 and 2200
64
+ assert.ok(delayTimes[1] >= 1800 && delayTimes[1] <= 2200);
65
+ });
66
+ });
@@ -1,4 +1,5 @@
1
1
  import { Output } from "./output.js";
2
+ export declare const resolvedSecrets: Set<string>;
2
3
  /**
3
4
  * Secret represents a lazy, secure credential that is fetched asynchronously
4
5
  * at deployment time instead of during the eager construction phase.
@@ -11,7 +12,7 @@ export declare class Secret extends Output<string> {
11
12
  * Helper method to seamlessly unpack either a static string, a standard Output, or a Secret.
12
13
  * Call this within resource builder deploy() methods.
13
14
  */
14
- static resolve(val: string | Output<string> | Secret | any): Promise<string>;
15
+ static resolve(val: string | Output<string> | Secret): Promise<string>;
15
16
  /**
16
17
  * Fetches a secret from a local environment variable.
17
18
  */
@@ -1,5 +1,6 @@
1
1
  import { Output } from "./output.js";
2
2
  import { Config } from "./config.js";
3
+ export const resolvedSecrets = new Set();
3
4
  /**
4
5
  * Secret represents a lazy, secure credential that is fetched asynchronously
5
6
  * at deployment time instead of during the eager construction phase.
@@ -15,11 +16,16 @@ export class Secret extends Output {
15
16
  try {
16
17
  if (Config.isGlobalDryRun()) {
17
18
  // Resolve immediately to a secure placeholder during dry-run testing
18
- this.resolve(`[SECRET:${this.secretName}]`);
19
+ const placeholder = `[SECRET:${this.secretName}]`;
20
+ this.resolve(placeholder);
21
+ resolvedSecrets.add(placeholder);
19
22
  return;
20
23
  }
21
24
  const val = await fetcher();
22
25
  this.resolve(val);
26
+ if (val && val.length >= 3) {
27
+ resolvedSecrets.add(val);
28
+ }
23
29
  }
24
30
  catch (err) {
25
31
  this.reject(err);
@@ -31,7 +37,11 @@ export class Secret extends Output {
31
37
  */
32
38
  static async resolve(val) {
33
39
  if (val instanceof Output) {
34
- return await val.get();
40
+ const resolved = await val.get();
41
+ if (val instanceof Secret && resolved && resolved.length >= 3) {
42
+ resolvedSecrets.add(resolved);
43
+ }
44
+ return resolved;
35
45
  }
36
46
  return val;
37
47
  }