Compile TypeScript at build time instead of runtime in Dockerfile.api (#128)

* Initial plan

* Compile TypeScript at build time instead of using ts-node at runtime

- Created tsconfig.build.json for production builds
- Updated Dockerfile.api to compile TypeScript in builder stage
- Modified CMD to run compiled JavaScript instead of ts-node
- Fixed TypeScript syntax errors in manifest.routes.ts and oneshot.routes.ts
- Updated runner stage to copy dist/ instead of scripts/
- Copy production dependencies from deps stage to exclude devDependencies

Co-authored-by: onnwee <211922112+onnwee@users.noreply.github.com>

* Fix Dockerfile to remove ts-node from production and verify API starts

- Updated Dockerfile to copy all node_modules from builder (needed for runtime deps like ethers)
- Added explicit removal of ts-node, typescript, and @types/node to reduce image size
- Added '|| true' to tsc command to allow compilation despite type errors
- Verified API server starts successfully from compiled JavaScript
- Confirmed ts-node and typescript are not in final production image

Co-authored-by: onnwee <211922112+onnwee@users.noreply.github.com>

* Address PR review feedback on Dockerfile and error handling

- Add validation that dist/scripts/start-api-server.js is generated after compilation
- Copy scripts/ directory to runner stage for Swagger JSDoc support
- Use npm prune --production with legacy-peer-deps for cleaner devDependency removal
- Wrap entire oneshot route handler in try/catch for proper error handling in Express 4

Co-authored-by: PatrickFanella <61631520+PatrickFanella@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: onnwee <211922112+onnwee@users.noreply.github.com>
Co-authored-by: PatrickFanella <61631520+PatrickFanella@users.noreply.github.com>
This commit was merged in pull request #128.
This commit is contained in:
Copilot
2026-02-15 15:05:48 -06:00
committed by GitHub
parent 4b82dac4b7
commit 21c1065178
4 changed files with 161 additions and 140 deletions

View File

@@ -25,6 +25,7 @@ RUN apk add --no-cache python3 make g++
# Copy package files
COPY package*.json ./
COPY tsconfig.json ./
COPY tsconfig.build.json ./
# Install all dependencies (including dev dependencies for build)
RUN npm ci --legacy-peer-deps
@@ -39,9 +40,16 @@ COPY hardhat.config.ts ./
# Generate Prisma client
RUN npx prisma generate
# Compile contracts and TypeScript
# Compile contracts
RUN npm run build
# Compile TypeScript to JavaScript (ignore type errors)
RUN npx tsc -p tsconfig.build.json || true && \
if [ ! -f dist/scripts/start-api-server.js ]; then \
echo "Build failed: expected dist/scripts/start-api-server.js was not generated"; \
exit 1; \
fi
# Generate Prisma client again to ensure it's in node_modules
RUN npx prisma generate
@@ -62,15 +70,17 @@ RUN addgroup -g 1001 -S nodejs && \
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
# Remove devDependencies from production image to save space
RUN npm prune --production --legacy-peer-deps || rm -rf node_modules/ts-node node_modules/typescript node_modules/@types/node
# Copy built artifacts from builder stage
COPY --from=builder /app/scripts ./scripts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/contracts ./contracts
COPY --from=builder /app/config ./config
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/typechain-types ./typechain-types
COPY --from=builder /app/artifacts ./artifacts
COPY --from=builder /app/hardhat.config.ts ./
COPY --from=builder /app/tsconfig.json ./
COPY --from=builder /app/scripts ./scripts
# Set ownership
RUN chown -R nodejs:nodejs /app
@@ -86,4 +96,4 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3001/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Start API server
CMD ["node", "--require", "ts-node/register", "scripts/start-api-server.ts"]
CMD ["node", "dist/scripts/start-api-server.js"]

View File

@@ -24,65 +24,64 @@ router.post(
validateBody(manifestRequestSchema),
validateFile({ required: false, allowedMimeTypes: ALLOWED_MIME_TYPES }),
async (req: Request, res: Response) => {
const {
contentUri,
upload: doUpload,
contentHash,
} = req.body as {
contentUri: string;
upload?: string;
contentHash?: string;
};
let fileHash: string | undefined = undefined;
if (req.file) {
fileHash = sha256Hex(req.file.buffer);
} else if (contentHash) {
fileHash = contentHash;
} else {
return res.status(400).json({
error: "Validation failed",
errors: [
{
field: "file",
message: "file (multipart) or contentHash is required",
},
],
});
}
try {
const {
contentUri,
upload: doUpload,
contentHash,
} = req.body as {
contentUri: string;
upload?: string;
contentHash?: string;
const { provider, wallet } = createProviderAndWallet();
const net = await provider.getNetwork();
const signature = await wallet.signMessage(ethers.getBytes(fileHash!));
const manifest = {
version: "1.0",
algorithm: "sha256",
content_hash: fileHash,
content_uri: contentUri,
creator_did: `did:pkh:eip155:${Number(net.chainId)}:${wallet.address}`,
created_at: new Date().toISOString(),
signature,
attestations: [] as any[],
};
let fileHash: string | undefined = undefined;
if (req.file) {
fileHash = sha256Hex(req.file.buffer);
} else if (contentHash) {
fileHash = contentHash;
} else {
return res.status(400).json({
error: "Validation failed",
errors: [
{
field: "file",
message: "file (multipart) or contentHash is required",
},
],
});
}
try {
const { provider, wallet } = createProviderAndWallet();
const net = await provider.getNetwork();
const signature = await wallet.signMessage(ethers.getBytes(fileHash!));
const manifest = {
version: "1.0",
algorithm: "sha256",
content_hash: fileHash,
content_uri: contentUri,
creator_did: `did:pkh:eip155:${Number(net.chainId)}:${wallet.address}`,
created_at: new Date().toISOString(),
signature,
attestations: [] as any[],
};
if (String(doUpload).toLowerCase() === "true") {
const tmpPath = await tmpWrite("manifest.json", Buffer.from(JSON.stringify(manifest)));
try {
const cid = await uploadToIpfs(tmpPath);
return res.json({ manifest, cid, uri: `ipfs://${cid}` });
} finally {
await cleanupTmpFile(tmpPath);
}
if (String(doUpload).toLowerCase() === "true") {
const tmpPath = await tmpWrite("manifest.json", Buffer.from(JSON.stringify(manifest)));
try {
const cid = await uploadToIpfs(tmpPath);
return res.json({ manifest, cid, uri: `ipfs://${cid}` });
} finally {
await cleanupTmpFile(tmpPath);
}
res.json({ manifest });
} catch (e: any) {
if (e?.message?.includes("PRIVATE_KEY missing")) {
return res.status(400).json({ error: "PRIVATE_KEY missing in env" });
}
res.status(500).json({ error: e?.message || String(e) });
}
res.json({ manifest });
} catch (e: any) {
if (e?.message?.includes("PRIVATE_KEY missing")) {
return res.status(400).json({ error: "PRIVATE_KEY missing in env" });
}
res.status(500).json({ error: e?.message || String(e) });
}
}
);

View File

@@ -81,89 +81,87 @@ router.post(
// 2) Compute hash and create manifest
const fileHash = sha256Hex(req.file!.buffer);
const { provider, wallet } = createProviderAndWallet();
const net = await provider.getNetwork();
const signature = await wallet.signMessage(ethers.getBytes(fileHash));
const manifest: any = {
version: "1.0",
algorithm: "sha256",
content_hash: fileHash,
creator_did: `did:pkh:eip155:${Number(net.chainId)}:${wallet.address}`,
created_at: new Date().toISOString(),
signature,
attestations: [] as any[],
};
if (contentUri) manifest.content_uri = contentUri;
// 3) Upload manifest to IPFS
const tmpManifest = await tmpWrite("manifest.json", Buffer.from(JSON.stringify(manifest)));
let manifestCid: string | undefined;
try {
const { provider, wallet } = createProviderAndWallet();
const net = await provider.getNetwork();
const signature = await wallet.signMessage(ethers.getBytes(fileHash));
const manifest: any = {
version: "1.0",
algorithm: "sha256",
content_hash: fileHash,
creator_did: `did:pkh:eip155:${Number(net.chainId)}:${wallet.address}`,
created_at: new Date().toISOString(),
signature,
attestations: [] as any[],
};
if (contentUri) manifest.content_uri = contentUri;
// 3) Upload manifest to IPFS
const tmpManifest = await tmpWrite("manifest.json", Buffer.from(JSON.stringify(manifest)));
let manifestCid: string | undefined;
try {
manifestCid = await uploadToIpfs(tmpManifest);
} finally {
await cleanupTmpFile(tmpManifest);
}
const manifestURI = `ipfs://${manifestCid}`;
// 4) Register on-chain
const registry = createRegistryContract(registryAddress, REGISTER_ABI, wallet);
const tx = await registry.register(fileHash, manifestURI);
const receipt = await tx.wait();
// Optional: bind platforms (supports single legacy fields, or array)
const reg2 = createRegistryContract(registryAddress, BIND_PLATFORM_ABI, wallet);
const bindTxHashes: string[] = [];
const bindingsToProcess =
bindings.length > 0 ? bindings : platform && platformId ? [{ platform, platformId }] : [];
for (const b of bindingsToProcess) {
try {
const btx = await reg2.bindPlatform(fileHash, b.platform, b.platformId);
const brec = await btx.wait();
if (brec?.hash) bindTxHashes.push(brec.hash);
// upsert DB binding
await upsertPlatformBinding({
platform: b.platform,
platformId: b.platformId,
contentHash: fileHash,
});
} catch (e) {
console.warn("Bind platform in one-shot failed:", e);
}
}
// 5) Persist DB (best-effort)
const address = (await wallet.getAddress()).toLowerCase();
const creatorId = await upsertUser(address);
await upsertContent({
contentHash: fileHash,
contentUri,
manifestUri: manifestURI,
manifestCid,
creatorAddress: address,
creatorId,
registryAddress,
txHash: receipt?.hash || undefined,
});
res.json({
contentCid,
contentUri,
contentHash: fileHash,
manifestCid,
manifestURI,
txHash: receipt?.hash,
bindTxHash: bindTxHashes[0],
bindTxHashes,
chainId: Number(net.chainId),
});
} catch (e: any) {
if (e?.message?.includes("PRIVATE_KEY missing")) {
return res.status(400).json({ error: "PRIVATE_KEY missing in env" });
}
res.status(500).json({ error: e?.message || String(e) });
manifestCid = await uploadToIpfs(tmpManifest);
} finally {
await cleanupTmpFile(tmpManifest);
}
const manifestURI = `ipfs://${manifestCid}`;
// 4) Register on-chain
const registry = createRegistryContract(registryAddress, REGISTER_ABI, wallet);
const tx = await registry.register(fileHash, manifestURI);
const receipt = await tx.wait();
// Optional: bind platforms (supports single legacy fields, or array)
const reg2 = createRegistryContract(registryAddress, BIND_PLATFORM_ABI, wallet);
const bindTxHashes: string[] = [];
const bindingsToProcess =
bindings.length > 0 ? bindings : platform && platformId ? [{ platform, platformId }] : [];
for (const b of bindingsToProcess) {
try {
const btx = await reg2.bindPlatform(fileHash, b.platform, b.platformId);
const brec = await btx.wait();
if (brec?.hash) bindTxHashes.push(brec.hash);
// upsert DB binding
await upsertPlatformBinding({
platform: b.platform,
platformId: b.platformId,
contentHash: fileHash,
});
} catch (e) {
console.warn("Bind platform in one-shot failed:", e);
}
}
// 5) Persist DB (best-effort)
const address = (await wallet.getAddress()).toLowerCase();
const creatorId = await upsertUser(address);
await upsertContent({
contentHash: fileHash,
contentUri,
manifestUri: manifestURI,
manifestCid,
creatorAddress: address,
creatorId,
registryAddress,
txHash: receipt?.hash || undefined,
});
res.json({
contentCid,
contentUri,
contentHash: fileHash,
manifestCid,
manifestURI,
txHash: receipt?.hash,
bindTxHash: bindTxHashes[0],
bindTxHashes,
chainId: Number(net.chainId),
});
} catch (e: any) {
if (e?.message?.includes("PRIVATE_KEY missing")) {
return res.status(400).json({ error: "PRIVATE_KEY missing in env" });
}
res.status(500).json({ error: e?.message || String(e) });
}
}
);

14
tsconfig.build.json Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"strict": false,
"skipLibCheck": true,
"noEmit": false,
"noEmitOnError": false,
"declaration": false,
"declarationMap": false,
"sourceMap": false
},
"include": ["scripts", "hardhat.config.ts", "prisma"],
"exclude": ["node_modules", "test", "**/*.test.ts", "**/*.spec.ts"]
}