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 f18da0169..4fe349c0d 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..ec0e10f86 --- /dev/null +++ b/apps/api/start-dev.mjs @@ -0,0 +1,16 @@ +import { spawn } from 'node:child_process'; + +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; +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/docs/decisions/0025-portless-local-https.md b/apps/docs/docs/decisions/0025-portless-local-https.md new file mode 100644 index 000000000..3dd8396a9 --- /dev/null +++ b/apps/docs/docs/decisions/0025-portless-local-https.md @@ -0,0 +1,75 @@ +--- +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 compatibility issues with Azure Functions' `--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 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 +- 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) +- `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 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/package.json b/apps/docs/package.json index f4791bf2d..4e84e3b71 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..f1bf821e5 --- /dev/null +++ b/apps/docs/start-dev.mjs @@ -0,0 +1,18 @@ +import { spawn } from 'node:child_process'; + +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. +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/apps/server-messaging-mock/package.json b/apps/server-messaging-mock/package.json index 95b871b78..89cc37afc 100644 --- a/apps/server-messaging-mock/package.json +++ b/apps/server-messaging-mock/package.json @@ -11,8 +11,10 @@ "prebuild": "biome lint", "build": "tsc --build", "clean": "rimraf dist", - "start": "node -r dotenv/config dist/src/index.js", - "dev": "tsc-watch --onSuccess \"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" }, "dependencies": { "@cellix/server-messaging-seedwork": "workspace:*", diff --git a/apps/server-messaging-mock/src/index.ts b/apps/server-messaging-mock/src/index.ts index cf4937bc5..153ef3ab8 100644 --- a/apps/server-messaging-mock/src/index.ts +++ b/apps/server-messaging-mock/src/index.ts @@ -1,6 +1,4 @@ import dotenv from 'dotenv'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { startMockMessagingServer, type MockMessagingServerConfig, @@ -17,27 +15,12 @@ const setupEnvironment = () => { setupEnvironment(); -// Detect certificate availability to determine protocol -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'); - // biome-ignore lint: using bracket notation for environment variable access const port = Number(process.env['PORT'] ?? 10000); -const fs = await import('node:fs'); -const hasCerts = fs.existsSync(certKeyPath) && fs.existsSync(certPath); - const config: MockMessagingServerConfig = { port, - useHttps: hasCerts, seedData: true, - host: hasCerts ? 'mock-messaging.sharethrift.localhost' : 'localhost', - certKeyPath, - certPath, seedMockData, }; diff --git a/apps/server-oauth2-mock/package.json b/apps/server-oauth2-mock/package.json index 4b50db82c..9d9946534 100644 --- a/apps/server-oauth2-mock/package.json +++ b/apps/server-oauth2-mock/package.json @@ -13,8 +13,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" }, "dependencies": { "@cellix/server-oauth2-seedwork": "workspace:*", diff --git a/apps/server-oauth2-mock/src/index.ts b/apps/server-oauth2-mock/src/index.ts index 1db2f6bfa..1b51fcaf3 100644 --- a/apps/server-oauth2-mock/src/index.ts +++ b/apps/server-oauth2-mock/src/index.ts @@ -1,6 +1,4 @@ import dotenv from 'dotenv'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { startMockOAuth2Server, type OAuth2Config, @@ -16,37 +14,18 @@ const setupEnvironment = () => { setupEnvironment(); -// Detect certificate availability to determine protocol and base URL -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'); - // biome-ignore lint/complexity/useLiteralKeys: Required for env var access const port = Number(process.env['PORT'] ?? 4000); -const fs = await import('node:fs'); -const hasCerts = fs.existsSync(certKeyPath) && fs.existsSync(certPath); - -console.log('[mock-oauth2-server] Project root:', projectRoot); -console.log('[mock-oauth2-server] Cert key path:', certKeyPath, 'exists:', fs.existsSync(certKeyPath)); -console.log('[mock-oauth2-server] Cert path:', certPath, 'exists:', fs.existsSync(certPath)); -console.log('[mock-oauth2-server] hasCerts:', hasCerts); - -const BASE_URL = hasCerts - ? `https://mock-auth.sharethrift.localhost:${port}` - : `http://localhost:${port}`; +const BASE_URL = process.env['BASE_URL'] ?? 'https://mock-auth.sharethrift.localhost:1355'; 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', ]); -// biome-ignore lint/complexity/useLiteralKeys: Required for env var access const allowedRedirectUri = process.env['ALLOWED_REDIRECT_URI'] || 'http://localhost:3000/auth-redirect-user'; @@ -54,30 +33,23 @@ const allowedRedirectUri = 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'], ]); const config: OAuth2Config = { - port: port, + port, baseUrl: BASE_URL, - host: hasCerts ? 'mock-auth.sharethrift.localhost' : 'localhost', - allowedRedirectUris: allowedRedirectUris, - allowedRedirectUri: allowedRedirectUri, - redirectUriToAudience: redirectUriToAudience, - hasCerts: hasCerts, - certKeyPath: certKeyPath, - certPath: certPath, + allowedRedirectUris, + allowedRedirectUri, + redirectUriToAudience, getUserProfile: (isAdminPortal) => { - // biome-ignore lint/complexity/useLiteralKeys: Required for env var access const email = isAdminPortal ? process.env['ADMIN_EMAIL'] || process.env['EMAIL'] || '' : process.env['EMAIL'] || ''; - // biome-ignore lint/complexity/useLiteralKeys: Required for env var access const given_name = isAdminPortal ? process.env['ADMIN_GIVEN_NAME'] || process.env['GIVEN_NAME'] || '' : process.env['GIVEN_NAME'] || ''; - // biome-ignore lint/complexity/useLiteralKeys: Required for env var access const family_name = isAdminPortal ? process.env['ADMIN_FAMILY_NAME'] || process.env['FAMILY_NAME'] || '' : process.env['FAMILY_NAME'] || ''; diff --git a/apps/server-payment-mock/package.json b/apps/server-payment-mock/package.json index 6d6265b7a..c0bb5e00a 100644 --- a/apps/server-payment-mock/package.json +++ b/apps/server-payment-mock/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-payment.sharethrift.localhost node dist/src/index.js", + "dev": "portless mock-payment.sharethrift.localhost tsx watch src/index.ts" }, "dependencies": { "@cellix/server-payment-seedwork": "workspace:*", diff --git a/apps/server-payment-mock/src/index.ts b/apps/server-payment-mock/src/index.ts index f8add0ecb..36bc680c6 100644 --- a/apps/server-payment-mock/src/index.ts +++ b/apps/server-payment-mock/src/index.ts @@ -1,5 +1,3 @@ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import dotenv from 'dotenv'; import { startMockPaymentServer, @@ -16,37 +14,16 @@ const setupEnvironment = () => { setupEnvironment(); -// Detect certificate availability to determine protocol and base URL -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'); - // biome-ignore lint:useLiteralKeys const port = Number(process.env['PORT'] ?? 3001); -const HOST = 'mock-payment.sharethrift.localhost'; - -const fs = await import('node:fs'); -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}:${port}` : `localhost:${port}`; -const FRONTEND_BASE_URL = `${PROTOCOL}://${FRONTEND_HOST}`; -const PAYMENT_BASE_URL = `${PROTOCOL}://${PAYMENT_HOST}`; +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'; const config: PaymentConfig = { port, - protocol: PROTOCOL as 'http' | 'https', - host: hasCerts ? HOST : 'localhost', - paymentHost: PAYMENT_HOST, frontendBaseUrl: FRONTEND_BASE_URL, paymentBaseUrl: PAYMENT_BASE_URL, - ...(hasCerts && { certKeyPath, certPath }), }; startMockPaymentServer(config).catch((err: unknown) => { 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 9f3b2420c..08fbd87d1 100644 --- a/apps/ui-sharethrift/package.json +++ b/apps/ui-sharethrift/package.json @@ -4,12 +4,12 @@ "version": "0.0.0", "type": "module", "scripts": { - "start": "vite", + "start": "portless sharethrift.localhost vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", "tswatch": "tsc --build --watch", - "storybook": "storybook dev -p 6006", + "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", diff --git a/apps/ui-sharethrift/vite.config.ts b/apps/ui-sharethrift/vite.config.ts index 4f09ec4a7..172d7d7d4 100644 --- a/apps/ui-sharethrift/vite.config.ts +++ b/apps/ui-sharethrift/vite.config.ts @@ -1,38 +1,12 @@ -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: { + port: Number(process.env.PORT) || undefined, + open: 'https://sharethrift.localhost:1355', + }, }; }); diff --git a/knip.json b/knip.json index 4f5c9ab13..3113b23c7 100644 --- a/knip.json +++ b/knip.json @@ -97,5 +97,5 @@ "vite", "axios" ], - "ignoreBinaries": ["func", "open", "concurrently", "container"] + "ignoreBinaries": ["func", "open", "concurrently", "container", "portless"] } diff --git a/package.json b/package.json index 330a18a75..696dfa602 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,11 @@ "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 && 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=@app/api", "format": "turbo run format", + "proxy:stop": "portless proxy stop || true", + "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", diff --git a/packages/cellix/server-messaging-seedwork/src/index.ts b/packages/cellix/server-messaging-seedwork/src/index.ts index b14132c70..a2f375d14 100644 --- a/packages/cellix/server-messaging-seedwork/src/index.ts +++ b/packages/cellix/server-messaging-seedwork/src/index.ts @@ -1,8 +1,6 @@ import express from 'express'; import type { Application, Request, Response, NextFunction } from 'express'; -import * as https from 'node:https'; import * as http from 'node:http'; -import * as fs from 'node:fs'; import { config as dotenvConfig } from 'dotenv'; import { setupConversationRoutes } from './routes/conversations.js'; import { setupMessageRoutes } from './routes/messages.js'; @@ -12,11 +10,8 @@ import type { Server } from 'node:http'; export interface MockMessagingServerConfig { port: number; - useHttps: boolean; seedData: boolean; seedMockData?: () => void; - certKeyPath?: string; - certPath?: string; host?: string; env?: NodeJS.ProcessEnv; } @@ -72,35 +67,17 @@ export function createMockMessagingApp(): Application { export function startMockMessagingServer(config: MockMessagingServerConfig): Promise { const app = createMockMessagingApp(); - const {port, useHttps, host, certKeyPath, certPath} = config; - const hasCerts = certKeyPath && certPath && fs.existsSync(certKeyPath) && fs.existsSync(certPath); return new Promise((resolve) => { - if (hasCerts && useHttps && certKeyPath && certPath) { - const httpsOptions = { - key: fs.readFileSync(certKeyPath), - cert: fs.readFileSync(certPath), - }; - const server = https.createServer(httpsOptions, app).listen(port, host, () => { - console.log(` Mock Messaging Server listening on https://${host}:${port}`); - if (config.seedData && config.seedMockData) { - config.seedMockData(); - } else { - console.log('Starting with empty data store (set seedData=true to seed)'); - } - resolve(server); - }); - } else { - const server = http.createServer(app).listen(port, () => { - const reason = hasCerts ? '(HTTP mode)' : '(no certs found)'; - console.log(` Mock Messaging Server listening on http://${host}:${port} ${reason}`); - if (config.seedData && config.seedMockData) { - config.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(config.port, () => { + console.log(` Mock Messaging Server listening on http://localhost:${config.port}`); + if (config.seedData && config.seedMockData) { + config.seedMockData(); + } else { + console.log('Starting with empty data store (set seedData=true to seed)'); + } + resolve(server); + }); }); } diff --git a/packages/cellix/server-oauth2-seedwork/src/index.ts b/packages/cellix/server-oauth2-seedwork/src/index.ts index cb6721de2..80227eb22 100644 --- a/packages/cellix/server-oauth2-seedwork/src/index.ts +++ b/packages/cellix/server-oauth2-seedwork/src/index.ts @@ -1,6 +1,5 @@ +// 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 fs from 'node:fs'; -import https from 'node:https'; import express from 'express'; import { exportJWK, @@ -19,13 +18,10 @@ app.disable('x-powered-by'); export type OAuth2Config = { port: number; baseUrl: string; - host: string; + host?: string; allowedRedirectUris: Set; allowedRedirectUri: string; redirectUriToAudience: Map; - certKeyPath?: string; - hasCerts?: boolean; - certPath?: string; env?: NodeJS.ProcessEnv; getUserProfile: (isAdminPortal: boolean) => { email: string; @@ -46,6 +42,7 @@ function normalizeUrl(urlString: string): string { return urlString; } } + // Type for user profile used in token claims interface TokenProfile { aud: string; @@ -136,7 +133,6 @@ async function buildTokenResponse( // Main async startup async function main(config: OAuth2Config) { - // Generate signing keypair with jose const { publicKey, privateKey } = await generateKeyPair('RS256'); const publicJwk = await exportJWK(publicKey); @@ -281,30 +277,11 @@ async function main(config: OAuth2Config) { return; }); - // Load SSL certificates for HTTPS - if (config?.hasCerts) { - const httpsOptions = { - key: fs.readFileSync(config?.certKeyPath || ''), - cert: fs.readFileSync(config?.certPath || ''), - }; - - https - .createServer(httpsOptions, app) - .listen(config.port, config.host, () => { - // eslint-disable-next-line no-console - console.log(`Mock OAuth2 server running on ${config.baseUrl}`); - console.log( - `JWKS endpoint running on ${config.baseUrl}/.well-known/jwks.json`, - ); - }); - } else { - // Fallback to HTTP when certs don't exist (CI/CD) - app.listen(config.port, config.host, () => { - // eslint-disable-next-line no-console - console.log(`Mock OAuth2 server running on ${config.baseUrl} (no certs found)`); - console.log(`JWKS endpoint running on ${config.baseUrl}/.well-known/jwks.json`); - }); - } + // HTTP server — portless handles TLS/proxy at the subdomain level + app.listen(config.port, () => { + console.log(`Mock OAuth2 server running on ${config.baseUrl}`); + console.log(`JWKS endpoint running on ${config.baseUrl}/.well-known/jwks.json`); + }); } export async function startMockOAuth2Server(config: OAuth2Config) { diff --git a/packages/cellix/server-payment-seedwork/src/index.ts b/packages/cellix/server-payment-seedwork/src/index.ts index 9b3c85668..67de05bc2 100644 --- a/packages/cellix/server-payment-seedwork/src/index.ts +++ b/packages/cellix/server-payment-seedwork/src/index.ts @@ -1,6 +1,4 @@ import * as crypto from 'node:crypto'; -import * as https from 'node:https'; -import * as fs from 'node:fs'; import express, { type Express, type Request, type Response, type NextFunction } from 'express'; import { generateKeyPair } from 'jose'; import { exportPKCS8 } from 'jose'; @@ -26,13 +24,8 @@ import type { export interface PaymentConfig { port: number; - protocol: 'http' | 'https'; - host: string; - paymentHost: string; frontendBaseUrl: string; paymentBaseUrl: string; - certKeyPath?: string; - certPath?: string; iframeJsPath?: string; } @@ -42,13 +35,6 @@ export function startMockPaymentServer(config: PaymentConfig): Promise { const CYBERSOURCE_MERCHANT_ID = 'simnova_sharethrift'; - // Check for certificates - const hasCerts = - config.certKeyPath && - config.certPath && - fs.existsSync(config.certKeyPath) && - fs.existsSync(config.certPath); - app.use(express.json()); // Serve static files for iframe.min.js (optional) @@ -56,7 +42,7 @@ export function startMockPaymentServer(config: PaymentConfig): Promise { app.use('/microform/bundle/:version', express.static(config.iframeJsPath)); } - // 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: Request, res: Response, next: NextFunction) => { res.header('Access-Control-Allow-Origin', config.frontendBaseUrl); res.header( @@ -1359,57 +1345,28 @@ export function startMockPaymentServer(config: PaymentConfig): Promise { // Start the server return new Promise((resolve, reject) => { const startServer = (portToTry: number, attempt = 0): void => { - if (hasCerts) { - const httpsOptions = { - key: fs.readFileSync(config.certKeyPath as string), - cert: fs.readFileSync(config.certPath as string), - }; - - const server = https.createServer(httpsOptions, app).listen(portToTry, config.host, () => { - console.log(` Mock Payment Server listening on ${config.protocol}://${config.paymentHost}`); - console.log(` CORS origin: ${config.frontendBaseUrl}`); - console.log(` Microform origin: ${config.paymentBaseUrl}`); - resolve(); - }); - - 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; - } - - reject(error); - }); - } else { - // Fallback to HTTP when certs don't exist (CI/CD) - const server = app.listen(portToTry, config.host, () => { - console.log(` Mock Payment Server listening on ${config.protocol}://localhost:${portToTry} (no certs found)`); - console.log(` CORS origin: ${config.frontendBaseUrl}`); - console.log(` Microform origin: ${config.paymentBaseUrl}`); - resolve(); - }); + // HTTP server — portless handles TLS/proxy at the subdomain level + const server = app.listen(portToTry, () => { + console.log(` Mock Payment Server externally reachable at: ${config.paymentBaseUrl}`); + console.log(` Internal bind (HTTP): http://localhost:${portToTry}`); + console.log(` CORS origin: ${config.frontendBaseUrl}`); + resolve(); + }); + + 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; - } - - reject(error); - }); - } + reject(error); + }); }; startServer(config.port); 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();