@fjall/generator 0.88.4 → 0.89.2

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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/ast/astCdnParser.d.ts +15 -0
  3. package/dist/src/ast/astCdnParser.js +114 -0
  4. package/dist/src/ast/astCommonParser.d.ts +90 -0
  5. package/dist/src/ast/astCommonParser.js +351 -0
  6. package/dist/src/ast/astComputeParser.d.ts +14 -2
  7. package/dist/src/ast/astComputeParser.js +55 -9
  8. package/dist/src/ast/astDatabaseParser.d.ts +104 -0
  9. package/dist/src/ast/astDatabaseParser.js +275 -0
  10. package/dist/src/ast/astInfrastructureParser.d.ts +23 -277
  11. package/dist/src/ast/astInfrastructureParser.js +83 -1456
  12. package/dist/src/ast/astMessagingParser.d.ts +25 -0
  13. package/dist/src/ast/astMessagingParser.js +78 -0
  14. package/dist/src/ast/astNetworkParser.d.ts +70 -0
  15. package/dist/src/ast/astNetworkParser.js +219 -0
  16. package/dist/src/ast/astPatternParser.d.ts +80 -0
  17. package/dist/src/ast/astPatternParser.js +155 -0
  18. package/dist/src/ast/astStorageParser.d.ts +18 -0
  19. package/dist/src/ast/astStorageParser.js +164 -0
  20. package/dist/src/ast/index.d.ts +1 -0
  21. package/dist/src/ast/index.js +4 -0
  22. package/dist/src/dns/bindParser.d.ts +13 -0
  23. package/dist/src/dns/bindParser.js +224 -0
  24. package/dist/src/dns/bindWriter.d.ts +2 -0
  25. package/dist/src/dns/bindWriter.js +52 -0
  26. package/dist/src/dns/index.d.ts +4 -0
  27. package/dist/src/dns/index.js +4 -0
  28. package/dist/src/dns/infrastructureWriter.d.ts +2 -0
  29. package/dist/src/dns/infrastructureWriter.js +58 -0
  30. package/dist/src/dns/types.d.ts +82 -0
  31. package/dist/src/dns/types.js +52 -0
  32. package/dist/src/generation/common.d.ts +1 -16
  33. package/dist/src/generation/common.js +2 -28
  34. package/dist/src/generation/compute.js +77 -28
  35. package/dist/src/generation/index.d.ts +2 -1
  36. package/dist/src/generation/index.js +3 -1
  37. package/dist/src/generation/messagingConnections.d.ts +33 -0
  38. package/dist/src/generation/messagingConnections.js +73 -0
  39. package/dist/src/generation/storage.d.ts +5 -1
  40. package/dist/src/generation/storage.js +9 -1
  41. package/dist/src/generation/storageConnections.d.ts +3 -3
  42. package/dist/src/generation/storageConnections.js +8 -4
  43. package/dist/src/index.d.ts +1 -0
  44. package/dist/src/index.js +2 -0
  45. package/dist/src/planning/resourcePlanning.js +0 -2
  46. package/dist/src/presets/tierTypes.d.ts +4 -1
  47. package/dist/src/schemas/applicationSchemas.d.ts +854 -0
  48. package/dist/src/schemas/applicationSchemas.js +80 -0
  49. package/dist/src/schemas/baseSchemas.d.ts +206 -0
  50. package/dist/src/schemas/baseSchemas.js +248 -0
  51. package/dist/src/schemas/cdnSchemas.d.ts +61 -0
  52. package/dist/src/schemas/cdnSchemas.js +62 -0
  53. package/dist/src/schemas/computeSchemas.d.ts +723 -0
  54. package/dist/src/schemas/computeSchemas.js +727 -0
  55. package/dist/src/schemas/constants.d.ts +12 -8
  56. package/dist/src/schemas/constants.js +14 -4
  57. package/dist/src/schemas/databaseSchemas.d.ts +638 -0
  58. package/dist/src/schemas/databaseSchemas.js +366 -0
  59. package/dist/src/schemas/messagingSchemas.d.ts +20 -0
  60. package/dist/src/schemas/messagingSchemas.js +29 -0
  61. package/dist/src/schemas/networkSchemas.d.ts +246 -0
  62. package/dist/src/schemas/networkSchemas.js +125 -0
  63. package/dist/src/schemas/patternSchemas.d.ts +708 -0
  64. package/dist/src/schemas/patternSchemas.js +294 -0
  65. package/dist/src/schemas/resourceSchemas.d.ts +24 -3530
  66. package/dist/src/schemas/resourceSchemas.js +24 -2011
  67. package/dist/src/schemas/storageSchemas.d.ts +93 -0
  68. package/dist/src/schemas/storageSchemas.js +119 -0
  69. package/dist/src/util/errorUtils.d.ts +1 -2
  70. package/dist/src/util/errorUtils.js +1 -15
  71. package/dist/src/validation/patterns.d.ts +9 -0
  72. package/dist/src/validation/patterns.js +9 -0
  73. package/package.json +4 -3
