puls-dev 0.3.5 → 0.3.7

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 (47) hide show
  1. package/README.md +165 -54
  2. package/dist/bin/install-shell.js +5 -6
  3. package/dist/bin/puls.js +10 -3
  4. package/dist/core/config.d.ts +4 -0
  5. package/dist/core/decorators.d.ts +4 -0
  6. package/dist/core/decorators.js +2 -0
  7. package/dist/core/parallel.test.js +4 -3
  8. package/dist/core/resource.d.ts +2 -1
  9. package/dist/core/resource.js +4 -2
  10. package/dist/core/stack.d.ts +4 -0
  11. package/dist/core/stack.js +8 -8
  12. package/dist/providers/aws/acm.test.d.ts +1 -0
  13. package/dist/providers/aws/acm.test.js +167 -0
  14. package/dist/providers/aws/cloudfront.test.d.ts +1 -0
  15. package/dist/providers/aws/cloudfront.test.js +170 -0
  16. package/dist/providers/aws/fargate.test.d.ts +1 -0
  17. package/dist/providers/aws/fargate.test.js +244 -0
  18. package/dist/providers/aws/rds.test.d.ts +1 -0
  19. package/dist/providers/aws/rds.test.js +219 -0
  20. package/dist/providers/aws/sqs.test.d.ts +1 -0
  21. package/dist/providers/aws/sqs.test.js +181 -0
  22. package/dist/providers/cloudflare/api.d.ts +15 -0
  23. package/dist/providers/cloudflare/api.js +199 -0
  24. package/dist/providers/cloudflare/index.d.ts +14 -0
  25. package/dist/providers/cloudflare/index.js +19 -0
  26. package/dist/providers/cloudflare/kv.d.ts +20 -0
  27. package/dist/providers/cloudflare/kv.js +69 -0
  28. package/dist/providers/cloudflare/kv.test.d.ts +1 -0
  29. package/dist/providers/cloudflare/kv.test.js +134 -0
  30. package/dist/providers/cloudflare/r2.d.ts +14 -0
  31. package/dist/providers/cloudflare/r2.js +57 -0
  32. package/dist/providers/cloudflare/r2.test.d.ts +1 -0
  33. package/dist/providers/cloudflare/r2.test.js +132 -0
  34. package/dist/providers/cloudflare/worker.d.ts +28 -0
  35. package/dist/providers/cloudflare/worker.js +172 -0
  36. package/dist/providers/cloudflare/worker.test.d.ts +1 -0
  37. package/dist/providers/cloudflare/worker.test.js +220 -0
  38. package/dist/providers/cloudflare/zone.d.ts +42 -0
  39. package/dist/providers/cloudflare/zone.js +280 -0
  40. package/dist/providers/cloudflare/zone.test.d.ts +1 -0
  41. package/dist/providers/cloudflare/zone.test.js +284 -0
  42. package/dist/providers/firebase/auth.test.d.ts +1 -0
  43. package/dist/providers/firebase/auth.test.js +145 -0
  44. package/dist/providers/firebase/hosting.test.js +7 -6
  45. package/dist/providers/firebase/storage.test.d.ts +1 -0
  46. package/dist/providers/firebase/storage.test.js +148 -0
  47. package/package.json +6 -2
