@appliance.sh/api-server 1.20.0 → 1.22.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/api-server",
3
- "version": "1.20.0",
3
+ "version": "1.22.0",
4
4
  "description": "",
5
5
  "author": "Eliot Lim",
6
6
  "repository": "https://github.com/appliance-sh/appliance.sh",
@@ -19,8 +19,8 @@
19
19
  "test:e2e": "vitest run --config vitest.e2e.config.ts"
20
20
  },
21
21
  "dependencies": {
22
- "@appliance.sh/infra": "1.20.0",
23
- "@appliance.sh/sdk": "1.20.0",
22
+ "@appliance.sh/infra": "1.22.0",
23
+ "@appliance.sh/sdk": "1.22.0",
24
24
  "@aws-sdk/client-ecr": "^3.1005.0",
25
25
  "@aws-sdk/client-s3": "^3.750.0",
26
26
  "express": "^5.2.1"
@@ -1,7 +1,7 @@
1
1
  import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
2
2
  import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr';
3
3
  import { applianceBaseConfig, applianceInput } from '@appliance.sh/sdk';
4
- import type { ApplianceContainer } from '@appliance.sh/sdk';
4
+ import type { ApplianceContainer, ApplianceFrameworkApp } from '@appliance.sh/sdk';
5
5
  import { execSync } from 'child_process';
6
6
  import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
@@ -10,6 +10,11 @@ import * as os from 'node:os';
10
10
  export interface ResolvedBuild {
11
11
  imageUri?: string;
12
12
  codeS3Key?: string;
13
+ runtime?: string;
14
+ handler?: string;
15
+ layers?: string[];
16
+ architectures?: string[];
17
+ environment?: Record<string, string>;
13
18
  }
14
19
 
15
20
  function getBaseConfig() {
@@ -18,6 +23,23 @@ function getBaseConfig() {
18
23
  return applianceBaseConfig.parse(JSON.parse(raw));
19
24
  }
20
25
 
26
+ const LAMBDA_ADAPTER_LAYER: Record<string, string> = {
27
+ 'linux/amd64': 'arn:aws:lambda:${region}:753240598075:layer:LambdaAdapterLayerX86:26',
28
+ 'linux/arm64': 'arn:aws:lambda:${region}:753240598075:layer:LambdaAdapterLayerArm64:26',
29
+ };
30
+
31
+ const FRAMEWORK_RUNTIMES: Record<string, string> = {
32
+ node: 'nodejs22.x',
33
+ python: 'python3.13',
34
+ auto: 'nodejs22.x',
35
+ other: 'nodejs22.x',
36
+ };
37
+
38
+ const FRAMEWORK_ARCHITECTURES: Record<string, string> = {
39
+ 'linux/amd64': 'x86_64',
40
+ 'linux/arm64': 'arm64',
41
+ };
42
+
21
43
  export class BuildService {
22
44
  async resolve(buildId: string, tag: string): Promise<ResolvedBuild> {
23
45
  const config = getBaseConfig();
@@ -55,8 +77,9 @@ export class BuildService {
55
77
 
56
78
  if (manifest.type === 'container') {
57
79
  return this.resolveContainer(tmpDir, manifest, tag, config);
80
+ } else if (manifest.type === 'framework') {
81
+ return this.resolveFramework(manifest, s3Key, config);
58
82
  } else {
59
- // For framework/other, the zip is already in S3 — just reference it
60
83
  return { codeS3Key: s3Key };
61
84
  }
62
85
  } finally {
@@ -64,6 +87,43 @@ export class BuildService {
64
87
  }
65
88
  }
66
89
 
90
+ /**
91
+ * Framework builds are fully pre-processed by the CLI (dependencies installed,
92
+ * run.sh generated). The server just resolves Lambda-specific params from the
93
+ * manifest metadata and points at the original uploaded zip.
94
+ */
95
+ private resolveFramework(
96
+ manifest: ApplianceFrameworkApp,
97
+ s3Key: string,
98
+ config: ReturnType<typeof getBaseConfig>
99
+ ): ResolvedBuild {
100
+ const port = manifest.port ?? 8080;
101
+ const framework = manifest.framework ?? 'auto';
102
+ const runtime = FRAMEWORK_RUNTIMES[framework] ?? FRAMEWORK_RUNTIMES['node'];
103
+
104
+ const platform = manifest.platform;
105
+ const layerArn = LAMBDA_ADAPTER_LAYER[platform]?.replace('${region}', config.aws.region);
106
+ if (!layerArn) throw new Error(`No Lambda Web Adapter layer for platform: ${platform}`);
107
+ const architecture = FRAMEWORK_ARCHITECTURES[platform];
108
+ if (!architecture) throw new Error(`No Lambda architecture for platform: ${platform}`);
109
+
110
+ return {
111
+ codeS3Key: s3Key,
112
+ runtime,
113
+ handler: 'run.sh',
114
+ layers: [layerArn],
115
+ architectures: [architecture],
116
+ environment: {
117
+ AWS_LAMBDA_EXEC_WRAPPER: '/opt/bootstrap',
118
+ AWS_LWA_PORT: String(port),
119
+ },
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Container builds are fully pre-processed by the CLI (Lambda Web Adapter
125
+ * already injected). The server just loads the image and pushes it to ECR.
126
+ */
67
127
  private async resolveContainer(
68
128
  tmpDir: string,
69
129
  manifest: ApplianceContainer,
@@ -76,31 +136,12 @@ export class BuildService {
76
136
  const imageTarPath = path.join(tmpDir, 'image.tar');
77
137
  if (!fs.existsSync(imageTarPath)) throw new Error('Build missing image.tar');
78
138
 
79
- // Load the user's original image into Docker and capture the loaded image reference
139
+ // Load the pre-built Lambda-ready image
80
140
  const loadOutput = execSync(`docker load -i "${imageTarPath}"`, { encoding: 'utf-8' });
81
- // Output is like "Loaded image: name:tag" or "Loaded image ID: sha256:abc..."
82
141
  const loadedMatch = loadOutput.match(/Loaded image(?: ID)?:\s*(.+)/);
83
142
  if (!loadedMatch) throw new Error(`Failed to parse docker load output: ${loadOutput}`);
84
143
  const loadedImage = loadedMatch[1].trim();
85
144
 
86
- // Wrap the image with the Lambda Web Adapter so the same plain HTTP
87
- // container works on both Lambda and ECS/Fargate without any changes.
88
- const lambdaImageName = `${manifest.name}-lambda`;
89
- const wrapperDockerfile = path.join(tmpDir, 'Dockerfile.lambda');
90
- fs.writeFileSync(
91
- wrapperDockerfile,
92
- [
93
- `FROM --platform=${manifest.platform} public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 AS adapter`,
94
- `FROM ${loadedImage}`,
95
- `COPY --from=adapter /lambda-adapter /opt/extensions/lambda-adapter`,
96
- `ENV AWS_LWA_PORT=${manifest.port}`,
97
- ].join('\n')
98
- );
99
- execSync(
100
- `docker build --platform ${manifest.platform} --provenance=false -f "${wrapperDockerfile}" -t "${lambdaImageName}" "${tmpDir}"`,
101
- { stdio: 'pipe' }
102
- );
103
-
104
145
  // Auth with ECR
105
146
  const ecr = new ECRClient({ region: config.aws.region });
106
147
  const authResult = await ecr.send(new GetAuthorizationTokenCommand({}));
@@ -116,15 +157,15 @@ export class BuildService {
116
157
  stdio: ['pipe', 'pipe', 'pipe'],
117
158
  });
118
159
 
119
- // Tag and push the Lambda-wrapped image
160
+ // Tag and push
120
161
  const remoteTag = `${ecrRepositoryUrl}:${tag}`;
121
- execSync(`docker tag ${lambdaImageName} ${remoteTag}`, { stdio: 'pipe' });
122
- execSync(`docker push ${remoteTag}`, { stdio: 'pipe' });
162
+ execSync(`docker tag "${loadedImage}" "${remoteTag}"`, { stdio: 'pipe' });
163
+ execSync(`docker push "${remoteTag}"`, { stdio: 'pipe' });
123
164
 
124
165
  // Get digest
125
166
  let imageUri: string;
126
167
  try {
127
- imageUri = execSync(`docker inspect --format='{{index .RepoDigests 0}}' ${remoteTag}`, {
168
+ imageUri = execSync(`docker inspect --format='{{index .RepoDigests 0}}' "${remoteTag}"`, {
128
169
  encoding: 'utf-8',
129
170
  }).trim();
130
171
  } catch {