@appliance.sh/infra 1.13.0 → 1.15.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.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "Deploy the Appliance Infrastructure",
5
5
  "repository": "https://github.com/appliance-sh/appliance.sh",
6
6
  "license": "MIT",
@@ -1,6 +1,5 @@
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
4
  import { ApplianceBaseConfigInput, ApplianceBaseType } from '@appliance.sh/sdk';
6
5
 
@@ -170,6 +169,193 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
170
169
  { parent: this, provider: opts?.globalProvider }
171
170
  );
172
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
+
173
359
  this.cloudfrontDistribution = new aws.cloudfront.Distribution(
174
360
  `${name}-distribution`,
175
361
  {
@@ -185,17 +371,24 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
185
371
  .apply((res) => res.id ?? ''),
186
372
  originRequestPolicyId: aws.cloudfront
187
373
  .getOriginRequestPolicyOutput(
188
- { name: 'Managed-AllViewerExceptHostHeader' },
374
+ { name: 'Managed-AllViewer' },
189
375
  {
190
376
  parent: this,
191
377
  provider: opts?.globalProvider,
192
378
  }
193
379
  )
194
380
  .apply((res) => res.id ?? ''),
195
- allowedMethods: ['HEAD', 'GET'],
196
- cachedMethods: ['HEAD', 'GET'],
381
+ allowedMethods: ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'],
382
+ cachedMethods: ['GET', 'HEAD'],
197
383
  targetOriginId: 'LambdaOrigin',
198
384
  viewerProtocolPolicy: 'redirect-to-https',
385
+ lambdaFunctionAssociations: [
386
+ {
387
+ eventType: 'origin-request',
388
+ lambdaArn: edgeFunction.qualifiedArn,
389
+ includeBody: true,
390
+ },
391
+ ],
199
392
  },
200
393
  origins: [
201
394
  {
@@ -222,28 +415,24 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
222
415
  { parent: this, provider: opts?.globalProvider }
223
416
  );
224
417
 
225
- new aws.lambda.Permission(
226
- `${name.replaceAll('.', '-')}-origin-invoke-function-url-permission`,
418
+ const distributionDeliverySource = new aws.cloudwatch.LogDeliverySource(
419
+ `${name.replace('.', '-')}-distribution-delivery-source`,
227
420
  {
228
- action: 'lambda:InvokeFunctionUrl',
229
- principal: 'cloudfront.amazonaws.com',
230
- function: lambdaOrigin.name,
231
- functionUrlAuthType: 'AWS_IAM',
232
- sourceArn: this.cloudfrontDistribution.arn,
421
+ region: 'us-east-1',
422
+ logType: 'ACCESS_LOGS',
423
+ resourceArn: this.cloudfrontDistribution.arn,
233
424
  },
234
- { parent: this, provider: opts?.provider }
425
+ { parent: this, provider: opts?.globalProvider }
235
426
  );
236
427
 
237
- new awsNative.lambda.Permission(
238
- `${name.replaceAll('.', '-')}-origin-invoke-function-permission`,
428
+ new aws.cloudwatch.LogDelivery(
429
+ `${name.replace('.', '-')}-distribution-logging`,
239
430
  {
240
- action: 'lambda:InvokeFunction',
241
- principal: 'cloudfront.amazonaws.com',
242
- sourceArn: this.cloudfrontDistribution.arn,
243
- functionName: lambdaOrigin.name,
244
- invokedViaFunctionUrl: true,
431
+ region: 'us-east-1',
432
+ deliverySourceName: distributionDeliverySource.name,
433
+ deliveryDestinationArn: distributionDeliveryDestination.arn,
245
434
  },
246
- { parent: this, provider: opts?.nativeProvider }
435
+ { parent: this, provider: opts?.globalProvider }
247
436
  );
248
437
 
249
438
  new aws.route53.Record(
@@ -260,9 +449,16 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
260
449
 
261
450
  this.config = {
262
451
  name: name,
263
- region: args.config.region,
264
452
  stateBackendUrl: pulumi.interpolate`s3://${state.bucket}`,
265
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
+ },
266
462
  };
267
463
 
268
464
  new aws.ssm.Parameter(