From 314ab049bb650120489259e920e52fd530f0ce41 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Wed, 12 Nov 2025 20:20:44 -0600 Subject: [PATCH] test(mobile): testing for ota updated on android scanner --- app/main.ts | 30 +-- app/src/internal/mobile/route.ts | 211 ++++++++++++++++++ .../internal/routerHandler/routeHandler.ts | 30 +-- 3 files changed, 239 insertions(+), 32 deletions(-) create mode 100644 app/src/internal/mobile/route.ts diff --git a/app/main.ts b/app/main.ts index 6bf9c09..e2a76f9 100644 --- a/app/main.ts +++ b/app/main.ts @@ -3,6 +3,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; import { toNodeHandler } from "better-auth/node"; import cors from "cors"; import express from "express"; +import fs from "fs"; import { createServer } from "http"; import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware"; import morgan from "morgan"; @@ -13,6 +14,7 @@ import swaggerUi from "swagger-ui-express"; import { fileURLToPath } from "url"; import { userMigrate } from "./src/internal/auth/controller/userMigrate.js"; import { schedulerManager } from "./src/internal/logistics/controller/schedulerManager.js"; +import { setupMobileRoutes } from "./src/internal/mobile/route.js"; import { printers } from "./src/internal/ocp/printers/printers.js"; import { setupRoutes } from "./src/internal/routerHandler/routeHandler.js"; import { baseModules } from "./src/internal/system/controller/modules/baseModules.js"; @@ -156,12 +158,20 @@ const main = async () => { }, methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], credentials: true, - exposedHeaders: ["set-cookie"], + exposedHeaders: [ + "set-cookie", + "expo-protocol-version", + "expo-sfv-version", + ], allowedHeaders: [ "Content-Type", "Authorization", "X-Requested-With", "XMLHttpRequest", + "expo-runtime-version", + "expo-platform", + "expo-channel-name", + "*", ], }), ); @@ -188,22 +198,6 @@ const main = async () => { res.sendFile(join(__dirname, "../lstDocs/build/index.html")); }); - // app ota updates - app.use( - basePath + "/api/mobile/updates", - express.static(join(__dirname, "../mobileLst/dist"), { - setHeaders(res) { - // OTA runtime needs to fetch these from the device - console.log("OTA check called"); - res.setHeader("Access-Control-Allow-Origin", "*"); - }, - }), - ); - - app.get(basePath + "/api/mobile", (_, res) => - res.status(200).json({ message: "LST OTA server is up." }), - ); - // server setup const server = createServer(app); @@ -223,7 +217,7 @@ const main = async () => { // start up the v1listener v1Listener(); addListeners(); - userMigrate(); + //userMigrate(); // some temp fixes manualFixes(); diff --git a/app/src/internal/mobile/route.ts b/app/src/internal/mobile/route.ts new file mode 100644 index 0000000..8b01036 --- /dev/null +++ b/app/src/internal/mobile/route.ts @@ -0,0 +1,211 @@ +import type { Express, Request, Response } from "express"; +import express, { Router } from "express"; +import { readdirSync, readFileSync, statSync } from "fs"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import crypto from "crypto"; +import fs from "fs"; + +export const setupMobileRoutes = (app: Express, basePath: string) => { + const router = Router(); + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const distPath = join(__dirname, "../../../../mobileLst/dist"); + + function generateAssetManifest(baseUrl: string) { + const assets: any[] = []; + const assetsDir = join(distPath, "assets"); + + try { + if (!fs.existsSync(assetsDir)) { + return assets; + } + + const files = readdirSync(assetsDir); + files.forEach((file) => { + const filePath = join(assetsDir, file); + const stats = statSync(filePath); + + if (stats.isFile()) { + const content = readFileSync(filePath); + const hash = crypto + .createHash("sha256") + .update(content) + .digest("hex"); + + assets.push({ + hash: hash, + key: file, + fileExtension: `.${file.split(".").pop()}`, + contentType: getContentType(file), + url: `${baseUrl}/assets/${file}`, + }); + } + }); + } catch (err) { + console.log("Error reading assets:", err); + } + + return assets; + } + + function getContentType(filename: string): string { + const ext = filename.split(".").pop()?.toLowerCase(); + const contentTypes: { [key: string]: string } = { + hbc: "application/javascript", + bundle: "application/javascript", + js: "application/javascript", + json: "application/json", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + ttf: "font/ttf", + otf: "font/otf", + woff: "font/woff", + woff2: "font/woff2", + }; + return contentTypes[ext || ""] || "application/octet-stream"; + } + + app.get(basePath + "/api/mobile/updates", (req, res) => { + console.log("=== OTA Update Request ==="); + console.log("Headers:", JSON.stringify(req.headers, null, 2)); + + const runtimeVersion = req.headers["expo-runtime-version"]; + const platform = req.headers["expo-platform"] || "android"; + const expectedRuntimeVersion = "1.0.0"; + + if (runtimeVersion !== expectedRuntimeVersion) { + console.log( + `Runtime mismatch: got ${runtimeVersion}, expected ${expectedRuntimeVersion}` + ); + return res.status(404).json({ + error: "No update available for this runtime version", + requestedVersion: runtimeVersion, + availableVersion: expectedRuntimeVersion, + }); + } + + try { + // const host = req.get('host'); + // // If it's the production domain, force https + // const protocol = host.includes('alpla.net') ? 'https' : req.protocol; + + // const baseUrl = `${protocol}://${host}/lst/api/mobile/updates` + + const host = req.get('host'); // Should be "usmcd1vms036:4000" + const protocol = 'http'; + const baseUrl = `${protocol}://${host}/api/mobile/updates`; + + // Find the .hbc file + const bundleDir = join(distPath, "_expo/static/js/android"); + + if (!fs.existsSync(bundleDir)) { + console.error("Bundle directory does not exist:", bundleDir); + return res + .status(500) + .json({ error: "Bundle directory not found" }); + } + + const bundleFiles = readdirSync(bundleDir); + console.log("Available bundle files:", bundleFiles); + + const bundleFile = bundleFiles.find((f) => f.endsWith(".hbc")); + + if (!bundleFile) { + console.error("No .hbc file found in:", bundleDir); + return res + .status(500) + .json({ error: "Hermes bundle (.hbc) not found" }); + } + + console.log("Using bundle file:", bundleFile); + + const bundlePath = join(bundleDir, bundleFile); + const bundleContent = readFileSync(bundlePath); + const bundleHash = crypto + .createHash("sha256") + .update(bundleContent) + .digest("hex"); + + const updateId = crypto.randomUUID(); + const createdAt = new Date().toISOString(); + + // This is the NEW manifest format for Expo SDK 50+ + const manifest = { + id: updateId, + createdAt: createdAt, + runtimeVersion: expectedRuntimeVersion, + launchAsset: { + hash: bundleHash, + key: bundleFile, + contentType: "application/javascript", + fileExtension: ".hbc", + url: `${baseUrl}/_expo/static/js/android/${bundleFile}`, + }, + assets: generateAssetManifest(baseUrl), + metadata: {}, + extra: { + expoClient: { + name: "LSTScanner", + slug: "lst-scanner-app", + version: "1.0.0", + runtimeVersion: expectedRuntimeVersion, + }, + }, + }; + + console.log( + "Returning manifest:", + JSON.stringify(manifest, null, 2) + ); + + res.setHeader("Content-Type", "application/json"); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("expo-protocol-version", "1"); + res.setHeader("expo-sfv-version", "0"); + res.json(manifest); + } catch (error: any) { + console.error("Error generating manifest:", error); + res.status(500).json({ + error: "Failed to generate manifest", + details: error.message, + stack: error.stack, + }); + } + }); + + // Serve static files + app.use( + basePath + "/api/mobile/updates", + express.static(distPath, { + setHeaders(res, path) { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Cache-Control", "public, max-age=31536000"); + + if (path.endsWith(".hbc")) { + res.setHeader("Content-Type", "application/javascript"); + } + }, + }) + ); + // app.use( + // basePath + "/api/mobile/updates", + // express.static(join(__dirname, mobileDir), { + // setHeaders(res) { + // // OTA runtime needs to fetch these from the device + // console.log("OTA check called"); + // res.setHeader("Access-Control-Allow-Origin", "*"); + // }, + // }) + // ); + + // app.get(basePath + "/api/mobile/updates", (req, res) => { + // res.redirect(basePath + "/api/mobile/updates/metadata.json"); + // }); + + app.get(basePath + "/api/mobile", (_, res) => + res.status(200).json({ message: "LST OTA server is up." }) + ); +}; diff --git a/app/src/internal/routerHandler/routeHandler.ts b/app/src/internal/routerHandler/routeHandler.ts index 646a35e..44bbc01 100644 --- a/app/src/internal/routerHandler/routeHandler.ts +++ b/app/src/internal/routerHandler/routeHandler.ts @@ -4,22 +4,24 @@ import { setupAuthRoutes } from "../auth/routes/routes.js"; import { setupForkliftRoutes } from "../forklifts/routes/routes.js"; import { setupLogisticsRoutes } from "../logistics/routes.js"; import { setupSystemRoutes } from "../system/routes.js"; +import { setupMobileRoutes } from "../mobile/route.js"; export const setupRoutes = (app: Express, basePath: string) => { - // all routes - setupAuthRoutes(app, basePath); - setupAdminRoutes(app, basePath); - setupSystemRoutes(app, basePath); - setupLogisticsRoutes(app, basePath); - setupForkliftRoutes(app, basePath); + // all routes + setupAuthRoutes(app, basePath); + setupAdminRoutes(app, basePath); + setupSystemRoutes(app, basePath); + setupLogisticsRoutes(app, basePath); + setupForkliftRoutes(app, basePath); + setupMobileRoutes(app, basePath); - // always try to go to the app weather we are in dev or in production. - app.get(basePath + "/", (req: Request, res: Response) => { - res.redirect(basePath + "/app"); - }); + // always try to go to the app weather we are in dev or in production. + app.get(basePath + "/", (req: Request, res: Response) => { + res.redirect(basePath + "/app"); + }); - // Fallback 404 handler - app.use((req: Request, res: Response) => { - res.status(404).json({ error: "Not Found" }); - }); + // Fallback 404 handler + app.use((req: Request, res: Response) => { + res.status(404).json({ error: "Not Found" }); + }); };