puls-dev 0.1.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 (93) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +148 -0
  3. package/dist/core/checker.d.ts +5 -0
  4. package/dist/core/checker.js +148 -0
  5. package/dist/core/config.d.ts +35 -0
  6. package/dist/core/config.js +15 -0
  7. package/dist/core/decorators.d.ts +26 -0
  8. package/dist/core/decorators.js +86 -0
  9. package/dist/core/output.d.ts +8 -0
  10. package/dist/core/output.js +19 -0
  11. package/dist/core/resource.d.ts +20 -0
  12. package/dist/core/resource.js +77 -0
  13. package/dist/core/stack.d.ts +20 -0
  14. package/dist/core/stack.js +120 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +12 -0
  17. package/dist/providers/aws/acm.d.ts +22 -0
  18. package/dist/providers/aws/acm.js +109 -0
  19. package/dist/providers/aws/api.d.ts +28 -0
  20. package/dist/providers/aws/api.js +36 -0
  21. package/dist/providers/aws/apigateway.d.ts +24 -0
  22. package/dist/providers/aws/apigateway.js +157 -0
  23. package/dist/providers/aws/cloudfront.d.ts +31 -0
  24. package/dist/providers/aws/cloudfront.js +205 -0
  25. package/dist/providers/aws/fargate.d.ts +43 -0
  26. package/dist/providers/aws/fargate.js +277 -0
  27. package/dist/providers/aws/index.d.ts +23 -0
  28. package/dist/providers/aws/index.js +29 -0
  29. package/dist/providers/aws/lambda.d.ts +30 -0
  30. package/dist/providers/aws/lambda.js +159 -0
  31. package/dist/providers/aws/list.d.ts +2 -0
  32. package/dist/providers/aws/list.js +44 -0
  33. package/dist/providers/aws/rds.d.ts +46 -0
  34. package/dist/providers/aws/rds.js +227 -0
  35. package/dist/providers/aws/route53.d.ts +38 -0
  36. package/dist/providers/aws/route53.js +218 -0
  37. package/dist/providers/aws/s3.d.ts +20 -0
  38. package/dist/providers/aws/s3.js +165 -0
  39. package/dist/providers/aws/secrets.d.ts +25 -0
  40. package/dist/providers/aws/secrets.js +151 -0
  41. package/dist/providers/aws/sqs.d.ts +33 -0
  42. package/dist/providers/aws/sqs.js +178 -0
  43. package/dist/providers/do/api.d.ts +11 -0
  44. package/dist/providers/do/api.js +52 -0
  45. package/dist/providers/do/certificate.d.ts +7 -0
  46. package/dist/providers/do/certificate.js +36 -0
  47. package/dist/providers/do/domain.d.ts +21 -0
  48. package/dist/providers/do/domain.js +81 -0
  49. package/dist/providers/do/droplet.d.ts +35 -0
  50. package/dist/providers/do/droplet.js +180 -0
  51. package/dist/providers/do/firewall.d.ts +23 -0
  52. package/dist/providers/do/firewall.js +94 -0
  53. package/dist/providers/do/index.d.ts +15 -0
  54. package/dist/providers/do/index.js +21 -0
  55. package/dist/providers/do/list.d.ts +2 -0
  56. package/dist/providers/do/list.js +59 -0
  57. package/dist/providers/do/load_balancer.d.ts +12 -0
  58. package/dist/providers/do/load_balancer.js +62 -0
  59. package/dist/providers/firebase/api.d.ts +4 -0
  60. package/dist/providers/firebase/api.js +62 -0
  61. package/dist/providers/firebase/auth.d.ts +35 -0
  62. package/dist/providers/firebase/auth.js +147 -0
  63. package/dist/providers/firebase/firestore.d.ts +28 -0
  64. package/dist/providers/firebase/firestore.js +120 -0
  65. package/dist/providers/firebase/functions.d.ts +50 -0
  66. package/dist/providers/firebase/functions.js +163 -0
  67. package/dist/providers/firebase/hosting.d.ts +14 -0
  68. package/dist/providers/firebase/hosting.js +144 -0
  69. package/dist/providers/firebase/index.d.ts +15 -0
  70. package/dist/providers/firebase/index.js +15 -0
  71. package/dist/providers/firebase/remoteconfig.d.ts +22 -0
  72. package/dist/providers/firebase/remoteconfig.js +135 -0
  73. package/dist/providers/firebase/storage.d.ts +34 -0
  74. package/dist/providers/firebase/storage.js +117 -0
  75. package/dist/providers/proxmox/api.d.ts +12 -0
  76. package/dist/providers/proxmox/api.js +50 -0
  77. package/dist/providers/proxmox/index.d.ts +15 -0
  78. package/dist/providers/proxmox/index.js +10 -0
  79. package/dist/providers/proxmox/list.d.ts +2 -0
  80. package/dist/providers/proxmox/list.js +15 -0
  81. package/dist/providers/proxmox/vm.d.ts +61 -0
  82. package/dist/providers/proxmox/vm.js +482 -0
  83. package/dist/types/aws.d.ts +55 -0
  84. package/dist/types/aws.js +48 -0
  85. package/dist/types/do.d.ts +19 -0
  86. package/dist/types/do.js +19 -0
  87. package/dist/types/gcp.d.ts +9 -0
  88. package/dist/types/gcp.js +9 -0
  89. package/dist/types/inventory.d.ts +87 -0
  90. package/dist/types/inventory.js +2 -0
  91. package/dist/types/proxmox.d.ts +11 -0
  92. package/dist/types/proxmox.js +28 -0
  93. package/package.json +56 -0
