alchemy 0.2.5 → 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 (151) hide show
  1. package/README.md +3 -3
  2. package/lib/ai/astro-file.d.ts +137 -0
  3. package/lib/ai/astro-file.js +173 -0
  4. package/lib/ai/client.d.ts +2 -10
  5. package/lib/ai/client.js +8 -24
  6. package/lib/ai/css-file.d.ts +131 -0
  7. package/lib/ai/css-file.js +158 -0
  8. package/lib/ai/data.d.ts +45 -5
  9. package/lib/ai/data.js +45 -10
  10. package/lib/ai/document.d.ts +44 -20
  11. package/lib/ai/document.js +95 -28
  12. package/lib/ai/html-file.d.ts +128 -0
  13. package/lib/ai/html-file.js +155 -0
  14. package/lib/ai/index.d.ts +7 -0
  15. package/lib/ai/index.js +7 -0
  16. package/lib/ai/json-file.d.ts +160 -0
  17. package/lib/ai/json-file.js +208 -0
  18. package/lib/ai/typescript-file.d.ts +140 -0
  19. package/lib/ai/typescript-file.js +176 -0
  20. package/lib/ai/vue-file.d.ts +126 -0
  21. package/lib/ai/vue-file.js +153 -0
  22. package/lib/ai/yaml-file.d.ts +169 -0
  23. package/lib/ai/yaml-file.js +236 -0
  24. package/lib/cloudflare/dns.d.ts +88 -0
  25. package/lib/cloudflare/dns.js +192 -0
  26. package/lib/cloudflare/index.d.ts +2 -1
  27. package/lib/cloudflare/index.js +2 -1
  28. package/lib/cloudflare/r2-rest-state-store.d.ts +125 -0
  29. package/lib/cloudflare/r2-rest-state-store.js +254 -0
  30. package/lib/cloudflare/response.d.ts +12 -0
  31. package/lib/cloudflare/response.js +0 -0
  32. package/lib/cloudflare/zone.d.ts +18 -0
  33. package/lib/cloudflare/zone.js +17 -4
  34. package/lib/dns/godaddy.d.ts +7 -0
  35. package/lib/dns/godaddy.js +21 -0
  36. package/lib/dns/import-dns.d.ts +82 -0
  37. package/lib/dns/import-dns.js +114 -0
  38. package/lib/dns/index.d.ts +2 -0
  39. package/lib/dns/index.js +2 -0
  40. package/lib/dns/record.d.ts +88 -0
  41. package/lib/dns/record.js +13 -0
  42. package/lib/fs/file-system-state-store.d.ts +17 -0
  43. package/lib/fs/file-system-state-store.js +87 -0
  44. package/lib/fs/folder.d.ts +10 -0
  45. package/lib/fs/folder.js +2 -2
  46. package/lib/fs/index.d.ts +4 -4
  47. package/lib/fs/index.js +4 -4
  48. package/lib/fs/{json-file.d.ts → static-json-file.d.ts} +2 -2
  49. package/lib/fs/static-json-file.js +14 -0
  50. package/lib/fs/{text-file.d.ts → static-text-file.d.ts} +2 -2
  51. package/lib/fs/{text-file.js → static-text-file.js} +1 -1
  52. package/lib/fs/{typescript-file.d.ts → static-typescript-file.d.ts} +2 -2
  53. package/lib/fs/{typescript-file.js → static-typescript-file.js} +1 -1
  54. package/lib/fs/{yaml-file.d.ts → static-yaml-file.d.ts} +2 -2
  55. package/lib/fs/{yaml-file.js → static-yaml-file.js} +1 -1
  56. package/lib/internal/getting-started.d.ts +10 -0
  57. package/lib/internal/getting-started.js +94 -0
  58. package/lib/internal/project.d.ts +13 -0
  59. package/lib/internal/project.js +101 -0
  60. package/lib/internal/providers.d.ts +23 -0
  61. package/lib/internal/providers.js +148 -0
  62. package/lib/resource.js +8 -1
  63. package/lib/scope.d.ts +1 -1
  64. package/lib/scope.js +4 -2
  65. package/lib/state.d.ts +1 -16
  66. package/lib/state.js +0 -87
  67. package/lib/test/bun.d.ts +11 -0
  68. package/lib/test/bun.js +36 -8
  69. package/lib/web/astro.d.ts +147 -0
  70. package/lib/web/astro.js +414 -0
  71. package/lib/{shadcn/component.js → web/shadcn-component.js} +2 -9
  72. package/lib/web/shadcn.d.ts +97 -0
  73. package/lib/web/shadcn.js +99 -0
  74. package/lib/web/tailwind.d.ts +64 -0
  75. package/lib/web/tailwind.js +97 -0
  76. package/lib/{vite → web}/vite.d.ts +5 -0
  77. package/lib/{vite → web}/vite.js +44 -44
  78. package/lib/web/vitepress/custom-theme.d.ts +173 -0
  79. package/lib/web/vitepress/custom-theme.js +336 -0
  80. package/lib/{vitepress → web/vitepress}/dependencies.d.ts +2 -2
  81. package/lib/{vitepress → web/vitepress}/dependencies.js +1 -1
  82. package/lib/web/vitepress/home-page.d.ts +244 -0
  83. package/lib/web/vitepress/home-page.js +134 -0
  84. package/lib/{vitepress → web/vitepress}/index.d.ts +1 -0
  85. package/lib/{vitepress → web/vitepress}/index.js +1 -0
  86. package/lib/{vitepress → web/vitepress}/vitepress.d.ts +10 -8
  87. package/lib/{vitepress → web/vitepress}/vitepress.js +11 -12
  88. package/package.json +6 -2
  89. package/src/ai/astro-file.ts +288 -0
  90. package/src/ai/client.ts +8 -28
  91. package/src/ai/css-file.ts +266 -0
  92. package/src/ai/data.ts +53 -16
  93. package/src/ai/document.ts +130 -33
  94. package/src/ai/html-file.ts +263 -0
  95. package/src/ai/index.ts +7 -0
  96. package/src/ai/json-file.ts +349 -0
  97. package/src/ai/typescript-file.ts +293 -0
  98. package/src/ai/vue-file.ts +261 -0
  99. package/src/ai/yaml-file.ts +368 -0
  100. package/src/apply.ts +0 -1
  101. package/src/cloudflare/dns.ts +344 -0
  102. package/src/cloudflare/index.ts +2 -1
  103. package/src/cloudflare/r2-rest-state-store.ts +353 -0
  104. package/src/cloudflare/response.ts +9 -0
  105. package/src/cloudflare/zone.ts +39 -23
  106. package/src/dns/godaddy.ts +29 -0
  107. package/src/dns/import-dns.ts +213 -0
  108. package/src/dns/index.ts +2 -0
  109. package/src/dns/record.ts +121 -0
  110. package/src/fs/file-system-state-store.ts +109 -0
  111. package/src/fs/folder.ts +14 -2
  112. package/src/fs/index.ts +4 -4
  113. package/src/fs/{json-file.ts → static-json-file.ts} +13 -3
  114. package/src/fs/{text-file.ts → static-text-file.ts} +5 -2
  115. package/src/fs/{typescript-file.ts → static-typescript-file.ts} +3 -3
  116. package/src/fs/{yaml-file.ts → static-yaml-file.ts} +5 -2
  117. package/src/internal/getting-started.ts +107 -0
  118. package/src/internal/project.ts +119 -0
  119. package/src/internal/providers.ts +209 -0
  120. package/src/resource.ts +10 -1
  121. package/src/scope.ts +5 -6
  122. package/src/state.ts +1 -111
  123. package/src/test/bun.ts +60 -10
  124. package/src/web/astro.ts +644 -0
  125. package/src/{shadcn/component.ts → web/shadcn-component.ts} +5 -13
  126. package/src/web/shadcn.ts +219 -0
  127. package/src/web/tailwind.ts +167 -0
  128. package/src/{vite → web}/vite.ts +54 -56
  129. package/src/web/vitepress/custom-theme.ts +514 -0
  130. package/src/{vitepress → web/vitepress}/dependencies.ts +2 -2
  131. package/src/web/vitepress/home-page.ts +363 -0
  132. package/src/{vitepress → web/vitepress}/index.ts +1 -0
  133. package/src/{vitepress → web/vitepress}/vitepress.ts +34 -27
  134. package/lib/cloudflare/state.d.ts +0 -82
  135. package/lib/cloudflare/state.js +0 -108
  136. package/lib/fs/json-file.js +0 -7
  137. package/lib/internal/docs.d.ts +0 -5
  138. package/lib/internal/docs.js +0 -287
  139. package/lib/shadcn/index.d.ts +0 -1
  140. package/lib/shadcn/index.js +0 -1
  141. package/lib/vite/index.d.ts +0 -1
  142. package/lib/vite/index.js +0 -1
  143. package/lib/vitepress/home-page.d.ts +0 -133
  144. package/lib/vitepress/home-page.js +0 -11
  145. package/src/cloudflare/state.ts +0 -138
  146. package/src/internal/docs.ts +0 -338
  147. package/src/shadcn/index.ts +0 -1
  148. package/src/vite/index.ts +0 -1
  149. package/src/vitepress/home-page.ts +0 -166
  150. /package/lib/{shadcn/component.d.ts → web/shadcn-component.d.ts} +0 -0
  151. /package/src/{vitepress → web/vitepress}/index.md +0 -0
