From f37eb77140a38b924f16280d6abb46d953555468 Mon Sep 17 00:00:00 2001 From: Jason Hankins Date: Wed, 25 Feb 2026 12:02:26 -0500 Subject: [PATCH 01/12] initial portless config, remove old subdomain work, add workarounds for api & docs to enable https --- .certs/.gitignore | 7 -- apps/api/package.json | 2 +- apps/api/start-dev.mjs | 4 + apps/docs/package.json | 2 +- apps/docs/start-dev.mjs | 4 + apps/ui-sharethrift/.env | 10 +-- apps/ui-sharethrift/package.json | 2 +- apps/ui-sharethrift/vite.config.ts | 33 +------- package.json | 6 +- .../cellix/mock-oauth2-server/package.json | 4 +- .../cellix/mock-oauth2-server/src/index.ts | 63 +++----------- .../cellix/mock-payment-server/package.json | 2 +- .../cellix/mock-payment-server/src/index.ts | 84 +++++-------------- .../messaging-service-mock/src/index.test.ts | 2 +- .../mock-messaging-server/package.json | 2 +- .../mock-messaging-server/src/index.ts | 48 +++-------- scripts/dev-cleanup.mjs | 36 ++++++++ 17 files changed, 107 insertions(+), 204 deletions(-) delete mode 100644 .certs/.gitignore create mode 100644 apps/api/start-dev.mjs create mode 100644 apps/docs/start-dev.mjs create mode 100644 scripts/dev-cleanup.mjs diff --git a/.certs/.gitignore b/.certs/.gitignore deleted file mode 100644 index 3153bdae2..000000000 --- a/.certs/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Ignore all certificate files -* - - -!.gitignore - - diff --git a/apps/api/package.json b/apps/api/package.json index 47845a3f6..a02b1423a 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -16,7 +16,7 @@ "lint": "biome lint", "clean": "rimraf dist", "prestart": "pnpm run clean && pnpm run build", - "start": "func start --typescript", + "start": "portless data-access.sharethrift.localhost node start-dev.mjs", "azurite": "azurite-blob --silent --location ../../__blobstorage__ & azurite-queue --silent --location ../../__queuestorage__ & azurite-table --silent --location ../../__tablestorage__" }, "dependencies": { diff --git a/apps/api/start-dev.mjs b/apps/api/start-dev.mjs new file mode 100644 index 000000000..0fdbf4601 --- /dev/null +++ b/apps/api/start-dev.mjs @@ -0,0 +1,4 @@ +import { spawn } from 'node:child_process'; + +const port = process.env.PORT ?? '7071'; +spawn('func', ['start', '--typescript', '--port', port], { stdio: 'inherit' }); diff --git a/apps/docs/package.json b/apps/docs/package.json index 081355f98..b849a1848 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "docusaurus": "docusaurus", - "start": "docusaurus start --port 3002 --host docs.sharethrift.localhost --no-open", + "start": "portless docs.sharethrift.localhost node start-dev.mjs", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", diff --git a/apps/docs/start-dev.mjs b/apps/docs/start-dev.mjs new file mode 100644 index 000000000..b164c9546 --- /dev/null +++ b/apps/docs/start-dev.mjs @@ -0,0 +1,4 @@ +import { spawn } from 'node:child_process'; + +const port = process.env.PORT ?? '3002'; +spawn('docusaurus', ['start', '--port', port, '--no-open'], { stdio: 'inherit' }); diff --git a/apps/ui-sharethrift/.env b/apps/ui-sharethrift/.env index db67a1ee4..0aab99724 100644 --- a/apps/ui-sharethrift/.env +++ b/apps/ui-sharethrift/.env @@ -1,11 +1,11 @@ VITE_B2C_CLIENTID=mock-client -VITE_B2C_AUTHORITY=https://mock-auth.sharethrift.localhost:4000 -VITE_B2C_REDIRECT_URI=https://sharethrift.localhost:3000/auth-redirect-user +VITE_B2C_AUTHORITY=https://mock-auth.sharethrift.localhost:1355 +VITE_B2C_REDIRECT_URI=https://sharethrift.localhost:1355/auth-redirect-user VITE_B2C_SCOPE=openid user-portal -VITE_FUNCTION_ENDPOINT=https://data-access.sharethrift.localhost:7072/api/graphql +VITE_FUNCTION_ENDPOINT=https://data-access.sharethrift.localhost:1355/api/graphql # Admin Portal OAuth Config VITE_B2C_ADMIN_CLIENTID=mock-client -VITE_B2C_ADMIN_AUTHORITY=https://mock-auth.sharethrift.localhost:4000 -VITE_B2C_ADMIN_REDIRECT_URI=https://sharethrift.localhost:3000/auth-redirect-admin +VITE_B2C_ADMIN_AUTHORITY=https://mock-auth.sharethrift.localhost:1355 +VITE_B2C_ADMIN_REDIRECT_URI=https://sharethrift.localhost:1355/auth-redirect-admin VITE_B2C_ADMIN_SCOPE=openid admin-portal \ No newline at end of file diff --git a/apps/ui-sharethrift/package.json b/apps/ui-sharethrift/package.json index df35b33ec..f0b3c7ee3 100644 --- a/apps/ui-sharethrift/package.json +++ b/apps/ui-sharethrift/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "start": "vite", + "start": "portless sharethrift.localhost vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", diff --git a/apps/ui-sharethrift/vite.config.ts b/apps/ui-sharethrift/vite.config.ts index 4f09ec4a7..e3b1e5695 100644 --- a/apps/ui-sharethrift/vite.config.ts +++ b/apps/ui-sharethrift/vite.config.ts @@ -1,38 +1,11 @@ -import fs from 'node:fs'; -import path from 'node:path'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; -const { NODE_ENV } = process.env; -const isDev = NODE_ENV === 'development'; - -// Check if certificates exist (local dev only) -const certKeyPath = path.resolve(__dirname, '../../.certs/sharethrift.localhost-key.pem'); -const certPath = path.resolve(__dirname, '../../.certs/sharethrift.localhost.pem'); -const hasCerts = fs.existsSync(certKeyPath) && fs.existsSync(certPath); - -const baseServerConfig = { - port: 3000, - open: true, -}; - -const localServerConfig = { - ...baseServerConfig, - host: '0.0.0.0', - ...(hasCerts - ? { - https: { - key: fs.readFileSync(certKeyPath), - cert: fs.readFileSync(certPath), - }, - } - : {}), - open: hasCerts ? 'https://sharethrift.localhost:3000' : 'http://localhost:3000', -}; - export default defineConfig(() => { return { plugins: [react()], - server: isDev ? localServerConfig : baseServerConfig, + server: { + open: true, + }, }; }); diff --git a/package.json b/package.json index 8cafe1189..417cf3cc5 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,7 @@ "build": "turbo run build", "test": "turbo run test", "lint": "turbo run lint", - "setup:certs": "node scripts/setup-local-certs.js", - "proxy:start": "node build-pipeline/scripts/local-https-proxy.js", - "dev": "pnpm run build && pnpm run setup:certs && turbo run //#proxy:start azurite gen:watch start --parallel", + "dev": "pnpm run build && node scripts/dev-cleanup.mjs && portless proxy start --https && turbo run azurite gen:watch start --parallel", "start": "turbo run build && concurrently pnpm:start:* --kill-others-on-fail --workspace=@sthrift/api", "format": "turbo run format", "gen": "graphql-codegen --config codegen.yml", @@ -35,7 +33,7 @@ "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", "test:integration": "turbo run test:integration", - "test:serenity": "turbo run test:serenity", + "test:serenity": "turbo run test:serenity", "test:serenity:report": "turbo run test:serenity:report", "test:unit": "turbo run test:unit", "test:watch": "turbo run test:watch --concurrency 15", diff --git a/packages/cellix/mock-oauth2-server/package.json b/packages/cellix/mock-oauth2-server/package.json index 1f402e89d..82aef470f 100644 --- a/packages/cellix/mock-oauth2-server/package.json +++ b/packages/cellix/mock-oauth2-server/package.json @@ -12,8 +12,8 @@ "clean": "rimraf dist node_modules tsconfig.tsbuildinfo && tsc --build --clean", "lint": "biome lint", "format": "biome format --write", - "start": "node dist/src/index.js", - "dev": "tsx watch src/index.ts", + "start": "portless mock-auth.sharethrift.localhost node dist/src/index.js", + "dev": "portless mock-auth.sharethrift.localhost tsx watch src/index.ts", "prebuild": "biome lint" }, "dependencies": { diff --git a/packages/cellix/mock-oauth2-server/src/index.ts b/packages/cellix/mock-oauth2-server/src/index.ts index c1e32462b..f50d731b7 100644 --- a/packages/cellix/mock-oauth2-server/src/index.ts +++ b/packages/cellix/mock-oauth2-server/src/index.ts @@ -1,8 +1,4 @@ import crypto, { type KeyObject, type webcrypto } from 'node:crypto'; -import fs from 'node:fs'; -import https from 'node:https'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import express from 'express'; import { exportJWK, @@ -17,7 +13,9 @@ setupEnvironment(); const app = express(); app.disable('x-powered-by'); -const port = 4000; +const port = Number(process.env['PORT'] ?? 4000); +// biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation for index signatures +const BASE_URL = process.env['BASE_URL'] ?? 'https://mock-auth.sharethrift.localhost:1355'; function normalizeUrl(urlString: string): string { try { @@ -35,15 +33,15 @@ function normalizeUrl(urlString: string): string { const allowedRedirectUris = new Set([ 'http://localhost:3000/auth-redirect-user', 'http://localhost:3000/auth-redirect-admin', - 'https://sharethrift.localhost:3000/auth-redirect-user', - 'https://sharethrift.localhost:3000/auth-redirect-admin', + 'https://sharethrift.localhost:1355/auth-redirect-user', + 'https://sharethrift.localhost:1355/auth-redirect-admin', ]); // Map redirect URIs to their corresponding audience identifiers const redirectUriToAudience = new Map([ ['http://localhost:3000/auth-redirect-user', 'user-portal'], ['http://localhost:3000/auth-redirect-admin', 'admin-portal'], - ['https://sharethrift.localhost:3000/auth-redirect-user', 'user-portal'], - ['https://sharethrift.localhost:3000/auth-redirect-admin', 'admin-portal'], + ['https://sharethrift.localhost:1355/auth-redirect-user', 'user-portal'], + ['https://sharethrift.localhost:1355/auth-redirect-admin', 'admin-portal'], ]); // Deprecated: kept for backwards compatibility. Uses process.env's index signature // (string | undefined) and falls back to a default redirect URI when not set. @@ -141,23 +139,6 @@ async function buildTokenResponse( // Main async startup async function main() { - // Always resolve .certs from monorepo root (works regardless of script location or cwd) - const projectRoot = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../../', - ); - const certKeyPath = path.join( - projectRoot, - '.certs/sharethrift.localhost-key.pem', - ); - const certPath = path.join(projectRoot, '.certs/sharethrift.localhost.pem'); - const hasCerts = fs.existsSync(certKeyPath) && fs.existsSync(certPath); - - // Set BASE_URL based on whether we have certificates - const BASE_URL = hasCerts - ? `https://mock-auth.sharethrift.localhost:${port}` - : `http://localhost:${port}`; - // Generate signing keypair with jose const { publicKey, privateKey } = await generateKeyPair('RS256'); const publicJwk = await exportJWK(publicKey); @@ -312,30 +293,12 @@ async function main() { return; }); - // Load SSL certificates for HTTPS - if (hasCerts) { - const httpsOptions = { - key: fs.readFileSync(certKeyPath), - cert: fs.readFileSync(certPath), - }; - - https - .createServer(httpsOptions, app) - .listen(port, 'mock-auth.sharethrift.localhost', () => { - // eslint-disable-next-line no-console - console.log(`Mock OAuth2 server running on ${BASE_URL}`); - console.log( - `JWKS endpoint running on ${BASE_URL}/.well-known/jwks.json`, - ); - }); - } else { - // Fallback to HTTP when certs don't exist (CI/CD) - app.listen(port, () => { - // eslint-disable-next-line no-console - console.log(`Mock OAuth2 server running on ${BASE_URL} (no certs found)`); - console.log(`JWKS endpoint running on ${BASE_URL}/.well-known/jwks.json`); - }); - } + // Start HTTP server (portless handles TLS/proxy at the subdomain level) + app.listen(port, () => { + // eslint-disable-next-line no-console + console.log(`Mock OAuth2 server running on ${BASE_URL}`); + console.log(`JWKS endpoint running on ${BASE_URL}/.well-known/jwks.json`); + }); } main(); diff --git a/packages/cellix/mock-payment-server/package.json b/packages/cellix/mock-payment-server/package.json index b34f69497..5b2238e16 100644 --- a/packages/cellix/mock-payment-server/package.json +++ b/packages/cellix/mock-payment-server/package.json @@ -21,7 +21,7 @@ "typescript": "^5.0.0" }, "scripts": { - "start": "node dist/src/index.js", + "start": "portless mock-payment.sharethrift.localhost node dist/src/index.js", "build": "tsc --build && node dist/src/copy-assets.js", "clean": "rimraf node_modules package-lock.json dist" } diff --git a/packages/cellix/mock-payment-server/src/index.ts b/packages/cellix/mock-payment-server/src/index.ts index cd7ddf3ef..658853e6b 100644 --- a/packages/cellix/mock-payment-server/src/index.ts +++ b/packages/cellix/mock-payment-server/src/index.ts @@ -1,7 +1,5 @@ import { fileURLToPath } from 'node:url'; import express from 'express'; -import https from 'node:https'; -import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { generateKeyPair } from 'jose'; @@ -31,21 +29,9 @@ import type { const app = express(); app.disable('x-powered-by'); const DEFAULT_PORT = Number(process.env['PORT'] ?? 3001); -const HOST = 'mock-payment.sharethrift.localhost'; -// Detect certificate availability to determine protocol (HTTPS vs HTTP) -const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../..'); -const certKeyPath = path.join(projectRoot, '.certs/sharethrift.localhost-key.pem'); -const certPath = path.join(projectRoot, '.certs/sharethrift.localhost.pem'); -const hasCerts = fs.existsSync(certKeyPath) && fs.existsSync(certPath); - -// Derive protocol and base URLs based on cert availability -const PROTOCOL = hasCerts ? 'https' : 'http'; -const FRONTEND_HOST = hasCerts ? 'sharethrift.localhost:3000' : 'localhost:3000'; -const PAYMENT_HOST = hasCerts ? `${HOST}:${DEFAULT_PORT}` : `localhost:${DEFAULT_PORT}`; - -const FRONTEND_BASE_URL = `${PROTOCOL}://${FRONTEND_HOST}`; -const PAYMENT_BASE_URL = `${PROTOCOL}://${PAYMENT_HOST}`; +const FRONTEND_BASE_URL = 'https://sharethrift.localhost:1355'; +const PAYMENT_BASE_URL = 'https://mock-payment.sharethrift.localhost:1355'; // Enable CORS for all origins (or restrict to 'https://sharethrift.localhost:3000' if needed) app.use((req, res, next) => { @@ -1355,55 +1341,27 @@ app.post( ); const startServer = (portToTry: number, attempt = 0): void => { - if (hasCerts) { - const httpsOptions = { - key: fs.readFileSync(certKeyPath), - cert: fs.readFileSync(certPath), - }; - - const server = https.createServer(httpsOptions, app).listen(portToTry, HOST, () => { - console.log(` Mock Payment Server listening on https://${HOST}:${portToTry}`); - console.log(` CORS origin: ${FRONTEND_BASE_URL}`); - console.log(` Microform origin: ${PAYMENT_BASE_URL}`); - }); - - server.on('error', (error: NodeJS.ErrnoException) => { - if (error.code === 'EADDRINUSE' && attempt < 5) { - const nextPort = portToTry + 1; - console.warn( - `Port ${portToTry} in use. Retrying mock-payment-server on ${nextPort}...`, - ); - server.close(() => { - startServer(nextPort, attempt + 1); - }); - return; - } + // HTTP server — portless handles TLS/proxy at the subdomain level + const server = app.listen(portToTry, () => { + console.log(` Mock Payment Server listening on http://localhost:${portToTry}`); + console.log(` CORS origin: ${FRONTEND_BASE_URL}`); + console.log(` Microform origin: ${PAYMENT_BASE_URL}`); + }); - console.error('Failed to start mock-payment-server', error); - }); - } else { - // Fallback to HTTP when certs don't exist (CI/CD) - const server = app.listen(portToTry, () => { - console.log(` Mock Payment Server listening on http://localhost:${portToTry} (no certs found)`); - console.log(` CORS origin: ${FRONTEND_BASE_URL}`); - console.log(` Microform origin: ${PAYMENT_BASE_URL}`); - }); - - server.on('error', (error: NodeJS.ErrnoException) => { - if (error.code === 'EADDRINUSE' && attempt < 5) { - const nextPort = portToTry + 1; - console.warn( - `Port ${portToTry} in use. Retrying mock-payment-server on ${nextPort}...`, - ); - server.close(() => { - startServer(nextPort, attempt + 1); - }); - return; - } + server.on('error', (error: NodeJS.ErrnoException) => { + if (error.code === 'EADDRINUSE' && attempt < 5) { + const nextPort = portToTry + 1; + console.warn( + `Port ${portToTry} in use. Retrying mock-payment-server on ${nextPort}...`, + ); + server.close(() => { + startServer(nextPort, attempt + 1); + }); + return; + } - console.error('Failed to start mock-payment-server', error); - }); - } + console.error('Failed to start mock-payment-server', error); + }); }; startServer(DEFAULT_PORT); diff --git a/packages/sthrift/messaging-service-mock/src/index.test.ts b/packages/sthrift/messaging-service-mock/src/index.test.ts index 3358baf39..eb0bcaba2 100644 --- a/packages/sthrift/messaging-service-mock/src/index.test.ts +++ b/packages/sthrift/messaging-service-mock/src/index.test.ts @@ -14,7 +14,7 @@ describe('ServiceMessagingMock Integration Tests', () => { process.env['MESSAGING_MOCK_URL'] = MOCK_SERVER_URL; // Force HTTP mode for tests to avoid certificate issues - mockServer = await startServer(MOCK_SERVER_PORT, true, false); +mockServer = await startServer(MOCK_SERVER_PORT, true); await new Promise((resolve) => setTimeout(resolve, 500)); }, 15000); diff --git a/packages/sthrift/mock-messaging-server/package.json b/packages/sthrift/mock-messaging-server/package.json index c15e1c736..1d540c7ea 100644 --- a/packages/sthrift/mock-messaging-server/package.json +++ b/packages/sthrift/mock-messaging-server/package.json @@ -11,7 +11,7 @@ "prebuild": "biome lint", "build": "tsc --build", "clean": "rimraf dist", - "start": "node -r dotenv/config dist/src/index.js", + "start": "portless mock-messaging.sharethrift.localhost node -r dotenv/config dist/src/index.js", "dev": "tsc-watch --onSuccess \"node -r dotenv/config dist/src/index.js\"", "test": "vitest run", "test:watch": "vitest" diff --git a/packages/sthrift/mock-messaging-server/src/index.ts b/packages/sthrift/mock-messaging-server/src/index.ts index de6761886..c310169a8 100644 --- a/packages/sthrift/mock-messaging-server/src/index.ts +++ b/packages/sthrift/mock-messaging-server/src/index.ts @@ -1,9 +1,5 @@ -import { fileURLToPath } from 'node:url'; import express from 'express'; -import https from 'node:https'; import http from 'node:http'; -import fs from 'node:fs'; -import path from 'node:path'; import type { Request, Response, Application } from 'express'; import type { Server } from 'node:http'; import { config } from 'dotenv'; @@ -75,41 +71,19 @@ export function createApp(): Application { return app; } -export function startServer(port = 10000, seedData = false, useHttps = true): Promise { +export function startServer(port = 10000, seedData = false): Promise { return new Promise((resolve) => { const app = createApp(); - // Always resolve .certs from monorepo root (works regardless of script location or cwd) - const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../..'); - const certKeyPath = path.join(projectRoot, '.certs/sharethrift.localhost-key.pem'); - const certPath = path.join(projectRoot, '.certs/sharethrift.localhost.pem'); - const hasCerts = fs.existsSync(certKeyPath) && fs.existsSync(certPath); - if (hasCerts && useHttps) { - const httpsOptions = { - key: fs.readFileSync(certKeyPath), - cert: fs.readFileSync(certPath), - }; - const server = https.createServer(httpsOptions, app).listen(port, 'mock-messaging.sharethrift.localhost', () => { - console.log(` Mock Messaging Server listening on https://mock-messaging.sharethrift.localhost:${port}`); - if (seedData) { - seedMockData(); - } else { - console.log('Starting with empty data store (set seedData=true to seed)'); - } - resolve(server); - }); - } else { - // Fallback to HTTP when certs don't exist or useHttps=false (tests/CI/CD) - const server = http.createServer(app).listen(port, () => { - const reason = !hasCerts ? '(no certs found)' : '(HTTP mode)'; - console.log(` Mock Messaging Server listening on http://localhost:${port} ${reason}`); - if (seedData) { - seedMockData(); - } else { - console.log('Starting with empty data store (set seedData=true to seed)'); - } - resolve(server); - }); - } + // HTTP server — portless handles TLS/proxy at the subdomain level + const server = http.createServer(app).listen(port, () => { + console.log(` Mock Messaging Server listening on http://localhost:${port}`); + if (seedData) { + seedMockData(); + } else { + console.log('Starting with empty data store (set seedData=true to seed)'); + } + resolve(server); + }); }); } diff --git a/scripts/dev-cleanup.mjs b/scripts/dev-cleanup.mjs new file mode 100644 index 000000000..f70b85f47 --- /dev/null +++ b/scripts/dev-cleanup.mjs @@ -0,0 +1,36 @@ +#!/usr/bin/env node +/** + * Kills any leftover dev server processes from a previous run before starting fresh. + * Runs portless proxy stop, then kills known mock-server and func processes by port/name. + */ +import { execSync, spawnSync } from 'node:child_process'; + +function run(cmd) { + try { + execSync(cmd, { stdio: 'ignore' }); + } catch { + // ignore — process may not exist + } +} + +// Stop portless proxy (clears all route registrations) +run('portless proxy stop'); + +// Kill any leftover mock server or func processes by matching their command lines +const patterns = [ + 'mock-messaging-server.*index.js', + 'mock-payment-server.*index.js', + 'mock-oauth2-server.*index.js', + 'mock-mongodb-memory-server.*index.js', + 'func start --typescript', + 'start-dev.mjs', +]; + +for (const pattern of patterns) { + run(`pkill -f '${pattern}'`); +} + +// Small pause to let OS reclaim ports before proxy restarts +await new Promise((r) => setTimeout(r, 500)); + +console.log('dev cleanup done'); From 8d297ddaf3fff8c9ea0162c6eadbb699225c75e7 Mon Sep 17 00:00:00 2001 From: Jason Hankins Date: Wed, 25 Feb 2026 16:12:44 -0500 Subject: [PATCH 02/12] fix docusaurus config to use portless --- apps/docs/docusaurus.config.ts | 32 ------- apps/docs/start-dev.mjs | 4 +- apps/ui-sharethrift/vite.config.ts | 2 +- package.json | 2 +- scripts/setup-local-certs.js | 139 ----------------------------- 5 files changed, 5 insertions(+), 174 deletions(-) delete mode 100644 scripts/setup-local-certs.js diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts index f9abac05b..4e14ac720 100644 --- a/apps/docs/docusaurus.config.ts +++ b/apps/docs/docusaurus.config.ts @@ -1,8 +1,6 @@ import { themes as prismThemes } from 'prism-react-renderer'; import type { Config } from '@docusaurus/types'; import type * as Preset from '@docusaurus/preset-classic'; -import path from 'node:path'; -import fs from 'node:fs'; // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) @@ -158,36 +156,6 @@ const config: Config = { darkTheme: prismThemes.dracula, }, } satisfies Preset.ThemeConfig, - - // Custom webpack configuration for HTTPS dev server - plugins: [ - function httpsPlugin() { - return { - name: 'https-plugin', - configureWebpack() { - const workspaceRoot = path.resolve(__dirname, '../../'); - const certKeyPath = path.join(workspaceRoot, '.certs/sharethrift.localhost-key.pem'); - const certPath = path.join(workspaceRoot, '.certs/sharethrift.localhost.pem'); - const hasCerts = fs.existsSync(certKeyPath) && fs.existsSync(certPath); - - if (hasCerts) { - return { - devServer: { - server: { - type: 'https', - options: { - key: fs.readFileSync(certKeyPath), - cert: fs.readFileSync(certPath), - }, - }, - }, - }; - } - return {}; - }, - }; - }, - ], }; export default config; diff --git a/apps/docs/start-dev.mjs b/apps/docs/start-dev.mjs index b164c9546..afcf08c65 100644 --- a/apps/docs/start-dev.mjs +++ b/apps/docs/start-dev.mjs @@ -1,4 +1,6 @@ import { spawn } from 'node:child_process'; const port = process.env.PORT ?? '3002'; -spawn('docusaurus', ['start', '--port', port, '--no-open'], { stdio: 'inherit' }); +// Use 127.0.0.1 explicitly to ensure IPv4 binding — portless proxy connects via IPv4, +// but Node.js may resolve 'localhost' to ::1 (IPv6) on macOS, causing Bad Gateway. +spawn('pnpm', ['exec', 'docusaurus', 'start', '--host', '127.0.0.1', '--port', port, '--no-open'], { stdio: 'inherit' }); diff --git a/apps/ui-sharethrift/vite.config.ts b/apps/ui-sharethrift/vite.config.ts index e3b1e5695..659fb3d09 100644 --- a/apps/ui-sharethrift/vite.config.ts +++ b/apps/ui-sharethrift/vite.config.ts @@ -5,7 +5,7 @@ export default defineConfig(() => { return { plugins: [react()], server: { - open: true, + open: 'https://sharethrift.localhost:1355', }, }; }); diff --git a/package.json b/package.json index 417cf3cc5..660add2f9 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "turbo run build", "test": "turbo run test", "lint": "turbo run lint", - "dev": "pnpm run build && node scripts/dev-cleanup.mjs && portless proxy start --https && turbo run azurite gen:watch start --parallel", + "dev": "pnpm run build && node scripts/dev-cleanup.mjs && portless proxy start --https && turbo run azurite gen:watch start --parallel", "start": "turbo run build && concurrently pnpm:start:* --kill-others-on-fail --workspace=@sthrift/api", "format": "turbo run format", "gen": "graphql-codegen --config codegen.yml", diff --git a/scripts/setup-local-certs.js b/scripts/setup-local-certs.js deleted file mode 100644 index 6c42102a6..000000000 --- a/scripts/setup-local-certs.js +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env node - -/** - * ShareThrift Local HTTPS Certificate Setup - * Automatically installs mkcert and generates wildcard SSL certificates for local development - */ - -import { execSync } from 'node:child_process'; -import { existsSync, mkdirSync, readdirSync, renameSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const CERT_DIR = join(__dirname, '..', '.certs'); -const CERT_FILE = join(CERT_DIR, 'sharethrift.localhost.pem'); -const KEY_FILE = join(CERT_DIR, 'sharethrift.localhost-key.pem'); - -function exec(command, options = {}) { - try { - return execSync(command, { stdio: 'inherit', ...options }); - } catch (error) { - if (!options.ignoreError) throw error; - return null; - } -} - -function checkCommand(command) { - try { - const platform = process.platform; - const checkCmd = platform === 'win32' ? `where ${command}` : `command -v ${command}`; - execSync(checkCmd, { stdio: 'ignore' }); - return true; - } catch { - return false; - } -} - -function main() { - console.log(' ShareThrift Local HTTPS Certificate Setup\n'); - - // Check if certificates already exist - if (existsSync(CERT_FILE) && existsSync(KEY_FILE)) { - console.log(' Certificates already exist - skipping setup\n'); - return; - } - - console.log(' Setting up local HTTPS certificates...\n'); - - // Check if mkcert is installed - if (!checkCommand('mkcert')) { - console.log(' Installing mkcert...'); - - const platform = process.platform; - - if (platform === 'darwin') { - // macOS - if (checkCommand('brew')) { - exec('brew install mkcert'); - } else { - console.error(' Error: Homebrew not found. Please install Homebrew first:'); - console.error(' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'); - process.exit(1); - } - } else if (platform === 'linux') { - // Linux - if (checkCommand('apt-get')) { - exec('sudo apt-get update && sudo apt-get install -y mkcert'); - } else if (checkCommand('yum')) { - exec('sudo yum install -y mkcert'); - } else { - console.error(' Error: Package manager not supported. Please install mkcert manually:'); - console.error(' https://github.com/FiloSottile/mkcert#installation'); - process.exit(1); - } - } else if (platform === 'win32') { - // Windows - if (checkCommand('choco')) { - exec('choco install mkcert -y'); - } else if (checkCommand('scoop')) { - exec('scoop install mkcert'); - } else { - console.error(' Error: Please install mkcert manually:'); - console.error(' https://github.com/FiloSottile/mkcert#windows'); - process.exit(1); - } - } else { - console.error(' Error: OS not supported. Please install mkcert manually:'); - console.error(' https://github.com/FiloSottile/mkcert#installation'); - process.exit(1); - } - - console.log(' mkcert installed\n'); - } else { - console.log(' mkcert already installed\n'); - } - - // Install the local CA - console.log(' Installing local Certificate Authority...'); - exec('mkcert -install'); - console.log(' CA installed\n'); - - // Generate wildcard certificate - console.log(' Generating wildcard certificate for *.sharethrift.localhost...'); - - // Ensure certificate directory exists - mkdirSync(CERT_DIR, { recursive: true }); - - process.chdir(CERT_DIR); - exec('mkcert "*.sharethrift.localhost" "sharethrift.localhost" localhost 127.0.0.1 ::1'); - - // Rename files to standard names - const generatedFiles = readdirSync(CERT_DIR) - .filter(file => file.includes('.localhost+4') && file.endsWith('.pem')); - - for (const file of generatedFiles) { - const oldPath = join(CERT_DIR, file); - if (file.includes('-key.pem')) { - renameSync(oldPath, KEY_FILE); - } else if (file.includes('.pem')) { - renameSync(oldPath, CERT_FILE); - } - } - - console.log(' Certificates generated\n'); - console.log(' Certificate location:'); - console.log(` ${CERT_DIR}\n`); - console.log(' Your local domains are now trusted for HTTPS:'); - console.log(' • https://sharethrift.localhost:3000 (UI)'); - console.log(' • https://data-access.sharethrift.localhost:7072 (API)'); - console.log(' • https://docs.sharethrift.localhost:3002 (Docs)'); - console.log(' • https://mock-auth.sharethrift.localhost:4000 (Auth)'); - console.log(' • https://mock-payment.sharethrift.localhost:3001 (Payment)'); - console.log(' • https://mock-messaging.sharethrift.localhost:10000 (Messaging)'); - console.log(' • mongodb://mongodb.sharethrift.localhost:50000 (MongoDB)\n'); - console.log(' Setup complete! Run: pnpm run dev\n'); -} - -main(); From 6cf3896d42a7aa810aa86711180a880b1076cfab Mon Sep 17 00:00:00 2001 From: Jason Hankins Date: Wed, 25 Feb 2026 16:16:51 -0500 Subject: [PATCH 03/12] update knip to ignore unlisted portless binary --- knip.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/knip.json b/knip.json index 10bb5b01f..e22bf768c 100644 --- a/knip.json +++ b/knip.json @@ -101,5 +101,5 @@ "graphql", "rxjs" ], - "ignoreBinaries": ["func", "open", "concurrently", "container"] + "ignoreBinaries": ["func", "open", "concurrently", "container", "portless"] } From 2f20998083ee1d39bfc88060512efd12fd50c2a5 Mon Sep 17 00:00:00 2001 From: Jason Hankins <111989518+jason-t-hankins@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:52:36 -0500 Subject: [PATCH 04/12] Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- scripts/dev-cleanup.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dev-cleanup.mjs b/scripts/dev-cleanup.mjs index f70b85f47..d721454b7 100644 --- a/scripts/dev-cleanup.mjs +++ b/scripts/dev-cleanup.mjs @@ -3,7 +3,7 @@ * Kills any leftover dev server processes from a previous run before starting fresh. * Runs portless proxy stop, then kills known mock-server and func processes by port/name. */ -import { execSync, spawnSync } from 'node:child_process'; +import { execSync } from 'node:child_process'; function run(cmd) { try { From eebfeb0684a0ae93633586d0d1faad08b964c6fd Mon Sep 17 00:00:00 2001 From: Jason Hankins Date: Thu, 26 Feb 2026 12:12:13 -0500 Subject: [PATCH 05/12] create ADR to document experience and recommend portless --- .../decisions/0025-portless-local-https.md | 76 ++++++++++ .../localhost-subdomain-setup.md | 133 ------------------ 2 files changed, 76 insertions(+), 133 deletions(-) create mode 100644 apps/docs/docs/decisions/0025-portless-local-https.md delete mode 100644 apps/docs/docs/technical-overview/localhost-subdomain-setup.md diff --git a/apps/docs/docs/decisions/0025-portless-local-https.md b/apps/docs/docs/decisions/0025-portless-local-https.md new file mode 100644 index 000000000..e6df18c14 --- /dev/null +++ b/apps/docs/docs/decisions/0025-portless-local-https.md @@ -0,0 +1,76 @@ +--- +sidebar_position: 25 +sidebar_label: 0025 Portless Local HTTPS +description: "Decision record for replacing mkcert + manual HTTPS proxy with portless for local development." +status: +date: 2026-02-26 +deciders: +--- + +# Local HTTPS Development: portless vs mkcert + manual proxy + +## Context and Problem Statement + +Local development benefits from HTTPS with named subdomains (e.g. `*.sharethrift.localhost`) to accurately mirror production behaviour: OAuth redirect URIs, CORS policies, cookies, and AI agents all depend on a consistent HTTPS origin. Without this, the local environment diverges from production in ways that are hard to detect until deployment. + +The original solution used `mkcert` to generate a wildcard certificate stored in `.certs/`, combined with a hand-written `local-https-proxy.js` to front Azure Functions, necessary due to `func start`'s broken `--cert` flag in v4.2.2. Each service ran on its own numbered port, requiring developers to remember and configure many separate port assignments. A solution is needed that is easier to maintain and reduces the mental load for developers. + +## Decision Drivers + +- Developers should not need to manually manage TLS certificates +- All local services should be accessible via consistent, named HTTPS URLs +- The approach must not affect production builds or CI pipelines +- Multiple ports across services makes `.env` configuration error-prone +- AI Agents should be able to work on the subdomains + +## Considered Options + +- **portless** - globally installed reverse proxy daemon that maps subdomains to local ports with auto-trusted TLS certificates +- **mkcert + manual HTTPS proxy** - existing approach using a wildcard cert and a custom Node.js HTTPS proxy + + +### Consequences - portless + +**Positive** + +- No certificate management; TLS certs are auto-generated and auto-trusted +- Single port (`1355`) for all services: `.env` and `local.settings.json` are simpler +- Subdomain names in URLs (`data-access`, `mock-auth`, etc.) make it immediately obvious which service is being called +- `local-https-proxy.js` is deleted, now one less script to maintain +- Removes the need for compatibility issues with Azure Function's --cert flag + +**Negative** + +- Requires a one-time global install: `pnpm install -g portless` +- `func start` and Docusaurus do not respect the `PORT` environment variable injected by portless; thin `start-dev.mjs` wrapper scripts are required to read `process.env.PORT` and pass `--port` explicitly +- On macOS, bare `localhost` resolves to `::1` (IPv6), but portless connects via `127.0.0.1` (IPv4); Docusaurus must be started with `--host 127.0.0.1` to avoid a Bad Gateway error +- `portless proxy start --https` silently no-ops if the proxy is already running in HTTP mode; a `dev-cleanup.mjs` script is needed to run `portless proxy stop` and kill zombie processes before each dev session + +### Consequences - mkcert + +**Positive** + +- No global tooling dependency and certificates live in the repo (gitignored) +- Works with any port assignment without daemon management + +**Negative** + +- Developers neeed to run `mkcert -install` and `mkcert` on each machine +- Certificate files require explicit exclusion from version control +- `local-https-proxy.js` must be kept in sync with Azure Functions behaviour +- CI required `fs.existsSync()` guards to skip HTTPS when certs are absent +- Many different port numbers to configure across `.env`, `local.settings.json`, and docs + +## Decision Outcome + +Chosen option: **portless** + +`portless` eliminates the entire certificate lifecycle (generation, installation, `.gitignore` entries, CI detection guards) and replaces the custom `local-https-proxy.js` with a zero-config daemon. All services become reachable via `https://.sharethrift.localhost:1355`, a single consistent pattern. The old multi-port layout is replaced by one port and named subdomains that will remain standard throughout the developer lifecycle. + +## More Information + +- [portless on npm](https://www.npmjs.com/package/portless) +- [portless docs](https://port1355.dev/) +- [mkcert repo](https://github.com/FiloSottile/mkcert) +- `scripts/dev-cleanup.mjs` — stops the proxy and kills known dev processes before each `pnpm run dev` +- `apps/api/start-dev.mjs`, `apps/docs/start-dev.mjs` — wrapper scripts that pass `PORT` to tools that require an explicit `--port` flag diff --git a/apps/docs/docs/technical-overview/localhost-subdomain-setup.md b/apps/docs/docs/technical-overview/localhost-subdomain-setup.md deleted file mode 100644 index 92dfab478..000000000 --- a/apps/docs/docs/technical-overview/localhost-subdomain-setup.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -sidebar_position: 3 -sidebar_label: Localhost Subdomain Setup -description: Guide to setting up HTTPS-enabled localhost subdomains for local development ---- - -# Localhost Subdomain Development Setup - -Sharethrift uses a subdomain-based architecture in local development with `*.sharethrift.localhost` domains and HTTPS to mirror production and support OAuth, cookie sharing, and CORS testing. - -## Local Development Subdomains - -### Application Services -- **Frontend UI**: `https://sharethrift.localhost:3000` -- **Backend API (HTTPS proxy)**: `https://data-access.sharethrift.localhost:7072` -- **Backend API (HTTP direct)**: `http://localhost:7071` -- **Documentation**: `https://docs.sharethrift.localhost:3002` - -### Mock Services -- **Payment**: `https://mock-payment.sharethrift.localhost:3001` -- **Messaging**: `https://mock-messaging.sharethrift.localhost:10000` -- **Auth**: `https://mock-auth.sharethrift.localhost:4000` -- **MongoDB**: `mongodb://mongodb.sharethrift.localhost:50000` - -## HTTPS Proxy Architecture - -Azure Functions Core Tools v4.2.2 has a broken `--cert` flag that ignores custom certificates. Instead, we use a custom Node.js HTTPS proxy: - -1. Azure Functions runs HTTP-only on port 7071 -2. HTTPS proxy on port 7072 with mkcert wildcard certificate -3. Routes `https://data-access.sharethrift.localhost:7072` → `http://localhost:7071` - -See `local-https-proxy.js` in the repository root. - -## Quick Setup - -### Install mkcert - -**macOS**: -```bash -brew install mkcert nss -``` - -**Windows**: -```bash -choco install mkcert -``` - -**Linux**: -```bash -sudo apt install libnss3-tools -curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64" -chmod +x mkcert-v*-linux-amd64 -sudo mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert -``` - -### Generate Certificates - -```bash -mkcert -install -mkdir -p .certs && cd .certs -mkcert "*.sharethrift.localhost" "sharethrift.localhost" localhost 127.0.0.1 ::1 -``` - -Certificates are stored in `/.certs` (gitignored). - - -## Cookie and CORS Configuration - -### Cross-Subdomain Cookies - -```typescript -res.cookie('session', token, { - domain: '.sharethrift.localhost', // Leading dot for subdomain sharing - secure: true, // HTTPS only - httpOnly: true, - sameSite: 'lax', -}); -``` - -### CORS Setup - -```typescript -const corsOptions = { - origin: [ - 'https://sharethrift.localhost', - 'https://data-access.sharethrift.localhost', - 'https://docs.sharethrift.localhost', - ], - credentials: true, -}; -``` - -## CI/CD Compatibility - -The subdomain setup is **local development only**. All services gracefully fall back to HTTP when certificates don't exist: - -- Certificate files are gitignored -- Services detect missing certs with `fs.existsSync()` -- CI/CD pipelines skip cert generation -- No environment variables needed - -**Local**: -```bash -pnpm run dev # → HTTPS with certs -``` - -**CI/CD**: -```bash -pnpm test # → HTTP fallback (no certs) -``` - -## Troubleshooting - -### Certificate Not Trusted -```bash -mkcert -install # Reinstall CA -# Restart browser -``` - -### Port Already in Use -```bash -sudo lsof -i :7072 -sudo kill -9 -``` - -### CORS Errors -Add subdomain to CORS `origin` array and ensure `credentials: true`. - -## Additional Resources - -- [mkcert](https://github.com/FiloSottile/mkcert) - Local certificate authority -- [Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) - Local runtime From 6a16268fbb967ada0ea042dc09df4eaa838fa954 Mon Sep 17 00:00:00 2001 From: Jason Hankins Date: Fri, 27 Feb 2026 10:21:02 -0500 Subject: [PATCH 06/12] remove unnecessary cleanup script, move proxy cleanup to package script --- .../decisions/0025-portless-local-https.md | 5 ++- package.json | 2 +- scripts/dev-cleanup.mjs | 36 ------------------- 3 files changed, 3 insertions(+), 40 deletions(-) delete mode 100644 scripts/dev-cleanup.mjs diff --git a/apps/docs/docs/decisions/0025-portless-local-https.md b/apps/docs/docs/decisions/0025-portless-local-https.md index e6df18c14..3dd8396a9 100644 --- a/apps/docs/docs/decisions/0025-portless-local-https.md +++ b/apps/docs/docs/decisions/0025-portless-local-https.md @@ -37,7 +37,7 @@ The original solution used `mkcert` to generate a wildcard certificate stored in - Single port (`1355`) for all services: `.env` and `local.settings.json` are simpler - Subdomain names in URLs (`data-access`, `mock-auth`, etc.) make it immediately obvious which service is being called - `local-https-proxy.js` is deleted, now one less script to maintain -- Removes the need for compatibility issues with Azure Function's --cert flag +- Removes compatibility issues with Azure Functions' `--cert` flag **Negative** @@ -55,7 +55,7 @@ The original solution used `mkcert` to generate a wildcard certificate stored in **Negative** -- Developers neeed to run `mkcert -install` and `mkcert` on each machine +- Developers need to run `mkcert -install` and `mkcert` on each machine - Certificate files require explicit exclusion from version control - `local-https-proxy.js` must be kept in sync with Azure Functions behaviour - CI required `fs.existsSync()` guards to skip HTTPS when certs are absent @@ -72,5 +72,4 @@ Chosen option: **portless** - [portless on npm](https://www.npmjs.com/package/portless) - [portless docs](https://port1355.dev/) - [mkcert repo](https://github.com/FiloSottile/mkcert) -- `scripts/dev-cleanup.mjs` — stops the proxy and kills known dev processes before each `pnpm run dev` - `apps/api/start-dev.mjs`, `apps/docs/start-dev.mjs` — wrapper scripts that pass `PORT` to tools that require an explicit `--port` flag diff --git a/package.json b/package.json index 660add2f9..6dc185353 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "turbo run build", "test": "turbo run test", "lint": "turbo run lint", - "dev": "pnpm run build && node scripts/dev-cleanup.mjs && portless proxy start --https && turbo run azurite gen:watch start --parallel", + "dev": "pnpm run build && portless proxy stop ; portless proxy start --https && turbo run azurite gen:watch start --parallel", "start": "turbo run build && concurrently pnpm:start:* --kill-others-on-fail --workspace=@sthrift/api", "format": "turbo run format", "gen": "graphql-codegen --config codegen.yml", diff --git a/scripts/dev-cleanup.mjs b/scripts/dev-cleanup.mjs deleted file mode 100644 index d721454b7..000000000 --- a/scripts/dev-cleanup.mjs +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env node -/** - * Kills any leftover dev server processes from a previous run before starting fresh. - * Runs portless proxy stop, then kills known mock-server and func processes by port/name. - */ -import { execSync } from 'node:child_process'; - -function run(cmd) { - try { - execSync(cmd, { stdio: 'ignore' }); - } catch { - // ignore — process may not exist - } -} - -// Stop portless proxy (clears all route registrations) -run('portless proxy stop'); - -// Kill any leftover mock server or func processes by matching their command lines -const patterns = [ - 'mock-messaging-server.*index.js', - 'mock-payment-server.*index.js', - 'mock-oauth2-server.*index.js', - 'mock-mongodb-memory-server.*index.js', - 'func start --typescript', - 'start-dev.mjs', -]; - -for (const pattern of patterns) { - run(`pkill -f '${pattern}'`); -} - -// Small pause to let OS reclaim ports before proxy restarts -await new Promise((r) => setTimeout(r, 500)); - -console.log('dev cleanup done'); From 322bb9fbaea44e8ec98bbb6450e8d2ac283bd116 Mon Sep 17 00:00:00 2001 From: Jason Hankins Date: Fri, 27 Feb 2026 10:47:16 -0500 Subject: [PATCH 07/12] error handling for doc/api ports, comment cleanup --- apps/api/start-dev.mjs | 11 ++++++++++- apps/docs/start-dev.mjs | 11 ++++++++++- packages/cellix/mock-oauth2-server/src/index.ts | 6 ++---- packages/cellix/mock-payment-server/src/index.ts | 2 +- packages/sthrift/mock-messaging-server/package.json | 2 +- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/api/start-dev.mjs b/apps/api/start-dev.mjs index 0fdbf4601..2a17719d7 100644 --- a/apps/api/start-dev.mjs +++ b/apps/api/start-dev.mjs @@ -1,4 +1,13 @@ import { spawn } from 'node:child_process'; -const port = process.env.PORT ?? '7071'; +const envPort = process.env.PORT; + +if (!envPort) { + console.error( + 'PORT environment variable is not set. Ensure portless (or your dev environment) is running and has injected a port.', + ); + process.exit(1); +} + +const port = envPort; spawn('func', ['start', '--typescript', '--port', port], { stdio: 'inherit' }); diff --git a/apps/docs/start-dev.mjs b/apps/docs/start-dev.mjs index afcf08c65..c2469f0cc 100644 --- a/apps/docs/start-dev.mjs +++ b/apps/docs/start-dev.mjs @@ -1,6 +1,15 @@ import { spawn } from 'node:child_process'; -const port = process.env.PORT ?? '3002'; +const envPort = process.env.PORT; + +if (!envPort) { + console.error( + 'PORT environment variable is not set. Ensure portless (or your dev environment) is running and has injected a port.', + ); + process.exit(1); +} + +const port = envPort; // Use 127.0.0.1 explicitly to ensure IPv4 binding — portless proxy connects via IPv4, // but Node.js may resolve 'localhost' to ::1 (IPv6) on macOS, causing Bad Gateway. spawn('pnpm', ['exec', 'docusaurus', 'start', '--host', '127.0.0.1', '--port', port, '--no-open'], { stdio: 'inherit' }); diff --git a/packages/cellix/mock-oauth2-server/src/index.ts b/packages/cellix/mock-oauth2-server/src/index.ts index f50d731b7..2f69062a0 100644 --- a/packages/cellix/mock-oauth2-server/src/index.ts +++ b/packages/cellix/mock-oauth2-server/src/index.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/complexity/useLiteralKeys: process.env has an index signature returning string | undefined; bracket notation is required to satisfy TypeScript's strict null checking for environment variable access import crypto, { type KeyObject, type webcrypto } from 'node:crypto'; import express from 'express'; import { @@ -14,7 +15,6 @@ setupEnvironment(); const app = express(); app.disable('x-powered-by'); const port = Number(process.env['PORT'] ?? 4000); -// biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation for index signatures const BASE_URL = process.env['BASE_URL'] ?? 'https://mock-auth.sharethrift.localhost:1355'; function normalizeUrl(urlString: string): string { @@ -43,10 +43,8 @@ const redirectUriToAudience = new Map([ ['https://sharethrift.localhost:1355/auth-redirect-user', 'user-portal'], ['https://sharethrift.localhost:1355/auth-redirect-admin', 'admin-portal'], ]); -// Deprecated: kept for backwards compatibility. Uses process.env's index signature -// (string | undefined) and falls back to a default redirect URI when not set. +// Deprecated: kept for backwards compatibility; falls back to a default redirect URI when not set. const allowedRedirectUri = -// biome-ignore lint: TypeScript requires bracket notation for index signatures process.env['ALLOWED_REDIRECT_URI'] || 'http://localhost:3000/auth-redirect-user'; // Type for user profile used in token claims diff --git a/packages/cellix/mock-payment-server/src/index.ts b/packages/cellix/mock-payment-server/src/index.ts index 658853e6b..9a3bfac41 100644 --- a/packages/cellix/mock-payment-server/src/index.ts +++ b/packages/cellix/mock-payment-server/src/index.ts @@ -33,7 +33,7 @@ const DEFAULT_PORT = Number(process.env['PORT'] ?? 3001); const FRONTEND_BASE_URL = 'https://sharethrift.localhost:1355'; const PAYMENT_BASE_URL = 'https://mock-payment.sharethrift.localhost:1355'; -// Enable CORS for all origins (or restrict to 'https://sharethrift.localhost:3000' if needed) +// Enable CORS for all origins (or restrict to sharethrift.localhost if needed) app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', FRONTEND_BASE_URL); res.header( diff --git a/packages/sthrift/mock-messaging-server/package.json b/packages/sthrift/mock-messaging-server/package.json index 1d540c7ea..89187a4e7 100644 --- a/packages/sthrift/mock-messaging-server/package.json +++ b/packages/sthrift/mock-messaging-server/package.json @@ -11,7 +11,7 @@ "prebuild": "biome lint", "build": "tsc --build", "clean": "rimraf dist", - "start": "portless mock-messaging.sharethrift.localhost node -r dotenv/config dist/src/index.js", + "start": "portless mock-messaging.sharethrift.localhost node -r dotenv/config dist/src/index.js", "dev": "tsc-watch --onSuccess \"node -r dotenv/config dist/src/index.js\"", "test": "vitest run", "test:watch": "vitest" From 532bcbb8a7ce46fd14ff62212474b524e96cde4b Mon Sep 17 00:00:00 2001 From: Jason Hankins Date: Fri, 27 Feb 2026 11:14:40 -0500 Subject: [PATCH 08/12] refactor dev script, use env vars in payment server, spawn child process for api/docs to handle error --- apps/api/start-dev.mjs | 5 ++++- apps/docs/start-dev.mjs | 5 ++++- package.json | 4 +++- packages/cellix/mock-payment-server/src/index.ts | 4 ++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/api/start-dev.mjs b/apps/api/start-dev.mjs index 2a17719d7..ec0e10f86 100644 --- a/apps/api/start-dev.mjs +++ b/apps/api/start-dev.mjs @@ -10,4 +10,7 @@ if (!envPort) { } const port = envPort; -spawn('func', ['start', '--typescript', '--port', port], { stdio: 'inherit' }); +const child = spawn('func', ['start', '--typescript', '--port', port], { stdio: 'inherit' }); +child.on('exit', (code, signal) => { + process.exitCode = signal ? 1 : (code ?? 1); +}); diff --git a/apps/docs/start-dev.mjs b/apps/docs/start-dev.mjs index c2469f0cc..f1bf821e5 100644 --- a/apps/docs/start-dev.mjs +++ b/apps/docs/start-dev.mjs @@ -12,4 +12,7 @@ if (!envPort) { const port = envPort; // Use 127.0.0.1 explicitly to ensure IPv4 binding — portless proxy connects via IPv4, // but Node.js may resolve 'localhost' to ::1 (IPv6) on macOS, causing Bad Gateway. -spawn('pnpm', ['exec', 'docusaurus', 'start', '--host', '127.0.0.1', '--port', port, '--no-open'], { stdio: 'inherit' }); +const child = spawn('pnpm', ['exec', 'docusaurus', 'start', '--host', '127.0.0.1', '--port', port, '--no-open'], { stdio: 'inherit' }); +child.on('exit', (code, signal) => { + process.exitCode = signal ? 1 : (code ?? 1); +}); diff --git a/package.json b/package.json index 6dc185353..a1945a6c4 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,11 @@ "build": "turbo run build", "test": "turbo run test", "lint": "turbo run lint", - "dev": "pnpm run build && portless proxy stop ; portless proxy start --https && turbo run azurite gen:watch start --parallel", + "dev": "pnpm run build && pnpm run proxy:stop && pnpm run proxy:start && turbo run azurite gen:watch start --parallel", "start": "turbo run build && concurrently pnpm:start:* --kill-others-on-fail --workspace=@sthrift/api", "format": "turbo run format", + "proxy:stop": "portless proxy stop || true", + "proxy:start": "portless proxy start --https", "gen": "graphql-codegen --config codegen.yml", "gen:watch": "graphql-codegen --config codegen.yml --watch", "tsbuild": "tsc --build", diff --git a/packages/cellix/mock-payment-server/src/index.ts b/packages/cellix/mock-payment-server/src/index.ts index 9a3bfac41..2b97612f0 100644 --- a/packages/cellix/mock-payment-server/src/index.ts +++ b/packages/cellix/mock-payment-server/src/index.ts @@ -30,8 +30,8 @@ const app = express(); app.disable('x-powered-by'); const DEFAULT_PORT = Number(process.env['PORT'] ?? 3001); -const FRONTEND_BASE_URL = 'https://sharethrift.localhost:1355'; -const PAYMENT_BASE_URL = 'https://mock-payment.sharethrift.localhost:1355'; +const FRONTEND_BASE_URL = process.env['FRONTEND_BASE_URL'] ?? 'https://sharethrift.localhost:1355'; +const PAYMENT_BASE_URL = process.env['PAYMENT_BASE_URL'] ?? 'https://mock-payment.sharethrift.localhost:1355'; // Enable CORS for all origins (or restrict to sharethrift.localhost if needed) app.use((req, res, next) => { From b4bda864f0e6acb9247b6b4da8630728920b4621 Mon Sep 17 00:00:00 2001 From: Jason Hankins Date: Fri, 27 Feb 2026 11:42:03 -0500 Subject: [PATCH 09/12] update storybook to use portless --- apps/ui-sharethrift/package.json | 2 +- packages/cellix/mock-payment-server/src/index.ts | 4 ++-- packages/sthrift/messaging-service-mock/src/index.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ui-sharethrift/package.json b/apps/ui-sharethrift/package.json index f0b3c7ee3..b1dab9036 100644 --- a/apps/ui-sharethrift/package.json +++ b/apps/ui-sharethrift/package.json @@ -9,7 +9,7 @@ "lint": "eslint .", "preview": "vite preview", "tswatch": "tsc --build --watch", - "storybook": "storybook dev -p 6006", + "storybook": "portless storybook.sharethrift.localhost storybook dev", "build-storybook": "storybook build", "test": "vitest run", "test:coverage:ui": "vitest run --coverage", diff --git a/packages/cellix/mock-payment-server/src/index.ts b/packages/cellix/mock-payment-server/src/index.ts index 2b97612f0..31a97090e 100644 --- a/packages/cellix/mock-payment-server/src/index.ts +++ b/packages/cellix/mock-payment-server/src/index.ts @@ -1343,9 +1343,9 @@ app.post( const startServer = (portToTry: number, attempt = 0): void => { // HTTP server — portless handles TLS/proxy at the subdomain level const server = app.listen(portToTry, () => { - console.log(` Mock Payment Server listening on http://localhost:${portToTry}`); + console.log(` Mock Payment Server externally reachable at: ${PAYMENT_BASE_URL}`); + console.log(` Internal bind (HTTP): http://localhost:${portToTry}`); console.log(` CORS origin: ${FRONTEND_BASE_URL}`); - console.log(` Microform origin: ${PAYMENT_BASE_URL}`); }); server.on('error', (error: NodeJS.ErrnoException) => { diff --git a/packages/sthrift/messaging-service-mock/src/index.test.ts b/packages/sthrift/messaging-service-mock/src/index.test.ts index eb0bcaba2..555e226b5 100644 --- a/packages/sthrift/messaging-service-mock/src/index.test.ts +++ b/packages/sthrift/messaging-service-mock/src/index.test.ts @@ -14,7 +14,7 @@ describe('ServiceMessagingMock Integration Tests', () => { process.env['MESSAGING_MOCK_URL'] = MOCK_SERVER_URL; // Force HTTP mode for tests to avoid certificate issues -mockServer = await startServer(MOCK_SERVER_PORT, true); + mockServer = await startServer(MOCK_SERVER_PORT, true); await new Promise((resolve) => setTimeout(resolve, 500)); }, 15000); From 0229ed0e78a3c11a066f7a32d32f425b1b9746f9 Mon Sep 17 00:00:00 2001 From: Jason Hankins Date: Fri, 27 Feb 2026 11:52:38 -0500 Subject: [PATCH 10/12] fix storybook to use proper port --- apps/ui-sharethrift/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui-sharethrift/package.json b/apps/ui-sharethrift/package.json index b1dab9036..ec65f3e75 100644 --- a/apps/ui-sharethrift/package.json +++ b/apps/ui-sharethrift/package.json @@ -9,7 +9,7 @@ "lint": "eslint .", "preview": "vite preview", "tswatch": "tsc --build --watch", - "storybook": "portless storybook.sharethrift.localhost storybook dev", + "storybook": "portless storybook.sharethrift.localhost sh -c 'storybook dev -p $PORT --host 127.0.0.1 --no-open'", "build-storybook": "storybook build", "test": "vitest run", "test:coverage:ui": "vitest run --coverage", From 53e0484ed31b17344423cee4cae76a708818efe6 Mon Sep 17 00:00:00 2001 From: Jason Hankins Date: Fri, 27 Feb 2026 12:21:19 -0500 Subject: [PATCH 11/12] sourcery suggestions --- apps/ui-sharethrift/vite.config.ts | 1 + packages/sthrift/mock-messaging-server/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/ui-sharethrift/vite.config.ts b/apps/ui-sharethrift/vite.config.ts index 659fb3d09..172d7d7d4 100644 --- a/apps/ui-sharethrift/vite.config.ts +++ b/apps/ui-sharethrift/vite.config.ts @@ -5,6 +5,7 @@ export default defineConfig(() => { return { plugins: [react()], server: { + port: Number(process.env.PORT) || undefined, open: 'https://sharethrift.localhost:1355', }, }; diff --git a/packages/sthrift/mock-messaging-server/src/index.ts b/packages/sthrift/mock-messaging-server/src/index.ts index c310169a8..e8956c053 100644 --- a/packages/sthrift/mock-messaging-server/src/index.ts +++ b/packages/sthrift/mock-messaging-server/src/index.ts @@ -71,7 +71,7 @@ export function createApp(): Application { return app; } -export function startServer(port = 10000, seedData = false): Promise { +export function startServer(port = Number(process.env['PORT'] ?? 10000), seedData = false): Promise { return new Promise((resolve) => { const app = createApp(); // HTTP server — portless handles TLS/proxy at the subdomain level From d5dbabe71025015eef037506b299433585f6528a Mon Sep 17 00:00:00 2001 From: Jason Hankins Date: Fri, 27 Feb 2026 14:13:22 -0500 Subject: [PATCH 12/12] automatically install portless globally for simple execution of dev env --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a1945a6c4..2ea165375 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "start": "turbo run build && concurrently pnpm:start:* --kill-others-on-fail --workspace=@sthrift/api", "format": "turbo run format", "proxy:stop": "portless proxy stop || true", - "proxy:start": "portless proxy start --https", + "proxy:start": "npm install -g portless && portless proxy start --https", "gen": "graphql-codegen --config codegen.yml", "gen:watch": "graphql-codegen --config codegen.yml --watch", "tsbuild": "tsc --build",