Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,3 @@ apps/api/deploy/

# Build-time evidence artifacts (generated)
build-artifacts/

10 changes: 5 additions & 5 deletions .snyk
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ ignore:
reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.'
expires: '2026-06-18T00:00:00.000Z'
created: '2026-05-18T11:04:00.000Z'
'SNYK-JS-POSTCSSSELECTORPARSER-16873882':
- '* > postcss-selector-parser':
reason: 'Transitive dependency in @docusaurus/core build pipeline; no upgrade or patch available from upstream. Not exploitable in current usage (build-time CSS processing only).'
expires: '2026-08-26T00:00:00.000Z'
created: '2026-05-26T00:00:00.000Z'
'SNYK-JS-OPENTELEMETRYEXPORTERPROMETHEUS-16758050':
- '* > @opentelemetry/exporter-prometheus@0.57.2':
reason: 'Requires upgrade of @opentelemetry/sdk-node to 0.217.0, which has type errors that break compilation. Created task to upgrade OTEL service to 2.x and resolve vulnerability that way.'
expires: '2026-07-28T00:00:00.000Z'
created: '2026-06-01T10:00:00.000Z'
'SNYK-JS-POSTCSSSELECTORPARSER-16873882':
- '* > postcss-selector-parser':
reason: 'Transitive dependency in Docusaurus CSS optimization/build tooling; Snyk reports no fixed version for postcss-selector-parser yet. Not exploitable at runtime because docs CSS is repository-controlled and processed at build time.'
expires: '2026-07-28T00:00:00.000Z'
created: '2026-05-27T00:00:00.000Z'
29 changes: 29 additions & 0 deletions apps/api/local-settings.e2e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "node",
"NODE_ENV": "development",
"languageWorkers__node__arguments": "",
"AZURE_STORAGE_CONNECTION_STRING": "UseDevelopmentStorage=true",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"ACCOUNT_PORTAL_OIDC_AUDIENCE": "mock-client",
"ACCOUNT_PORTAL_OIDC_ENDPOINT": "https://mock-auth.ownercommunity.localhost:1355/community/.well-known/jwks.json",
"ACCOUNT_PORTAL_OIDC_ISSUER": "https://mock-auth.ownercommunity.localhost:1355/community",
"ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER": true,
"APPLICATIONINSIGHTS_CONNECTION_STRING": "",
"CONFIG_VERSION": "3.0",
"COSMOSDB_CONNECTION_STRING": "mongodb://127.0.0.1:50000/owner-community?replicaSet=globaldb",
"COSMOSDB_DBNAME": "owner-community",
"STAFF_PORTAL_OIDC_AUDIENCE": "mock-client",
"STAFF_PORTAL_OIDC_ENDPOINT": "https://mock-auth.ownercommunity.localhost:1355/staff/.well-known/jwks.json",
"STAFF_PORTAL_OIDC_ISSUER": "https://mock-auth.ownercommunity.localhost:1355/staff",
"STAFF_PORTAL_OIDC_IGNORE_ISSUER": true,
"STORAGE_ACCOUNT_NAME": "devstoreaccount1",
"STORAGE_ACCOUNT_KEY": "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
},
"ConnectionStrings": {},
"Host": {
"LocalHttpPort": 7071,
"CORS": "*"
}
}
9 changes: 6 additions & 3 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"build": "tsgo --build && rolldown -c rolldown.config.ts",
"predev": "pnpm run prepare:deploy && pnpm run sync-local-settings",
"dev": "pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.mjs",
"predev:worktree": "pnpm run prepare:deploy && pnpm run sync-local-settings",
"dev:worktree": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs",
"prepare:deploy": "cellix-prepare-func-deploy",
"watch": "tsgo --watch",
"test": "vitest run --silent --reporter=dot",
Expand All @@ -21,8 +23,8 @@
"clean": "rimraf dist deploy",
"prestart": "pnpm run prepare:deploy && pnpm run sync-local-settings",
"start": "func start --typescript --script-root deploy/",
"sync-local-settings": "node -e \"const fs=require('node:fs'); fs.mkdirSync('deploy',{recursive:true}); if (fs.existsSync('local.settings.json')) fs.copyFileSync('local.settings.json','deploy/local.settings.json');\"",
"azurite": "azurite-blob --silent --location ../../__blobstorage__ & azurite-queue --silent --location ../../__queuestorage__ & azurite-table --silent --location ../../__tablestorage__"
"sync-local-settings": "node scripts/sync-local-settings.mjs",
"azurite": "node start-azurite.mjs"
},
"dependencies": {
"@azure/functions": "catalog:",
Expand All @@ -31,8 +33,8 @@
"@ocom/application-services": "workspace:*",
"@ocom/context-spec": "workspace:*",
"@ocom/event-handler": "workspace:*",
"@ocom/graphql-handler": "workspace:*",
"@ocom/graphql": "workspace:*",
"@ocom/graphql-handler": "workspace:*",
"@ocom/persistence": "workspace:*",
"@ocom/rest": "workspace:*",
"@ocom/service-apollo-server": "workspace:*",
Expand All @@ -47,6 +49,7 @@
"@cellix/config-typescript": "workspace:*",
"@cellix/config-vitest": "workspace:*",
"@vitest/coverage-istanbul": "catalog:",
"azurite": "^3.35.0",
"rimraf": "catalog:",
"rolldown": "1.0.0-beta.55",
"typescript": "catalog:",
Expand Down
63 changes: 63 additions & 0 deletions apps/api/scripts/sync-local-settings.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildPortlessUrl, getHostnames } from '../../../scripts/local-dev/portless-hostnames.mjs';
import { getAzuriteConnectionString } from '../../../scripts/local-dev/worktree-ports.mjs';

const apiDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const mode = process.argv[2] ?? (isE2E() ? 'e2e' : undefined);
const localSettingsPath = path.join(apiDir, 'local.settings.json');
const e2eLocalSettingsPath = path.join(apiDir, 'local-settings.e2e.json');
const targetPath = path.join(apiDir, 'deploy', 'local.settings.json');

mkdirSync(path.dirname(targetPath), { recursive: true });

if (!mode) {
if (existsSync(localSettingsPath)) {
copyFileSync(localSettingsPath, targetPath);
}
process.exit(0);
}

if (mode !== 'e2e') {
throw new Error('[sync-local-settings] Invalid mode: expected one of e2e');
}

if (!existsSync(e2eLocalSettingsPath)) {
throw new Error(`[sync-local-settings] Missing local settings for mode "e2e": ${e2eLocalSettingsPath}`);
}

const settings = JSON.parse(readFileSync(e2eLocalSettingsPath, 'utf-8'));
applyE2EOverrides(settings);
writeFileSync(targetPath, `${JSON.stringify(settings, null, '\t')}\n`);

function applyE2EOverrides(settings) {
const values = { ...(settings.Values ?? {}) };

// Worktree-scoped overrides: when WORKTREE_NAME is set the proxy hostnames
// and Azurite ports are scoped to that worktree, so we rewrite the URLs and
// connection strings here. Without WORKTREE_NAME the committed JSON values
// already match the default hostnames, so we leave them alone.
if (process.env.WORKTREE_NAME) {
const hostnames = getHostnames();
values.ACCOUNT_PORTAL_OIDC_ISSUER = buildPortlessUrl(hostnames.mockAuth, '/community');
values.ACCOUNT_PORTAL_OIDC_ENDPOINT = buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json');
values.STAFF_PORTAL_OIDC_ISSUER = buildPortlessUrl(hostnames.mockAuth, '/staff');
values.STAFF_PORTAL_OIDC_ENDPOINT = buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json');
const azurite = getAzuriteConnectionString(values);
values.AZURE_STORAGE_CONNECTION_STRING = azurite;
values.AzureWebJobsStorage = azurite;
}

// Runtime-only injection: the e2e harness spawns MongoMemoryServer on a
// random port and passes the connection string through process.env.
if (process.env.COSMOSDB_CONNECTION_STRING) {
values.COSMOSDB_CONNECTION_STRING = process.env.COSMOSDB_CONNECTION_STRING;
}

settings.Values = values;
}

function isE2E() {
return ['1', 'true', 'yes'].includes((process.env.E2E ?? '').toLowerCase());
}
47 changes: 47 additions & 0 deletions apps/api/start-azurite.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { spawn } from 'node:child_process';
import { isGracefulInterruptExit } from '../../scripts/local-dev/dev-process-exit.mjs';
import { getAzuritePorts } from '../../scripts/local-dev/worktree-ports.mjs';

const ports = getAzuritePorts();
const worktreeName = process.env.WORKTREE_NAME ?? '';
const storageSuffix = worktreeName ? `-${worktreeName}` : '';

