From ff099525ca756809ab58aa4bd381ad686ae44ee7 Mon Sep 17 00:00:00 2001 From: barsa Date: Fri, 12 Dec 2025 18:44:26 +0900 Subject: [PATCH] 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. --- apps/bff/package.json | 11 +- apps/bff/scripts/dev-watch.sh | 82 ++++++++ apps/bff/src/app/bootstrap.ts | 15 ++ apps/bff/src/infra/database/prisma.service.ts | 86 +++++++++ .../services/salesforce-connection.service.ts | 177 +++++++++++++----- apps/bff/src/main.ts | 45 +++++ docker/Prod - Portainer/env.example | 34 +++- portal-backend.latest.tar.gz.sha256 | 2 +- portal-frontend.latest.tar.gz.sha256 | 2 +- scripts/dev/manage.sh | 4 + 10 files changed, 399 insertions(+), 59 deletions(-) create mode 100755 apps/bff/scripts/dev-watch.sh diff --git a/apps/bff/package.json b/apps/bff/package.json index 22ebf5bc..7a41c31b 100644 --- a/apps/bff/package.json +++ b/apps/bff/package.json @@ -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", diff --git a/apps/bff/scripts/dev-watch.sh b/apps/bff/scripts/dev-watch.sh new file mode 100755 index 00000000..66313763 --- /dev/null +++ b/apps/bff/scripts/dev-watch.sh @@ -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" diff --git a/apps/bff/src/app/bootstrap.ts b/apps/bff/src/app/bootstrap.ts index 2254f72b..34572171 100644 --- a/apps/bff/src/app/bootstrap.ts +++ b/apps/bff/src/app/bootstrap.ts @@ -120,6 +120,21 @@ export async function bootstrap(): Promise { // 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. diff --git a/apps/bff/src/infra/database/prisma.service.ts b/apps/bff/src/infra/database/prisma.service.ts index 406f7f01..4b0e5416 100644 --- a/apps/bff/src/infra/database/prisma.service.ts +++ b/apps/bff/src/infra/database/prisma.service.ts @@ -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 } } diff --git a/apps/bff/src/integrations/salesforce/services/salesforce-connection.service.ts b/apps/bff/src/integrations/salesforce/services/salesforce-connection.service.ts index 76d18f37..a4de8e9e 100644 --- a/apps/bff/src/integrations/salesforce/services/salesforce-connection.service.ts +++ b/apps/bff/src/integrations/salesforce/services/salesforce-connection.service.ts @@ -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) => Promise<{ id?: string }>; update?: (data: Record & { Id: string }) => Promise; @@ -78,64 +128,83 @@ export class SalesforceConnection { const username = this.configService.get("SF_USERNAME"); const clientId = this.configService.get("SF_CLIENT_ID"); const privateKeyPath = this.configService.get("SF_PRIVATE_KEY_PATH"); + const privateKeyEnv = this.configService.get("SF_PRIVATE_KEY_BASE64"); const audience = this.configService.get("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; 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); } diff --git a/apps/bff/src/main.ts b/apps/bff/src/main.ts index d0d58a55..feeb50b0 100644 --- a/apps/bff/src/main.ts +++ b/apps/bff/src/main.ts @@ -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); } })(); diff --git a/docker/Prod - Portainer/env.example b/docker/Prod - Portainer/env.example index dec25d57..2bb4cdd2 100644 --- a/docker/Prod - Portainer/env.example +++ b/docker/Prod - Portainer/env.example @@ -35,8 +35,12 @@ POSTGRES_PASSWORD= JWT_SECRET= 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= @@ -64,9 +68,29 @@ SF_CLIENT_ID= SF_USERNAME= SF_EVENTS_ENABLED=true -# Salesforce Private Key (Base64 encoded) -# To encode: base64 -w 0 < sf-private.key -SF_PRIVATE_KEY_BASE64= +# 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= # ----------------------------------------------------------------------------- diff --git a/portal-backend.latest.tar.gz.sha256 b/portal-backend.latest.tar.gz.sha256 index 61b10544..2be94011 100644 --- a/portal-backend.latest.tar.gz.sha256 +++ b/portal-backend.latest.tar.gz.sha256 @@ -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 diff --git a/portal-frontend.latest.tar.gz.sha256 b/portal-frontend.latest.tar.gz.sha256 index 77b037e0..4a1c7663 100644 --- a/portal-frontend.latest.tar.gz.sha256 +++ b/portal-frontend.latest.tar.gz.sha256 @@ -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 diff --git a/scripts/dev/manage.sh b/scripts/dev/manage.sh index 97e66791..9bf3ad4f 100755 --- a/scripts/dev/manage.sh +++ b/scripts/dev/manage.sh @@ -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"