@@ -1,8 +1,9 @@
1
1
  export * from "./bindings";
2
2
  export * from "./bucket";
3
+ export * from "./dns";
3
4
  export * from "./durable-object-namespace";
4
5
  export * from "./kv-namespace";
5
- export * from "./state";
6
+ export * from "./r2-rest-state-store";
6
7
  export * from "./static-site";
7
8
  export * from "./worker";
8
9
  export * from "./wrangler.json";
@@ -0,0 +1,353 @@
1
+ import type { Scope } from "../scope";
2
+ import type { State, StateStore } from "../state";
3
+ import { withExponentialBackoff } from "../util/retry";
4
+ import { deserialize, serialize } from "../util/serde";
5
+ import { type CloudflareApi, createCloudflareApi } from "./api";
6
+
7
+ /**
8
+ * Options for CloudflareR2StateStore
9
+ */
10
+ export interface CloudflareR2StateStoreOptions {
11
+ /**
12
+ * The prefix to use for object keys in the R2 bucket
13
+ * This allows multiple state stores to use the same R2 bucket
14
+ */
15
+ prefix?: string;
16
+
17
+ /**
18
+ * The R2 bucket name to use
19
+ * Required - the bucket must already exist
20
+ */
21
+ bucketName: string;
22
+
23
+ /**
24
+ * API key to use (overrides CLOUDFLARE_API_KEY env var)
25
+ */
26
+ apiKey?: string;
27
+
28
+ /**
29
+ * Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
30
+ */
31
+ accountId?: string;
32
+
33
+ /**
34
+ * Email to use with API Key authentication (overrides CLOUDFLARE_EMAIL env var)
35
+ */
36
+ email?: string;
37
+ }
38
+
39
+ /**
40
+ * State store implementation using Cloudflare R2 API
41
+ * Uses R2 for immediate consistency compared to KV's eventual consistency
42
+ */
43
+ export class R2RestStateStore implements StateStore {
44
+ private api: CloudflareApi;
45
+ private prefix: string;
46
+ private bucketName: string;
47
+ private apiKey: string | undefined;
48
+ private accountId: string | undefined;
49
+ private email: string | undefined;
50
+ private initialized = false;
51
+
52
+ /**
53
+ * Create a new CloudflareR2StateStore
54
+ *
55
+ * @param scope The scope this store belongs to
56
+ * @param options Options for the state store
57
+ */
58
+ constructor(
59
+ public readonly scope: Scope,
60
+ options: CloudflareR2StateStoreOptions,
61
+ ) {
62
+ // Use the scope's chain to build the prefix, similar to how FileSystemStateStore builds its directory
63
+ const scopePath = scope.chain.join("/");
64
+ this.prefix = options.prefix
65
+ ? `${options.prefix}${scopePath}/`
66
+ : `alchemy/${scopePath}/`;
67
+
68
+ if (!options.bucketName) {
69
+ throw new Error("bucketName is required for CloudflareR2StateStore");
70
+ }
71
+ this.bucketName = options.bucketName;
72
+
73
+ this.apiKey = options.apiKey;
74
+ this.accountId = options.accountId;
75
+ this.email = options.email;
76
+
77
+ // We'll initialize the API in init() to allow for async creation
78
+ this.api = null as any;
79
+ }
80
+
81
+ /**
82
+ * Initialize the R2 client
83
+ */
84
+ async init(): Promise<void> {
85
+ if (this.initialized) return;
86
+
87
+ // Create Cloudflare API client with automatic account discovery
88
+ this.api = await createCloudflareApi({
89
+ apiKey: this.apiKey,
90
+ accountId: this.accountId,
91
+ email: this.email,
92
+ });
93
+
94
+ this.initialized = true;
95
+ }
96
+
97
+ /**
98
+ * R2 buckets cannot be deleted programmatically via this method
99
+ */
100
+ async deinit(): Promise<void> {
101
+ // We don't delete the bucket here, only via explicit resource deletion
102
+ }
103
+
104
+ /**
105
+ * List all resources in the state store
106
+ */
107
+ async list(): Promise<string[]> {
108
+ await this.ensureInitialized();
109
+
110
+ // Using pagination to get all objects
111
+ let keys: string[] = [];
112
+ let cursor: string | null = null;
113
+
114
+ do {
115
+ const params = new URLSearchParams({
116
+ prefix: this.prefix,
117
+ limit: "1000",
118
+ });
119
+
120
+ if (cursor) {
121
+ params.append("cursor", cursor);
122
+ }
123
+
124
+ const listPath = `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects?${params.toString()}`;
125
+ const response = await this.api.get(listPath);
126
+
127
+ if (!response.ok) {
128
+ const errorData: any = await response.json().catch(() => ({
129
+ errors: [{ message: response.statusText }],
130
+ }));
131
+ throw new Error(
132
+ `Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`,
133
+ );
134
+ }
135
+
136
+ const data = (await response.json()) as any;
137
+
138
+ // The result structure may be under "result" key in Cloudflare's API
139
+ const result = data.result || data;
140
+ const objects = result.objects || [];
141
+
142
+ // Add keys to our list, removing the prefix and converting from storage format
143
+ keys = keys.concat(
144
+ objects.map((obj: any) => {
145
+ const keyName = obj.key || obj.name;
146
+ return this.convertKeyFromStorage(keyName.slice(this.prefix.length));
147
+ }),
148
+ );
149
+
150
+ // Update cursor for next page if available
151
+ cursor =
152
+ result.truncated || result.cursor_pagination
153
+ ? result.cursor || null
154
+ : null;
155
+ } while (cursor);
156
+
157
+ return keys;
158
+ }
159
+
160
+ /**
161
+ * Count the number of items in the state store
162
+ */
163
+ async count(): Promise<number> {
164
+ const keys = await this.list();
165
+ return keys.length;
166
+ }
167
+
168
+ /**
169
+ * Get a state by key
170
+ *
171
+ * @param key The key to look up
172
+ * @returns The state or undefined if not found
173
+ */
174
+ async get(key: string): Promise<State | undefined> {
175
+ await this.ensureInitialized();
176
+
177
+ try {
178
+ const response = await this.api.get(
179
+ `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`,
180
+ );
181
+
182
+ if (!response.ok) {
183
+ if (response.status === 404) {
184
+ return undefined;
185
+ }
186
+
187
+ const errorData: any = await response.json().catch(() => ({
188
+ errors: [{ message: response.statusText }],
189
+ }));
190
+ throw new Error(
191
+ `Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`,
192
+ );
193
+ }
194
+
195
+ // Parse and deserialize the state data
196
+ const rawData = await response.json();
197
+ const state = (await deserialize(this.scope, rawData)) as State;
198
+
199
+ // Create a new state object with proper output
200
+ return {
201
+ ...state,
202
+ output: {
203
+ ...(state.output || {}),
204
+ Scope: this.scope,
205
+ },
206
+ };
207
+ } catch (error: any) {
208
+ if (error.message?.includes("404")) {
209
+ return undefined;
210
+ }
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Get multiple states by their keys
217
+ *
218
+ * @param ids Array of keys to fetch
219
+ * @returns Record mapping keys to their states
220
+ */
221
+ async getBatch(ids: string[]): Promise<Record<string, State>> {
222
+ const result: Record<string, State> = {};
223
+
224
+ // R2 REST API doesn't have a batch get operation, so we need to make multiple requests
225
+ const promises = ids.map(async (id) => {
226
+ const state = await this.get(id);
227
+ if (state) {
228
+ result[id] = state;
229
+ }
230
+ });
231
+
232
+ await Promise.all(promises);
233
+ return result;
234
+ }
235
+
236
+ /**
237
+ * Get all states in the store
238
+ *
239
+ * @returns Record mapping all keys to their states
240
+ */
241
+ async all(): Promise<Record<string, State>> {
242
+ const keys = await this.list();
243
+ return this.getBatch(keys);
244
+ }
245
+
246
+ /**
247
+ * Set a state for a key
248
+ *
249
+ * @param key The key to set
250
+ * @param value The state to store
251
+ */
252
+ async set(key: string, value: State): Promise<void> {
253
+ await this.ensureInitialized();
254
+
255
+ const objectKey = this.getObjectKey(key);
256
+
257
+ // Serialize the state to handle cyclic structures
258
+ const serializedData = await serialize(this.scope, value);
259
+
260
+ // Using withExponentialBackoff for reliability
261
+ await withExponentialBackoff(
262
+ async () => {
263
+ const response = await this.api.put(
264
+ `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${objectKey}`,
265
+ serializedData,
266
+ {
267
+ headers: {
268
+ "Content-Type": "application/json",
269
+ },
270
+ },
271
+ );
272
+
273
+ if (!response.ok) {
274
+ const errorData: any = await response.json().catch(() => ({
275
+ errors: [{ message: response.statusText }],
276
+ }));
277
+ throw new Error(
278
+ `Error writing to R2: ${errorData.errors?.[0]?.message || response.statusText}`,
279
+ );
280
+ }
281
+
282
+ return response;
283
+ },
284
+ // Retry on transient errors
285
+ (error) =>
286
+ error.message?.includes("503") || error.message?.includes("timeout"),
287
+ 5, // 5 retry attempts
288
+ 1000, // Start with 1 second delay
289
+ );
290
+ }
291
+
292
+ /**
293
+ * Delete a state by key
294
+ *
295
+ * @param key The key to delete
296
+ */
297
+ async delete(key: string): Promise<void> {
298
+ await this.ensureInitialized();
299
+
300
+ const response = await this.api.delete(
301
+ `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`,
302
+ );
303
+
304
+ if (!response.ok && response.status !== 404) {
305
+ const errorData: any = await response.json().catch(() => ({
306
+ errors: [{ message: response.statusText }],
307
+ }));
308
+ throw new Error(
309
+ `Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`,
310
+ );
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Convert key for storage by replacing slashes with colons
316
+ * since R2 treats slashes as directory separators
317
+ *
318
+ * @param key The original key
319
+ * @returns Key with slashes replaced by colons
320
+ */
321
+ private convertKeyForStorage(key: string): string {
322
+ return key.replaceAll("/", ":");
323
+ }
324
+
325
+ /**
326
+ * Convert key from storage by replacing colons with slashes
327
+ *
328
+ * @param key The storage key
329
+ * @returns Key with colons replaced by slashes
330
+ */
331
+ private convertKeyFromStorage(key: string): string {
332
+ return key.replaceAll(":", "/");
333
+ }
334
+
335
+ /**
336
+ * Get the full object key for storage
337
+ *
338
+ * @param key The original key
339
+ * @returns The key with prefix for use in the R2 bucket
340
+ */
341
+ private getObjectKey(key: string): string {
342
+ return `${this.prefix}${this.convertKeyForStorage(key)}`;
343
+ }
344
+
345
+ /**
346
+ * Ensure the store is initialized before operations
347
+ */
348
+ private async ensureInitialized(): Promise<void> {
349
+ if (!this.initialized) {
350
+ await this.init();
351
+ }
352
+ }
353
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Cloudflare API response format
3
+ */
4
+ export interface CloudflareResponse<T = unknown> {
5
+ result: T;
6
+ success: boolean;
7
+ errors: Array<{ code: number; message: string }>;
8
+ messages: string[];
9
+ }
@@ -341,8 +341,8 @@ export const Zone = Resource(
341
341
  );
342
342
  }
343
343
 
344
- const result = (await response.json()) as CloudflareZoneResponse;
345
- const zoneData = result.result;
344
+ const zoneData = ((await response.json()) as { result: CloudflareZone })
345
+ .result;
346
346
 
347
347
  // Update zone settings if provided
348
348
  if (props.settings) {
@@ -380,18 +380,39 @@ export const Zone = Resource(
380
380
  });
381
381
 
382
382
  const body = await response.text();
383
+ let zoneData;
383
384
  if (!response.ok) {
384
385
  if (response.status === 400 && body.includes("already exists")) {
385
- // TODO: should we error?
386
+ // Zone already exists, fetch it instead
387
+ console.warn(
388
+ `Zone '${props.name}' already exists during Zone create, adopting it...`,
389
+ );
390
+ const getResponse = await api.get(`/zones?name=${props.name}`);
391
+
392
+ if (!getResponse.ok) {
393
+ throw new Error(
394
+ `Error fetching existing zone '${props.name}': ${getResponse.statusText}`,
395
+ );
396
+ }
397
+
398
+ const zones = (
399
+ (await getResponse.json()) as { result: CloudflareZone[] }
400
+ ).result;
401
+ if (zones.length === 0) {
402
+ throw new Error(
403
+ `Zone '${props.name}' does not exist, but the name is reserved for another user.`,
404
+ );
405
+ }
406
+ zoneData = zones[0];
386
407
  } else {
387
408
  throw new Error(
388
409
  `Error creating zone '${props.name}': ${response.statusText}\n${body}`,
389
410
  );
390
411
  }
412
+ } else {
413
+ zoneData = (JSON.parse(body) as { result: CloudflareZone }).result;
391
414
  }
392
415
 
393
- const zoneData = (JSON.parse(body) as CloudflareZoneResponse).result;
394
-
395
416
  // Update zone settings if provided
396
417
  if (props.settings) {
397
418
  await updateZoneSettings(api, zoneData.id, props.settings);
@@ -522,25 +543,20 @@ async function getZoneSettings(
522
543
  }
523
544
 
524
545
  /**
525
- * Cloudflare Zone API response
546
+ * Cloudflare Zone response format
526
547
  */
527
- interface CloudflareZoneResponse {
528
- result: {
548
+ export interface CloudflareZone {
549
+ id: string;
550
+ name: string;
551
+ type: "full" | "partial" | "secondary";
552
+ status: string;
553
+ paused: boolean;
554
+ account: {
529
555
  id: string;
530
- name: string;
531
- type: "full" | "partial" | "secondary";
532
- status: string;
533
- paused: boolean;
534
- account: {
535
- id: string;
536
- };
537
- name_servers: string[];
538
- original_name_servers: string[] | null;
539
- created_on: string;
540
- modified_on: string;
541
- activated_on: string | null;
542
556
  };
543
- success: boolean;
544
- errors: Array<{ code: number; message: string }>;
545
- messages: string[];
557
+ name_servers: string[];
558
+ original_name_servers: string[] | null;
559
+ created_on: string;
560
+ modified_on: string;
561
+ activated_on: string | null;
546
562
  }
@@ -0,0 +1,29 @@
1
+ export type UpdateNameserversOptions = {
2
+ domain: string;
3
+ apiKey: string;
4
+ apiSecret: string;
5
+ nameservers: string[];
6
+ };
7
+
8
+ export async function updateNameservers(options: UpdateNameserversOptions) {
9
+ const url = `https://api.godaddy.com/v1/domains/${options.domain}/nameservers`;
10
+
11
+ const response = await fetch(url, {
12
+ method: "PUT",
13
+ headers: {
14
+ Authorization: `sso-key ${options.apiKey}:${options.apiSecret}`,
15
+ "Content-Type": "application/json",
16
+ Accept: "application/json",
17
+ },
18
+ body: JSON.stringify({
19
+ nameservers: options.nameservers,
20
+ }),
21
+ });
22
+
23
+ if (response.ok) {
24
+ console.log(`✅ Nameservers updated for ${options.domain}`);
25
+ } else {
26
+ const error = await response.json();
27
+ console.error(`❌ Failed to update nameservers:`, error);
28
+ }
29
+ }
@@ -0,0 +1,213 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ import { DEFAULT_RECORD_TYPES, type DnsRecordType } from "./record";
4
+
5
+ /**
6
+ * DNS record response structure from Cloudflare DNS API
7
+ * with both data (original) and content (for compatibility)
8
+ */
9
+ export interface DnsApiRecord {
10
+ name: string;
11
+ type: DnsRecordType;
12
+ TTL: number;
13
+ data: string;
14
+ content: string; // Added for compatibility with DnsRecords
15
+ ttl: number; // Normalized ttl (lowercase)
16
+ priority?: number; // Priority for MX and SRV records
17
+ }
18
+
19
+ /**
20
+ * Cloudflare DNS-over-HTTPS API response structure
21
+ */
22
+ interface CloudflareDNSResponse {
23
+ Status: number;
24
+ TC: boolean;
25
+ RD: boolean;
26
+ RA: boolean;
27
+ AD: boolean;
28
+ CD: boolean;
29
+ Question: Array<{
30
+ name: string;
31
+ type: number;
32
+ }>;
33
+ Answer?: {
34
+ name: string;
35
+ type: number;
36
+ TTL: number;
37
+ data: string;
38
+ }[];
39
+ }
40
+
41
+ /**
42
+ * Properties for importing DNS records
43
+ */
44
+ export interface ImportDnsRecordsProps {
45
+ /**
46
+ * The domain to fetch DNS records for
47
+ */
48
+ domain: string;
49
+
50
+ /**
51
+ * Specific record types to fetch. If not provided, defaults to all supported types.
52
+ */
53
+ recordTypes?: DnsRecordType[];
54
+
55
+ /**
56
+ * Bump the resource to force a new import
57
+ */
58
+ bump?: number;
59
+ }
60
+
61
+ /**
62
+ * Output returned after DNS records import
63
+ */
64
+ export interface ImportDnsRecords
65
+ extends Resource<"dns::ImportDnsRecords">,
66
+ ImportDnsRecordsProps {
67
+ /**
68
+ * The DNS records as a flat array, directly compatible with DnsRecords function
69
+ */
70
+ records: DnsApiRecord[];
71
+
72
+ /**
73
+ * Time at which the records were imported
74
+ */
75
+ importedAt: number;
76
+ }
77
+
78
+ /**
79
+ * Map numeric DNS record type to string type
80
+ */
81
+ function mapDnsRecordType(type: number): DnsRecordType {
82
+ const typeMap: Record<number, DnsRecordType> = {
83
+ 1: "A",
84
+ 2: "NS",
85
+ 5: "CNAME",
86
+ 6: "SOA",
87
+ 15: "MX",
88
+ 16: "TXT",
89
+ 28: "AAAA",
90
+ 33: "SRV",
91
+ 12: "PTR",
92
+ };
93
+
94
+ return typeMap[type] || "A";
95
+ }
96
+
97
+ /**
98
+ * Import DNS records for a domain using Cloudflare's DNS-over-HTTPS API.
99
+ * This resource allows you to fetch DNS records for a domain and store them
100
+ * in a structured format.
101
+ *
102
+ * @example
103
+ * // Import all default record types
104
+ * const allRecords = await ImportDnsRecords("example.com", {
105
+ * domain: "example.com"
106
+ * });
107
+ *
108
+ * @example
109
+ * // Import only specific record types
110
+ * const specificRecords = await ImportDnsRecords("example.com", {
111
+ * domain: "example.com",
112
+ * recordTypes: ["A", "MX"]
113
+ * });
114
+ *
115
+ * @example
116
+ * // Import DNS records and transfer them to a Cloudflare zone
117
+ * const dnsRecords = await ImportDnsRecords("dns-records", {
118
+ * domain: "example.com",
119
+ * });
120
+ *
121
+ * const zone = await Zone("example.com", {
122
+ * name: "example.com",
123
+ * type: "full",
124
+ * });
125
+ *
126
+ * // Records are directly compatible with DnsRecords function
127
+ * await DnsRecords("transfer-dns-records", {
128
+ * zoneId: zone.id,
129
+ * records: dnsRecords.records,
130
+ * });
131
+ */
132
+ export const ImportDnsRecords = Resource(
133
+ "dns::ImportDnsRecords",
134
+ async function (
135
+ this: Context<ImportDnsRecords>,
136
+ id: string,
137
+ props: ImportDnsRecordsProps,
138
+ ): Promise<ImportDnsRecords> {
139
+ // For delete phase, just return destroyed state since this is a read-only resource
140
+ if (this.phase === "delete") {
141
+ return this.destroy();
142
+ }
143
+
144
+ const recordTypes = props.recordTypes || DEFAULT_RECORD_TYPES;
145
+ const allRecords: DnsApiRecord[] = [];
146
+
147
+ for (const type of recordTypes) {
148
+ try {
149
+ const res = await fetch(
150
+ `https://cloudflare-dns.com/dns-query?name=${props.domain}&type=${type}`,
151
+ {
152
+ headers: {
153
+ accept: "application/dns-json",
154
+ },
155
+ },
156
+ );
157
+
158
+ if (!res.ok) {
159
+ throw new Error(`Failed to fetch ${type} records: ${res.statusText}`);
160
+ }
161
+
162
+ const data = (await res.json()) as CloudflareDNSResponse;
163
+
164
+ if (data.Answer) {
165
+ // Transform records to be compatible with DnsRecords function
166
+ const compatRecords = data.Answer.map((record) => {
167
+ const recordType = mapDnsRecordType(record.type);
168
+ const result: DnsApiRecord = {
169
+ name: record.name,
170
+ type: recordType,
171
+ TTL: record.TTL,
172
+ data: record.data,
173
+ content: record.data, // Default mapping
174
+ ttl: record.TTL, // Normalized lowercase ttl
175
+ };
176
+
177
+ // Special handling for MX records to split priority and content
178
+ if (recordType === "MX") {
179
+ const parts = record.data.split(" ");
180
+ if (parts.length >= 2) {
181
+ const priority = Number.parseInt(parts[0], 10);
182
+ // Join the rest as hostname in case there are spaces
183
+ const hostname = parts.slice(1).join(" ");
184
+
185
+ if (!isNaN(priority)) {
186
+ result.priority = priority;
187
+ result.content = hostname;
188
+ }
189
+ }
190
+ }
191
+
192
+ return result;
193
+ });
194
+
195
+ allRecords.push(...compatRecords);
196
+ }
197
+ } catch (error) {
198
+ console.warn(
199
+ `Failed to fetch ${type} records for ${props.domain}:`,
200
+ error,
201
+ );
202
+ }
203
+ }
204
+
205
+ // Return the resource with fetched records as a flat array
206
+ return this({
207
+ domain: props.domain,
208
+ recordTypes: [...recordTypes],
209
+ records: allRecords,
210
+ importedAt: Date.now(),
211
+ });
212
+ },
213
+ );
@@ -0,0 +1,2 @@
1
+ export * from "./import-dns";
2
+ export * from "./record";