const blobDir = `../../__blobstorage__${storageSuffix}`;
const queueDir = `../../__queuestorage__${storageSuffix}`;
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
const tableDir = `../../__tablestorage__${storageSuffix}`;

const procSpecs = [
['azurite-blob', ['--silent', '--blobPort', String(ports.blob), '--location', blobDir]],
['azurite-queue', ['--silent', '--queuePort', String(ports.queue), '--location', queueDir]],
['azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', tableDir]],
];
const procs = procSpecs.map(([command, args]) => {
const proc = spawn(command, args, { stdio: 'inherit' });
proc.on('error', (error) => {
console.error(`[azurite] failed to start ${command}: ${error.message}`);
for (const p of procs) p.kill();
process.exit(1);
});
return proc;
});

console.log(`[azurite] started (blob=${ports.blob}, queue=${ports.queue}, table=${ports.table})`);

let exited = 0;
for (const proc of procs) {
proc.on('exit', (code, signal) => {
if (isGracefulInterruptExit(signal, code)) {
if (++exited === procs.length) process.exit(0);
return;
}
console.error(`[azurite] process exited unexpectedly: code=${code} signal=${signal}`);
for (const p of procs) p.kill();
process.exit(code ?? 1);
});
}
process.on('SIGINT', () => {
for (const p of procs) p.kill('SIGINT');
});
process.on('SIGTERM', () => {
for (const p of procs) p.kill('SIGTERM');
});
29 changes: 20 additions & 9 deletions apps/api/start-dev.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { spawn } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs';
import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs';
import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs';
import { getAzuriteConnectionString, getMongoConnectionString } from '../../scripts/local-dev/worktree-ports.mjs';

const envPort = process.env.PORT;

Expand All @@ -18,6 +20,22 @@ const childEnv = {
NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(),
};

// Only inject worktree-scoped overrides when running in worktree mode.
// When WORKTREE_NAME is absent, local.settings.json remains the source of truth.
// Use `??=` so callers can override any individual value via process.env.
if (process.env.WORKTREE_NAME) {
const hostnames = getHostnames();
childEnv.ACCOUNT_PORTAL_OIDC_ISSUER ??= buildPortlessUrl(hostnames.mockAuth, '/community');
childEnv.ACCOUNT_PORTAL_OIDC_ENDPOINT ??= buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json');
childEnv.STAFF_PORTAL_OIDC_ISSUER ??= buildPortlessUrl(hostnames.mockAuth, '/staff');
childEnv.STAFF_PORTAL_OIDC_ENDPOINT ??= buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json');
childEnv.COSMOSDB_CONNECTION_STRING ??= getMongoConnectionString();
childEnv.AZURE_STORAGE_CONNECTION_STRING ??= getAzuriteConnectionString();
childEnv.AzureWebJobsStorage ??= getAzuriteConnectionString();
// Disable the Node.js inspector — port 5858 is already used by the primary worktree.
childEnv.languageWorkers__node__arguments ??= '';
}

// `--cors '*'` matches Host.CORS in local.settings.json but does not depend on
// that file existing — local.settings.json is gitignored, so CI has no CORS
// allowance otherwise and the UI's cross-origin GraphQL requests are blocked.
Expand All @@ -26,11 +44,4 @@ const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/'
env: childEnv,
});

child.on('exit', (code, signal) => {
// Turbo sends signals to interrupt persistent tasks; treat those as graceful exits.
if (isGracefulInterruptExit(signal, code)) {
process.exitCode = 0;
return;
}
process.exitCode = code ?? 1;
});
forwardChildExit(child);
7 changes: 6 additions & 1 deletion apps/api/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
"build": {
"cache": true,
"dependsOn": ["^build", "//#gen"],
"inputs": ["$TURBO_EXTENDS$", "rolldown.config.ts", "host.json", "$TURBO_ROOT$/build-pipeline/scripts/**"],
"inputs": ["$TURBO_EXTENDS$", "rolldown.config.ts", "host.json", "$TURBO_ROOT$/scripts/local-dev/**"],
"outputs": ["$TURBO_EXTENDS$", "deploy/**"]
},
"dev": {
"dependsOn": ["build"],
"interruptible": true,
"inputs": []
},
"dev:worktree": {
"dependsOn": ["build"],
"interruptible": true,
"inputs": []
}
}
}
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"docusaurus": "docusaurus",
"dev": "pnpm exec portless docs.ownercommunity.localhost --force node start-dev.mjs",
"dev:worktree": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs",
"start": "docusaurus start --port 3001",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
Expand Down
11 changes: 2 additions & 9 deletions apps/docs/start-dev.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { spawn } from 'node:child_process';
import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs';
import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs';