@@ -0,0 +1,164 @@
1
+ import * as ts from "typescript";
2
+ import { S3_STACK_PLACEMENTS, } from "../schemas/resourceSchemas.js";
3
+ import { S3_ENCRYPTION_TYPES, BACKUP_VAULT_TIERS, } from "../schemas/constants.js";
4
+ import { asBoolean, asNumber, asString, asStringUnion, captureExtraProperties, collectFromAst, extractVariableName, isFactoryBuildCall, isFactoryMethodCall, isParsedObject, parseObjectLiteral, } from "./astCommonParser.js";
5
+ const S3_BUCKET_CLASSES = new Set(["S3Bucket"]);
6
+ export { S3_BUCKET_CLASSES };
7
+ // ---- Extraction helpers ----
8
+ function extractS3Resource(varDecl, newExpr, bucketClass) {
9
+ const variableName = ts.isIdentifier(varDecl.name) ? varDecl.name.text : "";
10
+ if (newExpr.arguments && newExpr.arguments.length >= 2) {
11
+ const nameArg = newExpr.arguments[1];
12
+ const resourceName = ts.isStringLiteral(nameArg) ? nameArg.text : "";
13
+ const configArg = newExpr.arguments.length >= 3 ? newExpr.arguments[2] : undefined;
14
+ const config = configArg && ts.isObjectLiteralExpression(configArg)
15
+ ? parseObjectLiteral(configArg)
16
+ : {};
17
+ return {
18
+ variableName,
19
+ resourceName,
20
+ bucketClass,
21
+ config,
22
+ node: varDecl,
23
+ };
24
+ }
25
+ return null;
26
+ }
27
+ function extractS3FactoryResource(addStorageCall, buildCall) {
28
+ if (buildCall.arguments.length < 2)
29
+ return null;
30
+ const nameArg = buildCall.arguments[0];
31
+ const configArg = buildCall.arguments[1];
32
+ if (!ts.isStringLiteral(nameArg) || !ts.isObjectLiteralExpression(configArg))
33
+ return null;
34
+ const config = parseObjectLiteral(configArg);
35
+ const variableName = extractVariableName(addStorageCall) ?? "";
36
+ return {
37
+ variableName,
38
+ resourceName: nameArg.text,
39
+ bucketClass: "StorageFactory",
40
+ config,
41
+ node: addStorageCall,
42
+ };
43
+ }
44
+ // ---- Public API ----
45
+ /** Find S3 resources created via new S3Bucket() constructor pattern */
46
+ export function findS3Resources(sourceFile) {
47
+ return collectFromAst(sourceFile, (node) => {
48
+ if (!ts.isVariableDeclaration(node) || !node.initializer)
49
+ return null;
50
+ if (!ts.isNewExpression(node.initializer))
51
+ return null;
52
+ const newExpr = node.initializer;
53
+ if (!ts.isIdentifier(newExpr.expression))
54
+ return null;
55
+ const bucketClass = newExpr.expression.text;
56
+ if (!S3_BUCKET_CLASSES.has(bucketClass))
57
+ return null;
58
+ return extractS3Resource(node, newExpr, bucketClass);
59
+ });
60
+ }
61
+ /** Find S3 resources created via app.addStorage(StorageFactory.build(...)) factory pattern */
62
+ export function findS3FactoryResources(sourceFile) {
63
+ return collectFromAst(sourceFile, (node) => {
64
+ if (!ts.isCallExpression(node) || !isFactoryMethodCall(node, "addStorage"))
65
+ return null;
66
+ const storageArg = node.arguments[0];
67
+ if (!isFactoryBuildCall(storageArg, "StorageFactory"))
68
+ return null;
69
+ return extractS3FactoryResource(node, storageArg);
70
+ });
71
+ }
72
+ /** Convert parsed S3 resources to plan format */
73
+ export function convertS3Resources(s3Resources) {
74
+ return s3Resources.map((s3) => ({
75
+ name: s3.resourceName,
76
+ ...(asString(s3.config.bucketName) && {
77
+ bucketName: asString(s3.config.bucketName),
78
+ }),
79
+ ...(asBoolean(s3.config.publicReadAccess) === true && {
80
+ publicReadAccess: true,
81
+ }),
82
+ ...(isParsedObject(s3.config.websiteHosting) &&
83
+ (() => {
84
+ const hosting = s3.config.websiteHosting;
85
+ return {
86
+ websiteHosting: {
87
+ indexDocument: asString(hosting.indexDocument) ?? "index.html",
88
+ ...(asString(hosting.errorDocument) && {
89
+ errorDocument: asString(hosting.errorDocument),
90
+ }),
91
+ },
92
+ };
93
+ })()),
94
+ ...(asStringUnion(s3.config.backupVaultTier, BACKUP_VAULT_TIERS) && {
95
+ backupVaultTier: asStringUnion(s3.config.backupVaultTier, BACKUP_VAULT_TIERS),
96
+ }),
97
+ ...(asBoolean(s3.config.versioned) !== undefined && {
98
+ versioned: asBoolean(s3.config.versioned),
99
+ }),
100
+ ...(asStringUnion(s3.config.encryption, S3_ENCRYPTION_TYPES) && {
101
+ encryption: asStringUnion(s3.config.encryption, S3_ENCRYPTION_TYPES),
102
+ }),
103
+ ...(asString(s3.config.kmsKeyArn) && {
104
+ kmsKeyArn: asString(s3.config.kmsKeyArn),
105
+ }),
106
+ ...(Array.isArray(s3.config.cors) &&
107
+ s3.config.cors.length > 0 && {
108
+ cors: s3.config.cors.filter(isParsedObject).map((rule) => ({
109
+ allowedOrigins: Array.isArray(rule.allowedOrigins)
110
+ ? rule.allowedOrigins.filter((o) => typeof o === "string")
111
+ : [],
112
+ allowedMethods: Array.isArray(rule.allowedMethods)
113
+ ? rule.allowedMethods.filter((m) => typeof m === "string")
114
+ : [],
115
+ })),
116
+ }),
117
+ ...(isParsedObject(s3.config.deployment) &&
118
+ (() => {
119
+ const deployment = s3.config.deployment;
120
+ return {
121
+ deployment: {
122
+ source: asString(deployment.source) ?? "",
123
+ ...(asBoolean(deployment.prune) !== undefined && {
124
+ prune: asBoolean(deployment.prune),
125
+ }),
126
+ ...(isParsedObject(deployment.cacheControl) &&
127
+ (() => {
128
+ const cache = deployment.cacheControl;
129
+ return {
130
+ cacheControl: {
131
+ ...(asNumber(cache.maxAge) !== undefined && {
132
+ maxAge: asNumber(cache.maxAge),
133
+ }),
134
+ ...(asBoolean(cache.immutable) !== undefined && {
135
+ immutable: asBoolean(cache.immutable),
136
+ }),
137
+ },
138
+ };
139
+ })()),
140
+ },
141
+ };
142
+ })()),
143
+ ...(asStringUnion(s3.config.stackPlacement, S3_STACK_PLACEMENTS) && {
144
+ stackPlacement: asStringUnion(s3.config.stackPlacement, S3_STACK_PLACEMENTS),
145
+ }),
146
+ ...(s3.variableName && { variableName: s3.variableName }),
147
+ ...(() => {
148
+ const S3_KNOWN_KEYS = new Set([
149
+ "bucketName",
150
+ "publicReadAccess",
151
+ "websiteHosting",
152
+ "backupVaultTier",
153
+ "versioned",
154
+ "encryption",
155
+ "kmsKeyArn",
156
+ "cors",
157
+ "deployment",
158
+ "stackPlacement",
159
+ ]);
160
+ const extras = captureExtraProperties(s3.config, S3_KNOWN_KEYS);
161
+ return extras.length > 0 ? { extraProperties: extras } : {};
162
+ })(),
163
+ }));
164
+ }
@@ -1,2 +1,3 @@
1
+ export { type IdentifierRef, type ExpressionRef, type CallRef, type ParsedValue, type ParsedObject, isPlainObject, isIdentifierRef, isExpressionRef, isCallRef, isParsedObject, asString, asNumber, asBoolean, asStringArray, asStringUnion, captureExtraProperties, } from "./astCommonParser.js";
1
2
  export { type ParsedInfrastructure, type ParsedDatabaseResource, type ParsedS3Resource, type ParsedComputeResource, type ParsedSQSResource, type ParsedCDNResource, type ParsedNetworkResource, type ParsedDynamoDBResource, type ParsedLambdaConfig, type ParsedPatternResource, type ImportInfo, type StatementType, type ClassifiedStatement, type CustomCodeBlock, parseInfrastructure, convertToResourcePlan, classifyStatements, extractCustomCodeBlocks, findManagedResourcePosition, getLastManagedStatementOfType, getManagedResourcesByType, } from "./astInfrastructureParser.js";
