@friggframework/devtools 2.0.0--canary.461.84ff4f5.0 → 2.0.0--canary.461.ec909cf.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.
@@ -231,7 +231,26 @@ const createVPCInfrastructure = (AppDefinition) => {
231
231
  Tags: [
232
232
  {
233
233
  Key: 'Name',
234
- Value: '${self:service}-${self:provider.stage}-public-subnet',
234
+ Value: '${self:service}-${self:provider.stage}-public-subnet-1',
235
+ },
236
+ { Key: 'ManagedBy', Value: 'Frigg' },
237
+ { Key: 'Service', Value: '${self:service}' },
238
+ { Key: 'Stage', Value: '${self:provider.stage}' },
239
+ { Key: 'Type', Value: 'Public' },
240
+ ],
241
+ },
242
+ },
243
+ FriggPublicSubnet2: {
244
+ Type: 'AWS::EC2::Subnet',
245
+ Properties: {
246
+ VpcId: { Ref: 'FriggVPC' },
247
+ CidrBlock: '10.0.4.0/24',
248
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
249
+ MapPublicIpOnLaunch: true,
250
+ Tags: [
251
+ {
252
+ Key: 'Name',
253
+ Value: '${self:service}-${self:provider.stage}-public-subnet-2',
235
254
  },
236
255
  { Key: 'ManagedBy', Value: 'Frigg' },
237
256
  { Key: 'Service', Value: '${self:service}' },
@@ -342,6 +361,13 @@ const createVPCInfrastructure = (AppDefinition) => {
342
361
  RouteTableId: { Ref: 'FriggPublicRouteTable' },
343
362
  },
344
363
  },
364
+ FriggPublicSubnet2RouteTableAssociation: {
365
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
366
+ Properties: {
367
+ SubnetId: { Ref: 'FriggPublicSubnet2' },
368
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
369
+ },
370
+ },
345
371
  FriggPrivateRouteTable: {
346
372
  Type: 'AWS::EC2::RouteTable',
347
373
  Properties: {
@@ -601,7 +627,9 @@ const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
601
627
  defaultSecurityGroupId: 'AWS_DISCOVERY_SECURITY_GROUP_ID',
602
628
  privateSubnetId1: 'AWS_DISCOVERY_SUBNET_ID_1',
603
629
  privateSubnetId2: 'AWS_DISCOVERY_SUBNET_ID_2',
604
- publicSubnetId: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID',
630
+ publicSubnetId: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID', // Keep for backward compat
631
+ publicSubnetId1: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID_1',
632
+ publicSubnetId2: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID_2',
605
633
  defaultRouteTableId: 'AWS_DISCOVERY_ROUTE_TABLE_ID',
606
634
  defaultKmsKeyId: 'AWS_DISCOVERY_KMS_KEY_ID',
607
635
  };
@@ -651,6 +679,10 @@ const createBaseDefinition = (
651
679
  'node_modules/.prisma/**',
652
680
  'node_modules/prisma/**',
653
681
  'node_modules/@friggframework/core/generated/**',
682
+
683
+ // Exclude nested node_modules from symlinked frigg packages (for npm link development)
684
+ 'node_modules/@friggframework/core/node_modules/**',
685
+ 'node_modules/@friggframework/devtools/node_modules/**',
654
686
  ],
655
687
  };
656
688
 
@@ -1334,7 +1366,34 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1334
1366
  Tags: [
1335
1367
  {
1336
1368
  Key: 'Name',
1337
- Value: '${self:service}-${self:provider.stage}-public',
1369
+ Value: '${self:service}-${self:provider.stage}-public-1',
1370
+ },
1371
+ { Key: 'Type', Value: 'Public' },
1372
+ { Key: 'ManagedBy', Value: 'Frigg' },
1373
+ ],
1374
+ },
1375
+ };
1376
+
1377
+ // Create second public subnet in different AZ for Aurora
1378
+ let publicSubnet2Cidr;
1379
+ if (vpcManagement === 'create-new') {
1380
+ const generatedCidrs = { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] };
1381
+ publicSubnet2Cidr = { 'Fn::Select': [3, generatedCidrs] };
1382
+ } else {
1383
+ publicSubnet2Cidr = '172.31.251.0/24';
1384
+ }
1385
+
1386
+ definition.resources.Resources.FriggPublicSubnet2 = {
1387
+ Type: 'AWS::EC2::Subnet',
1388
+ Properties: {
1389
+ VpcId: subnetVpcId,
1390
+ CidrBlock: publicSubnet2Cidr,
1391
+ MapPublicIpOnLaunch: true,
1392
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
1393
+ Tags: [
1394
+ {
1395
+ Key: 'Name',
1396
+ Value: '${self:service}-${self:provider.stage}-public-2',
1338
1397
  },
1339
1398
  { Key: 'Type', Value: 'Public' },
1340
1399
  { Key: 'ManagedBy', Value: 'Frigg' },
@@ -1347,6 +1406,12 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1347
1406
  { Ref: 'FriggPrivateSubnet2' },
1348
1407
  ];
1349
1408
 
1409
+ // Map created subnets to discoveredResources for Aurora to use
1410
+ discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
1411
+ discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
1412
+ discoveredResources.privateSubnetId1 = { Ref: 'FriggPrivateSubnet1' };
1413
+ discoveredResources.privateSubnetId2 = { Ref: 'FriggPrivateSubnet2' };
1414
+
1350
1415
  if (
1351
1416
  !AppDefinition.vpc.natGateway ||
1352
1417
  AppDefinition.vpc.natGateway.management === 'discover'
@@ -1417,6 +1482,15 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1417
1482
  },
1418
1483
  };
1419
1484
 
1485
+ definition.resources.Resources.FriggPublicSubnet2RouteTableAssociation =
1486
+ {
1487
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1488
+ Properties: {
1489
+ SubnetId: { Ref: 'FriggPublicSubnet2' },
1490
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1491
+ },
1492
+ };
1493
+
1420
1494
  definition.resources.Resources.FriggLambdaRouteTable = {
1421
1495
  Type: 'AWS::EC2::RouteTable',
1422
1496
  Properties: {
@@ -1640,7 +1714,28 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1640
1714
  Tags: [
1641
1715
  {
1642
1716
  Key: 'Name',
1643
- Value: '${self:service}-${self:provider.stage}-public-subnet',
1717
+ Value: '${self:service}-${self:provider.stage}-public-subnet-1',
1718
+ },
1719
+ { Key: 'Type', Value: 'Public' },
1720
+ ],
1721
+ },
1722
+ };
1723
+
1724
+ definition.resources.Resources.FriggPublicSubnet2 = {
1725
+ Type: 'AWS::EC2::Subnet',
1726
+ Properties: {
1727
+ VpcId: discoveredResources.defaultVpcId,
1728
+ CidrBlock:
1729
+ AppDefinition.vpc.natGateway
1730
+ ?.publicSubnetCidr2 || '172.31.251.0/24',
1731
+ AvailabilityZone: {
1732
+ 'Fn::Select': [1, { 'Fn::GetAZs': '' }],
1733
+ },
1734
+ MapPublicIpOnLaunch: true,
1735
+ Tags: [
1736
+ {
1737
+ Key: 'Name',
1738
+ Value: '${self:service}-${self:provider.stage}-public-subnet-2',
1644
1739
  },
1645
1740
  { Key: 'Type', Value: 'Public' },
1646
1741
  ],
@@ -1683,6 +1778,19 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1683
1778
  RouteTableId: { Ref: 'FriggPublicRouteTable' },
1684
1779
  },
1685
1780
  };
1781
+
1782
+ definition.resources.Resources.FriggPublicSubnet2RouteTableAssociation =
1783
+ {
1784
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1785
+ Properties: {
1786
+ SubnetId: { Ref: 'FriggPublicSubnet2' },
1787
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1788
+ },
1789
+ };
1790
+
1791
+ // Map created public subnets to discoveredResources for Aurora
1792
+ discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
1793
+ discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
1686
1794
  }