@@ -0,0 +1,219 @@
1
+ import { test, describe, beforeEach, afterEach, mock } from "node:test";
2
+ import assert from "node:assert";
3
+ import { RDSClient } from "@aws-sdk/client-rds";
4
+ import { EC2Client } from "@aws-sdk/client-ec2";
5
+ import { RDSBuilder } from "./rds.js";
6
+ import { Config } from "../../core/config.js";
7
+ describe("RDSBuilder Unit Tests", () => {
8
+ let originalRdsSend;
9
+ let originalEc2Send;
10
+ let rdsCalls = [];
11
+ let ec2Calls = [];
12
+ let mockRdsResponses = {};
13
+ let mockEc2Responses = {};
14
+ function stubSend(calls, responses) {
15
+ return async function (command) {
16
+ const commandName = command.constructor.name;
17
+ calls.push({ commandName, input: command.input });
18
+ const handler = responses[commandName];
19
+ if (handler) {
20
+ if (typeof handler === "function")
21
+ return handler(command.input);
22
+ if (handler instanceof Error)
23
+ throw handler;
24
+ return handler;
25
+ }
26
+ return {};
27
+ };
28
+ }
29
+ beforeEach(() => {
30
+ Config.set({ dryRun: false, providers: { aws: { region: "us-east-1" } } });
31
+ rdsCalls = [];
32
+ ec2Calls = [];
33
+ mockRdsResponses = {};
34
+ mockEc2Responses = {};
35
+ originalRdsSend = RDSClient.prototype.send;
36
+ originalEc2Send = EC2Client.prototype.send;
37
+ RDSClient.prototype.send = stubSend(rdsCalls, mockRdsResponses);
38
+ EC2Client.prototype.send = stubSend(ec2Calls, mockEc2Responses);
39
+ });
40
+ afterEach(() => {
41
+ RDSClient.prototype.send = originalRdsSend;
42
+ EC2Client.prototype.send = originalEc2Send;
43
+ mock.restoreAll();
44
+ });
45
+ test("returns null when DB instance is not found during discovery", async () => {
46
+ const err = new Error("not found");
47
+ err.name = "DBInstanceNotFound";
48
+ mockRdsResponses["DescribeDBInstancesCommand"] = err;
49
+ const builder = new RDSBuilder("my-db");
50
+ const result = await builder.discoveryPromise;
51
+ assert.strictEqual(result, null);
52
+ assert.strictEqual(rdsCalls[0].commandName, "DescribeDBInstancesCommand");
53
+ });
54
+ test("discovers existing instance and populates resolved fields", async () => {
55
+ mockRdsResponses["DescribeDBInstancesCommand"] = {
56
+ DBInstances: [
57
+ {
58
+ DBInstanceIdentifier: "my-db",
59
+ DBInstanceStatus: "available",
60
+ DBInstanceArn: "arn:aws:rds:us-east-1:123:db:my-db",
61
+ Endpoint: {
62
+ Address: "my-db.xyz.us-east-1.rds.amazonaws.com",
63
+ Port: 5432,
64
+ },
65
+ },
66
+ ],
67
+ };
68
+ const builder = new RDSBuilder("my-db");
69
+ const result = await builder.discoveryPromise;
70
+ assert.ok(result);
71
+ assert.strictEqual(builder.resolvedArn, "arn:aws:rds:us-east-1:123:db:my-db");
72
+ assert.strictEqual(builder.resolvedEndpoint, "my-db.xyz.us-east-1.rds.amazonaws.com");
73
+ assert.strictEqual(builder.resolvedPort, 5432);
74
+ });
75
+ test("performs dry-run without any write API calls", async () => {
76
+ Config.set({ dryRun: true, providers: { aws: { region: "us-east-1" } } });
77
+ const err = new Error("not found");
78
+ err.name = "DBInstanceNotFound";
79
+ mockRdsResponses["DescribeDBInstancesCommand"] = err;
80
+ const builder = new RDSBuilder("my-db");
81
+ builder
82
+ .engine({ engine: "postgres", version: "16" })
83
+ .size("db.t3.micro")
84
+ .storage(20);
85
+ const result = await builder.deploy();
86
+ assert.ok(result);
87
+ assert.ok(result.endpoint.includes("DRYRUN"));
88
+ assert.strictEqual(result.port, 5432);
89
+ const writeCalls = rdsCalls.filter((c) => !c.commandName.startsWith("Describe"));
90
+ assert.strictEqual(writeCalls.length, 0);
91
+ });
92
+ test("creates new DB instance with subnet group and security group", async () => {
93
+ const err = new Error("not found");
94
+ err.name = "DBInstanceNotFound";
95
+ mockRdsResponses["DescribeDBInstancesCommand"] = err;
96
+ // ensureSubnetGroup
97
+ const noGroup = new Error("not found");
98
+ noGroup.name = "DBSubnetGroupNotFoundFault";
99
+ mockRdsResponses["DescribeDBSubnetGroupsCommand"] = noGroup;
100
+ mockRdsResponses["CreateDBSubnetGroupCommand"] = {};
101
+ // ensureSecurityGroup
102
+ mockEc2Responses["DescribeVpcsCommand"] = {
103
+ Vpcs: [{ VpcId: "vpc-1", CidrBlock: "10.0.0.0/16" }],
104
+ };
105
+ mockEc2Responses["DescribeSubnetsCommand"] = {
106
+ Subnets: [{ SubnetId: "subnet-1" }],
107
+ };
108
+ mockEc2Responses["DescribeSecurityGroupsCommand"] = { SecurityGroups: [] };
109
+ mockEc2Responses["CreateSecurityGroupCommand"] = { GroupId: "sg-1" };
110
+ // CreateDBInstance- return available immediately so the waitFor resolves
111
+ mockRdsResponses["CreateDBInstanceCommand"] = {};
112
+ let describeCount = 0;
113
+ mockRdsResponses["DescribeDBInstancesCommand"] = (input) => {
114
+ describeCount++;
115
+ if (describeCount === 1) {
116
+ // First call: discovery in constructor- instance not found
117
+ const notFound = new Error("not found");
118
+ notFound.name = "DBInstanceNotFound";
119
+ throw notFound;
120
+ }
121
+ // Subsequent calls: waitFor polling- instance now available
122
+ return {
123
+ DBInstances: [
124
+ {
125
+ DBInstanceIdentifier: "my-db",
126
+ DBInstanceStatus: "available",
127
+ DBInstanceArn: "arn:aws:rds:us-east-1:123:db:my-db",
128
+ Endpoint: { Address: "my-db.xyz.rds.amazonaws.com", Port: 5432 },
129
+ },
130
+ ],
131
+ };
132
+ };
133
+ // Fast-forward poll timer
134
+ mock.method(global, "setTimeout", (fn) => fn());
135
+ const builder = new RDSBuilder("my-db");
136
+ builder
137
+ .engine({ engine: "postgres", version: "16" })
138
+ .size("db.t3.small")
139
+ .storage(40)
140
+ .credentials("admin", "secret");
141
+ const result = await builder.deploy();
142
+ assert.ok(result);
143
+ const createCall = rdsCalls.find((c) => c.commandName === "CreateDBInstanceCommand");
144
+ assert.ok(createCall);
145
+ assert.strictEqual(createCall.input.DBInstanceIdentifier, "my-db");
146
+ assert.strictEqual(createCall.input.Engine, "postgres");
147
+ assert.strictEqual(createCall.input.EngineVersion, "16");
148
+ assert.strictEqual(createCall.input.DBInstanceClass, "db.t3.small");
149
+ assert.strictEqual(createCall.input.AllocatedStorage, 40);
150
+ });
151
+ test("modifies existing instance on update", async () => {
152
+ mockRdsResponses["DescribeDBInstancesCommand"] = {
153
+ DBInstances: [
154
+ {
155
+ DBInstanceIdentifier: "my-db",
156
+ DBInstanceStatus: "available",
157
+ DBInstanceArn: "arn:aws:rds:us-east-1:123:db:my-db",
158
+ Endpoint: { Address: "my-db.xyz.rds.amazonaws.com", Port: 5432 },
159
+ },
160
+ ],
161
+ };
162
+ // ensureSubnetGroup (existing)
163
+ mockRdsResponses["DescribeDBSubnetGroupsCommand"] = {
164
+ DBSubnetGroups: [{ DBSubnetGroupName: "puls-my-db-subnet-group" }],
165
+ };
166
+ const builder = new RDSBuilder("my-db");
167
+ builder
168
+ .engine({ engine: "postgres", version: "16" })
169
+ .size("db.t3.medium")
170
+ .subnets(["subnet-1"])
171
+ .securityGroups(["sg-1"])
172
+ .credentials("admin", "secret");
173
+ await builder.deploy();
174
+ const modifyCall = rdsCalls.find((c) => c.commandName === "ModifyDBInstanceCommand");
175
+ assert.ok(modifyCall);
176
+ assert.strictEqual(modifyCall.input.DBInstanceClass, "db.t3.medium");
177
+ assert.strictEqual(modifyCall.input.ApplyImmediately, true);
178
+ });
179
+ test("deletes instance on destroy", async () => {
180
+ mockRdsResponses["DescribeDBInstancesCommand"] = {
181
+ DBInstances: [
182
+ {
183
+ DBInstanceIdentifier: "my-db",
184
+ DBInstanceStatus: "available",
185
+ DBInstanceArn: "arn:aws:rds:us-east-1:123:db:my-db",
186
+ Endpoint: { Address: "my-db.xyz.rds.amazonaws.com", Port: 5432 },
187
+ },
188
+ ],
189
+ };
190
+ const builder = new RDSBuilder("my-db");
191
+ await builder.discoveryPromise;
192
+ const result = await builder.destroy();
193
+ assert.deepStrictEqual(result, { destroyed: "my-db" });
194
+ const deleteCall = rdsCalls.find((c) => c.commandName === "DeleteDBInstanceCommand");
195
+ assert.ok(deleteCall);
196
+ assert.strictEqual(deleteCall.input.DBInstanceIdentifier, "my-db");
197
+ assert.strictEqual(deleteCall.input.SkipFinalSnapshot, true);
198
+ });
199
+ test("getDiff returns field changes against live state", () => {
200
+ const err = new Error("not found");
201
+ err.name = "DBInstanceNotFound";
202
+ mockRdsResponses["DescribeDBInstancesCommand"] = err;
203
+ const builder = new RDSBuilder("my-db");
204
+ builder
205
+ .engine({ engine: "postgres", version: "16" })
206
+ .size("db.t3.small")
207
+ .storage(40);
208
+ const diffs = builder.getDiff({
209
+ Engine: "postgres",
210
+ EngineVersion: "15", // differs
211
+ DBInstanceClass: "db.t3.micro", // differs
212
+ AllocatedStorage: 40,
213
+ PubliclyAccessible: false,
214
+ });
215
+ assert.strictEqual(diffs.length, 2);
216
+ assert.ok(diffs.some((d) => d.field === "engineVersion"));
217
+ assert.ok(diffs.some((d) => d.field === "instanceClass"));
218
+ });
219
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,181 @@
1
+ import { test, describe, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { SQSClient } from '@aws-sdk/client-sqs';
4
+ import { SQSBuilder } from './sqs.js';
5
+ import { Config } from '../../core/config.js';
6
+ describe('SQSBuilder Unit Tests', () => {
7
+ let originalSend;
8
+ let calls = [];
9
+ let mockResponses = {};
10
+ beforeEach(() => {
11
+ Config.set({ dryRun: false, providers: { aws: { region: 'us-east-1' } } });
12
+ calls = [];
13
+ mockResponses = {};
14
+ originalSend = SQSClient.prototype.send;
15
+ SQSClient.prototype.send = async function (command) {
16
+ const commandName = command.constructor.name;
17
+ calls.push({ commandName, input: command.input });
18
+ const handler = mockResponses[commandName];
19
+ if (handler) {
20
+ if (typeof handler === 'function')
21
+ return handler(command.input);
22
+ if (handler instanceof Error)
23
+ throw handler;
24
+ return handler;
25
+ }
26
+ return {};
27
+ };
28
+ });
29
+ afterEach(() => {
30
+ SQSClient.prototype.send = originalSend;
31
+ });
32
+ test('returns null when queue does not exist', async () => {
33
+ const err = new Error('no queue');
34
+ err.name = 'QueueDoesNotExist';
35
+ mockResponses['GetQueueUrlCommand'] = err;
36
+ const builder = new SQSBuilder('my-queue');
37
+ const result = await builder.discoveryPromise;
38
+ assert.strictEqual(result, null);
39
+ assert.strictEqual(calls[0].commandName, 'GetQueueUrlCommand');
40
+ });
41
+ test('discovers existing queue and populates resolvedUrl and resolvedArn', async () => {
42
+ mockResponses['GetQueueUrlCommand'] = {
43
+ QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123/my-queue',
44
+ };
45
+ mockResponses['GetQueueAttributesCommand'] = {
46
+ Attributes: { QueueArn: 'arn:aws:sqs:us-east-1:123:my-queue' },
47
+ };
48
+ const builder = new SQSBuilder('my-queue');
49
+ const result = await builder.discoveryPromise;
50
+ assert.ok(result);
51
+ assert.strictEqual(builder.resolvedUrl, 'https://sqs.us-east-1.amazonaws.com/123/my-queue');
52
+ assert.strictEqual(builder.resolvedArn, 'arn:aws:sqs:us-east-1:123:my-queue');
53
+ });
54
+ test('performs dry-run without creating the queue', async () => {
55
+ Config.set({ dryRun: true, providers: { aws: { region: 'us-east-1' } } });
56
+ const err = new Error('no queue');
57
+ err.name = 'QueueDoesNotExist';
58
+ mockResponses['GetQueueUrlCommand'] = err;
59
+ const builder = new SQSBuilder('my-queue');
60
+ builder.retention(7).timeout(60);
61
+ const result = await builder.deploy();
62
+ assert.ok(result);
63
+ assert.ok(result.url.includes('DRYRUN'));
64
+ assert.ok(result.arn.includes('DRYRUN'));
65
+ const writeCalls = calls.filter((c) => c.commandName === 'CreateQueueCommand');
66
+ assert.strictEqual(writeCalls.length, 0);
67
+ });
68
+ test('creates new standard queue with configured attributes', async () => {
69
+ const err = new Error('no queue');
70
+ err.name = 'QueueDoesNotExist';
71
+ mockResponses['GetQueueUrlCommand'] = err;
72
+ mockResponses['CreateQueueCommand'] = {
73
+ QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123/my-queue',
74
+ };
75
+ mockResponses['GetQueueAttributesCommand'] = {
76
+ Attributes: { QueueArn: 'arn:aws:sqs:us-east-1:123:my-queue' },
77
+ };
78
+ const builder = new SQSBuilder('my-queue');
79
+ builder.retention(7).timeout(45).delay(5);
80
+ const result = await builder.deploy();
81
+ assert.ok(result);
82
+ assert.strictEqual(result.name, 'my-queue');
83
+ assert.strictEqual(result.url, 'https://sqs.us-east-1.amazonaws.com/123/my-queue');
84
+ const createCall = calls.find((c) => c.commandName === 'CreateQueueCommand');
85
+ assert.ok(createCall);
86
+ assert.strictEqual(createCall.input.Attributes.VisibilityTimeout, '45');
87
+ assert.strictEqual(createCall.input.Attributes.MessageRetentionPeriod, String(7 * 86400));
88
+ assert.strictEqual(createCall.input.Attributes.DelaySeconds, '5');
89
+ });
90
+ test('creates FIFO queue with .fifo suffix appended automatically', async () => {
91
+ const err = new Error('no queue');
92
+ err.name = 'QueueDoesNotExist';
93
+ mockResponses['GetQueueUrlCommand'] = err;
94
+ mockResponses['CreateQueueCommand'] = {
95
+ QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123/my-queue.fifo',
96
+ };
97
+ mockResponses['GetQueueAttributesCommand'] = {
98
+ Attributes: { QueueArn: 'arn:aws:sqs:us-east-1:123:my-queue.fifo' },
99
+ };
100
+ const builder = new SQSBuilder('my-queue');
101
+ builder.fifo().deduplication();
102
+ const result = await builder.deploy();
103
+ assert.ok(result);
104
+ assert.strictEqual(result.name, 'my-queue.fifo');
105
+ const createCall = calls.find((c) => c.commandName === 'CreateQueueCommand');
106
+ assert.ok(createCall);
107
+ assert.strictEqual(createCall.input.QueueName, 'my-queue.fifo');
108
+ assert.strictEqual(createCall.input.Attributes.FifoQueue, 'true');
109
+ assert.strictEqual(createCall.input.Attributes.ContentBasedDeduplication, 'true');
110
+ });
111
+ test('creates DLQ before main queue and wires redrive policy', async () => {
112
+ const err = new Error('no queue');
113
+ err.name = 'QueueDoesNotExist';
114
+ let getUrlCallCount = 0;
115
+ mockResponses['GetQueueUrlCommand'] = (input) => {
116
+ getUrlCallCount++;
117
+ if (getUrlCallCount === 1)
118
+ throw err; // discovery: queue not found
119
+ // DLQ lookup in ensureQueue: also not found
120
+ if (input.QueueName === 'my-dlq')
121
+ throw err;
122
+ return { QueueUrl: `https://sqs.us-east-1.amazonaws.com/123/${input.QueueName}` };
123
+ };
124
+ mockResponses['CreateQueueCommand'] = (input) => ({
125
+ QueueUrl: `https://sqs.us-east-1.amazonaws.com/123/${input.QueueName}`,
126
+ });
127
+ mockResponses['GetQueueAttributesCommand'] = (input) => ({
128
+ Attributes: { QueueArn: `arn:aws:sqs:us-east-1:123:${input.QueueUrl?.split('/').pop()}` },
129
+ });
130
+ const builder = new SQSBuilder('my-queue');
131
+ builder.dlq('my-dlq', 5);
132
+ const result = await builder.deploy();
133
+ assert.ok(result);
134
+ const dlqCreate = calls.find((c) => c.commandName === 'CreateQueueCommand' && c.input.QueueName === 'my-dlq');
135
+ assert.ok(dlqCreate);
136
+ const mainCreate = calls.find((c) => c.commandName === 'CreateQueueCommand' && c.input.QueueName === 'my-queue');
137
+ assert.ok(mainCreate);
138
+ const redrivePolicy = JSON.parse(mainCreate.input.Attributes.RedrivePolicy);
139
+ assert.strictEqual(redrivePolicy.maxReceiveCount, 5);
140
+ assert.ok(redrivePolicy.deadLetterTargetArn.includes('my-dlq'));
141
+ });
142
+ test('updates mutable attributes on existing queue', async () => {
143
+ mockResponses['GetQueueUrlCommand'] = {
144
+ QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123/my-queue',
145
+ };
146
+ mockResponses['GetQueueAttributesCommand'] = {
147
+ Attributes: { QueueArn: 'arn:aws:sqs:us-east-1:123:my-queue' },
148
+ };
149
+ const builder = new SQSBuilder('my-queue');
150
+ builder.retention(14).timeout(120);
151
+ await builder.deploy();
152
+ const setAttr = calls.find((c) => c.commandName === 'SetQueueAttributesCommand');
153
+ assert.ok(setAttr);
154
+ assert.strictEqual(setAttr.input.Attributes.VisibilityTimeout, '120');
155
+ assert.strictEqual(setAttr.input.Attributes.MessageRetentionPeriod, String(14 * 86400));
156
+ });
157
+ test('deletes queue on destroy', async () => {
158
+ mockResponses['GetQueueUrlCommand'] = {
159
+ QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123/my-queue',
160
+ };
161
+ mockResponses['GetQueueAttributesCommand'] = {
162
+ Attributes: { QueueArn: 'arn:aws:sqs:us-east-1:123:my-queue' },
163
+ };
164
+ const builder = new SQSBuilder('my-queue');
165
+ await builder.discoveryPromise;
166
+ const result = await builder.destroy();
167
+ assert.deepStrictEqual(result, { destroyed: 'my-queue' });
168
+ const deleteCall = calls.find((c) => c.commandName === 'DeleteQueueCommand');
169
+ assert.ok(deleteCall);
170
+ assert.strictEqual(deleteCall.input.QueueUrl, 'https://sqs.us-east-1.amazonaws.com/123/my-queue');
171
+ });
172
+ test('skips destroy when queue does not exist', async () => {
173
+ const err = new Error('no queue');
174
+ err.name = 'QueueDoesNotExist';
175
+ mockResponses['GetQueueUrlCommand'] = err;
176
+ const builder = new SQSBuilder('my-queue');
177
+ const result = await builder.destroy();
178
+ assert.deepStrictEqual(result, { destroyed: 'my-queue' });
179
+ assert.ok(!calls.some((c) => c.commandName === 'DeleteQueueCommand'));
180
+ });
181
+ });
@@ -0,0 +1,15 @@
1
+ export declare class CloudflareApiClient {
2
+ private token;
3
+ private static readonly BASE;
4
+ constructor(token: string);
5
+ private get authHeaders();
6
+ private createCfOfflineMock;
7
+ private request;
8
+ get<T>(path: string): Promise<T>;
9
+ post<T>(path: string, body: unknown): Promise<T>;
10
+ put<T>(path: string, body: unknown, headers?: Record<string, string>): Promise<T>;
11
+ patch<T>(path: string, body: unknown): Promise<T>;
12
+ delete(path: string, body?: unknown): Promise<void>;
13
+ }
14
+ export declare function getCloudflareApi(): CloudflareApiClient;
15
+ export declare function getCloudflareAccountId(): string;
@@ -0,0 +1,199 @@
1
+ import { Config } from "../../core/config.js";
2
+ import { withRetry } from "../../core/retry.js";
3
+ import { resourceContextStorage } from "../../core/context.js";
4
+ export class CloudflareApiClient {
5
+ token;
6
+ static BASE = "https://api.cloudflare.com/client/v4";
7
+ constructor(token) {
8
+ this.token = token;
9
+ }
10
+ get authHeaders() {
11
+ return {
12
+ Authorization: `Bearer ${this.token}`,
13
+ "Content-Type": "application/json",
14
+ };
15
+ }
16
+ createCfOfflineMock(method, path, body) {
17
+ if (path.includes("/zones") && !path.includes("/dns_records") && !path.includes("/routes")) {
18
+ if (method === "GET") {
19
+ return {
20
+ result: [
21
+ { id: "mock-zone-id", name: "mock-domain.com", status: "active" }
22
+ ]
23
+ };
24
+ }
25
+ return {
26
+ result: { id: "mock-zone-id", name: "mock-domain.com", status: "active" }
27
+ };
28
+ }
29
+ if (path.includes("/dns_records")) {
30
+ if (method === "GET") {
31
+ return { result: [] };
32
+ }
33
+ return { result: { id: "mock-dns-record-id" } };
34
+ }
35
+ if (path.includes("/namespaces")) {
36
+ if (method === "GET") {
37
+ return {
38
+ result: [
39
+ { id: "mock-kv-namespace-id", title: "mock-namespace" }
40
+ ]
41
+ };
42
+ }
43
+ return { result: { id: "mock-kv-namespace-id", title: "mock-namespace" } };
44
+ }
45
+ if (path.includes("/workers/scripts")) {
46
+ return { result: { id: "mock-worker-script-id" } };
47
+ }
48
+ if (path.includes("/routes")) {
49
+ if (method === "GET") {
50
+ return { result: [] };
51
+ }
52
+ return { result: { id: "mock-route-id" } };
53
+ }
54
+ if (path.includes("/r2/buckets")) {
55
+ if (method === "GET") {
56
+ return { result: { buckets: [] } };
57
+ }
58
+ return { result: {} };
59
+ }
60
+ return new Proxy({}, {
61
+ get(target, prop) {
62
+ if (prop === "then")
63
+ return undefined;
64
+ if (prop === "id")
65
+ return "mock-cf-id-123456";
66
+ if (prop === "name")
67
+ return "mock-cf-name";
68
+ if (prop === "status")
69
+ return "active";
70
+ if (prop.endsWith("s"))
71
+ return [];
72
+ return `mock-cf-${prop.toLowerCase()}`;
73
+ }
74
+ });
75
+ }
76
+ async request(fn) {
77
+ return withRetry(fn, {
78
+ retryable: (err) => {
79
+ const match = err.message.match(/: (\d+)/);
80
+ const status = match ? parseInt(match[1], 10) : null;
81
+ return status === 429 || (status && status >= 500) || err.message.includes("ETIMEDOUT");
82
+ }
83
+ });
84
+ }
85
+ async get(path) {
86
+ const context = resourceContextStorage.getStore();
87
+ const abortSignal = context?.abortSignal;
88
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
89
+ return Promise.resolve(this.createCfOfflineMock("GET", path));
90
+ }
91
+ return this.request(async () => {
92
+ const res = await fetch(`${CloudflareApiClient.BASE}${path}`, {
93
+ headers: this.authHeaders,
94
+ ...(abortSignal && { signal: abortSignal })
95
+ });
96
+ if (!res.ok)
97
+ throw new Error(`Cloudflare API GET ${path}: ${res.status} ${await res.text()}`);
98
+ return res.json();
99
+ });
100
+ }
101
+ async post(path, body) {
102
+ const context = resourceContextStorage.getStore();
103
+ const abortSignal = context?.abortSignal;
104
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
105
+ return Promise.resolve(this.createCfOfflineMock("POST", path, body));
106
+ }
107
+ return this.request(async () => {
108
+ const res = await fetch(`${CloudflareApiClient.BASE}${path}`, {
109
+ method: "POST",
110
+ headers: this.authHeaders,
111
+ body: JSON.stringify(body),
112
+ ...(abortSignal && { signal: abortSignal })
113
+ });
114
+ if (!res.ok)
115
+ throw new Error(`Cloudflare API POST ${path}: ${res.status} ${await res.text()}`);
116
+ return res.json();
117
+ });
118
+ }
119
+ async put(path, body, headers) {
120
+ const context = resourceContextStorage.getStore();
121
+ const abortSignal = context?.abortSignal;
122
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
123
+ return Promise.resolve(this.createCfOfflineMock("PUT", path, body));
124
+ }
125
+ return this.request(async () => {
126
+ const isMultipartOrRaw = body instanceof FormData || typeof body === "string" || body instanceof Buffer;
127
+ const customHeaders = headers ?? (isMultipartOrRaw ? {} : { "Content-Type": "application/json" });
128
+ const reqHeaders = {
129
+ Authorization: `Bearer ${this.token}`,
130
+ ...customHeaders,
131
+ };
132
+ const res = await fetch(`${CloudflareApiClient.BASE}${path}`, {
133
+ method: "PUT",
134
+ headers: reqHeaders,
135
+ body: isMultipartOrRaw ? body : JSON.stringify(body),
136
+ ...(abortSignal && { signal: abortSignal })
137
+ });
138
+ if (!res.ok)
139
+ throw new Error(`Cloudflare API PUT ${path}: ${res.status} ${await res.text()}`);
140
+ return res.json();
141
+ });
142
+ }
143
+ async patch(path, body) {
144
+ const context = resourceContextStorage.getStore();
145
+ const abortSignal = context?.abortSignal;
146
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
147
+ return Promise.resolve(this.createCfOfflineMock("PATCH", path, body));
148
+ }
149
+ return this.request(async () => {
150
+ const res = await fetch(`${CloudflareApiClient.BASE}${path}`, {
151
+ method: "PATCH",
152
+ headers: this.authHeaders,
153
+ body: JSON.stringify(body),
154
+ ...(abortSignal && { signal: abortSignal })
155
+ });
156
+ if (!res.ok)
157
+ throw new Error(`Cloudflare API PATCH ${path}: ${res.status} ${await res.text()}`);
158
+ return res.json();
159
+ });
160
+ }
161
+ async delete(path, body) {
162
+ const context = resourceContextStorage.getStore();
163
+ const abortSignal = context?.abortSignal;
164
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
165
+ return Promise.resolve();
166
+ }
167
+ return this.request(async () => {
168
+ const res = await fetch(`${CloudflareApiClient.BASE}${path}`, {
169
+ method: "DELETE",
170
+ headers: this.authHeaders,
171
+ ...(body !== undefined && { body: JSON.stringify(body) }),
172
+ ...(abortSignal && { signal: abortSignal })
173
+ });
174
+ if (!res.ok && res.status !== 404) {
175
+ throw new Error(`Cloudflare API DELETE ${path}: ${res.status} ${await res.text()}`);
176
+ }
177
+ });
178
+ }
179
+ }
180
+ export function getCloudflareApi() {
181
+ const token = Config.get().providers.cloudflare?.token;
182
+ if (!token) {
183
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
184
+ return new CloudflareApiClient("mock-cf-token");
185
+ }
186
+ throw new Error('Cloudflare token not configured. Call CF.init({ token: "..." })');
187
+ }
188
+ return new CloudflareApiClient(token);
189
+ }
190
+ export function getCloudflareAccountId() {
191
+ const accId = Config.get().providers.cloudflare?.accountId;
192
+ if (!accId) {
193
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
194
+ return "mock-cf-account-id";
195
+ }
196
+ throw new Error("Cloudflare account ID not configured. Account ID is required for Workers, KV namespaces, and R2 buckets.");
197
+ }
198
+ return accId;
199
+ }
@@ -0,0 +1,14 @@
1
+ import { ZoneBuilder } from "./zone.js";
2
+ import { WorkerBuilder } from "./worker.js";
3
+ import { KVBuilder } from "./kv.js";
4
+ import { R2Builder } from "./r2.js";
5
+ export declare const CF: {
6
+ init: (opts: {
7
+ token: string;
8
+ accountId?: string;
9
+ }) => void;
10
+ Zone: (domainName: string) => ZoneBuilder;
11
+ Worker: (name: string) => WorkerBuilder;
12
+ KV: (title: string) => KVBuilder;
13
+ R2: (bucketName: string) => R2Builder;
14
+ };
@@ -0,0 +1,19 @@
1
+ import { Config } from "../../core/config.js";
2
+ import { ZoneBuilder } from "./zone.js";
3
+ import { WorkerBuilder } from "./worker.js";
4
+ import { KVBuilder } from "./kv.js";
5
+ import { R2Builder } from "./r2.js";
6
+ export const CF = {
7
+ init: (opts) => {
8
+ Config.set({
9
+ providers: {
10
+ ...Config.get().providers,
11
+ cloudflare: opts,
12
+ },
13
+ });
14
+ },
15
+ Zone: (domainName) => new ZoneBuilder(domainName),
16
+ Worker: (name) => new WorkerBuilder(name),
17
+ KV: (title) => new KVBuilder(title),
18
+ R2: (bucketName) => new R2Builder(bucketName),
19
+ };
@@ -0,0 +1,20 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { Output } from "../../core/output.js";
3
+ export declare class KVBuilder extends BaseBuilder {
4
+ title: string;
5
+ readonly out: {
6
+ id: Output<string>;
7
+ };
8
+ resolvedId: string | null;
9
+ constructor(title: string);
10
+ private discoverNamespace;
11
+ deploy(): Promise<{
12
+ title: string;
13
+ id: string | null;
14
+ }>;
15
+ destroy(): Promise<{
16
+ destroyed: boolean;
17
+ } | {
18
+ destroyed: string;
19
+ }>;
20
+ }