const port = process.env.PORT ?? '3001';

const child = spawn('docusaurus', ['start', '--port', port, '--host', '127.0.0.1', '--no-open'], {
stdio: 'inherit',
});

child.on('exit', (code, signal) => {
// Turbo sends signals to interrupt persistent tasks; treat those as graceful exits.
if (isGracefulInterruptExit(signal, code)) {
process.exitCode = 0;
return;
}
process.exitCode = code ?? 1;
});
forwardChildExit(child);
6 changes: 6 additions & 0 deletions apps/docs/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"interruptible": false,
"inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"]
},
"dev:worktree": {
"dependsOn": [],
"persistent": true,
"interruptible": false,
"inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"]
},
"test": {
"inputs": ["$TURBO_EXTENDS$", "!docs/**", "!blog/**", "!static/**"]
},
Expand Down
3 changes: 2 additions & 1 deletion apps/server-mongodb-memory-mock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"format": "biome format --write",
"format:check": "biome format .",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
"dev": "tsx src/index.ts",
"dev:worktree": "node start-mongo.mjs"
},
"dependencies": {
"@cellix/server-mongodb-memory-mock-seedwork": "workspace:*",
Expand Down
12 changes: 12 additions & 0 deletions apps/server-mongodb-memory-mock/start-mongo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { spawn } from 'node:child_process';
import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs';
import { getMongoPort } from '../../scripts/local-dev/worktree-ports.mjs';

const MONGO_PORT = getMongoPort();

const child = spawn('tsx', ['src/index.ts'], {
stdio: 'inherit',
env: { ...process.env, PORT: String(MONGO_PORT) },
});

forwardChildExit(child);
6 changes: 6 additions & 0 deletions apps/server-mongodb-memory-mock/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"persistent": true,
"interruptible": true,
"inputs": []
},
"dev:worktree": {
"dependsOn": ["build"],
"persistent": true,
"interruptible": true,
"inputs": []
}
}
}
1 change: 1 addition & 0 deletions apps/server-oauth2-mock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"format:check": "biome format .",
"start": "node dist/index.js",
"dev": "pnpm exec portless mock-auth.ownercommunity.localhost --force tsx src/index.ts",
"dev:worktree": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest"
Expand Down
9 changes: 5 additions & 4 deletions apps/server-oauth2-mock/src/portal-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,17 @@ function buildPortalFromConfig(config: MockOidcConfig, parsedEnv: Record<string,
const clientIdVar = config.envVars.clientId;
const redirectUriVar = config.envVars.redirectUri;

const clientId = parsedEnv[clientIdVar];
const redirectUri = parsedEnv[redirectUriVar];
// process.env takes precedence — allows worktree-scoped overrides injected at startup
const clientId = process.env[clientIdVar] ?? parsedEnv[clientIdVar];
const redirectUri = process.env[redirectUriVar] ?? parsedEnv[redirectUriVar];

if (!clientId) {
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${clientIdVar} not found in .env`);
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${clientIdVar} not found in .env or process.env`);
return null;
}

if (!redirectUri) {
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${redirectUriVar} not found in .env`);
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${redirectUriVar} not found in .env or process.env`);
return null;
}

Expand Down
20 changes: 20 additions & 0 deletions apps/server-oauth2-mock/start-dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { spawn } from 'node:child_process';
import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs';
import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs';

const childEnv = { ...process.env };

if (process.env.WORKTREE_NAME) {
const hostnames = getHostnames();
childEnv.BASE_URL = buildPortlessUrl(hostnames.mockAuth);
// Override redirect URIs so portal-discovery picks up worktree-scoped URLs.
childEnv.VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI = buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect');
childEnv.VITE_APP_UI_STAFF_AAD_REDIRECT_URI = buildPortlessUrl(hostnames.uiStaff, '/auth-redirect');
}

const child = spawn('tsx', ['src/index.ts'], {
stdio: 'inherit',
env: childEnv,
});

forwardChildExit(child);
Loading
Loading