2
3
  export { type SurgicalModificationResult, type InsertionOptions, type UpdateOptions, type DeleteOptions, type FileValidationResult, addResourceSurgically, updateResourceSurgically, removeResourceSurgically, ensureImports, injectCustomCodeBlocks, validateModifiedFile, } from "./astSurgicalModification.js";
@@ -1,2 +1,6 @@
1
+ // Common AST parsing utilities
2
+ export { isPlainObject, isIdentifierRef, isExpressionRef, isCallRef, isParsedObject, asString, asNumber, asBoolean, asStringArray, asStringUnion, captureExtraProperties, } from "./astCommonParser.js";
3
+ // Main orchestration and types
1
4
  export { parseInfrastructure, convertToResourcePlan, classifyStatements, extractCustomCodeBlocks, findManagedResourcePosition, getLastManagedStatementOfType, getManagedResourcesByType, } from "./astInfrastructureParser.js";
5
+ // Surgical modification
2
6
  export { addResourceSurgically, updateResourceSurgically, removeResourceSurgically, ensureImports, injectCustomCodeBlocks, validateModifiedFile, } from "./astSurgicalModification.js";
@@ -0,0 +1,13 @@
1
+ import { type Result } from "../types/Result.js";
2
+ import type { ParsedZoneFile } from "./types.js";
3
+ export declare function parseZoneFile(content: string): Result<ParsedZoneFile, Error>;
4
+ /**
5
+ * Resolve an ALIAS value (e.g. "fjall:ecs:my-app") to a target DNS name
6
+ * using a map of app outputs. Returns undefined if the alias cannot be resolved.
7
+ *
8
+ * Alias format: fjall:<type>:<name>
9
+ * - fjall:ecs:<name> — resolves to ALB DNS name
10
+ * - fjall:cdn:<name> — resolves to CloudFront domain name
11
+ * - fjall:s3:<name> — resolves to S3 website endpoint
12
+ */
13
+ export declare function resolveAlias(alias: string, appOutputs: Map<string, string>): string | undefined;
@@ -0,0 +1,224 @@
1
+ import { success, failure } from "../types/Result.js";
2
+ import { DnsRecordTypeSchema, BIND_DIRECTIVES, ALIAS_PREFIX } from "./types.js";
3
+ const RECORD_TYPES = new Set(DnsRecordTypeSchema.options);
4
+ function isRecordType(token) {
5
+ return RECORD_TYPES.has(token.toUpperCase());
6
+ }
7
+ function stripTrailingDot(value) {
8
+ return value.endsWith(".") ? value.slice(0, -1) : value;
9
+ }
10
+ function stripInlineComment(line) {
11
+ let inQuote = false;
12
+ for (let i = 0; i < line.length; i++) {
13
+ if (line[i] === '"') {
14
+ inQuote = !inQuote;
15
+ }
16
+ else if (line[i] === ";" && !inQuote) {
17
+ return line.slice(0, i).trimEnd();
18
+ }
19
+ }
20
+ return line;
21
+ }
22
+ function parseTxtValue(tokens, startIndex) {
23
+ const raw = tokens.slice(startIndex).join(" ");
24
+ const parts = [];
25
+ let current = "";
26
+ let inQuote = false;
27
+ for (const ch of raw) {
28
+ if (ch === '"') {
29
+ if (inQuote) {
30
+ parts.push(current);
31
+ current = "";
32
+ }
33
+ inQuote = !inQuote;
34
+ }
35
+ else if (inQuote) {
36
+ current += ch;
37
+ }
38
+ }
39
+ return parts.join(" ");
40
+ }
41
+ function parseDelegateDirective(line) {
42
+ const auto = /;\s*auto\s*$/.test(line);
43
+ const cleaned = line.replace(/;\s*auto\s*$/, "").trim();
44
+ const match = cleaned.match(/^\$FJALL-DELEGATE\s+(\S+)\s+TO\s+(\S+)$/i);
45
+ if (!match)
46
+ return undefined;
47
+ return { subdomain: match[1], targetAccount: match[2], auto };
48
+ }
49
+ function parseCertDirective(tokens) {
50
+ if (tokens.length < 2)
51
+ return undefined;
52
+ const domainName = tokens[1];
53
+ const sans = tokens.slice(2);
54
+ return { domainName, subjectAlternativeNames: sans };
55
+ }
56
+ function tokenise(line) {
57
+ const tokens = [];
58
+ let current = "";
59
+ let inQuote = false;
60
+ for (const ch of line) {
61
+ if (ch === '"') {
62
+ inQuote = !inQuote;
63
+ current += ch;
64
+ }
65
+ else if (/\s/.test(ch) && !inQuote) {
66
+ if (current.length > 0) {
67
+ tokens.push(current);
68
+ current = "";
69
+ }
70
+ }
71
+ else {
72
+ current += ch;
73
+ }
74
+ }
75
+ if (current.length > 0)
76
+ tokens.push(current);
77
+ return tokens;
78
+ }
79
+ function parseRecord(tokens) {
80
+ if (tokens.length < 2)
81
+ return undefined;
82
+ let idx = 0;
83
+ const name = tokens[idx++];
84
+ let ttl;
85
+ let type;
86
+ // Next token could be TTL, class (IN), or record type
87
+ while (idx < tokens.length && type === undefined) {
88
+ const upper = tokens[idx].toUpperCase();
89
+ if (upper === "IN") {
90
+ idx++;
91
+ }
92
+ else if (isRecordType(upper)) {
93
+ type = upper;
94
+ idx++;
95
+ }
96
+ else if (/^\d+$/.test(tokens[idx])) {
97
+ ttl = parseInt(tokens[idx], 10);
98
+ idx++;
99
+ }
100
+ else {
101
+ return undefined;
102
+ }
103
+ }
104
+ if (!type)
105
+ return undefined;
106
+ const remaining = tokens.slice(idx);
107
+ switch (type) {
108
+ case "MX": {
109
+ if (remaining.length < 2)
110
+ return undefined;
111
+ const priority = parseInt(remaining[0], 10);
112
+ const value = remaining.slice(1).join(" ");
113
+ return { name, type, ttl, value, priority };
114
+ }
115
+ case "SRV": {
116
+ if (remaining.length < 4)
117
+ return undefined;
118
+ const priority = parseInt(remaining[0], 10);
119
+ const weight = parseInt(remaining[1], 10);
120
+ const port = parseInt(remaining[2], 10);
121
+ const value = remaining[3];
122
+ return { name, type, ttl, value, priority, weight, port };
123
+ }
124
+ case "TXT": {
125
+ const value = parseTxtValue(tokens, idx);
126
+ return { name, type, ttl, value };
127
+ }
128
+ case "CAA": {
129
+ const value = remaining.join(" ");
130
+ return { name, type, ttl, value };
131
+ }
132
+ case "A": {
133
+ if (remaining.length === 0)
134
+ return undefined;
135
+ const firstToken = remaining[0];
136
+ if (firstToken.toUpperCase() === "ALIAS") {
137
+ if (remaining.length < 2)
138
+ return undefined;
139
+ const value = remaining.slice(1).join(" ");
140
+ return { name, type, ttl, value: `${ALIAS_PREFIX}${value}` };
141
+ }
142
+ return { name, type, ttl, value: remaining[0] };
143
+ }
144
+ default: {
145
+ if (remaining.length === 0)
146
+ return undefined;
147
+ return { name, type, ttl, value: remaining[0] };
148
+ }
149
+ }
150
+ }
151
+ export function parseZoneFile(content) {
152
+ const lines = content.split("\n");
153
+ let origin;
154
+ let ttl = 300;
155
+ const records = [];
156
+ const delegations = [];
157
+ const certificates = [];
158
+ for (const rawLine of lines) {
159
+ const trimmed = rawLine.trim();
160
+ if (trimmed === "" || trimmed.startsWith(";"))
161
+ continue;
162
+ if (trimmed.toUpperCase().startsWith(BIND_DIRECTIVES.FJALL_DELEGATE)) {
163
+ const directive = parseDelegateDirective(trimmed);
164
+ if (directive) {
165
+ delegations.push(directive);
166
+ }
167
+ continue;
168
+ }
169
+ const stripped = stripInlineComment(trimmed);
170
+ if (stripped === "")
171
+ continue;
172
+ const tokens = tokenise(stripped);
173
+ if (tokens.length === 0)
174
+ continue;
175
+ const directive = tokens[0].toUpperCase();
176
+ if (directive === BIND_DIRECTIVES.ORIGIN) {
177
+ if (tokens.length < 2)
178
+ continue;
179
+ origin = stripTrailingDot(tokens[1]);
180
+ continue;
181
+ }
182
+ if (directive === BIND_DIRECTIVES.TTL) {
183
+ if (tokens.length < 2)
184
+ continue;
185
+ ttl = parseInt(tokens[1], 10);
186
+ continue;
187
+ }
188
+ if (directive === BIND_DIRECTIVES.FJALL_CERT) {
189
+ const cert = parseCertDirective(tokens);
190
+ if (cert) {
191
+ certificates.push(cert);
192
+ }
193
+ continue;
194
+ }
195
+ const record = parseRecord(tokens);
196
+ if (record) {
197
+ records.push(record);
198
+ }
199
+ }
200
+ if (!origin) {
201
+ return failure(new Error("Missing $ORIGIN directive"));
202
+ }
203
+ return success({ origin, ttl, records, delegations, certificates });
204
+ }
205
+ /**
206
+ * Resolve an ALIAS value (e.g. "fjall:ecs:my-app") to a target DNS name
207
+ * using a map of app outputs. Returns undefined if the alias cannot be resolved.
208
+ *
209
+ * Alias format: fjall:<type>:<name>
210
+ * - fjall:ecs:<name> — resolves to ALB DNS name
211
+ * - fjall:cdn:<name> — resolves to CloudFront domain name
212
+ * - fjall:s3:<name> — resolves to S3 website endpoint
213
+ */
214
+ export function resolveAlias(alias, appOutputs) {
215
+ if (!alias.startsWith("fjall:"))
216
+ return undefined;
217
+ const parts = alias.split(":");
218
+ if (parts.length < 3)
219
+ return undefined;
220
+ const resourceType = parts[1];
221
+ const resourceName = parts[2];
222
+ const lookupKey = `${resourceType}:${resourceName}`;
223
+ return appOutputs.get(lookupKey) ?? appOutputs.get(alias);
224
+ }
@@ -0,0 +1,2 @@
1
+ import type { ParsedZoneFile } from "./types.js";
2
+ export declare function generateZoneFile(zone: ParsedZoneFile): string;
@@ -0,0 +1,52 @@
1
+ import { BIND_DIRECTIVES, ALIAS_PREFIX } from "./types.js";
2
+ function padRight(value, width) {
3
+ return value.length >= width
4
+ ? value
5
+ : value + " ".repeat(width - value.length);
6
+ }
7
+ function formatRecord(record, defaultTtl) {
8
+ const nameCol = padRight(record.name, 24);
9
+ const hasTtl = record.ttl !== undefined && record.ttl !== defaultTtl;
10
+ const ttlStr = hasTtl ? `${record.ttl} ` : "";
11
+ switch (record.type) {
12
+ case "MX":
13
+ return `${nameCol} ${ttlStr}IN MX ${record.priority ?? 10} ${record.value}`;
14
+ case "SRV":
15
+ return `${nameCol} ${ttlStr}IN SRV ${record.priority ?? 0} ${record.weight ?? 0} ${record.port ?? 0} ${record.value}`;
16
+ case "TXT":
17
+ return `${nameCol} ${ttlStr}IN TXT "${record.value}"`;
18
+ case "CAA":
19
+ return `${nameCol} ${ttlStr}IN CAA ${record.value}`;
20
+ default: {
21
+ if (record.type === "A" && record.value.startsWith(ALIAS_PREFIX)) {
22
+ return `${nameCol} ${ttlStr}IN A ${record.value}`;
23
+ }
24
+ return `${nameCol} ${ttlStr}IN ${record.type} ${record.value}`;
25
+ }
26
+ }
27
+ }
28
+ export function generateZoneFile(zone) {
29
+ const lines = [];
30
+ lines.push("; zone.bind — managed by fjall");
31
+ lines.push(`${BIND_DIRECTIVES.ORIGIN} ${zone.origin}.`);
32
+ lines.push(`${BIND_DIRECTIVES.TTL} ${zone.ttl}`);
33
+ lines.push("");
34
+ if (zone.certificates.length > 0) {
35
+ for (const cert of zone.certificates) {
36
+ const parts = [cert.domainName, ...cert.subjectAlternativeNames];
37
+ lines.push(`${BIND_DIRECTIVES.FJALL_CERT} ${parts.join(" ")}`);
38
+ }
39
+ lines.push("");
40
+ }
41
+ if (zone.delegations.length > 0) {
42
+ for (const delegation of zone.delegations) {
43
+ const suffix = delegation.auto ? " ; auto" : "";
44
+ lines.push(`${BIND_DIRECTIVES.FJALL_DELEGATE} ${delegation.subdomain} TO ${delegation.targetAccount}${suffix}`);
45
+ }
46
+ lines.push("");
47
+ }
48
+ for (const record of zone.records) {
49
+ lines.push(formatRecord(record, zone.ttl));
50
+ }
51
+ return lines.join("\n") + "\n";
52
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./types.js";
2
+ export { parseZoneFile, resolveAlias } from "./bindParser.js";
3
+ export { generateZoneFile } from "./bindWriter.js";
4
+ export { generateInfrastructureFromZone } from "./infrastructureWriter.js";
@@ -0,0 +1,4 @@
1
+ export * from "./types.js";
2
+ export { parseZoneFile, resolveAlias } from "./bindParser.js";
3
+ export { generateZoneFile } from "./bindWriter.js";
4
+ export { generateInfrastructureFromZone } from "./infrastructureWriter.js";
@@ -0,0 +1,2 @@
1
+ import { type ParsedZoneFile } from "./types.js";
2
+ export declare function generateInfrastructureFromZone(domainName: string, zone: ParsedZoneFile): string;
@@ -0,0 +1,58 @@
1
+ import { toPascalCase } from "../generation/common.js";
2
+ import { ALIAS_PREFIX, } from "./types.js";
3
+ function escapeString(value) {
4
+ return value
5
+ .replace(/\\/g, "\\\\")
6
+ .replace(/"/g, '\\"')
7
+ .replace(/\$/g, "\\$")
8
+ .replace(/\n/g, "\\n")
9
+ .replace(/\r/g, "\\r");
10
+ }
11
+ export function generateInfrastructureFromZone(domainName, zone) {
12
+ const safeZoneName = toPascalCase(domainName.split(".").join(""));
13
+ const records = zone.records
14
+ .filter((r) => !r.value.startsWith(ALIAS_PREFIX))
15
+ .map((r) => {
16
+ const parts = [
17
+ ` { type: "${escapeString(r.type)}", name: "${escapeString(r.name)}", value: "${escapeString(r.value)}"`,
18
+ ];
19
+ if (r.priority !== undefined)
20
+ parts.push(`, priority: ${r.priority}`);
21
+ if (r.ttl !== undefined)
22
+ parts.push(`, ttl: ${r.ttl}`);
23
+ return parts.join("") + " }";
24
+ });
25
+ const aliasRecords = zone.records
26
+ .filter((r) => r.value.startsWith(ALIAS_PREFIX))
27
+ .map((r) => ` // ALIAS: ${escapeString(r.name)} → ${escapeString(r.value)} (resolved at deploy time)`);
28
+ const delegations = zone.delegations
29
+ .filter((d) => d.auto)
30
+ .map((d) => ` { subdomain: "${escapeString(d.subdomain)}", targetAccount: "${escapeString(d.targetAccount)}" }`);
31
+ const certificates = zone.certificates.map((c) => {
32
+ const sans = c.subjectAlternativeNames.length > 0
33
+ ? `, subjectAlternativeNames: [${c.subjectAlternativeNames.map((s) => `"${escapeString(s)}"`).join(", ")}]`
34
+ : "";
35
+ return ` { domainName: "${escapeString(c.domainName)}"${sans} }`;
36
+ });
37
+ return `import {
38
+ App,
39
+ DomainFactory,
40
+ } from "@fjall/components-infrastructure";
41
+
42
+ const app = App.getApp("${safeZoneName}Domain");
43
+
44
+ DomainFactory.build("${safeZoneName}", {
45
+ type: "domain",
46
+ zoneName: "${escapeString(domainName)}",
47
+ records: [
48
+ ${records.join(",\n")}
49
+ ],
50
+ delegations: [
51
+ ${delegations.join(",\n")}
52
+ ],
53
+ certificates: [
54
+ ${certificates.join(",\n")}
55
+ ],
56
+ });
57
+ ${aliasRecords.length > 0 ? "\n" + aliasRecords.join("\n") + "\n" : ""}`;
58
+ }
@@ -0,0 +1,82 @@
1
+ import { z } from "zod";
2
+ export { DNS_APEX, getDomainExportNames, type ManagedDomainExports, } from "@fjall/util";
3
+ export declare const DnsRecordTypeSchema: z.ZodEnum<{
4
+ A: "A";
5
+ AAAA: "AAAA";
6
+ CNAME: "CNAME";
7
+ MX: "MX";
8
+ TXT: "TXT";
9
+ NS: "NS";
10
+ SRV: "SRV";
11
+ CAA: "CAA";
12
+ }>;
13
+ export type DnsRecordType = z.infer<typeof DnsRecordTypeSchema>;
14
+ export declare const DnsRecordSchema: z.ZodObject<{
15
+ name: z.ZodString;
16
+ type: z.ZodEnum<{
17
+ A: "A";
18
+ AAAA: "AAAA";
19
+ CNAME: "CNAME";
20
+ MX: "MX";
21
+ TXT: "TXT";
22
+ NS: "NS";
23
+ SRV: "SRV";
24
+ CAA: "CAA";
25
+ }>;
26
+ ttl: z.ZodOptional<z.ZodNumber>;
27
+ value: z.ZodString;
28
+ priority: z.ZodOptional<z.ZodNumber>;
29
+ weight: z.ZodOptional<z.ZodNumber>;
30
+ port: z.ZodOptional<z.ZodNumber>;
31
+ }, z.core.$strict>;
32
+ export declare const DelegationDirectiveSchema: z.ZodObject<{
33
+ subdomain: z.ZodString;
34
+ targetAccount: z.ZodString;
35
+ auto: z.ZodBoolean;
36
+ }, z.core.$strict>;
37
+ export declare const CertificateDirectiveSchema: z.ZodObject<{
38
+ domainName: z.ZodString;
39
+ subjectAlternativeNames: z.ZodArray<z.ZodString>;
40
+ }, z.core.$strict>;
41
+ export declare const ParsedZoneFileSchema: z.ZodObject<{
42
+ origin: z.ZodString;
43
+ ttl: z.ZodNumber;
44
+ records: z.ZodArray<z.ZodObject<{
45
+ name: z.ZodString;
46
+ type: z.ZodEnum<{
47
+ A: "A";
48
+ AAAA: "AAAA";
49
+ CNAME: "CNAME";
50
+ MX: "MX";
51
+ TXT: "TXT";
52
+ NS: "NS";
53
+ SRV: "SRV";
54
+ CAA: "CAA";
55
+ }>;
56
+ ttl: z.ZodOptional<z.ZodNumber>;
57
+ value: z.ZodString;
58
+ priority: z.ZodOptional<z.ZodNumber>;
59
+ weight: z.ZodOptional<z.ZodNumber>;
60
+ port: z.ZodOptional<z.ZodNumber>;
61
+ }, z.core.$strict>>;
62
+ delegations: z.ZodArray<z.ZodObject<{
63
+ subdomain: z.ZodString;
64
+ targetAccount: z.ZodString;
65
+ auto: z.ZodBoolean;
66
+ }, z.core.$strict>>;
67
+ certificates: z.ZodArray<z.ZodObject<{
68
+ domainName: z.ZodString;
69
+ subjectAlternativeNames: z.ZodArray<z.ZodString>;
70
+ }, z.core.$strict>>;
71
+ }, z.core.$strict>;
72
+ export type ParsedZoneFile = z.infer<typeof ParsedZoneFileSchema>;
73
+ export type DnsRecord = z.infer<typeof DnsRecordSchema>;
74
+ export type DelegationDirective = z.infer<typeof DelegationDirectiveSchema>;
75
+ export type CertificateDirective = z.infer<typeof CertificateDirectiveSchema>;
76
+ export declare const BIND_DIRECTIVES: Readonly<{
77
+ readonly ORIGIN: "$ORIGIN";
78
+ readonly TTL: "$TTL";
79
+ readonly FJALL_DELEGATE: "$FJALL-DELEGATE";
80
+ readonly FJALL_CERT: "$FJALL-CERT";
81
+ }>;
82
+ export declare const ALIAS_PREFIX: "ALIAS ";
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ export { DNS_APEX, getDomainExportNames, } from "@fjall/util";
3
+ export const DnsRecordTypeSchema = z.enum([
4
+ "A",
5
+ "AAAA",
6
+ "CNAME",
7
+ "MX",
8
+ "TXT",
9
+ "NS",
10
+ "SRV",
11
+ "CAA",
12
+ ]);
13
+ export const DnsRecordSchema = z
14
+ .object({
15
+ name: z.string(),
16
+ type: DnsRecordTypeSchema,
17
+ ttl: z.number().optional(),
18
+ value: z.string(),
19
+ priority: z.number().optional(),
20
+ weight: z.number().optional(),
21
+ port: z.number().optional(),
22
+ })
23
+ .strict();
24
+ export const DelegationDirectiveSchema = z
25
+ .object({
26
+ subdomain: z.string(),
27
+ targetAccount: z.string(),
28
+ auto: z.boolean(),
29
+ })
30
+ .strict();
31
+ export const CertificateDirectiveSchema = z
32
+ .object({
33
+ domainName: z.string(),
34
+ subjectAlternativeNames: z.array(z.string()),
35
+ })
36
+ .strict();
37
+ export const ParsedZoneFileSchema = z
38
+ .object({
39
+ origin: z.string(),
40
+ ttl: z.number(),
41
+ records: z.array(DnsRecordSchema),
42
+ delegations: z.array(DelegationDirectiveSchema),
43
+ certificates: z.array(CertificateDirectiveSchema),
44
+ })
45
+ .strict();
46
+ export const BIND_DIRECTIVES = Object.freeze({
47
+ ORIGIN: "$ORIGIN",
48
+ TTL: "$TTL",
49
+ FJALL_DELEGATE: "$FJALL-DELEGATE",
50
+ FJALL_CERT: "$FJALL-CERT",
51
+ });
52
+ export const ALIAS_PREFIX = "ALIAS ";