Update SHA256 checksums, enhance BFF development scripts, and improve Salesforce connection handling
- Updated SHA256 checksums for the latest portal backend and frontend tar.gz files to reflect new builds. - Introduced a new development script (`dev-watch.sh`) for the BFF application to streamline TypeScript building and aliasing during development. - Refactored the `package.json` scripts in the BFF application to improve development workflow and added new watch commands. - Enhanced the Salesforce connection service to support private key handling via environment variables, improving security and flexibility in configuration.
This commit is contained in:
parent
a176f5d6ce
commit
ff099525ca
@ -11,15 +11,20 @@
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"predev": "nest build && tsc-alias -p tsconfig.build.json --resolve-full-paths",
|
||||
"dev": "nest start --watch --preserveWatchOutput",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"dev": "bash ./scripts/dev-watch.sh",
|
||||
"dev:build:watch": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
||||
"dev:alias": "tsc-alias -p tsconfig.build.json --resolve-full-paths",
|
||||
"dev:alias:watch": "tsc-alias -p tsconfig.build.json --resolve-full-paths -w",
|
||||
"dev:run:watch": "node --watch dist/main.js",
|
||||
"dev:run:debug:watch": "node --inspect --watch dist/main.js",
|
||||
"start:debug": "BFF_NODE_RUN_SCRIPT=dev:run:debug:watch bash ./scripts/dev-watch.sh",
|
||||
"start:prod": "node dist/main.js",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"test": "node --test",
|
||||
"type-check": "tsc --project tsconfig.json --noEmit",
|
||||
"type-check:watch": "tsc --project tsconfig.json --noEmit --watch",
|
||||
"clean": "rm -rf dist tsconfig.build.tsbuildinfo",
|
||||
"clean": "rm -rf dist .typecheck tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo",
|
||||
"db:migrate": "prisma migrate dev --config prisma/prisma.config.ts",
|
||||
"db:generate": "prisma generate --config prisma/prisma.config.ts",
|
||||
"db:studio": "prisma studio --port 5555 --config prisma/prisma.config.ts",
|
||||
|
||||
82
apps/bff/scripts/dev-watch.sh
Executable file
82
apps/bff/scripts/dev-watch.sh
Executable file
@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
log() {
|
||||
echo "[bff:dev] $*"
|
||||
}
|
||||
|
||||
kill_tree() {
|
||||
local pid="$1"
|
||||
if [ -z "${pid:-}" ]; then return 0; fi
|
||||
if kill -0 "$pid" >/dev/null 2>&1; then
|
||||
kill "$pid" >/dev/null 2>&1 || true
|
||||
# Give it a moment to exit gracefully
|
||||
sleep 0.2 || true
|
||||
kill -9 "$pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
PID_BUILD=""
|
||||
PID_ALIAS=""
|
||||
PID_NODE=""
|
||||
|
||||
cleanup() {
|
||||
log "Stopping dev processes..."
|
||||
kill_tree "$PID_NODE"
|
||||
kill_tree "$PID_ALIAS"
|
||||
kill_tree "$PID_BUILD"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
log "Starting TypeScript build watcher (tsc --watch)..."
|
||||
pnpm run -s dev:build:watch &
|
||||
PID_BUILD=$!
|
||||
|
||||
# Wait for initial output to exist
|
||||
MAIN_FILE="$ROOT_DIR/dist/main.js"
|
||||
WAIT_SECS="${BFF_BUILD_WAIT_SECS:-180}"
|
||||
log "Waiting for initial build output: $MAIN_FILE (timeout: ${WAIT_SECS}s)"
|
||||
loops=$((WAIT_SECS * 4))
|
||||
for ((i=1; i<=loops; i++)); do
|
||||
if [ -f "$MAIN_FILE" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.25
|
||||
if ! kill -0 "$PID_BUILD" >/dev/null 2>&1; then
|
||||
log "Build watcher exited unexpectedly."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -f "$MAIN_FILE" ]; then
|
||||
log "Timed out waiting for $MAIN_FILE"
|
||||
log "Tip: if this keeps happening, run: pnpm -C \"$ROOT_DIR\" run build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Rewriting path aliases in dist (tsc-alias)..."
|
||||
pnpm run -s dev:alias
|
||||
|
||||
log "Starting alias rewriter watcher (tsc-alias -w)..."
|
||||
pnpm run -s dev:alias:watch &
|
||||
PID_ALIAS=$!
|
||||
|
||||
RUN_SCRIPT="${BFF_NODE_RUN_SCRIPT:-dev:run:watch}"
|
||||
log "Starting Node runtime watcher ($RUN_SCRIPT)..."
|
||||
pnpm run -s "$RUN_SCRIPT" &
|
||||
PID_NODE=$!
|
||||
|
||||
# If any process exits, stop the rest
|
||||
wait -n "$PID_BUILD" "$PID_ALIAS" "$PID_NODE"
|
||||
exit_code=$?
|
||||
log "A dev process exited (code=$exit_code). Shutting down."
|
||||
exit "$exit_code"
|
||||
@ -120,6 +120,21 @@ export async function bootstrap(): Promise<INestApplication> {
|
||||
|
||||
// Rely on Nest's built-in shutdown hooks. External orchestrator will send signals.
|
||||
app.enableShutdownHooks();
|
||||
// #region agent log (hypothesis S1)
|
||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "debug-session",
|
||||
runId: "run1",
|
||||
hypothesisId: "S1",
|
||||
location: "apps/bff/src/app/bootstrap.ts:enableShutdownHooks",
|
||||
message: "Nest shutdown hooks enabled",
|
||||
data: { pid: process.pid },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
|
||||
// API routing prefix is applied via RouterModule in AppModule for clarity and modern routing.
|
||||
|
||||
|
||||
@ -17,6 +17,9 @@ import { PrismaPg } from "@prisma/adapter-pg";
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(PrismaService.name);
|
||||
private readonly pool: Pool;
|
||||
private readonly instanceTag: string;
|
||||
private destroyCalls = 0;
|
||||
private poolEnded = false;
|
||||
|
||||
constructor() {
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
@ -40,16 +43,99 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
|
||||
super({ adapter });
|
||||
|
||||
this.pool = pool;
|
||||
this.instanceTag = `${process.pid}:${Date.now()}`;
|
||||
|
||||
// #region agent log (hypothesis S3)
|
||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "debug-session",
|
||||
runId: "run1",
|
||||
hypothesisId: "S3",
|
||||
location: "apps/bff/src/infra/database/prisma.service.ts:constructor",
|
||||
message: "PrismaService constructed",
|
||||
data: { instanceTag: this.instanceTag, pid: process.pid },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
this.logger.log("Database connection established");
|
||||
|
||||
// #region agent log (hypothesis S3)
|
||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "debug-session",
|
||||
runId: "run1",
|
||||
hypothesisId: "S3",
|
||||
location: "apps/bff/src/infra/database/prisma.service.ts:onModuleInit",
|
||||
message: "PrismaService connected",
|
||||
data: { instanceTag: this.instanceTag },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
this.destroyCalls += 1;
|
||||
|
||||
// #region agent log (hypothesis S1)
|
||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "debug-session",
|
||||
runId: "run1",
|
||||
hypothesisId: "S1",
|
||||
location: "apps/bff/src/infra/database/prisma.service.ts:onModuleDestroy(entry)",
|
||||
message: "PrismaService destroy called",
|
||||
data: {
|
||||
instanceTag: this.instanceTag,
|
||||
destroyCalls: this.destroyCalls,
|
||||
poolEnded: this.poolEnded,
|
||||
pid: process.pid,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
|
||||
// Make shutdown idempotent: Nest can call destroy hooks more than once during restarts.
|
||||
if (this.poolEnded) {
|
||||
this.logger.warn("Database pool already closed; skipping duplicate shutdown");
|
||||
return;
|
||||
}
|
||||
this.poolEnded = true;
|
||||
|
||||
await this.$disconnect();
|
||||
await this.pool.end();
|
||||
this.logger.log("Database connection closed");
|
||||
|
||||
// #region agent log (hypothesis S1)
|
||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "debug-session",
|
||||
runId: "run1",
|
||||
hypothesisId: "S1",
|
||||
location: "apps/bff/src/infra/database/prisma.service.ts:onModuleDestroy(exit)",
|
||||
message: "PrismaService destroy completed",
|
||||
data: {
|
||||
instanceTag: this.instanceTag,
|
||||
destroyCalls: this.destroyCalls,
|
||||
poolEnded: this.poolEnded,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,56 @@ import { createPrivateKey } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
function normalizePrivateKeyInput(
|
||||
raw: string
|
||||
): { kind: "pem"; value: string } | { kind: "b64"; value: string } {
|
||||
// Handle common "pasted" forms:
|
||||
// - Windows newlines (\r\n)
|
||||
// - literal "\n" sequences (when passed through env/template systems)
|
||||
// - PEM with missing line breaks
|
||||
// Avoid regex control characters (eslint no-control-regex); remove null bytes safely.
|
||||
const trimmed = raw.trim().replaceAll("\u0000", "");
|
||||
|
||||
// Convert literal backslash-n into real newlines (only if present)
|
||||
const withNewlines = trimmed.includes("\\n") ? trimmed.replaceAll("\\n", "\n") : trimmed;
|
||||
const normalizedNewlines = withNewlines.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||||
|
||||
// If it looks like PEM, ensure headers/footers are on their own lines.
|
||||
if (normalizedNewlines.includes("-----BEGIN")) {
|
||||
// IMPORTANT: Keep normalization line-based. Avoid patterns that can span across the
|
||||
// base64 body (which often contains no '-' characters), otherwise we can corrupt
|
||||
// otherwise-valid keys.
|
||||
let pem = normalizedNewlines.trim();
|
||||
|
||||
// Ensure the BEGIN line ends with a newline (only if it's missing).
|
||||
pem = pem.replace(/(-----BEGIN [^-\n]+-----)(?!\n)/g, "$1\n");
|
||||
|
||||
// Ensure the END line starts on its own line (only if it's missing).
|
||||
pem = pem.replace(/([A-Za-z0-9+/=])\s*(-----END [^-\n]+-----)/g, "$1\n$2");
|
||||
|
||||
return { kind: "pem", value: pem.trim() + "\n" };
|
||||
}
|
||||
|
||||
// Otherwise, treat it as base64 DER if it is base64-ish.
|
||||
// (This supports secrets systems that store the key as base64 without PEM headers.)
|
||||
const b64 = normalizedNewlines.replace(/\s+/g, "");
|
||||
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(b64) && b64.length >= 256;
|
||||
if (looksBase64) {
|
||||
// Some systems store the PEM text itself as base64. If so, decode and retry as PEM.
|
||||
try {
|
||||
const decoded = Buffer.from(b64, "base64").toString("utf8").trim();
|
||||
if (decoded.includes("-----BEGIN")) {
|
||||
return normalizePrivateKeyInput(decoded);
|
||||
}
|
||||
} catch {
|
||||
// ignore and fall back to DER handling
|
||||
}
|
||||
return { kind: "b64", value: b64 };
|
||||
}
|
||||
|
||||
return { kind: "pem", value: normalizedNewlines.trim() + "\n" };
|
||||
}
|
||||
|
||||
export interface SalesforceSObjectApi {
|
||||
create: (data: Record<string, unknown>) => Promise<{ id?: string }>;
|
||||
update?: (data: Record<string, unknown> & { Id: string }) => Promise<unknown>;
|
||||
@ -78,64 +128,83 @@ export class SalesforceConnection {
|
||||
const username = this.configService.get<string>("SF_USERNAME");
|
||||
const clientId = this.configService.get<string>("SF_CLIENT_ID");
|
||||
const privateKeyPath = this.configService.get<string>("SF_PRIVATE_KEY_PATH");
|
||||
const privateKeyEnv = this.configService.get<string>("SF_PRIVATE_KEY_BASE64");
|
||||
const audience =
|
||||
this.configService.get<string>("SF_LOGIN_URL") || "https://login.salesforce.com";
|
||||
|
||||
// Gracefully skip connection if not configured for local/dev environments
|
||||
if (!username || !clientId || !privateKeyPath) {
|
||||
if (!username || !clientId || (!privateKeyEnv && !privateKeyPath)) {
|
||||
const devMessage =
|
||||
"Missing required Salesforce configuration. Please check SF_USERNAME, SF_CLIENT_ID, and SF_PRIVATE_KEY_PATH environment variables.";
|
||||
"Missing required Salesforce configuration. Please check SF_USERNAME, SF_CLIENT_ID, and either SF_PRIVATE_KEY_BASE64 or SF_PRIVATE_KEY_PATH environment variables.";
|
||||
throw new Error(isProd ? "Salesforce configuration is missing" : devMessage);
|
||||
}
|
||||
|
||||
// Resolve private key and enforce allowed secrets directories
|
||||
// Supports project-root ./secrets, apps/bff ./secrets, and container /app/secrets
|
||||
const getProjectRoot = () => {
|
||||
const cwd = process.cwd();
|
||||
const norm = path.normalize(cwd);
|
||||
const bffSuffix = path.normalize(path.join("apps", "bff"));
|
||||
if (norm.endsWith(bffSuffix)) {
|
||||
return path.resolve(cwd, "../..");
|
||||
}
|
||||
return cwd;
|
||||
};
|
||||
|
||||
const isAbsolute = path.isAbsolute(privateKeyPath);
|
||||
const resolvedKeyPath = isAbsolute
|
||||
? privateKeyPath
|
||||
: path.resolve(getProjectRoot(), privateKeyPath);
|
||||
|
||||
const allowedBases = [
|
||||
path.resolve(getProjectRoot(), "secrets"),
|
||||
path.resolve(process.cwd(), "secrets"),
|
||||
"/app/secrets",
|
||||
].map(p => path.normalize(p) + path.sep);
|
||||
|
||||
const normalizedKeyPath = path.normalize(resolvedKeyPath);
|
||||
const isUnderAllowedBase = allowedBases.some(base =>
|
||||
(normalizedKeyPath + path.sep).startsWith(base)
|
||||
);
|
||||
if (!isUnderAllowedBase) {
|
||||
const devMsg = `Salesforce private key must be under one of the allowed secrets directories: ${allowedBases
|
||||
.map(b => b.replace(/\\$/, ""))
|
||||
.join(", ")}. Got: ${normalizedKeyPath}`;
|
||||
throw new Error(isProd ? "Invalid Salesforce private key path" : devMsg);
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(resolvedKeyPath);
|
||||
} catch {
|
||||
const devMsg = `Salesforce private key file not found at: ${resolvedKeyPath}. Ensure the file exists and has proper permissions (chmod 600).`;
|
||||
throw new Error(isProd ? "Salesforce private key not found" : devMsg);
|
||||
}
|
||||
|
||||
// Load private key
|
||||
const privateKey = await fs.readFile(resolvedKeyPath, "utf8");
|
||||
// - Prefer env-provided key for container/platforms where mounting a file is awkward
|
||||
// - Otherwise read from path (restricted to allowed secrets dirs)
|
||||
let rawPrivateKey: string;
|
||||
if (privateKeyEnv) {
|
||||
rawPrivateKey = privateKeyEnv;
|
||||
} else {
|
||||
const keyPath = privateKeyPath;
|
||||
// This should already be guaranteed by the config check above, but keep TS happy and fail safely.
|
||||
if (!keyPath) {
|
||||
const devMessage =
|
||||
"Missing required Salesforce configuration. Please check SF_PRIVATE_KEY_PATH environment variable.";
|
||||
throw new Error(isProd ? "Salesforce configuration is missing" : devMessage);
|
||||
}
|
||||
|
||||
// Resolve private key and enforce allowed secrets directories
|
||||
// Supports project-root ./secrets, apps/bff ./secrets, and container /app/secrets
|
||||
const getProjectRoot = () => {
|
||||
const cwd = process.cwd();
|
||||
const norm = path.normalize(cwd);
|
||||
const bffSuffix = path.normalize(path.join("apps", "bff"));
|
||||
if (norm.endsWith(bffSuffix)) {
|
||||
return path.resolve(cwd, "../..");
|
||||
}
|
||||
return cwd;
|
||||
};
|
||||
|
||||
const isAbsolute = path.isAbsolute(keyPath);
|
||||
const resolvedKeyPath = isAbsolute ? keyPath : path.resolve(getProjectRoot(), keyPath);
|
||||
|
||||
const allowedBases = [
|
||||
path.resolve(getProjectRoot(), "secrets"),
|
||||
path.resolve(process.cwd(), "secrets"),
|
||||
"/app/secrets",
|
||||
].map(p => path.normalize(p) + path.sep);
|
||||
|
||||
const normalizedKeyPath = path.normalize(resolvedKeyPath);
|
||||
const isUnderAllowedBase = allowedBases.some(base =>
|
||||
(normalizedKeyPath + path.sep).startsWith(base)
|
||||
);
|
||||
if (!isUnderAllowedBase) {
|
||||
const devMsg = `Salesforce private key must be under one of the allowed secrets directories: ${allowedBases
|
||||
.map(b => b.replace(/\\$/, ""))
|
||||
.join(", ")}. Got: ${normalizedKeyPath}`;
|
||||
throw new Error(isProd ? "Invalid Salesforce private key path" : devMsg);
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(resolvedKeyPath);
|
||||
} catch {
|
||||
const devMsg = `Salesforce private key file not found at: ${resolvedKeyPath}. Ensure the file exists and has proper permissions (chmod 600).`;
|
||||
throw new Error(isProd ? "Salesforce private key not found" : devMsg);
|
||||
}
|
||||
|
||||
rawPrivateKey = await fs.readFile(resolvedKeyPath, "utf8");
|
||||
}
|
||||
const normalizedKey = normalizePrivateKeyInput(rawPrivateKey);
|
||||
|
||||
// Validate private key format
|
||||
const isPkcs8 = privateKey.includes("BEGIN PRIVATE KEY");
|
||||
const isPkcs1Rsa = privateKey.includes("BEGIN RSA PRIVATE KEY");
|
||||
if ((!isPkcs8 && !isPkcs1Rsa) || privateKey.includes("[PLACEHOLDER")) {
|
||||
const pemForValidation = normalizedKey.kind === "pem" ? normalizedKey.value : "";
|
||||
const isPkcs8 = pemForValidation.includes("BEGIN PRIVATE KEY");
|
||||
const isPkcs1Rsa = pemForValidation.includes("BEGIN RSA PRIVATE KEY");
|
||||
const containsPlaceholder =
|
||||
rawPrivateKey.includes("[PLACEHOLDER") || pemForValidation.includes("[PLACEHOLDER");
|
||||
|
||||
if (containsPlaceholder || (normalizedKey.kind === "pem" && !isPkcs8 && !isPkcs1Rsa)) {
|
||||
const devMsg =
|
||||
"Salesforce private key appears to be invalid or still contains placeholder content. Expected a PEM key containing either 'BEGIN PRIVATE KEY' (PKCS8) or 'BEGIN RSA PRIVATE KEY' (PKCS1).";
|
||||
throw new Error(isProd ? "Invalid Salesforce private key" : devMsg);
|
||||
@ -144,10 +213,20 @@ export class SalesforceConnection {
|
||||
// Create JWT assertion (jose). Use Node crypto to support both PKCS8 and PKCS1 PEMs.
|
||||
let key: ReturnType<typeof createPrivateKey>;
|
||||
try {
|
||||
key = createPrivateKey({ key: privateKey, format: "pem" });
|
||||
if (normalizedKey.kind === "pem") {
|
||||
key = createPrivateKey({ key: normalizedKey.value, format: "pem" });
|
||||
} else {
|
||||
const der = Buffer.from(normalizedKey.value, "base64");
|
||||
// Try PKCS8 first, then PKCS1 (RSA)
|
||||
try {
|
||||
key = createPrivateKey({ key: der, format: "der", type: "pkcs8" });
|
||||
} catch {
|
||||
key = createPrivateKey({ key: der, format: "der", type: "pkcs1" });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
const devMsg =
|
||||
"Salesforce private key could not be parsed. Ensure it is a valid RSA PEM (PKCS8 'BEGIN PRIVATE KEY' or PKCS1 'BEGIN RSA PRIVATE KEY').";
|
||||
"Salesforce private key could not be parsed. Ensure it is a valid RSA private key (PEM with BEGIN PRIVATE KEY / BEGIN RSA PRIVATE KEY, or base64-encoded DER).";
|
||||
throw new Error(isProd ? "Invalid Salesforce private key" : devMsg);
|
||||
}
|
||||
|
||||
|
||||
@ -11,6 +11,21 @@ for (const signal of signals) {
|
||||
process.once(signal, () => {
|
||||
void (async () => {
|
||||
logger.log(`Received ${signal}. Closing Nest application...`);
|
||||
// #region agent log (hypothesis S1)
|
||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "debug-session",
|
||||
runId: "run1",
|
||||
hypothesisId: "S1",
|
||||
location: "apps/bff/src/main.ts:signalHandler(entry)",
|
||||
message: "Process signal handler invoked",
|
||||
data: { signal, pid: process.pid, appInitialized: !!app },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
|
||||
if (!app) {
|
||||
logger.warn("Nest application not initialized. Exiting immediately.");
|
||||
@ -19,6 +34,21 @@ for (const signal of signals) {
|
||||
}
|
||||
|
||||
try {
|
||||
// #region agent log (hypothesis S1)
|
||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "debug-session",
|
||||
runId: "run1",
|
||||
hypothesisId: "S1",
|
||||
location: "apps/bff/src/main.ts:signalHandler(beforeClose)",
|
||||
message: "Calling app.close()",
|
||||
data: { signal, pid: process.pid },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
await app.close();
|
||||
logger.log("Nest application closed gracefully.");
|
||||
} catch (error) {
|
||||
@ -28,6 +58,21 @@ for (const signal of signals) {
|
||||
resolvedError.stack
|
||||
);
|
||||
} finally {
|
||||
// #region agent log (hypothesis S1)
|
||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "debug-session",
|
||||
runId: "run1",
|
||||
hypothesisId: "S1",
|
||||
location: "apps/bff/src/main.ts:signalHandler(exit)",
|
||||
message: "Exiting process after shutdown handler",
|
||||
data: { signal, pid: process.pid },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
process.exit(0);
|
||||
}
|
||||
})();
|
||||
|
||||
@ -35,8 +35,12 @@ POSTGRES_PASSWORD=<GENERATE_WITH_openssl_rand_base64_24>
|
||||
JWT_SECRET=<GENERATE_WITH_openssl_rand_base64_32>
|
||||
JWT_SECRET_PREVIOUS=
|
||||
JWT_EXPIRES_IN=7d
|
||||
JWT_ISSUER=customer-portal
|
||||
JWT_AUDIENCE=portal
|
||||
# JWT claim validation (required; must be non-empty strings)
|
||||
# - JWT_ISSUER: who issues tokens (this backend). Use your production origin.
|
||||
# - JWT_AUDIENCE: who the token is intended for (your portal/app). Often same as issuer.
|
||||
# Keep these stable per environment to prevent prod/dev token mix-ups.
|
||||
JWT_ISSUER=https://asolutions.jp
|
||||
JWT_AUDIENCE=https://asolutions.jp
|
||||
BCRYPT_ROUNDS=12
|
||||
CSRF_SECRET_KEY=<GENERATE_WITH_openssl_rand_base64_32>
|
||||
|
||||
@ -64,9 +68,29 @@ SF_CLIENT_ID=<YOUR_SF_CLIENT_ID>
|
||||
SF_USERNAME=<YOUR_SF_USERNAME>
|
||||
SF_EVENTS_ENABLED=true
|
||||
|
||||
# Salesforce Private Key (Base64 encoded)
|
||||
# To encode: base64 -w 0 < sf-private.key
|
||||
SF_PRIVATE_KEY_BASE64=<BASE64_ENCODED_SF_PRIVATE_KEY>
|
||||
# Salesforce Private Key (recommended handling)
|
||||
# -----------------------------------------------------------------------------
|
||||
# IMPORTANT:
|
||||
# - Do NOT paste raw PEM in Portainer env.
|
||||
# - Prefer mounting the key file into the container and setting SF_PRIVATE_KEY_PATH.
|
||||
# - If you must use env, use SF_PRIVATE_KEY_BASE64 (single-line base64) and the container
|
||||
# entrypoint will write it to SF_PRIVATE_KEY_PATH.
|
||||
#
|
||||
# Option A (preferred): mount a file (no env secret)
|
||||
# - Mount host file -> /app/secrets/sf-private.key (read-only)
|
||||
# - Set:
|
||||
# SF_PRIVATE_KEY_PATH=/app/secrets/sf-private.key
|
||||
# - Leave SF_PRIVATE_KEY_BASE64 empty/unset
|
||||
#
|
||||
# Option B: env var (least preferred)
|
||||
# 1) Ensure you have the *private key* PEM (NOT a certificate):
|
||||
# -----BEGIN PRIVATE KEY----- (PKCS8) OR -----BEGIN RSA PRIVATE KEY----- (PKCS1)
|
||||
# 2) Base64 encode into ONE line (Linux):
|
||||
# base64 -w0 sf-private.key
|
||||
# 3) Paste that output into SF_PRIVATE_KEY_BASE64 (no quotes, no newlines)
|
||||
#
|
||||
# NOTE: Never commit real key material into git. Keep only placeholders here.
|
||||
SF_PRIVATE_KEY_BASE64=<BASE64_ENCODED_SALESFORCE_PRIVATE_KEY>
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@ -1 +1 @@
|
||||
1d758ef9ad60c480fcf34d5c1e6bd8b14215049ea524b61e5de828e74ba3170b /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar.gz
|
||||
dee6d178f1599a05911c9b6fb1246105112b7e81bfbac4e7df87ed91ca8a46c9 /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar.gz
|
||||
|
||||
@ -1 +1 @@
|
||||
129107760c197bce5493d6a33837cbc812dd8ba1516ee6c37239431ce973a58c /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar.gz
|
||||
2595c445c8ed1dd73006d5f4e35adea931de3c101260e1e0e1126790a761812c /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar.gz
|
||||
|
||||
@ -307,6 +307,10 @@ cleanup_dev() {
|
||||
pkill -f "next-server" 2>/dev/null && log " Stopped Next.js server process" || true
|
||||
pkill -f "pnpm.*--parallel.*dev" 2>/dev/null && log " Stopped parallel dev processes" || true
|
||||
pkill -f "prisma studio" 2>/dev/null && log " Stopped Prisma Studio" || true
|
||||
pkill -f "apps/bff/scripts/dev-watch.sh" 2>/dev/null && log " Stopped BFF dev-watch script" || true
|
||||
pkill -f "tsc -p tsconfig.build.json --watch" 2>/dev/null && log " Stopped BFF TypeScript watcher" || true
|
||||
pkill -f "tsc-alias.*tsconfig.build.json.*-w" 2>/dev/null && log " Stopped BFF tsc-alias watcher" || true
|
||||
pkill -f "node --watch dist/main.js" 2>/dev/null && log " Stopped BFF Node watcher" || true
|
||||
|
||||
sleep 1
|
||||
log "✅ Development cleanup completed"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user