@@ -0,0 +1,218 @@
1
+ import { ListHostedZonesByNameCommand, CreateHostedZoneCommand, ChangeResourceRecordSetsCommand, } from '@aws-sdk/client-route-53';
2
+ import { RegisterDomainCommand, GetOperationDetailCommand, CheckDomainAvailabilityCommand, } from '@aws-sdk/client-route-53-domains';
3
+ import { BaseBuilder } from '../../core/resource.js';
4
+ import { Output } from '../../core/output.js';
5
+ import { ACMCertificateBuilder } from './acm.js';
6
+ import { getR53Client, getR53DomainsClient } from './api.js';
7
+ export class Route53Builder extends BaseBuilder {
8
+ out = {
9
+ zone: new Output(),
10
+ };
11
+ zoneName;
12
+ zoneId;
13
+ records = [];
14
+ _isRegistering = false;
15
+ _registrantContact;
16
+ _wantsWildcardSSL = false;
17
+ constructor(zoneName = '') {
18
+ super(zoneName || 'route53-pending');
19
+ this.zoneName = zoneName;
20
+ this.discoveryPromise = this.discoverZone(zoneName);
21
+ }
22
+ async discoverZone(name) {
23
+ if (!name)
24
+ return null;
25
+ try {
26
+ const r53 = getR53Client();
27
+ const result = await r53.send(new ListHostedZonesByNameCommand({ DNSName: name, MaxItems: 5 }));
28
+ const match = (result.HostedZones ?? []).find(z => z.Name === `${name}.`);
29
+ if (match) {
30
+ this.zoneId = match.Id.replace('/hostedzone/', '');
31
+ return match;
32
+ }
33
+ return null;
34
+ }
35
+ catch (e) {
36
+ if (e.name === 'CredentialsProviderError')
37
+ return null;
38
+ throw e;
39
+ }
40
+ }
41
+ randomDomain() {
42
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
43
+ const id = Array.from({ length: 12 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
44
+ this.zoneName = `${id}.com`;
45
+ this.name = this.zoneName;
46
+ this.discoveryPromise = this.discoverZone(this.zoneName);
47
+ return this;
48
+ }
49
+ cert() {
50
+ return this.sidecars.find(s => s instanceof ACMCertificateBuilder);
51
+ }
52
+ withWildcardSSL() {
53
+ this._wantsWildcardSSL = true;
54
+ return this;
55
+ }
56
+ register(contact) {
57
+ this._isRegistering = true;
58
+ this._registrantContact = contact;
59
+ return this;
60
+ }
61
+ record(name, type, value) {
62
+ this.records.push({ name, type, value });
63
+ return this;
64
+ }
65
+ pointer(name, target) {
66
+ this.records.push({ name, type: 'A', value: target, isAlias: true });
67
+ return this;
68
+ }
69
+ async deploy() {
70
+ const dryRun = this.isDryRunActive();
71
+ const existing = await this.discoveryPromise;
72
+ const r53 = getR53Client();
73
+ console.log(`\nšŸ—ŗļø Finalizing Route53 Zone "${this.zoneName}"...`);
74
+ // Register domain first so NS records are publicly resolvable before cert validation
75
+ if (this._isRegistering) {
76
+ await this.registerDomain(dryRun);
77
+ }
78
+ if (!existing) {
79
+ if (dryRun) {
80
+ console.log(` šŸ“ [PLAN] Create hosted zone ${this.zoneName}`);
81
+ this.out.zone.resolve({ name: this.zoneName, id: 'PENDING' });
82
+ }
83
+ else {
84
+ // Route53 Domains auto-creates the hosted zone on registration — check again before creating
85
+ const recheck = await this.discoverZone(this.zoneName);
86
+ if (!recheck) {
87
+ const result = await r53.send(new CreateHostedZoneCommand({
88
+ Name: this.zoneName,
89
+ CallerReference: `puls-${Date.now()}`,
90
+ }));
91
+ this.zoneId = result.HostedZone.Id.replace('/hostedzone/', '');
92
+ console.log(`šŸš€ Created hosted zone ${this.zoneName} (id=${this.zoneId})`);
93
+ }
94
+ else {
95
+ console.log(` āœ… Hosted zone ${this.zoneName} created by domain registration (id=${this.zoneId})`);
96
+ }
97
+ }
98
+ }
99
+ else {
100
+ console.log(` āœ… Hosted zone ${this.zoneName} exists (id=${this.zoneId})`);
101
+ }
102
+ if (this.zoneId)
103
+ this.out.zone.resolve({ name: this.zoneName, id: this.zoneId });
104
+ if (this._wantsWildcardSSL && !this.cert()) {
105
+ this.sidecars.push(new ACMCertificateBuilder(this.zoneName, true));
106
+ }
107
+ const cert = this.cert();
108
+ if (cert) {
109
+ cert.forZone(this); // zoneId is set by now — cert writes its own validation CNAMEs
110
+ await cert.deploy();
111
+ }
112
+ // Regular records
113
+ if (this.records.length > 0 && !dryRun && this.zoneId) {
114
+ const resolved = this.records.map(r => ({
115
+ type: r.type,
116
+ name: r.name,
117
+ value: r.value instanceof BaseBuilder ? `[alias: ${r.value.name}]` : r.value,
118
+ }));
119
+ await this.upsertRecords(r53, resolved.map(r => ({ ...r, ttl: 300 })));
120
+ }
121
+ for (const rec of this.records) {
122
+ const val = rec.value instanceof BaseBuilder ? `[Alias to ${rec.value.name}]` : rec.value;
123
+ console.log(` āœ… [${dryRun ? 'PLAN' : 'OK'}] ${rec.type}: ${rec.name}.${this.zoneName} → ${val}`);
124
+ }
125
+ return { zone: this.zoneName, id: this.zoneId };
126
+ }
127
+ async registerDomain(dryRun) {
128
+ const domains = getR53DomainsClient();
129
+ const c = this._registrantContact;
130
+ if (dryRun) {
131
+ console.log(` šŸ“ [PLAN] Register domain ${this.zoneName} via Route53 Domains`);
132
+ if (c)
133
+ console.log(` └─ Registrant: ${c.FIRSTNAME} ${c.LASTNAME} <${c.EMAIL}>`);
134
+ return;
135
+ }
136
+ // Check availability before attempting registration
137
+ const avail = await domains.send(new CheckDomainAvailabilityCommand({ DomainName: this.zoneName }));
138
+ if (avail.Availability !== 'AVAILABLE') {
139
+ console.log(` ā„¹ļø Domain ${this.zoneName} is not available for registration (${avail.Availability}) — skipping`);
140
+ return;
141
+ }
142
+ const contact = c ? this.mapContact(c) : undefined;
143
+ if (!contact)
144
+ throw new Error(`register() called without contact details — provide a RegistrantContact`);
145
+ console.log(` šŸ“‹ Registering domain ${this.zoneName}... (est. ~5 min)`);
146
+ const result = await domains.send(new RegisterDomainCommand({
147
+ DomainName: this.zoneName,
148
+ DurationInYears: 1,
149
+ AutoRenew: true,
150
+ AdminContact: contact,
151
+ RegistrantContact: contact,
152
+ TechContact: contact,
153
+ PrivacyProtectAdminContact: true,
154
+ PrivacyProtectRegistrantContact: true,
155
+ PrivacyProtectTechContact: true,
156
+ }));
157
+ console.log(`šŸš€ Domain registration submitted (operationId=${result.OperationId})`);
158
+ await this.waitFor(`domain "${this.zoneName}" to become active`, async () => {
159
+ const op = await domains.send(new GetOperationDetailCommand({ OperationId: result.OperationId }));
160
+ if (op.Status === 'ERROR' || op.Status === 'FAILED') {
161
+ throw new Error(`Domain registration failed (${op.Status}): ${op.Message}`);
162
+ }
163
+ return op.Status === 'SUCCESSFUL';
164
+ }, { intervalMs: 15_000, timeoutMs: 900_000 });
165
+ console.log(` āœ… Domain ${this.zoneName} registered`);
166
+ }
167
+ mapContact(c) {
168
+ return {
169
+ FirstName: c.FIRSTNAME,
170
+ LastName: c.LASTNAME,
171
+ Email: c.EMAIL,
172
+ PhoneNumber: this.normalizePhone(c.MOBILE),
173
+ ContactType: c.CONTACT_TYPE,
174
+ OrganizationName: c.ORGANIZATION,
175
+ AddressLine1: c.ADDRESSLINE,
176
+ City: c.CITY,
177
+ ZipCode: c.ZIPCODE,
178
+ CountryCode: c.COUNTRY,
179
+ };
180
+ }
181
+ // Route53 Domains requires +CC.subscriber format (e.g. +46.708339809)
182
+ normalizePhone(phone) {
183
+ if (phone.includes('.'))
184
+ return phone;
185
+ const digits = phone.replace(/^\+/, '');
186
+ // +1 (US/CA) and +7 (RU/KZ) are single-digit country codes
187
+ if (digits.startsWith('1') || digits.startsWith('7')) {
188
+ return `+${digits[0]}.${digits.slice(1)}`;
189
+ }
190
+ // Default: treat first 2 digits as country code
191
+ return `+${digits.slice(0, 2)}.${digits.slice(2)}`;
192
+ }
193
+ async upsertCnames(records) {
194
+ if (!this.zoneId)
195
+ throw new Error(`Zone ${this.zoneName} has no ID — was it deployed?`);
196
+ const r53 = getR53Client();
197
+ await this.upsertRecords(r53, records.map(r => ({ type: 'CNAME', name: `${r.name}.${this.zoneName}`, value: r.value, ttl: 300 })));
198
+ for (const r of records) {
199
+ console.log(` āœ… CNAME ${r.name}.${this.zoneName} → ${r.value}`);
200
+ }
201
+ }
202
+ async upsertRecords(r53, records) {
203
+ await r53.send(new ChangeResourceRecordSetsCommand({
204
+ HostedZoneId: this.zoneId,
205
+ ChangeBatch: {
206
+ Changes: records.map(r => ({
207
+ Action: 'UPSERT',
208
+ ResourceRecordSet: {
209
+ Name: r.name,
210
+ Type: r.type,
211
+ TTL: r.ttl,
212
+ ResourceRecords: [{ Value: r.value }],
213
+ },
214
+ })),
215
+ },
216
+ }));
217
+ }
218
+ }
@@ -0,0 +1,20 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ import { CloudFrontBuilder } from './cloudfront.js';
3
+ export declare class S3BucketBuilder extends BaseBuilder {
4
+ bucketName: string;
5
+ private _versioning;
6
+ private _allowedDistributions;
7
+ private _region?;
8
+ private _uploadPath?;
9
+ constructor(bucketName: string);
10
+ region(r: string): this;
11
+ private discoverBucket;
12
+ versioning(enabled?: boolean): this;
13
+ allowFrom(...distributions: CloudFrontBuilder[]): this;
14
+ upload(filePath: string): this;
15
+ deploy(): Promise<{
16
+ name: string;
17
+ }>;
18
+ private uploadFile;
19
+ private updateBucketPolicy;
20
+ }
@@ -0,0 +1,165 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { basename, extname } from 'node:path';
3
+ import { HeadBucketCommand, CreateBucketCommand, GetBucketPolicyCommand, PutBucketPolicyCommand, PutObjectCommand, } from '@aws-sdk/client-s3';
4
+ import { BaseBuilder } from '../../core/resource.js';
5
+ import { getS3Client } from './api.js';
6
+ import { Config } from '../../core/config.js';
7
+ export class S3BucketBuilder extends BaseBuilder {
8
+ bucketName;
9
+ _versioning = false;
10
+ _allowedDistributions = [];
11
+ _region;
12
+ _uploadPath;
13
+ constructor(bucketName) {
14
+ super(bucketName);
15
+ this.bucketName = bucketName;
16
+ this.discoveryPromise = this.discoverBucket(bucketName);
17
+ }
18
+ region(r) {
19
+ this._region = r;
20
+ this.discoveryPromise = this.discoverBucket(this.bucketName);
21
+ return this;
22
+ }
23
+ async discoverBucket(name) {
24
+ try {
25
+ await getS3Client(this._region).send(new HeadBucketCommand({ Bucket: name }));
26
+ return true;
27
+ }
28
+ catch (e) {
29
+ const status = e.$metadata?.httpStatusCode;
30
+ if (status === 404 || e.name === 'NotFound')
31
+ return false;
32
+ if (status === 301 || status === 403)
33
+ return true; // exists in different region or access denied
34
+ if (e.name === 'CredentialsProviderError')
35
+ return false;
36
+ throw e;
37
+ }
38
+ }
39
+ versioning(enabled = true) {
40
+ this._versioning = enabled;
41
+ return this;
42
+ }
43
+ allowFrom(...distributions) {
44
+ this._allowedDistributions.push(...distributions);
45
+ return this;
46
+ }
47
+ upload(filePath) {
48
+ this._uploadPath = filePath;
49
+ return this;
50
+ }
51
+ async deploy() {
52
+ const dryRun = this.isDryRunActive();
53
+ const exists = await this.discoveryPromise;
54
+ const region = this._region ?? Config.get().providers.aws?.region ?? 'us-east-1';
55
+ const s3 = getS3Client(region);
56
+ console.log(`\n🪣 Finalizing S3 Bucket "${this.bucketName}"...`);
57
+ if (!exists) {
58
+ if (dryRun) {
59
+ console.log(` šŸ“ [PLAN] Create bucket ${this.bucketName} (${region})`);
60
+ }
61
+ else {
62
+ const createCmd = { Bucket: this.bucketName };
63
+ if (region !== 'us-east-1') {
64
+ createCmd.CreateBucketConfiguration = { LocationConstraint: region };
65
+ }
66
+ await s3.send(new CreateBucketCommand(createCmd));
67
+ console.log(`šŸš€ Created bucket ${this.bucketName}`);
68
+ }
69
+ }
70
+ else {
71
+ console.log(` āœ… Bucket ${this.bucketName} already exists.`);
72
+ }
73
+ if (this._allowedDistributions.length > 0) {
74
+ const unresolved = this._allowedDistributions.filter(d => !d.resolvedArn);
75
+ if (unresolved.length > 0) {
76
+ throw new Error(`[S3:${this.bucketName}] allowFrom() has unresolved distributions: ` +
77
+ unresolved.map(d => `"${d.name}"`).join(', ') +
78
+ '. Declare the bucket after all CloudFront distributions in your Stack.');
79
+ }
80
+ const newArns = this._allowedDistributions.map(d => d.resolvedArn);
81
+ if (dryRun) {
82
+ console.log(` šŸ“ [PLAN] Append ${newArns.length} CloudFront OAC ARN(s) to bucket policy`);
83
+ for (const arn of newArns)
84
+ console.log(` └─ ${arn}`);
85
+ }
86
+ else {
87
+ await this.updateBucketPolicy(s3, newArns);
88
+ }
89
+ }
90
+ if (this._uploadPath) {
91
+ if (dryRun) {
92
+ console.log(` šŸ“ [PLAN] Upload ${basename(this._uploadPath)} → s3://${this.bucketName}/`);
93
+ }
94
+ else {
95
+ await this.uploadFile(s3, this._uploadPath);
96
+ }
97
+ }
98
+ await this.deploySidecars();
99
+ return { name: this.bucketName };
100
+ }
101
+ async uploadFile(s3, filePath) {
102
+ const key = basename(filePath);
103
+ const body = readFileSync(filePath);
104
+ const contentTypeMap = {
105
+ '.json': 'application/json',
106
+ '.js': 'application/javascript',
107
+ '.html': 'text/html',
108
+ '.css': 'text/css',
109
+ '.png': 'image/png',
110
+ '.jpg': 'image/jpeg',
111
+ '.svg': 'image/svg+xml',
112
+ };
113
+ const contentType = contentTypeMap[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
114
+ await s3.send(new PutObjectCommand({
115
+ Bucket: this.bucketName,
116
+ Key: key,
117
+ Body: body,
118
+ ContentType: contentType,
119
+ }));
120
+ console.log(` āœ… Uploaded ${key} → s3://${this.bucketName}/${key}`);
121
+ }
122
+ async updateBucketPolicy(s3, newArns) {
123
+ let policy = { Version: '2012-10-17', Statement: [] };
124
+ try {
125
+ const existing = await s3.send(new GetBucketPolicyCommand({ Bucket: this.bucketName }));
126
+ if (existing.Policy)
127
+ policy = JSON.parse(existing.Policy);
128
+ }
129
+ catch (e) {
130
+ if (e.name !== 'NoSuchBucketPolicy')
131
+ throw e;
132
+ }
133
+ // Find any existing CloudFront-principal statement regardless of Sid
134
+ let stmt = policy.Statement.find((s) => s.Principal?.Service === 'cloudfront.amazonaws.com' && s.Effect === 'Allow');
135
+ if (!stmt) {
136
+ stmt = {
137
+ Sid: 'AllowCloudFrontServicePrincipal',
138
+ Effect: 'Allow',
139
+ Principal: { Service: 'cloudfront.amazonaws.com' },
140
+ Action: 's3:GetObject',
141
+ Resource: `arn:aws:s3:::${this.bucketName}/*`,
142
+ Condition: { StringEquals: { 'AWS:SourceArn': [] } },
143
+ };
144
+ policy.Statement.push(stmt);
145
+ }
146
+ // Condition key may be 'aws:SourceArn' or 'AWS:SourceArn' depending on how it was created
147
+ const cond = stmt.Condition?.StringEquals ?? {};
148
+ const sourceArnKey = Object.keys(cond).find(k => k.toLowerCase() === 'aws:sourcearn') ?? 'AWS:SourceArn';
149
+ if (!stmt.Condition)
150
+ stmt.Condition = { StringEquals: {} };
151
+ if (!stmt.Condition.StringEquals)
152
+ stmt.Condition.StringEquals = {};
153
+ const existing = stmt.Condition.StringEquals[sourceArnKey];
154
+ const existingArns = Array.isArray(existing) ? existing : existing ? [existing] : [];
155
+ const merged = [...new Set([...existingArns, ...newArns])];
156
+ stmt.Condition.StringEquals[sourceArnKey] = merged;
157
+ await s3.send(new PutBucketPolicyCommand({
158
+ Bucket: this.bucketName,
159
+ Policy: JSON.stringify(policy),
160
+ }));
161
+ console.log(` āœ… Updated bucket policy — ${merged.length} distribution ARN(s) allowed`);
162
+ for (const arn of newArns)
163
+ console.log(` └─ ${arn}`);
164
+ }
165
+ }
@@ -0,0 +1,25 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ export declare class SecretsBuilder extends BaseBuilder {
3
+ private _value?;
4
+ private _description?;
5
+ private _forceDelete;
6
+ private _pendingDeletion;
7
+ resolvedValue: string | null;
8
+ resolvedArn: string | null;
9
+ constructor(secretId: string);
10
+ private fetchSecret;
11
+ awaitValue(): Promise<string | null>;
12
+ plainText(v: string): this;
13
+ keyValue(obj: object): this;
14
+ description(d: string): this;
15
+ forceDelete(): this;
16
+ deploy(): Promise<{
17
+ name: string;
18
+ arn: string | null;
19
+ value: string | null;
20
+ }>;
21
+ destroy(): Promise<{
22
+ destroyed: string;
23
+ }>;
24
+ }
25
+ export declare function resolveEnvVars(env: Record<string, string | SecretsBuilder>): Promise<Record<string, string>>;
@@ -0,0 +1,151 @@
1
+ import { GetSecretValueCommand, CreateSecretCommand, PutSecretValueCommand, DeleteSecretCommand, } from "@aws-sdk/client-secrets-manager";
2
+ import { BaseBuilder } from "../../core/resource.js";
3
+ import { getSecretsClient } from "./api.js";
4
+ export class SecretsBuilder extends BaseBuilder {
5
+ _value;
6
+ _description;
7
+ _forceDelete = false;
8
+ _pendingDeletion = false;
9
+ resolvedValue = null;
10
+ resolvedArn = null;
11
+ constructor(secretId) {
12
+ super(secretId);
13
+ this.discoveryPromise = this.fetchSecret(secretId);
14
+ }
15
+ async fetchSecret(secretId) {
16
+ try {
17
+ const result = await getSecretsClient().send(new GetSecretValueCommand({ SecretId: secretId }));
18
+ this.resolvedValue = result.SecretString ?? null;
19
+ this.resolvedArn = result.ARN ?? null;
20
+ return result;
21
+ }
22
+ catch (e) {
23
+ if (e.name === "ResourceNotFoundException")
24
+ return null;
25
+ if (e.name === "CredentialsProviderError")
26
+ return null;
27
+ if (e.name === "InvalidRequestException") {
28
+ this._pendingDeletion = true;
29
+ return null;
30
+ }
31
+ throw e;
32
+ }
33
+ }
34
+ // Awaits eager discovery — used by resolveEnvVars so callers don't need discoveryPromise directly
35
+ async awaitValue() {
36
+ await this.discoveryPromise;
37
+ return this.resolvedValue;
38
+ }
39
+ plainText(v) {
40
+ this._value = v;
41
+ return this;
42
+ }
43
+ keyValue(obj) {
44
+ this._value = JSON.stringify(obj);
45
+ return this;
46
+ }
47
+ description(d) {
48
+ this._description = d;
49
+ return this;
50
+ }
51
+ forceDelete() {
52
+ this._forceDelete = true;
53
+ return this;
54
+ }
55
+ async deploy() {
56
+ const dryRun = this.isDryRunActive();
57
+ const existing = await this.discoveryPromise;
58
+ const client = getSecretsClient();
59
+ console.log(`\nšŸ” Finalizing Secret "${this.name}"...`);
60
+ if (dryRun) {
61
+ if (existing) {
62
+ console.log(` āœ… Secret "${this.name}" exists`);
63
+ if (this.resolvedValue !== null)
64
+ console.log(` šŸ’¬ Value: ${this.resolvedValue}`);
65
+ if (this._value)
66
+ console.log(` šŸ“ [PLAN] Update secret value`);
67
+ }
68
+ else {
69
+ console.log(` šŸ“ [PLAN] Create secret "${this.name}"`);
70
+ if (this._description)
71
+ console.log(` └─ Description: ${this._description}`);
72
+ }
73
+ return { name: this.name, arn: this.resolvedArn, value: this.resolvedValue };
74
+ }
75
+ if (!existing) {
76
+ if (!this._value) {
77
+ console.log(` āš ļø Secret "${this.name}" does not exist — add .plainText() or .keyValue() to create it`);
78
+ return { name: this.name, arn: null, value: null };
79
+ }
80
+ const result = await client.send(new CreateSecretCommand({
81
+ Name: this.name,
82
+ SecretString: this._value,
83
+ Description: this._description,
84
+ }));
85
+ this.resolvedArn = result.ARN ?? null;
86
+ this.resolvedValue = this._value;
87
+ console.log(`šŸš€ Created secret "${this.name}"`);
88
+ }
89
+ else {
90
+ console.log(` āœ… Secret "${this.name}" exists`);
91
+ if (this.resolvedValue !== null)
92
+ console.log(` šŸ’¬ Value: ${this.resolvedValue}`);
93
+ if (this._value && this._value !== this.resolvedValue) {
94
+ await client.send(new PutSecretValueCommand({
95
+ SecretId: this.name,
96
+ SecretString: this._value,
97
+ }));
98
+ this.resolvedValue = this._value;
99
+ console.log(` āœ… Updated secret value`);
100
+ }
101
+ }
102
+ await this.deploySidecars();
103
+ return { name: this.name, arn: this.resolvedArn, value: this.resolvedValue };
104
+ }
105
+ async destroy() {
106
+ const dryRun = this.isDryRunActive();
107
+ const existing = await this.discoveryPromise;
108
+ console.log(`\nšŸ—‘ļø Destroying Secret "${this.name}"...`);
109
+ if (!existing) {
110
+ if (this._pendingDeletion)
111
+ console.log(` ā³ Secret "${this.name}" is already scheduled for deletion`);
112
+ else
113
+ console.log(` āœ… Secret "${this.name}" does not exist — nothing to do`);
114
+ return { destroyed: this.name };
115
+ }
116
+ if (dryRun) {
117
+ const mode = this._forceDelete
118
+ ? "immediate"
119
+ : "scheduled (30-day recovery window)";
120
+ console.log(` šŸ“ [PLAN] Delete secret "${this.name}" — ${mode}`);
121
+ return { destroyed: this.name };
122
+ }
123
+ await getSecretsClient().send(new DeleteSecretCommand({
124
+ SecretId: this.name,
125
+ ForceDeleteWithoutRecovery: this._forceDelete,
126
+ ...(this._forceDelete ? {} : { RecoveryWindowInDays: 30 }),
127
+ }));
128
+ const mode = this._forceDelete
129
+ ? "immediately"
130
+ : "scheduled for deletion (30-day recovery window)";
131
+ console.log(` āœ… Secret "${this.name}" ${mode}`);
132
+ return { destroyed: this.name };
133
+ }
134
+ }
135
+ // Resolve a mix of plain strings and SecretsBuilders into plain strings.
136
+ // Called at deploy time in Lambda and Fargate, after all discoveryPromises have settled.
137
+ export async function resolveEnvVars(env) {
138
+ const resolved = {};
139
+ for (const [k, v] of Object.entries(env)) {
140
+ if (v instanceof SecretsBuilder) {
141
+ await v.awaitValue();
142
+ if (v.resolvedValue === null)
143
+ throw new Error(`Secret "${v.name}" has no value — create it first or call .plainText()/.keyValue() in the stack`);
144
+ resolved[k] = v.resolvedValue;
145
+ }
146
+ else {
147
+ resolved[k] = v;
148
+ }
149
+ }
150
+ return resolved;
151
+ }
@@ -0,0 +1,33 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ export declare class SQSBuilder extends BaseBuilder {
3
+ private _fifo;
4
+ private _deduplication;
5
+ private _visibilityTimeout;
6
+ private _retentionDays;
7
+ private _delaySeconds;
8
+ private _dlqName?;
9
+ private _dlqMaxReceives;
10
+ resolvedUrl: string | null;
11
+ resolvedArn: string | null;
12
+ resolvedDlqUrl: string | null;
13
+ resolvedDlqArn: string | null;
14
+ constructor(name: string);
15
+ fifo(enabled?: boolean): this;
16
+ deduplication(enabled?: boolean): this;
17
+ timeout(seconds: number): this;
18
+ retention(days: number): this;
19
+ delay(seconds: number): this;
20
+ dlq(name: string, maxReceives?: number): this;
21
+ private queueName;
22
+ private discoverQueue;
23
+ private ensureQueue;
24
+ private buildAttributes;
25
+ deploy(): Promise<{
26
+ name: string;
27
+ url: string | null;
28
+ arn: string | null;
29
+ }>;
30
+ destroy(): Promise<{
31
+ destroyed: string;
32
+ }>;
33
+ }