@appliance.sh/infra 1.12.1 → 1.14.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appliance.sh/infra",
3
- "version": "1.12.1",
3
+ "version": "1.14.0",
4
4
  "description": "Deploy the Appliance Infrastructure",
5
5
  "repository": "https://github.com/appliance-sh/appliance.sh",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@ export async function applianceInfra() {
36
36
  const applianceBase = new baseController(
37
37
  `${base}`,
38
38
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
- { config: baseConfig.data as any },
39
+ { config: baseConfig.data },
40
40
  {
41
41
  globalProvider: baseGlobalProvider,
42
42
  provider: baseRegionalProvider,
@@ -1,11 +1,10 @@
1
1
  import * as pulumi from '@pulumi/pulumi';
2
2
  import * as aws from '@pulumi/aws';
3
- import * as awsNative from '@pulumi/aws-native';
4
3
 
5
- import { ApplianceBaseAwsPublicInput } from '@appliance.sh/sdk';
4
+ import { ApplianceBaseConfigInput, ApplianceBaseType } from '@appliance.sh/sdk';
6
5
 
7
6
  export type ApplianceBaseAwsPublicArgs = {
8
- config: ApplianceBaseAwsPublicInput;
7
+ config: ApplianceBaseConfigInput;
9
8
  };
10
9
 
11
10
  export interface ApplianceBaseAwsPublicOpts extends pulumi.ComponentResourceOptions {
@@ -21,9 +20,15 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
21
20
  public readonly certificateArn?: pulumi.Output<string>;
22
21
  public readonly cloudfrontDistribution?: aws.cloudfront.Distribution;
23
22
 
23
+ public readonly config;
24
+
24
25
  constructor(name: string, args: ApplianceBaseAwsPublicArgs, opts?: ApplianceBaseAwsPublicOpts) {
25
26
  super('appliance-infra:appliance-base-aws-public', name, args, opts);
26
27
 
28
+ if (args.config.type !== ApplianceBaseType.ApplianceAwsPublic) {
29
+ throw new Error('Invalid config');
30
+ }
31
+
27
32
  if (args.config.dns.createZone) {
28
33
  this.zone = new aws.route53.Zone(
29
34
  `${name}-zone`,
@@ -91,6 +96,33 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
91
96
  ).arn;
92
97
  }
93
98
 
99
+ const state = new aws.s3.Bucket(
100
+ `${name}-state`,
101
+ {
102
+ acl: 'private',
103
+ forceDestroy: true,
104
+ },
105
+ { parent: this, provider: opts?.provider }
106
+ );
107
+
108
+ new aws.s3.BucketVersioning(
109
+ `${name}-state-versioning`,
110
+ {
111
+ bucket: state.bucket,
112
+ versioningConfiguration: { status: 'Enabled' },
113
+ },
114
+ { parent: this, provider: opts?.provider }
115
+ );
116
+
117
+ new aws.s3.BucketServerSideEncryptionConfiguration(
118
+ `${name}-state-sse`,
119
+ {
120
+ bucket: state.bucket,
121
+ rules: [{ applyServerSideEncryptionByDefault: { sseAlgorithm: 'AES256' } }],
122
+ },
123
+ { parent: this, provider: opts?.provider }
124
+ );
125
+
94
126
  const lambdaOrigin = new aws.lambda.CallbackFunction(
95
127
  `${name}-origin`,
96
128
  {
@@ -137,6 +169,193 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
137
169
  { parent: this, provider: opts?.globalProvider }
138
170
  );
139
171
 
172
+ const edgeRouterRole = new aws.iam.Role(
173
+ `${name}-edge-router-role`,
174
+ {
175
+ assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
176
+ Service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'],
177
+ }),
178
+ },
179
+ { parent: this, provider: opts?.globalProvider }
180
+ );
181
+
182
+ new aws.iam.RolePolicyAttachment(
183
+ `${name}-edge-router-role-logging`,
184
+ {
185
+ role: edgeRouterRole.name,
186
+ policyArn: aws.iam.ManagedPolicy.AWSLambdaBasicExecutionRole,
187
+ },
188
+ { parent: this, provider: opts?.globalProvider }
189
+ );
190
+
191
+ const edgeFunction = new aws.lambda.CallbackFunction(
192
+ `${name.replaceAll('.', '-')}-edge-router`,
193
+ {
194
+ role: edgeRouterRole,
195
+ runtime: 'nodejs22.x',
196
+ timeout: 5,
197
+ publish: true,
198
+ loggingConfig: {
199
+ logGroup: `/appliance/base/${name}/edge-router-logs`,
200
+ logFormat: 'Text',
201
+ },
202
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
203
+ callback: async (event: any) => {
204
+ const request = event.Records[0].cf.request;
205
+ const headers = request.headers;
206
+ const host = headers.host[0].value;
207
+
208
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
209
+ const dns = require('dns').promises;
210
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
211
+ const crypto = require('crypto');
212
+
213
+ let originUrl: URL;
214
+ let signatureRequired = false;
215
+ try {
216
+ const txtRecords = await dns.resolveTxt(`origin.${host}`);
217
+ const functionUrl = txtRecords[0][0];
218
+ originUrl = new URL(functionUrl);
219
+ signatureRequired = true;
220
+ } catch (e) {
221
+ console.error(`Failed to resolve TXT record for origin.${host}`, e);
222
+ originUrl = new URL(lambdaOriginFunctionUrl.functionUrl.get());
223
+ }
224
+
225
+ if (signatureRequired) {
226
+ // Extract the region from the Lambda Function URL hostname
227
+ // Format: <function-id>.lambda-url.<region>.on.aws
228
+ const hostnameParts = originUrl.hostname.split('.');
229
+ const regionIndex = hostnameParts.indexOf('lambda-url');
230
+ const targetRegion =
231
+ regionIndex >= 0 && hostnameParts[regionIndex + 1] ? hostnameParts[regionIndex + 1] : 'us-east-1';
232
+
233
+ // Helper functions for SigV4 signing
234
+ const sha256 = (data: string | Buffer) => crypto.createHash('sha256').update(data).digest();
235
+ const hmacSha256 = (key: Buffer | string, data: string) =>
236
+ crypto.createHmac('sha256', key).update(data).digest();
237
+ const toHex = (buffer: Buffer) => buffer.toString('hex');
238
+
239
+ const getSignatureKey = (secretKey: string, dateStamp: string, regionName: string, serviceName: string) => {
240
+ const kDate = hmacSha256(`AWS4${secretKey}`, dateStamp);
241
+ const kRegion = hmacSha256(kDate, regionName);
242
+ const kService = hmacSha256(kRegion, serviceName);
243
+ const kSigning = hmacSha256(kService, 'aws4_request');
244
+ return kSigning;
245
+ };
246
+
247
+ // Sign the request using SigV4
248
+ // Get credentials from environment (Lambda@Edge has access to execution role credentials)
249
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
250
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
251
+ const sessionToken = process.env.AWS_SESSION_TOKEN;
252
+
253
+ if (accessKeyId && secretAccessKey) {
254
+ const method = request.method;
255
+ const service = 'lambda';
256
+ const canonicalUri = request.uri || '/';
257
+ const canonicalQuerystring = request.querystring || '';
258
+
259
+ const now = new Date();
260
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
261
+ const dateStamp = amzDate.substring(0, 8);
262
+
263
+ // Create canonical headers
264
+ const payloadHash = toHex(
265
+ sha256(request.body?.data ? Buffer.from(request.body.data, request.body.encoding || 'base64') : '')
266
+ );
267
+
268
+ const canonicalHeaders =
269
+ `host:${originUrl.hostname}\n` +
270
+ `x-amz-content-sha256:${payloadHash}\n` +
271
+ `x-amz-date:${amzDate}\n` +
272
+ (sessionToken ? `x-amz-security-token:${sessionToken}\n` : '');
273
+
274
+ const signedHeaders = sessionToken
275
+ ? 'host;x-amz-content-sha256;x-amz-date;x-amz-security-token'
276
+ : 'host;x-amz-content-sha256;x-amz-date';
277
+
278
+ const canonicalRequest = [
279
+ method,
280
+ canonicalUri,
281
+ canonicalQuerystring,
282
+ canonicalHeaders,
283
+ signedHeaders,
284
+ payloadHash,
285
+ ].join('\n');
286
+
287
+ const algorithm = 'AWS4-HMAC-SHA256';
288
+ const credentialScope = `${dateStamp}/${targetRegion}/${service}/aws4_request`;
289
+ const stringToSign = [algorithm, amzDate, credentialScope, toHex(sha256(canonicalRequest))].join('\n');
290
+
291
+ const signingKey = getSignatureKey(secretAccessKey, dateStamp, targetRegion, service);
292
+ const signature = toHex(hmacSha256(signingKey, stringToSign));
293
+
294
+ const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
295
+
296
+ // Add SigV4 headers to the request
297
+ request.headers['authorization'] = [{ key: 'Authorization', value: authorizationHeader }];
298
+ request.headers['x-amz-date'] = [{ key: 'X-Amz-Date', value: amzDate }];
299
+ request.headers['x-amz-content-sha256'] = [{ key: 'X-Amz-Content-Sha256', value: payloadHash }];
300
+ if (sessionToken) {
301
+ request.headers['x-amz-security-token'] = [{ key: 'X-Amz-Security-Token', value: sessionToken }];
302
+ }
303
+ }
304
+ }
305
+
306
+ request.origin = {
307
+ custom: {
308
+ domainName: originUrl.hostname,
309
+ port: 443,
310
+ protocol: 'https',
311
+ path: request.path,
312
+ sslProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'],
313
+ readTimeout: 30,
314
+ keepaliveTimeout: 5,
315
+ customHeaders: {},
316
+ },
317
+ };
318
+ request.headers['host'] = [{ key: 'host', value: originUrl.hostname }];
319
+ request.headers['X-Forwarded-Host'] = [{ key: 'X-Forwarded-Host', value: host }];
320
+
321
+ return request;
322
+ },
323
+ },
324
+ { parent: this, provider: opts?.globalProvider }
325
+ );
326
+
327
+ new aws.lambda.Permission(
328
+ `${name}-edge-router-invoke-url-permission`,
329
+ {
330
+ function: edgeFunction.name,
331
+ action: 'lambda:InvokeFunction',
332
+ principal: 'edgelambda.amazonaws.com',
333
+ statementId: 'AllowExecutionFromCloudFront',
334
+ },
335
+ { parent: this, provider: opts?.globalProvider }
336
+ );
337
+
338
+ const distributionLogs = new aws.cloudwatch.LogGroup(
339
+ `${name}-distribution-logs`,
340
+ {
341
+ name: pulumi.interpolate`/appliance/base/${name}/distribution-logs`,
342
+ retentionInDays: 7,
343
+ },
344
+ { parent: this, provider: opts?.globalProvider }
345
+ );
346
+
347
+ const distributionDeliveryDestination = new aws.cloudwatch.LogDeliveryDestination(
348
+ `${name.replace('.', '-')}-distribution-delivery-destination`,
349
+ {
350
+ outputFormat: 'json',
351
+ deliveryDestinationType: 'CWL',
352
+ deliveryDestinationConfiguration: {
353
+ destinationResourceArn: distributionLogs.arn,
354
+ },
355
+ },
356
+ { parent: this, provider: opts?.globalProvider }
357
+ );
358
+
140
359
  this.cloudfrontDistribution = new aws.cloudfront.Distribution(
141
360
  `${name}-distribution`,
142
361
  {
@@ -152,17 +371,24 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
152
371
  .apply((res) => res.id ?? ''),
153
372
  originRequestPolicyId: aws.cloudfront
154
373
  .getOriginRequestPolicyOutput(
155
- { name: 'Managed-AllViewerExceptHostHeader' },
374
+ { name: 'Managed-AllViewer' },
156
375
  {
157
376
  parent: this,
158
377
  provider: opts?.globalProvider,
159
378
  }
160
379
  )
161
380
  .apply((res) => res.id ?? ''),
162
- allowedMethods: ['HEAD', 'GET'],
163
- cachedMethods: ['HEAD', 'GET'],
381
+ allowedMethods: ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'],
382
+ cachedMethods: ['GET', 'HEAD'],
164
383
  targetOriginId: 'LambdaOrigin',
165
384
  viewerProtocolPolicy: 'redirect-to-https',
385
+ lambdaFunctionAssociations: [
386
+ {
387
+ eventType: 'origin-request',
388
+ lambdaArn: edgeFunction.qualifiedArn,
389
+ includeBody: true,
390
+ },
391
+ ],
166
392
  },
167
393
  origins: [
168
394
  {
@@ -189,28 +415,24 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
189
415
  { parent: this, provider: opts?.globalProvider }
190
416
  );
191
417
 
192
- new aws.lambda.Permission(
193
- `${name.replaceAll('.', '-')}-origin-invoke-function-url-permission`,
418
+ const distributionDeliverySource = new aws.cloudwatch.LogDeliverySource(
419
+ `${name.replace('.', '-')}-distribution-delivery-source`,
194
420
  {
195
- action: 'lambda:InvokeFunctionUrl',
196
- principal: 'cloudfront.amazonaws.com',
197
- function: lambdaOrigin.name,
198
- functionUrlAuthType: 'AWS_IAM',
199
- sourceArn: this.cloudfrontDistribution.arn,
421
+ region: 'us-east-1',
422
+ logType: 'ACCESS_LOGS',
423
+ resourceArn: this.cloudfrontDistribution.arn,
200
424
  },
201
- { parent: this, provider: opts?.provider }
425
+ { parent: this, provider: opts?.globalProvider }
202
426
  );
203
427
 
204
- new awsNative.lambda.Permission(
205
- `${name.replaceAll('.', '-')}-origin-invoke-function-permission`,
428
+ new aws.cloudwatch.LogDelivery(
429
+ `${name.replace('.', '-')}-distribution-logging`,
206
430
  {
207
- action: 'lambda:InvokeFunction',
208
- principal: 'cloudfront.amazonaws.com',
209
- sourceArn: this.cloudfrontDistribution.arn,
210
- functionName: lambdaOrigin.name,
211
- invokedViaFunctionUrl: true,
431
+ region: 'us-east-1',
432
+ deliverySourceName: distributionDeliverySource.name,
433
+ deliveryDestinationArn: distributionDeliveryDestination.arn,
212
434
  },
213
- { parent: this, provider: opts?.nativeProvider }
435
+ { parent: this, provider: opts?.globalProvider }
214
436
  );
215
437
 
216
438
  new aws.route53.Record(
@@ -224,5 +446,31 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
224
446
  },
225
447
  { parent: this, provider: opts?.globalProvider }
226
448
  );
449
+
450
+ this.config = {
451
+ name: name,
452
+ stateBackendUrl: pulumi.interpolate`s3://${state.bucket}`,
453
+ domainName: args.config.dns.domainName,
454
+ type: ApplianceBaseType.ApplianceAwsPublic,
455
+ aws: {
456
+ region: args.config.region,
457
+ zoneId: this.zoneId,
458
+ cloudfrontDistributionId: this.cloudfrontDistribution.id,
459
+ cloudfrontDistributionDomainName: this.cloudfrontDistribution.domainName,
460
+ edgeRouterRoleArn: edgeRouterRole.arn,
461
+ },
462
+ };
463
+
464
+ new aws.ssm.Parameter(
465
+ `${name}-base-config`,
466
+ {
467
+ name: `/appliance/base/${name}/config`,
468
+ type: 'SecureString',
469
+ value: pulumi.jsonStringify(this.config),
470
+ },
471
+ { parent: this, provider: opts?.provider }
472
+ );
473
+
474
+ this.registerOutputs(this.config);
227
475
  }
228
476
  }
@@ -1,8 +1,8 @@
1
1
  import * as pulumi from '@pulumi/pulumi';
2
- import { ApplianceBaseAwsVpcInput } from '@appliance.sh/sdk';
2
+ import { ApplianceBaseConfigInput, ApplianceBaseType } from '@appliance.sh/sdk';
3
3
 
4
4
  export type ApplianceBaseAwsVpcArgs = {
5
- config: ApplianceBaseAwsVpcInput;
5
+ config: ApplianceBaseConfigInput;
6
6
  };
7
7
 
8
8
  export interface ApplianceBaseAwsVpcOpts extends pulumi.ComponentResourceOptions {
@@ -14,5 +14,9 @@ export interface ApplianceBaseAwsVpcOpts extends pulumi.ComponentResourceOptions
14
14
  export class ApplianceBaseAwsVpc extends pulumi.ComponentResource {
15
15
  constructor(name: string, args: ApplianceBaseAwsVpcArgs, opts?: ApplianceBaseAwsVpcOpts) {
16
16
  super('appliance-infra:appliance-base-aws-vpc', name, args, opts);
17
+
18
+ if (args.config.type !== ApplianceBaseType.ApplianceAwsVpc) {
19
+ throw new Error('Invalid config');
20
+ }
17
21
  }
18
22
  }