@appliance.sh/api-server 1.22.0 → 1.22.1
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.22.
|
|
3
|
+
"version": "1.22.1",
|
|
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.22.
|
|
23
|
-
"@appliance.sh/sdk": "1.22.
|
|
22
|
+
"@appliance.sh/infra": "1.22.1",
|
|
23
|
+
"@appliance.sh/sdk": "1.22.1",
|
|
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,11 +1,12 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { timingSafeEqual } from 'crypto';
|
|
2
|
+
import { createHash, timingSafeEqual } from 'crypto';
|
|
3
3
|
import { apiKeyInput } from '@appliance.sh/sdk';
|
|
4
4
|
import { apiKeyService } from '../../services/api-key.service';
|
|
5
5
|
|
|
6
6
|
function constantTimeEqual(a: string, b: string): boolean {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const ha = createHash('sha256').update(a).digest();
|
|
8
|
+
const hb = createHash('sha256').update(b).digest();
|
|
9
|
+
return timingSafeEqual(ha, hb);
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export const bootstrapRoutes = Router();
|
|
@@ -2,7 +2,7 @@ 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
4
|
import type { ApplianceContainer, ApplianceFrameworkApp } from '@appliance.sh/sdk';
|
|
5
|
-
import {
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
6
6
|
import * as fs from 'node:fs';
|
|
7
7
|
import * as path from 'node:path';
|
|
8
8
|
import * as os from 'node:os';
|
|
@@ -60,7 +60,8 @@ export class BuildService {
|
|
|
60
60
|
|
|
61
61
|
try {
|
|
62
62
|
// List zip contents and validate paths before extracting
|
|
63
|
-
|
|
63
|
+
// Validate zip contents before extracting
|
|
64
|
+
const entries = execFileSync('zipinfo', ['-1', zipPath], { encoding: 'utf-8' }).trim().split('\n');
|
|
64
65
|
for (const entryPath of entries) {
|
|
65
66
|
const resolved = path.resolve(tmpDir, entryPath);
|
|
66
67
|
if (!resolved.startsWith(tmpDir + path.sep) && resolved !== tmpDir) {
|
|
@@ -68,7 +69,15 @@ export class BuildService {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
execFileSync('unzip', ['-o', '-q', zipPath, '-d', tmpDir], { stdio: 'pipe' });
|
|
73
|
+
|
|
74
|
+
// Reject symlinks after extraction
|
|
75
|
+
for (const entryPath of entries) {
|
|
76
|
+
const fullPath = path.join(tmpDir, entryPath);
|
|
77
|
+
if (fs.existsSync(fullPath) && fs.lstatSync(fullPath).isSymbolicLink()) {
|
|
78
|
+
throw new Error(`Zip contains symlink: ${entryPath}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
72
81
|
|
|
73
82
|
// Read the manifest
|
|
74
83
|
const manifestPath = path.join(tmpDir, 'appliance.json');
|
|
@@ -123,10 +132,11 @@ export class BuildService {
|
|
|
123
132
|
/**
|
|
124
133
|
* Container builds are fully pre-processed by the CLI (Lambda Web Adapter
|
|
125
134
|
* already injected). The server just loads the image and pushes it to ECR.
|
|
135
|
+
* All subprocess calls use execFileSync (array args) to prevent shell injection.
|
|
126
136
|
*/
|
|
127
137
|
private async resolveContainer(
|
|
128
138
|
tmpDir: string,
|
|
129
|
-
|
|
139
|
+
_manifest: ApplianceContainer,
|
|
130
140
|
tag: string,
|
|
131
141
|
config: ReturnType<typeof getBaseConfig>
|
|
132
142
|
): Promise<ResolvedBuild> {
|
|
@@ -134,10 +144,13 @@ export class BuildService {
|
|
|
134
144
|
if (!ecrRepositoryUrl) throw new Error('ECR repository not configured');
|
|
135
145
|
|
|
136
146
|
const imageTarPath = path.join(tmpDir, 'image.tar');
|
|
137
|
-
if (!fs.existsSync(imageTarPath))
|
|
147
|
+
if (!fs.existsSync(imageTarPath)) {
|
|
148
|
+
const extracted = fs.readdirSync(tmpDir);
|
|
149
|
+
throw new Error(`Build missing image.tar. Extracted contents: ${extracted.join(', ')}`);
|
|
150
|
+
}
|
|
138
151
|
|
|
139
152
|
// Load the pre-built Lambda-ready image
|
|
140
|
-
const loadOutput =
|
|
153
|
+
const loadOutput = execFileSync('docker', ['load', '-i', imageTarPath], { encoding: 'utf-8' });
|
|
141
154
|
const loadedMatch = loadOutput.match(/Loaded image(?: ID)?:\s*(.+)/);
|
|
142
155
|
if (!loadedMatch) throw new Error(`Failed to parse docker load output: ${loadOutput}`);
|
|
143
156
|
const loadedImage = loadedMatch[1].trim();
|
|
@@ -152,20 +165,20 @@ export class BuildService {
|
|
|
152
165
|
|
|
153
166
|
const decoded = Buffer.from(authData.authorizationToken, 'base64').toString();
|
|
154
167
|
const [username, password] = decoded.split(':');
|
|
155
|
-
|
|
168
|
+
execFileSync('docker', ['login', '--username', username, '--password-stdin', authData.proxyEndpoint], {
|
|
156
169
|
input: password,
|
|
157
170
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
158
171
|
});
|
|
159
172
|
|
|
160
173
|
// Tag and push
|
|
161
174
|
const remoteTag = `${ecrRepositoryUrl}:${tag}`;
|
|
162
|
-
|
|
163
|
-
|
|
175
|
+
execFileSync('docker', ['tag', loadedImage, remoteTag], { stdio: 'pipe' });
|
|
176
|
+
execFileSync('docker', ['push', remoteTag], { stdio: 'pipe' });
|
|
164
177
|
|
|
165
178
|
// Get digest
|
|
166
179
|
let imageUri: string;
|
|
167
180
|
try {
|
|
168
|
-
imageUri =
|
|
181
|
+
imageUri = execFileSync('docker', ['inspect', '--format={{index .RepoDigests 0}}', remoteTag], {
|
|
169
182
|
encoding: 'utf-8',
|
|
170
183
|
}).trim();
|
|
171
184
|
} catch {
|