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,120 @@
1
+ import "reflect-metadata";
2
+ import { BaseBuilder } from "./resource.js";
3
+ const _registry = new Map();
4
+ function formatEntry(val) {
5
+ if (!val || typeof val !== "object")
6
+ return { primary: String(val) };
7
+ // Known shapes
8
+ if ("destroyed" in val)
9
+ return { primary: val.destroyed ? "šŸ—‘ļø destroyed" : "─ not found" };
10
+ if (val.zone)
11
+ return { primary: val.zone };
12
+ if (val.name && val.id)
13
+ return { primary: `${val.name} [${val.id}]` };
14
+ if (val.name)
15
+ return { primary: val.name };
16
+ if (val.arn)
17
+ return { primary: val.arn };
18
+ // Generic: pull all scalar values
19
+ const pairs = Object.entries(val).filter(([, v]) => typeof v === "string" || typeof v === "number");
20
+ if (pairs.length === 0)
21
+ return { primary: JSON.stringify(val) };
22
+ // Try compact inline (values only, dot-separated)
23
+ const inline = pairs.map(([, v]) => v).join(" Ā· ");
24
+ if (inline.length <= 52)
25
+ return { primary: inline };
26
+ // Too long — first value as primary, rest as sub-lines
27
+ const [[, first], ...rest] = pairs;
28
+ return {
29
+ primary: first,
30
+ sub: rest.map(([k, v]) => `${k} ${v}`),
31
+ };
32
+ }
33
+ function printOutputs(stackName, outputs) {
34
+ const title = ` ${stackName} `;
35
+ const keyWidth = Math.max(...Object.keys(outputs).map((k) => k.length));
36
+ const rows = Object.entries(outputs).map(([key, val]) => ({
37
+ key,
38
+ ...formatEntry(val),
39
+ }));
40
+ // textWidth = width of row text content (without the 2-space padding on each side)
41
+ const textWidth = Math.max(...rows.flatMap(({ key, primary, sub }) => [
42
+ keyWidth + 2 + primary.length,
43
+ ...(sub ?? []).map((s) => keyWidth + 6 + s.length),
44
+ ]));
45
+ // innerWidth = total chars between │ delimiters: text + 2-space padding each side
46
+ const innerWidth = Math.max(textWidth + 4, title.length);
47
+ const line = "─".repeat(innerWidth);
48
+ console.log(`\n ā”Œ${line}┐`);
49
+ console.log(` │${title.padEnd(innerWidth)}│`);
50
+ console.log(` ā”œ${line}┤`);
51
+ for (const { key, primary, sub } of rows) {
52
+ const mainRow = `${key.padEnd(keyWidth)} ${primary}`;
53
+ console.log(` │ ${mainRow.padEnd(innerWidth - 4)} │`);
54
+ for (const s of sub ?? []) {
55
+ const subRow = `${"".padEnd(keyWidth)} ${s}`;
56
+ console.log(` │ ${subRow.padEnd(innerWidth - 4)} │`);
57
+ }
58
+ }
59
+ console.log(` ā””${line}ā”˜`);
60
+ }
61
+ export class Stack {
62
+ /** @internal — called by @Deploy to register the instance for cross-stack references. */
63
+ static _register(cls, instance) {
64
+ _registry.set(cls, instance);
65
+ }
66
+ /**
67
+ * Returns the already-constructed instance of another Stack so you can reference
68
+ * its resource Output fields before deployment completes.
69
+ *
70
+ * The target stack must be decorated with @Deploy and imported before this call.
71
+ *
72
+ * @example
73
+ * class DNSStack extends Stack {
74
+ * private infra = Stack.from(InfraStack);
75
+ * dns = DO.Domain("example.com").pointer("app", this.infra.app.ip);
76
+ * }
77
+ */
78
+ static from(cls) {
79
+ const instance = _registry.get(cls);
80
+ if (!instance)
81
+ throw new Error(`Stack "${cls.name}" is not registered. Make sure it is decorated with @Deploy and its module is imported before referencing it.`);
82
+ return instance;
83
+ }
84
+ async deploy() {
85
+ console.log(`\nšŸ—ļø Deploying Stack: ${this.constructor.name}`);
86
+ const props = Object.getOwnPropertyNames(this);
87
+ const outputs = {};
88
+ for (const prop of props) {
89
+ const resource = this[prop];
90
+ if (resource instanceof BaseBuilder) {
91
+ const isProtected = Reflect.getMetadata("protected", this, prop);
92
+ const isDestroyed = Reflect.getMetadata("destroy", this, prop);
93
+ if (isProtected)
94
+ resource.protect();
95
+ outputs[prop] = isDestroyed
96
+ ? await resource.destroy()
97
+ : await resource.deploy();
98
+ }
99
+ }
100
+ printOutputs(this.constructor.name, outputs);
101
+ return outputs;
102
+ }
103
+ async destroy() {
104
+ console.log(`\nšŸ’„ Tearing down Stack: ${this.constructor.name}`);
105
+ const props = Object.getOwnPropertyNames(this).reverse();
106
+ const outputs = {};
107
+ for (const prop of props) {
108
+ const resource = this[prop];
109
+ if (resource instanceof BaseBuilder) {
110
+ if (Reflect.getMetadata("protected", this, prop)) {
111
+ console.log(` šŸ”’ Skipping protected resource "${prop}"`);
112
+ continue;
113
+ }
114
+ outputs[prop] = await resource.destroy();
115
+ }
116
+ }
117
+ printOutputs(this.constructor.name, outputs);
118
+ return outputs;
119
+ }
120
+ }
@@ -0,0 +1,12 @@
1
+ export * from "./core/stack.js";
2
+ export * from "./core/decorators.js";
3
+ export * from "./core/checker.js";
4
+ export * from "./core/resource.js";
5
+ export { AWS } from "./providers/aws/index.js";
6
+ export { DO } from "./providers/do/index.js";
7
+ export { Proxmox } from "./providers/proxmox/index.js";
8
+ export { Firebase } from "./providers/firebase/index.js";
9
+ export * as AWS_TYPES from "./types/aws.js";
10
+ export * as DO_TYPES from "./types/do.js";
11
+ export * as PROXMOX_TYPES from "./types/proxmox.js";
12
+ export * as INVENTORY_TYPES from "./types/inventory.js";
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export * from "./core/stack.js";
2
+ export * from "./core/decorators.js";
3
+ export * from "./core/checker.js";
4
+ export * from "./core/resource.js";
5
+ export { AWS } from "./providers/aws/index.js";
6
+ export { DO } from "./providers/do/index.js";
7
+ export { Proxmox } from "./providers/proxmox/index.js";
8
+ export { Firebase } from "./providers/firebase/index.js";
9
+ export * as AWS_TYPES from "./types/aws.js";
10
+ export * as DO_TYPES from "./types/do.js";
11
+ export * as PROXMOX_TYPES from "./types/proxmox.js";
12
+ export * as INVENTORY_TYPES from "./types/inventory.js";
@@ -0,0 +1,22 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ interface ZoneRef {
3
+ zoneId?: string;
4
+ zoneName: string;
5
+ }
6
+ export declare class ACMCertificateBuilder extends BaseBuilder {
7
+ domainName: string;
8
+ wildcard: boolean;
9
+ resolvedArn: string | null;
10
+ validationRecords: {
11
+ name: string;
12
+ value: string;
13
+ }[];
14
+ private _zone?;
15
+ constructor(domainName: string, wildcard?: boolean);
16
+ forZone(zone: ZoneRef): this;
17
+ private discoverCertificate;
18
+ deploy(): Promise<{
19
+ arn: string | null;
20
+ }>;
21
+ }
22
+ export {};
@@ -0,0 +1,109 @@
1
+ import { ListCertificatesCommand, RequestCertificateCommand, DescribeCertificateCommand, } from '@aws-sdk/client-acm';
2
+ import { ChangeResourceRecordSetsCommand } from '@aws-sdk/client-route-53';
3
+ import { BaseBuilder } from '../../core/resource.js';
4
+ import { getACMClient, getR53Client } from './api.js';
5
+ export class ACMCertificateBuilder extends BaseBuilder {
6
+ domainName;
7
+ wildcard;
8
+ resolvedArn = null;
9
+ validationRecords = [];
10
+ _zone;
11
+ constructor(domainName, wildcard = true) {
12
+ super(`acm-${domainName}`);
13
+ this.domainName = domainName;
14
+ this.wildcard = wildcard;
15
+ this.discoveryPromise = this.discoverCertificate(domainName, wildcard);
16
+ }
17
+ forZone(zone) {
18
+ this._zone = zone;
19
+ return this;
20
+ }
21
+ async discoverCertificate(domain, wildcard) {
22
+ try {
23
+ const acm = getACMClient();
24
+ const primaryName = wildcard ? `*.${domain}` : domain;
25
+ const list = await acm.send(new ListCertificatesCommand({ CertificateStatuses: ['ISSUED', 'PENDING_VALIDATION'] }));
26
+ for (const cert of list.CertificateSummaryList ?? []) {
27
+ if (cert.DomainName === primaryName && cert.CertificateArn) {
28
+ this.resolvedArn = cert.CertificateArn;
29
+ return cert;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+ catch (e) {
35
+ if (e.name === 'CredentialsProviderError')
36
+ return null;
37
+ throw e;
38
+ }
39
+ }
40
+ async deploy() {
41
+ const dryRun = this.isDryRunActive();
42
+ const existing = await this.discoveryPromise;
43
+ console.log(`\nšŸ” Finalizing ACM Certificate for "${this.domainName}"...`);
44
+ if (existing) {
45
+ console.log(` āœ… Certificate already exists (${existing.Status ?? 'ISSUED'}): ${this.resolvedArn}`);
46
+ return { arn: this.resolvedArn };
47
+ }
48
+ const primaryName = this.wildcard ? `*.${this.domainName}` : this.domainName;
49
+ const sanNames = this.wildcard ? [this.domainName] : [];
50
+ if (dryRun) {
51
+ console.log(` šŸ“ [PLAN] Request ${this.wildcard ? 'wildcard ' : ''}certificate: ${primaryName}`);
52
+ console.log(` šŸ“ [PLAN] DNS validation CNAMEs will be auto-written to Route53`);
53
+ this.resolvedArn = `arn:aws:acm:us-east-1:DRYRUN:certificate/pending`;
54
+ return { arn: this.resolvedArn };
55
+ }
56
+ const acm = getACMClient();
57
+ const result = await acm.send(new RequestCertificateCommand({
58
+ DomainName: primaryName,
59
+ SubjectAlternativeNames: sanNames.length ? sanNames : undefined,
60
+ ValidationMethod: 'DNS',
61
+ }));
62
+ this.resolvedArn = result.CertificateArn;
63
+ console.log(`šŸš€ Requested certificate ${primaryName} (arn=${this.resolvedArn})`);
64
+ // Step 1: wait until ALL DomainValidationOptions have their ResourceRecord
65
+ // (wildcard + apex SANs each get an entry; ACM can generate them at different times)
66
+ await this.waitFor('validation records to be generated', async () => {
67
+ const detail = await acm.send(new DescribeCertificateCommand({ CertificateArn: this.resolvedArn }));
68
+ const options = detail.Certificate?.DomainValidationOptions ?? [];
69
+ if (options.length === 0)
70
+ return false;
71
+ const withRecords = options.filter(o => o.ResourceRecord);
72
+ if (withRecords.length < options.length)
73
+ return false;
74
+ this.validationRecords = withRecords.map(o => ({
75
+ name: o.ResourceRecord.Name,
76
+ value: o.ResourceRecord.Value,
77
+ }));
78
+ return true;
79
+ }, { intervalMs: 5_000, timeoutMs: 60_000 });
80
+ // Step 2: write them into Route53 automatically — no manual DNS work needed
81
+ if (this._zone?.zoneId) {
82
+ // Wildcard certs produce duplicate CNAME names across SANs — deduplicate before sending
83
+ const unique = Array.from(new Map(this.validationRecords.map(r => [r.name, r])).values());
84
+ const r53 = getR53Client();
85
+ await r53.send(new ChangeResourceRecordSetsCommand({
86
+ HostedZoneId: this._zone.zoneId,
87
+ ChangeBatch: {
88
+ Changes: unique.map(r => ({
89
+ Action: 'UPSERT',
90
+ ResourceRecordSet: {
91
+ Name: r.name.replace(/\.$/, ''),
92
+ Type: 'CNAME',
93
+ TTL: 300,
94
+ ResourceRecords: [{ Value: r.value.replace(/\.$/, '') }],
95
+ },
96
+ })),
97
+ },
98
+ }));
99
+ console.log(` āœ… Auto-wrote ${unique.length} validation CNAME(s) to Route53`);
100
+ }
101
+ // Step 3: now wait for ISSUED — records are in DNS so this will actually resolve
102
+ await this.waitFor(`certificate "${this.domainName}" to be validated`, async () => {
103
+ const detail = await acm.send(new DescribeCertificateCommand({ CertificateArn: this.resolvedArn }));
104
+ return detail.Certificate?.Status === 'ISSUED';
105
+ }, { intervalMs: 15_000, timeoutMs: 600_000 });
106
+ console.log(` āœ… Certificate issued: ${this.resolvedArn}`);
107
+ return { arn: this.resolvedArn };
108
+ }
109
+ }
@@ -0,0 +1,28 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
2
+ import { CloudFrontClient } from '@aws-sdk/client-cloudfront';
3
+ import { Route53Client } from '@aws-sdk/client-route-53';
4
+ import { Route53DomainsClient } from '@aws-sdk/client-route-53-domains';
5
+ import { ACMClient } from '@aws-sdk/client-acm';
6
+ import { LambdaClient } from '@aws-sdk/client-lambda';
7
+ import { IAMClient } from '@aws-sdk/client-iam';
8
+ import { ApiGatewayV2Client } from '@aws-sdk/client-apigatewayv2';
9
+ import { ECSClient } from '@aws-sdk/client-ecs';
10
+ import { EC2Client } from '@aws-sdk/client-ec2';
11
+ import { CloudWatchLogsClient } from '@aws-sdk/client-cloudwatch-logs';
12
+ import { RDSClient } from '@aws-sdk/client-rds';
13
+ import { SQSClient } from '@aws-sdk/client-sqs';
14
+ import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
15
+ export declare const getS3Client: (region?: string) => S3Client;
16
+ export declare const getCFClient: () => CloudFrontClient;
17
+ export declare const getR53Client: () => Route53Client;
18
+ export declare const getR53DomainsClient: () => Route53DomainsClient;
19
+ export declare const getACMClient: () => ACMClient;
20
+ export declare const getIAMClient: () => IAMClient;
21
+ export declare const getLambdaClient: (region?: string) => LambdaClient;
22
+ export declare const getAPIGWClient: (region?: string) => ApiGatewayV2Client;
23
+ export declare const getECSClient: (region?: string) => ECSClient;
24
+ export declare const getEC2Client: (region?: string) => EC2Client;
25
+ export declare const getCWLogsClient: (region?: string) => CloudWatchLogsClient;
26
+ export declare const getRDSClient: (region?: string) => RDSClient;
27
+ export declare const getSQSClient: (region?: string) => SQSClient;
28
+ export declare const getSecretsClient: (region?: string) => SecretsManagerClient;
@@ -0,0 +1,36 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
2
+ import { CloudFrontClient } from '@aws-sdk/client-cloudfront';
3
+ import { Route53Client } from '@aws-sdk/client-route-53';
4
+ import { Route53DomainsClient } from '@aws-sdk/client-route-53-domains';
5
+ import { ACMClient } from '@aws-sdk/client-acm';
6
+ import { LambdaClient } from '@aws-sdk/client-lambda';
7
+ import { IAMClient } from '@aws-sdk/client-iam';
8
+ import { ApiGatewayV2Client } from '@aws-sdk/client-apigatewayv2';
9
+ import { ECSClient } from '@aws-sdk/client-ecs';
10
+ import { EC2Client } from '@aws-sdk/client-ec2';
11
+ import { CloudWatchLogsClient } from '@aws-sdk/client-cloudwatch-logs';
12
+ import { RDSClient } from '@aws-sdk/client-rds';
13
+ import { SQSClient } from '@aws-sdk/client-sqs';
14
+ import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
15
+ import { Config } from '../../core/config.js';
16
+ function getRegion() {
17
+ const region = Config.get().providers.aws?.region;
18
+ if (!region)
19
+ throw new Error('AWS region not configured. Call AWS.init({ region: "..." })');
20
+ return region;
21
+ }
22
+ export const getS3Client = (region) => new S3Client({ region: region ?? getRegion() });
23
+ // CloudFront, Route53, ACM, Route53 Domains, and IAM are all global — must use us-east-1
24
+ export const getCFClient = () => new CloudFrontClient({ region: 'us-east-1' });
25
+ export const getR53Client = () => new Route53Client({ region: 'us-east-1' });
26
+ export const getR53DomainsClient = () => new Route53DomainsClient({ region: 'us-east-1' });
27
+ export const getACMClient = () => new ACMClient({ region: 'us-east-1' });
28
+ export const getIAMClient = () => new IAMClient({ region: 'us-east-1' });
29
+ export const getLambdaClient = (region) => new LambdaClient({ region: region ?? getRegion() });
30
+ export const getAPIGWClient = (region) => new ApiGatewayV2Client({ region: region ?? getRegion() });
31
+ export const getECSClient = (region) => new ECSClient({ region: region ?? getRegion() });
32
+ export const getEC2Client = (region) => new EC2Client({ region: region ?? getRegion() });
33
+ export const getCWLogsClient = (region) => new CloudWatchLogsClient({ region: region ?? getRegion() });
34
+ export const getRDSClient = (region) => new RDSClient({ region: region ?? getRegion() });
35
+ export const getSQSClient = (region) => new SQSClient({ region: region ?? getRegion() });
36
+ export const getSecretsClient = (region) => new SecretsManagerClient({ region: region ?? getRegion() });
@@ -0,0 +1,24 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ import { LambdaBuilder } from './lambda.js';
3
+ export declare class APIGatewayBuilder extends BaseBuilder {
4
+ private _routes;
5
+ resolvedId: string | null;
6
+ resolvedEndpoint: string | null;
7
+ constructor(name: string);
8
+ private discoverApi;
9
+ route(methodPath: string, fn: LambdaBuilder): this;
10
+ proxy(fn: LambdaBuilder): this;
11
+ deploy(): Promise<{
12
+ name: string;
13
+ endpoint: string;
14
+ id?: undefined;
15
+ } | {
16
+ name: string;
17
+ endpoint: string | null;
18
+ id: string | null;
19
+ }>;
20
+ private grantInvokePermission;
21
+ destroy(): Promise<{
22
+ destroyed: string;
23
+ }>;
24
+ }
@@ -0,0 +1,157 @@
1
+ import { GetApisCommand, CreateApiCommand, GetIntegrationsCommand, CreateIntegrationCommand, GetRoutesCommand, CreateRouteCommand, DeleteApiCommand, } from '@aws-sdk/client-apigatewayv2';
2
+ import { AddPermissionCommand } from '@aws-sdk/client-lambda';
3
+ import { BaseBuilder } from '../../core/resource.js';
4
+ import { getAPIGWClient, getLambdaClient } from './api.js';
5
+ import { Config } from '../../core/config.js';
6
+ export class APIGatewayBuilder extends BaseBuilder {
7
+ _routes = [];
8
+ resolvedId = null;
9
+ resolvedEndpoint = null;
10
+ constructor(name) {
11
+ super(name);
12
+ this.discoveryPromise = this.discoverApi(name);
13
+ }
14
+ async discoverApi(name) {
15
+ try {
16
+ const gw = getAPIGWClient();
17
+ const list = await gw.send(new GetApisCommand({}));
18
+ const match = list.Items?.find(a => a.Name === name);
19
+ if (match) {
20
+ this.resolvedId = match.ApiId;
21
+ this.resolvedEndpoint = match.ApiEndpoint ?? null;
22
+ }
23
+ return match ?? null;
24
+ }
25
+ catch (e) {
26
+ if (e.name === 'CredentialsProviderError')
27
+ return null;
28
+ throw e;
29
+ }
30
+ }
31
+ route(methodPath, fn) {
32
+ const spaceIdx = methodPath.indexOf(' ');
33
+ const method = methodPath.slice(0, spaceIdx).toUpperCase();
34
+ const path = methodPath.slice(spaceIdx + 1);
35
+ this._routes.push({ method, path, fn });
36
+ return this;
37
+ }
38
+ // Single Lambda proxy — forwards all traffic to one function
39
+ proxy(fn) {
40
+ return this.route('ANY /{proxy+}', fn);
41
+ }
42
+ async deploy() {
43
+ const dryRun = this.isDryRunActive();
44
+ const existing = await this.discoveryPromise;
45
+ const region = Config.get().providers.aws?.region ?? 'us-east-1';
46
+ const gw = getAPIGWClient();
47
+ console.log(`\n⚔ Finalizing API Gateway "${this.name}"...`);
48
+ if (dryRun) {
49
+ console.log(` šŸ“ [PLAN] ${existing ? 'Update' : 'Create'} HTTP API "${this.name}"`);
50
+ for (const r of this._routes) {
51
+ console.log(` └─ ${r.method} ${r.path} → ${r.fn.name}`);
52
+ }
53
+ this.resolvedEndpoint = `https://DRYRUN.execute-api.${region}.amazonaws.com`;
54
+ return { name: this.name, endpoint: this.resolvedEndpoint };
55
+ }
56
+ // Create API if it doesn't exist
57
+ if (!existing) {
58
+ const created = await gw.send(new CreateApiCommand({
59
+ Name: this.name,
60
+ ProtocolType: 'HTTP',
61
+ // Auto-deploy on $default stage — no manual deployment step needed
62
+ RouteSelectionExpression: '$request.method $request.path',
63
+ }));
64
+ this.resolvedId = created.ApiId;
65
+ this.resolvedEndpoint = created.ApiEndpoint;
66
+ console.log(`šŸš€ Created HTTP API "${this.name}" (id=${this.resolvedId})`);
67
+ // $default stage with auto-deploy
68
+ await gw.send(new (await import('@aws-sdk/client-apigatewayv2')).CreateStageCommand({
69
+ ApiId: this.resolvedId,
70
+ StageName: '$default',
71
+ AutoDeploy: true,
72
+ }));
73
+ }
74
+ else {
75
+ console.log(` āœ… HTTP API "${this.name}" exists (id=${this.resolvedId})`);
76
+ }
77
+ // Reconcile routes — only create what's missing
78
+ const existingIntegrations = await gw.send(new GetIntegrationsCommand({ ApiId: this.resolvedId }));
79
+ const existingRoutes = await gw.send(new GetRoutesCommand({ ApiId: this.resolvedId }));
80
+ const integrationByFnArn = new Map();
81
+ for (const i of existingIntegrations.Items ?? []) {
82
+ if (i.IntegrationUri)
83
+ integrationByFnArn.set(i.IntegrationUri, i.IntegrationId);
84
+ }
85
+ const existingRouteKeys = new Set((existingRoutes.Items ?? []).map(r => r.RouteKey));
86
+ for (const r of this._routes) {
87
+ const fnArn = r.fn.resolvedArn;
88
+ if (!fnArn)
89
+ throw new Error(`[APIGateway:${this.name}] Lambda "${r.fn.name}" has no resolvedArn — deploy it before the API.`);
90
+ const integrationUri = `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${fnArn}/invocations`;
91
+ // Reuse existing integration for this Lambda or create a new one
92
+ let integrationId = integrationByFnArn.get(integrationUri);
93
+ if (!integrationId) {
94
+ const integ = await gw.send(new CreateIntegrationCommand({
95
+ ApiId: this.resolvedId,
96
+ IntegrationType: 'AWS_PROXY',
97
+ IntegrationUri: integrationUri,
98
+ PayloadFormatVersion: '2.0',
99
+ }));
100
+ integrationId = integ.IntegrationId;
101
+ integrationByFnArn.set(integrationUri, integrationId);
102
+ console.log(` āœ… Created integration → ${r.fn.name}`);
103
+ }
104
+ const routeKey = `${r.method} ${r.path}`;
105
+ if (!existingRouteKeys.has(routeKey)) {
106
+ await gw.send(new CreateRouteCommand({
107
+ ApiId: this.resolvedId,
108
+ RouteKey: routeKey,
109
+ Target: `integrations/${integrationId}`,
110
+ }));
111
+ console.log(` āœ… Route: ${routeKey} → ${r.fn.name}`);
112
+ }
113
+ else {
114
+ console.log(` āœ… Route already exists: ${routeKey}`);
115
+ }
116
+ // Grant API Gateway permission to invoke this Lambda (idempotent)
117
+ await this.grantInvokePermission(fnArn, region);
118
+ }
119
+ console.log(` 🌐 Endpoint: ${this.resolvedEndpoint}`);
120
+ await this.deploySidecars();
121
+ return { name: this.name, endpoint: this.resolvedEndpoint, id: this.resolvedId };
122
+ }
123
+ async grantInvokePermission(fnArn, region) {
124
+ const accountId = fnArn.split(':')[4];
125
+ const statementId = `puls-apigw-${this.resolvedId}`;
126
+ try {
127
+ await getLambdaClient().send(new AddPermissionCommand({
128
+ FunctionName: fnArn,
129
+ StatementId: statementId,
130
+ Action: 'lambda:InvokeFunction',
131
+ Principal: 'apigateway.amazonaws.com',
132
+ SourceArn: `arn:aws:execute-api:${region}:${accountId}:${this.resolvedId}/*/*`,
133
+ }));
134
+ }
135
+ catch (e) {
136
+ // Permission already exists — idempotent
137
+ if (e.name !== 'ResourceConflictException')
138
+ throw e;
139
+ }
140
+ }
141
+ async destroy() {
142
+ const dryRun = this.isDryRunActive();
143
+ const existing = await this.discoveryPromise;
144
+ console.log(`\nšŸ—‘ļø Destroying API Gateway "${this.name}"...`);
145
+ if (!existing) {
146
+ console.log(` āœ… API "${this.name}" does not exist — nothing to do`);
147
+ return { destroyed: this.name };
148
+ }
149
+ if (dryRun) {
150
+ console.log(` šŸ“ [PLAN] Delete HTTP API "${this.name}" (id=${this.resolvedId})`);
151
+ return { destroyed: this.name };
152
+ }
153
+ await getAPIGWClient().send(new DeleteApiCommand({ ApiId: this.resolvedId }));
154
+ console.log(` āœ… Deleted API "${this.name}"`);
155
+ return { destroyed: this.name };
156
+ }
157
+ }
@@ -0,0 +1,31 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ import { S3BucketBuilder } from './s3.js';
3
+ import { Route53Builder } from './route53.js';
4
+ export declare class CloudFrontBuilder extends BaseBuilder {
5
+ resolvedArn: string | null;
6
+ resolvedId: string | null;
7
+ private _origin;
8
+ private _aliases;
9
+ private _prefixes;
10
+ private _zone?;
11
+ private _referenceId?;
12
+ private _kvsName?;
13
+ private _certRef?;
14
+ private _invalidatePaths?;
15
+ constructor(name: string);
16
+ private discoverDistribution;
17
+ invalidate(paths: string[]): this;
18
+ withRedirector(opts: {
19
+ kvs: string;
20
+ }): this;
21
+ copyFrom(distributionId: string): this;
22
+ forDomain(zone: Route53Builder, prefixes: string[]): this;
23
+ origin(source: S3BucketBuilder | string): this;
24
+ dns(aliases: string | string[]): this;
25
+ deploy(): Promise<{
26
+ id: string | null;
27
+ arn: string | null;
28
+ name: string;
29
+ }>;
30
+ private buildBaseConfig;
31
+ }