@fjall/generator 0.88.4 → 0.89.4
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.
- package/LICENSE +21 -0
- package/dist/src/ast/astCdnParser.d.ts +15 -0
- package/dist/src/ast/astCdnParser.js +114 -0
- package/dist/src/ast/astCommonParser.d.ts +90 -0
- package/dist/src/ast/astCommonParser.js +351 -0
- package/dist/src/ast/astComputeParser.d.ts +14 -2
- package/dist/src/ast/astComputeParser.js +55 -9
- package/dist/src/ast/astDatabaseParser.d.ts +104 -0
- package/dist/src/ast/astDatabaseParser.js +275 -0
- package/dist/src/ast/astInfrastructureParser.d.ts +23 -277
- package/dist/src/ast/astInfrastructureParser.js +83 -1456
- package/dist/src/ast/astMessagingParser.d.ts +25 -0
- package/dist/src/ast/astMessagingParser.js +78 -0
- package/dist/src/ast/astNetworkParser.d.ts +70 -0
- package/dist/src/ast/astNetworkParser.js +219 -0
- package/dist/src/ast/astPatternParser.d.ts +80 -0
- package/dist/src/ast/astPatternParser.js +155 -0
- package/dist/src/ast/astStorageParser.d.ts +18 -0
- package/dist/src/ast/astStorageParser.js +164 -0
- package/dist/src/ast/index.d.ts +1 -0
- package/dist/src/ast/index.js +4 -0
- package/dist/src/dns/bindParser.d.ts +13 -0
- package/dist/src/dns/bindParser.js +224 -0
- package/dist/src/dns/bindWriter.d.ts +2 -0
- package/dist/src/dns/bindWriter.js +52 -0
- package/dist/src/dns/index.d.ts +4 -0
- package/dist/src/dns/index.js +4 -0
- package/dist/src/dns/infrastructureWriter.d.ts +2 -0
- package/dist/src/dns/infrastructureWriter.js +58 -0
- package/dist/src/dns/types.d.ts +82 -0
- package/dist/src/dns/types.js +52 -0
- package/dist/src/generation/common.d.ts +1 -16
- package/dist/src/generation/common.js +2 -28
- package/dist/src/generation/compute.js +77 -28
- package/dist/src/generation/index.d.ts +2 -1
- package/dist/src/generation/index.js +3 -1
- package/dist/src/generation/messagingConnections.d.ts +33 -0
- package/dist/src/generation/messagingConnections.js +73 -0
- package/dist/src/generation/storage.d.ts +5 -1
- package/dist/src/generation/storage.js +9 -1
- package/dist/src/generation/storageConnections.d.ts +3 -3
- package/dist/src/generation/storageConnections.js +8 -4
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/planning/resourcePlanning.js +0 -2
- package/dist/src/presets/tierTypes.d.ts +4 -1
- package/dist/src/schemas/applicationSchemas.d.ts +854 -0
- package/dist/src/schemas/applicationSchemas.js +80 -0
- package/dist/src/schemas/baseSchemas.d.ts +206 -0
- package/dist/src/schemas/baseSchemas.js +248 -0
- package/dist/src/schemas/cdnSchemas.d.ts +61 -0
- package/dist/src/schemas/cdnSchemas.js +62 -0
- package/dist/src/schemas/computeSchemas.d.ts +723 -0
- package/dist/src/schemas/computeSchemas.js +727 -0
- package/dist/src/schemas/constants.d.ts +12 -8
- package/dist/src/schemas/constants.js +14 -4
- package/dist/src/schemas/databaseSchemas.d.ts +638 -0
- package/dist/src/schemas/databaseSchemas.js +366 -0
- package/dist/src/schemas/messagingSchemas.d.ts +20 -0
- package/dist/src/schemas/messagingSchemas.js +29 -0
- package/dist/src/schemas/networkSchemas.d.ts +246 -0
- package/dist/src/schemas/networkSchemas.js +125 -0
- package/dist/src/schemas/patternSchemas.d.ts +708 -0
- package/dist/src/schemas/patternSchemas.js +294 -0
- package/dist/src/schemas/resourceSchemas.d.ts +24 -3530
- package/dist/src/schemas/resourceSchemas.js +24 -2011
- package/dist/src/schemas/storageSchemas.d.ts +93 -0
- package/dist/src/schemas/storageSchemas.js +119 -0
- package/dist/src/util/errorUtils.d.ts +1 -2
- package/dist/src/util/errorUtils.js +1 -15
- package/dist/src/validation/patterns.d.ts +9 -0
- package/dist/src/validation/patterns.js +9 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +5 -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
|
+
}
|
package/dist/src/ast/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/src/ast/index.js
CHANGED
|
@@ -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,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,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 ";
|