1687
1795
 
1688
1796
  definition.resources.Resources.FriggNATGateway = {
@@ -2032,18 +2140,43 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
2032
2140
 
2033
2141
  const createAuroraInfrastructure = (definition, AppDefinition, discoveredResources) => {
2034
2142
  const dbConfig = AppDefinition.database.postgres;
2143
+ const publiclyAccessible = dbConfig.publiclyAccessible === true;
2035
2144
 
2036
2145
  console.log('šŸ”§ Creating Aurora Serverless v2 infrastructure...');
2146
+ console.log(` Publicly Accessible: ${publiclyAccessible}`);
2147
+
2148
+ // 1. DB Subnet Group
2149
+ // Use public subnets if publicly accessible, private subnets otherwise
2150
+ let subnetIds;
2151
+ if (publiclyAccessible) {
2152
+ subnetIds = [discoveredResources.publicSubnetId1, discoveredResources.publicSubnetId2];
2153
+ console.log(` Using public subnets: ${subnetIds.join(', ')}`);
2154
+
2155
+ // Safety check - this should have been caught earlier, but double-check
2156
+ if (!subnetIds[0] || !subnetIds[1]) {
2157
+ throw new Error(
2158
+ 'Public subnets are required for publicly accessible Aurora deployment but were not found. ' +
2159
+ 'This should have been caught earlier in validation.'
2160
+ );
2161
+ }
2162
+ } else {
2163
+ subnetIds = [discoveredResources.privateSubnetId1, discoveredResources.privateSubnetId2];
2164
+ console.log(` Using private subnets: ${subnetIds.join(', ')}`);
2165
+
2166
+ // Safety check - this should have been caught earlier, but double-check
2167
+ if (!subnetIds[0] || !subnetIds[1]) {
2168
+ throw new Error(
2169
+ 'Private subnets are required for private Aurora deployment but were not found. ' +
2170
+ 'This should have been caught earlier in validation.'
2171
+ );
2172
+ }
2173
+ }
2037
2174
 
2038
- // 1. DB Subnet Group (using Lambda private subnets)
2039
2175
  definition.resources.Resources.FriggDBSubnetGroup = {
2040
2176
  Type: 'AWS::RDS::DBSubnetGroup',
2041
2177
  Properties: {
2042
- DBSubnetGroupDescription: 'Subnet group for Frigg Aurora cluster',
2043
- SubnetIds: [
2044
- discoveredResources.privateSubnetId1,
2045
- discoveredResources.privateSubnetId2
2046
- ],
2178
+ DBSubnetGroupDescription: `Subnet group for Frigg Aurora cluster (${publiclyAccessible ? 'public' : 'private'})`,
2179
+ SubnetIds: subnetIds,
2047
2180
  Tags: [
2048
2181
  { Key: 'Name', Value: '${self:service}-${self:provider.stage}-db-subnet-group' },
2049
2182
  { Key: 'ManagedBy', Value: 'Frigg' },
@@ -2053,27 +2186,58 @@ const createAuroraInfrastructure = (definition, AppDefinition, discoveredResourc
2053
2186
  }
2054
2187
  };
2055
2188
 
2056
- // 2. Security Group (allow Lambda SG to access 5432)
2057
- // In create-new VPC mode, Lambda uses FriggLambdaSecurityGroup
2058
- // In other modes, use discovered default security group
2059
- const lambdaSecurityGroupId = AppDefinition.vpc?.management === 'create-new'
2060
- ? { Ref: 'FriggLambdaSecurityGroup' }
2061
- : discoveredResources.defaultSecurityGroupId;
2189
+ // 2. Security Group
2190
+ // Build security group ingress rules based on configuration
2191
+ const securityGroupIngress = [];
2192
+
2193
+ // Always allow Lambda functions to access the database (if VPC is configured)
2194
+ if (AppDefinition.vpc?.enable) {
2195
+ const lambdaSecurityGroupId = AppDefinition.vpc?.management === 'create-new'
2196
+ ? { Ref: 'FriggLambdaSecurityGroup' }
2197
+ : discoveredResources.defaultSecurityGroupId;
2198
+
2199
+ securityGroupIngress.push({
2200
+ IpProtocol: 'tcp',
2201
+ FromPort: 5432,
2202
+ ToPort: 5432,
2203
+ SourceSecurityGroupId: lambdaSecurityGroupId,
2204
+ Description: 'PostgreSQL access from Lambda functions'
2205
+ });
2206
+ }
2207
+
2208
+ // Add IP whitelist rules for public access
2209
+ if (publiclyAccessible && dbConfig.allowedIpAddresses) {
2210
+ const allowedIps = Array.isArray(dbConfig.allowedIpAddresses)
2211
+ ? dbConfig.allowedIpAddresses
2212
+ : [dbConfig.allowedIpAddresses];
2213
+
2214
+ console.log(` Adding ${allowedIps.length} whitelisted IP address(es)`);
2215
+
2216
+ allowedIps.forEach((ip, index) => {
2217
+ // Ensure IP has CIDR notation
2218
+ const cidrIp = ip.includes('/') ? ip : `${ip}/32`;
2219
+ securityGroupIngress.push({
2220
+ IpProtocol: 'tcp',
2221
+ FromPort: 5432,
2222
+ ToPort: 5432,
2223
+ CidrIp: cidrIp,
2224
+ Description: `PostgreSQL access from whitelisted IP ${index + 1}`
2225
+ });
2226
+ });
2227
+ }
2228
+
2229
+ // If publicly accessible but no IPs specified, warn the user
2230
+ if (publiclyAccessible && !dbConfig.allowedIpAddresses) {
2231
+ console.log(' āš ļø WARNING: Database is publicly accessible but no IP whitelist configured!');
2232
+ console.log(' āš ļø Add allowedIpAddresses to your database.postgres config for security.');
2233
+ }
2062
2234
 
2063
2235
  definition.resources.Resources.FriggAuroraSecurityGroup = {
2064
2236
  Type: 'AWS::EC2::SecurityGroup',
2065
2237
  Properties: {
2066
- GroupDescription: 'Security group for Frigg Aurora PostgreSQL',
2238
+ GroupDescription: `Security group for Frigg Aurora PostgreSQL (${publiclyAccessible ? 'public' : 'private'})`,
2067
2239
  VpcId: discoveredResources.defaultVpcId,
2068
- SecurityGroupIngress: [
2069
- {
2070
- IpProtocol: 'tcp',
2071
- FromPort: 5432,
2072
- ToPort: 5432,
2073
- SourceSecurityGroupId: lambdaSecurityGroupId,
2074
- Description: 'PostgreSQL access from Lambda functions'
2075
- }
2076
- ],
2240
+ SecurityGroupIngress: securityGroupIngress,
2077
2241
  Tags: [
2078
2242
  { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-sg' },
2079
2243
  { Key: 'ManagedBy', Value: 'Frigg' },
@@ -2143,7 +2307,7 @@ const createAuroraInfrastructure = (definition, AppDefinition, discoveredResourc
2143
2307
  Engine: 'aurora-postgresql',
2144
2308
  DBInstanceClass: 'db.serverless',
2145
2309
  DBClusterIdentifier: { Ref: 'FriggAuroraCluster' },
2146
- PubliclyAccessible: false,
2310
+ PubliclyAccessible: publiclyAccessible,
2147
2311
  EnablePerformanceInsights: dbConfig.enablePerformanceInsights || false,
2148
2312
  Tags: [
2149
2313
  { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-instance' },
@@ -2177,12 +2341,11 @@ const createAuroraInfrastructure = (definition, AppDefinition, discoveredResourc
2177
2341
  // 8. Set DATABASE_URL environment variable
2178
2342
  definition.provider.environment.DATABASE_URL = {
2179
2343
  'Fn::Sub': [
2180
- 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2344
+ 'postgresql://${Username}:${Password}@${Endpoint}:5432/${DatabaseName}',
2181
2345
  {
2182
2346
  Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2183
2347
  Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2184
2348
  Endpoint: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint'] },
2185
- Port: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Port'] },
2186
2349
  DatabaseName: dbConfig.databaseName || 'frigg_db'
2187
2350
  }
2188
2351
  ]
@@ -2347,26 +2510,47 @@ const configurePostgres = (definition, AppDefinition, discoveredResources) => {
2347
2510
  return;
2348
2511
  }
2349
2512
 
2350
- // Validate VPC is enabled (required for Aurora deployment)
2351
- if (!AppDefinition.vpc?.enable) {
2513
+ const dbConfig = AppDefinition.database.postgres;
2514
+ const publiclyAccessible = dbConfig.publiclyAccessible === true;
2515
+
2516
+ // Validate VPC is enabled for private deployments
2517
+ // Public deployments can work without VPC if using default VPC
2518
+ if (!publiclyAccessible && !AppDefinition.vpc?.enable) {
2352
2519
  throw new Error(
2353
- 'Aurora PostgreSQL requires VPC deployment. ' +
2354
- 'Set vpc.enable to true in your app definition.'
2520
+ 'Aurora PostgreSQL requires VPC deployment for private access. ' +
2521
+ 'Either set vpc.enable to true, or set database.postgres.publiclyAccessible to true for public access.'
2355
2522
  );
2356
2523
  }
2357
2524
 
2358
- // Validate private subnets exist (Aurora requires at least 2 subnets in different AZs)
2359
- // Skip validation if VPC management is 'create-new' (subnets will be created)
2525
+ // Validate subnets based on deployment type
2360
2526
  const vpcManagement = AppDefinition.vpc?.management || 'discover';
2361
- if (vpcManagement !== 'create-new' && (!discoveredResources.privateSubnetId1 || !discoveredResources.privateSubnetId2)) {
2362
- throw new Error(
2363
- 'Aurora PostgreSQL requires at least 2 private subnets in different availability zones. ' +
2364
- 'No private subnets were discovered in your VPC. ' +
2365
- 'Please create private subnets or use VPC management mode "create-new".'
2366
- );
2527
+
2528
+ if (publiclyAccessible) {
2529
+ // For public deployments, validate public subnets exist
2530
+ if (vpcManagement !== 'create-new' && (!discoveredResources.publicSubnetId1 || !discoveredResources.publicSubnetId2)) {
2531
+ throw new Error(
2532
+ 'Aurora PostgreSQL with publiclyAccessible requires at least 2 public subnets in different availability zones. ' +
2533
+ 'No public subnets were discovered in your VPC. ' +
2534
+ 'Options:\n' +
2535
+ ' 1. Create public subnets in your VPC with Internet Gateway attached\n' +
2536
+ ' 2. Use VPC management mode "create-new" (will create public subnets automatically)\n' +
2537
+ ' 3. Set publiclyAccessible to false and use private subnets instead'
2538
+ );
2539
+ }
2540
+ } else {
2541
+ // For private deployments, validate private subnets exist
2542
+ if (vpcManagement !== 'create-new' && (!discoveredResources.privateSubnetId1 || !discoveredResources.privateSubnetId2)) {
2543
+ throw new Error(
2544
+ 'Aurora PostgreSQL requires at least 2 private subnets in different availability zones for private deployment. ' +
2545
+ 'No private subnets were discovered in your VPC. ' +
2546
+ 'Options:\n' +
2547
+ ' 1. Create private subnets in your VPC\n' +
2548
+ ' 2. Use VPC management mode "create-new" (will create private subnets automatically)\n' +
2549
+ ' 3. Set publiclyAccessible to true and use public subnets instead'
2550
+ );
2551
+ }
2367
2552
  }
2368
2553
 
2369
- const dbConfig = AppDefinition.database.postgres;
2370
2554
  const management = dbConfig.management || 'discover';
2371
2555
 
2372
2556
  console.log(`\n🐘 PostgreSQL Management Mode: ${management}`);
@@ -7,7 +7,9 @@ const createDiscoveryResponse = (overrides = {}) => ({
7
7
  defaultSecurityGroupId: 'sg-123456',
8
8
  privateSubnetId1: 'subnet-123456',
9
9
  privateSubnetId2: 'subnet-789012',
10
- publicSubnetId: 'subnet-public',
10
+ publicSubnetId: 'subnet-public', // Keep for backward compat
11
+ publicSubnetId1: 'subnet-public-1',
12
+ publicSubnetId2: 'subnet-public-2',
11
13
  defaultRouteTableId: 'rtb-123456',
12
14
  defaultKmsKeyId:
13
15
  'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.461.84ff4f5.0",
4
+ "version": "2.0.0--canary.461.ec909cf.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-ec2": "^3.835.0",
7
7
  "@aws-sdk/client-kms": "^3.835.0",
@@ -11,8 +11,8 @@
11
11
  "@babel/eslint-parser": "^7.18.9",
12
12
  "@babel/parser": "^7.25.3",
13
13
  "@babel/traverse": "^7.25.3",
14
- "@friggframework/schemas": "2.0.0--canary.461.84ff4f5.0",
15
- "@friggframework/test": "2.0.0--canary.461.84ff4f5.0",
14
+ "@friggframework/schemas": "2.0.0--canary.461.ec909cf.0",
15
+ "@friggframework/test": "2.0.0--canary.461.ec909cf.0",
16
16
  "@hapi/boom": "^10.0.1",
17
17
  "@inquirer/prompts": "^5.3.8",
18
18
  "axios": "^1.7.2",
@@ -34,8 +34,8 @@
34
34
  "serverless-http": "^2.7.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@friggframework/eslint-config": "2.0.0--canary.461.84ff4f5.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.461.84ff4f5.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.461.ec909cf.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.461.ec909cf.0",
39
39
  "aws-sdk-client-mock": "^4.1.0",
40
40
  "aws-sdk-client-mock-jest": "^4.1.0",
41
41
  "jest": "^30.1.3",
@@ -70,5 +70,5 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  },
73
- "gitHead": "84ff4f5a8ab85a143a491d4337965e534c1a73fb"
73
+ "gitHead": "ec909cf5076fa52ca3e914ee671a1c13c2cb11ee"
74
74
  }