From f78c3be4db18b826cbc50640b84e97c6bd72a4ea Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 19 May 2026 15:36:01 -0400 Subject: [PATCH 01/12] initial commit for verify --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 7f0e592c1..c64caa156 100644 --- a/readme.md +++ b/readme.md @@ -27,6 +27,7 @@ Our Docusaurus website will help you get started in running and contributing to ## Developer usage + - Full local dev (builds, starts the portless HTTPS proxy, starts Azurite, and runs the app-level dev servers): ```bash From 09e9ab0a295574df2ae1dca1209839f730fb9f35 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 19 May 2026 15:57:41 -0400 Subject: [PATCH 02/12] staging changes for portless modification - using worktree as an env variable and building them into the url for our various portless hosted services --- .snyk | 15 + apps/api/package.json | 7 +- apps/api/start-azurite.mjs | 57 + apps/api/start-dev.mjs | 17 +- apps/api/turbo.json | 5 + apps/docs/package.json | 1 + apps/docs/turbo.json | 6 + apps/server-mongodb-memory-mock/package.json | 3 +- .../start-mongo.mjs | 35 + apps/server-mongodb-memory-mock/turbo.json | 6 + apps/server-oauth2-mock/package.json | 1 + .../src/portal-discovery.ts | 9 +- apps/server-oauth2-mock/start-dev.mjs | 24 + apps/server-oauth2-mock/turbo.json | 6 + apps/ui-community/mock-oidc.users.json | 11 + apps/ui-community/package.json | 1 + apps/ui-community/start-dev.mjs | 24 + apps/ui-community/turbo.json | 6 + apps/ui-staff/package.json | 1 + apps/ui-staff/start-dev.mjs | 23 + build-pipeline/scripts/portless-hostnames.mjs | 88 ++ build-pipeline/scripts/worktree-ports.mjs | 100 ++ knip.json | 5 +- package.json | 4 +- .../server-oauth2-mock-seedwork/package.json | 2 +- .../ocom-verification/e2e-tests/cucumber.js | 2 +- .../ocom-verification/e2e-tests/package.json | 4 +- .../src/shared/support/oauth2-login.ts | 14 +- .../src/shared/support/servers/index.ts | 12 +- .../shared/support/servers/portless-server.ts | 115 +- .../shared/support/servers/test-api-server.ts | 49 +- .../servers/test-community-vite-server.ts | 40 + .../support/servers/test-environment.ts | 30 +- .../support/servers/test-oauth2-server.ts | 74 +- .../support/servers/test-vite-server.ts | 41 - .../shared/support/shared-infrastructure.ts | 47 +- .../verification-shared/src/servers/index.ts | 1 + .../src/servers/test-server.interface.ts | 40 + .../verification-shared/src/settings/index.ts | 2 + .../src/settings/local-settings.ts | 37 +- .../src/settings/portless-settings.ts | 65 ++ .../src/settings/timeout-settings.ts | 48 + packages/ocom/service-otel/package.json | 2 +- pnpm-lock.yaml | 1034 ++++++++++------- pnpm-workspace.yaml | 9 +- turbo.json | 7 + 46 files changed, 1543 insertions(+), 587 deletions(-) create mode 100644 apps/api/start-azurite.mjs create mode 100644 apps/server-mongodb-memory-mock/start-mongo.mjs create mode 100644 apps/server-oauth2-mock/start-dev.mjs create mode 100644 apps/ui-community/start-dev.mjs create mode 100644 apps/ui-staff/start-dev.mjs create mode 100644 build-pipeline/scripts/portless-hostnames.mjs create mode 100644 build-pipeline/scripts/worktree-ports.mjs create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-vite-server.ts create mode 100644 packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts create mode 100644 packages/ocom-verification/verification-shared/src/settings/portless-settings.ts create mode 100644 packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts diff --git a/.snyk b/.snyk index 97bb87848..02a4e4b14 100644 --- a/.snyk +++ b/.snyk @@ -76,3 +76,18 @@ ignore: reason: 'Transitive dependency in Docusaurus; not exploitable in current usage.' expires: '2026-06-28T00:00:00.000Z' created: '2026-05-11T10:00:00.000Z' + 'SNYK-JS-AI-16734889': + - '* > ai@5.0.105': + 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-AISDKPROVIDERUTILS-16734888': + - '* > @ai-sdk/provider-utils@3.0.18': + 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-AISDKPROVIDERUTILS-16735288': + - '* > @ai-sdk/provider-utils@3.0.18': + 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' diff --git a/apps/api/package.json b/apps/api/package.json index fce77a1f4..6041983b2 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -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:portless": "pnpm run prepare:deploy && pnpm run sync-local-settings", + "dev:portless": "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", @@ -22,7 +24,7 @@ "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__" + "azurite": "node start-azurite.mjs" }, "dependencies": { "@azure/functions": "catalog:", @@ -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:*", @@ -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:", diff --git a/apps/api/start-azurite.mjs b/apps/api/start-azurite.mjs new file mode 100644 index 000000000..ac3dff4e4 --- /dev/null +++ b/apps/api/start-azurite.mjs @@ -0,0 +1,57 @@ +import { spawn } from 'node:child_process'; +import net from 'node:net'; +import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; +import { getAzuritePorts } from '../../build-pipeline/scripts/worktree-ports.mjs'; + +const ports = getAzuritePorts(); +const worktreeName = process.env.WORKTREE_NAME ?? ''; +const storageSuffix = worktreeName ? `-${worktreeName}` : ''; + +function isPortListening(port) { + return new Promise((resolve) => { + const socket = net.createConnection({ port, host: '127.0.0.1' }); + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('error', () => { + socket.destroy(); + resolve(false); + }); + }); +} + +if (await isPortListening(ports.blob)) { + console.log(`[azurite] already running (blob port ${ports.blob}), skipping`); + process.exit(0); +} + +const blobDir = `../../__blobstorage__${storageSuffix}`; +const queueDir = `../../__queuestorage__${storageSuffix}`; +const tableDir = `../../__tablestorage__${storageSuffix}`; + +const procs = [ + spawn('azurite-blob', ['--silent', '--blobPort', String(ports.blob), '--location', blobDir], { stdio: 'inherit' }), + spawn('azurite-queue', ['--silent', '--queuePort', String(ports.queue), '--location', queueDir], { stdio: 'inherit' }), + spawn('azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', tableDir], { stdio: 'inherit' }), +]; + +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'); +}); diff --git a/apps/api/start-dev.mjs b/apps/api/start-dev.mjs index 7c705d8c7..978b8744b 100644 --- a/apps/api/start-dev.mjs +++ b/apps/api/start-dev.mjs @@ -2,6 +2,8 @@ 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 { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; +import { getAzuriteConnectionString, getMongoConnectionString } from '../../build-pipeline/scripts/worktree-ports.mjs'; const envPort = process.env.PORT; @@ -18,7 +20,20 @@ const childEnv = { NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(), }; -const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort], { +// Only inject worktree-scoped overrides when running in worktree mode. +// When WORKTREE_NAME is absent, local.settings.json remains the source of truth. +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.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 = ''; +} + +const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort, '--cors', '*'], { stdio: 'inherit', env: childEnv, }); diff --git a/apps/api/turbo.json b/apps/api/turbo.json index 25ef0ca81..0f908f3c8 100644 --- a/apps/api/turbo.json +++ b/apps/api/turbo.json @@ -11,6 +11,11 @@ "dependsOn": ["build"], "interruptible": true, "inputs": [] + }, + "dev:portless": { + "dependsOn": ["build"], + "interruptible": true, + "inputs": [] } } } diff --git a/apps/docs/package.json b/apps/docs/package.json index 64b40e9f8..10766a652 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -5,6 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "dev": "pnpm exec portless docs.ownercommunity.localhost --force node start-dev.mjs", + "dev:portless": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "start": "docusaurus start --port 3001", "build": "docusaurus build", "swizzle": "docusaurus swizzle", diff --git a/apps/docs/turbo.json b/apps/docs/turbo.json index 94418f57b..d39e3fdc8 100644 --- a/apps/docs/turbo.json +++ b/apps/docs/turbo.json @@ -7,6 +7,12 @@ "interruptible": false, "inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"] }, + "dev:portless": { + "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/**"] }, diff --git a/apps/server-mongodb-memory-mock/package.json b/apps/server-mongodb-memory-mock/package.json index d34485afc..2372d4635 100644 --- a/apps/server-mongodb-memory-mock/package.json +++ b/apps/server-mongodb-memory-mock/package.json @@ -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:portless": "node start-mongo.mjs" }, "dependencies": { "@cellix/server-mongodb-memory-mock-seedwork": "workspace:*", diff --git a/apps/server-mongodb-memory-mock/start-mongo.mjs b/apps/server-mongodb-memory-mock/start-mongo.mjs new file mode 100644 index 000000000..1b39ae1ef --- /dev/null +++ b/apps/server-mongodb-memory-mock/start-mongo.mjs @@ -0,0 +1,35 @@ +import net from 'node:net'; +import { getMongoPort } from '../../build-pipeline/scripts/worktree-ports.mjs'; + +const MONGO_PORT = getMongoPort(); + +function isPortListening(port) { + return new Promise((resolve) => { + const socket = net.createConnection({ port, host: '127.0.0.1' }); + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('error', () => { + socket.destroy(); + resolve(false); + }); + }); +} + +if (await isPortListening(MONGO_PORT)) { + console.log(`[mongo-mock] already running on port ${MONGO_PORT}, skipping`); + process.exit(0); +} + +// Not running — start it via tsx with the worktree-scoped port +const { + default: { spawn }, +} = await import('node:child_process'); +const child = spawn('tsx', ['src/index.ts'], { + stdio: 'inherit', + env: { ...process.env, PORT: String(MONGO_PORT) }, +}); +child.on('exit', (code) => { + process.exit(code ?? 1); +}); diff --git a/apps/server-mongodb-memory-mock/turbo.json b/apps/server-mongodb-memory-mock/turbo.json index 01cb45a99..b0f2f18a9 100644 --- a/apps/server-mongodb-memory-mock/turbo.json +++ b/apps/server-mongodb-memory-mock/turbo.json @@ -7,6 +7,12 @@ "persistent": true, "interruptible": true, "inputs": [] + }, + "dev:portless": { + "dependsOn": ["build"], + "persistent": true, + "interruptible": true, + "inputs": [] } } } diff --git a/apps/server-oauth2-mock/package.json b/apps/server-oauth2-mock/package.json index c371c0518..bc92782ff 100644 --- a/apps/server-oauth2-mock/package.json +++ b/apps/server-oauth2-mock/package.json @@ -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:portless": "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" diff --git a/apps/server-oauth2-mock/src/portal-discovery.ts b/apps/server-oauth2-mock/src/portal-discovery.ts index 43b3329c5..34c63cc87 100644 --- a/apps/server-oauth2-mock/src/portal-discovery.ts +++ b/apps/server-oauth2-mock/src/portal-discovery.ts @@ -119,16 +119,17 @@ function buildPortalFromConfig(config: MockOidcConfig, parsedEnv: Record { + if (isGracefulInterruptExit(signal, code)) { + process.exitCode = 0; + return; + } + process.exitCode = code ?? 1; +}); diff --git a/apps/server-oauth2-mock/turbo.json b/apps/server-oauth2-mock/turbo.json index d2d188a5b..325758390 100644 --- a/apps/server-oauth2-mock/turbo.json +++ b/apps/server-oauth2-mock/turbo.json @@ -9,6 +9,12 @@ "interruptible": true, "persistent": true, "inputs": ["$TURBO_DEFAULT$", "../**/mock-oidc*.json"] + }, + "dev:portless": { + "dependsOn": ["build"], + "interruptible": true, + "persistent": true, + "inputs": ["$TURBO_DEFAULT$", "../**/mock-oidc*.json", "start-dev.mjs"] } } } diff --git a/apps/ui-community/mock-oidc.users.json b/apps/ui-community/mock-oidc.users.json index a9953d8c4..b37ea3537 100644 --- a/apps/ui-community/mock-oidc.users.json +++ b/apps/ui-community/mock-oidc.users.json @@ -20,5 +20,16 @@ "family_name": "Doe", "tid": "test-tenant-id" } + }, + { + "username": "owner@test.example", + "sub": "aaaaaaaa-bbbb-1ccc-9ddd-eeeeeeeeee01", + "password": "password", + "claims": { + "email": "owner@test.example", + "given_name": "Test", + "family_name": "Owner", + "tid": "test-tenant-id" + } } ] diff --git a/apps/ui-community/package.json b/apps/ui-community/package.json index 5e8926d5d..86bc14f1d 100644 --- a/apps/ui-community/package.json +++ b/apps/ui-community/package.json @@ -10,6 +10,7 @@ "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", "dev": "pnpm exec portless ownercommunity.localhost --force vite", + "dev:portless": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs new file mode 100644 index 000000000..a13661535 --- /dev/null +++ b/apps/ui-community/start-dev.mjs @@ -0,0 +1,24 @@ +import { spawn } from 'node:child_process'; +import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; + +const hostnames = getHostnames(); + +const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { + stdio: 'inherit', + env: { + ...process.env, + VITE_APP_UI_COMMUNITY_B2C_AUTHORITY: buildPortlessUrl(hostnames.mockAuth, '/community'), + VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'), + VITE_COMMON_API_ENDPOINT: buildPortlessUrl(hostnames.api, '/api/graphql'), + VITE_APP_UI_COMMUNITY_BASE_URL: buildPortlessUrl(hostnames.uiCommunity), + }, +}); + +child.on('exit', (code, signal) => { + if (isGracefulInterruptExit(signal, code)) { + process.exitCode = 0; + return; + } + process.exitCode = code ?? 1; +}); diff --git a/apps/ui-community/turbo.json b/apps/ui-community/turbo.json index d5f21a945..e253b58b7 100644 --- a/apps/ui-community/turbo.json +++ b/apps/ui-community/turbo.json @@ -6,6 +6,12 @@ "persistent": true, "interruptible": false, "inputs": [".env", "package.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] + }, + "dev:portless": { + "dependsOn": ["^build"], + "persistent": true, + "interruptible": false, + "inputs": [".env", "package.json", "start-dev.mjs", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] } } } diff --git a/apps/ui-staff/package.json b/apps/ui-staff/package.json index 1b07d4527..bd35eaf13 100644 --- a/apps/ui-staff/package.json +++ b/apps/ui-staff/package.json @@ -10,6 +10,7 @@ "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", "dev": "pnpm exec portless staff.ownercommunity.localhost --force vite", + "dev:portless": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs new file mode 100644 index 000000000..4b656ce70 --- /dev/null +++ b/apps/ui-staff/start-dev.mjs @@ -0,0 +1,23 @@ +import { spawn } from 'node:child_process'; +import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; + +const hostnames = getHostnames(); + +const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { + stdio: 'inherit', + env: { + ...process.env, + VITE_APP_UI_STAFF_AAD_AUTHORITY: buildPortlessUrl(hostnames.mockAuth, '/staff'), + VITE_APP_UI_STAFF_AAD_REDIRECT_URI: buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'), + VITE_COMMON_API_ENDPOINT: buildPortlessUrl(hostnames.api, '/api/graphql'), + }, +}); + +child.on('exit', (code, signal) => { + if (isGracefulInterruptExit(signal, code)) { + process.exitCode = 0; + return; + } + process.exitCode = code ?? 1; +}); diff --git a/build-pipeline/scripts/portless-hostnames.mjs b/build-pipeline/scripts/portless-hostnames.mjs new file mode 100644 index 000000000..f5df73258 --- /dev/null +++ b/build-pipeline/scripts/portless-hostnames.mjs @@ -0,0 +1,88 @@ +/** + * Shared portless hostname computation for git worktree isolation. + * + * Hostnames are derived from the tracked .env files, so this module contains + * no hardcoded service names. When `WORKTREE_NAME` is set the worktree suffix + * is spliced in before `.localhost`, giving each worktree its own subdomain + * on the shared proxy port. + * + * Default (no worktree): + * ownercommunity.localhost (read from VITE_APP_UI_COMMUNITY_BASE_URL) + * data-access.ownercommunity.localhost (read from VITE_COMMON_API_ENDPOINT) + * + * With WORKTREE_NAME=feature-a: + * ownercommunity.feature-a.localhost + * data-access.ownercommunity.feature-a.localhost + */ + +import { existsSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PORTLESS_PORT = 1355; +const scriptDir = fileURLToPath(new URL('.', import.meta.url)); +const workspaceRoot = resolve(scriptDir, '../..'); + +function readDotEnv(filePath) { + if (!existsSync(filePath)) return {}; + const result = {}; + for (const line of readFileSync(filePath, 'utf-8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) continue; + result[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); + } + return result; +} + +function hostnameFrom(url) { + try { + return new URL(url).hostname; + } catch { + return null; + } +} + +/** Splice `.` in before `.localhost` in an existing hostname. */ +function applyWorktreeSuffix(hostname, worktreeName) { + if (!worktreeName) return hostname; + return hostname.replace('.localhost', `.${worktreeName}.localhost`); +} + +/** + * Returns all service hostnames scoped to the current worktree (if any). + * Hostname shapes are read from the tracked .env files — no names are + * hardcoded in this module. + */ +export function getHostnames() { + const uiEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-community/.env')); + const staffEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-staff/.env')); + const wt = process.env.WORKTREE_NAME ?? ''; + + const uiCommunity = hostnameFrom(uiEnv['VITE_APP_UI_COMMUNITY_BASE_URL'] ?? ''); + const api = hostnameFrom(uiEnv['VITE_COMMON_API_ENDPOINT'] ?? ''); + const mockAuth = hostnameFrom(uiEnv['VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'] ?? ''); + const uiStaff = hostnameFrom(staffEnv['VITE_APP_UI_STAFF_AAD_REDIRECT_URI'] ?? ''); + + if (!uiCommunity || !api || !mockAuth || !uiStaff) { + throw new Error('portless-hostnames: could not derive all hostnames from .env files. ' + 'Ensure apps/ui-community/.env and apps/ui-staff/.env are present.'); + } + + return { + uiCommunity: applyWorktreeSuffix(uiCommunity, wt), + uiStaff: applyWorktreeSuffix(uiStaff, wt), + api: applyWorktreeSuffix(api, wt), + mockAuth: applyWorktreeSuffix(mockAuth, wt), + docs: applyWorktreeSuffix(`docs.${uiCommunity}`, wt), + }; +} + +/** + * Builds a full portless-proxied URL for the given hostname and optional path. + */ +export function buildPortlessUrl(hostname, path = '') { + return `https://${hostname}:${PORTLESS_PORT}${path}`; +} + +export { PORTLESS_PORT }; diff --git a/build-pipeline/scripts/worktree-ports.mjs b/build-pipeline/scripts/worktree-ports.mjs new file mode 100644 index 000000000..9a86b183c --- /dev/null +++ b/build-pipeline/scripts/worktree-ports.mjs @@ -0,0 +1,100 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * Worktree-scoped port computation for service isolation. + * + * When WORKTREE_NAME is set, each worktree gets a deterministic port offset + * so MongoDB and Azurite instances don't collide between worktrees. + * + * Default worktree (no WORKTREE_NAME): uses base ports (50000, 10000–10002). + * Named worktree: base + deterministic offset derived from the name's hash. + * + * Collision safety: the unset case always returns 0, and any named worktree + * always returns ≥ 100, so the default worktree can never collide with a + * named one. With 49 buckets the chance of two *named* worktrees colliding + * is ~2% per pair — acceptable for the typical 1–3 concurrent worktrees. + */ + +const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const apiLocalSettingsPaths = [path.join(workspaceRoot, 'apps', 'api', 'deploy', 'local.settings.json'), path.join(workspaceRoot, 'apps', 'api', 'local.settings.json')]; +let apiLocalSettingsValues; + +function getSetting(name) { + return process.env[name] ?? getApiLocalSetting(name); +} + +function getApiLocalSetting(name) { + apiLocalSettingsValues ??= readApiLocalSettingsValues(); + return apiLocalSettingsValues[name]; +} + +function readApiLocalSettingsValues() { + for (const settingsPath of apiLocalSettingsPaths) { + if (!fs.existsSync(settingsPath)) continue; + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + return settings.Values ?? {}; + } + return {}; +} + +/** + * Returns a deterministic port offset in the range [100, 4900] (step 100) + * for the current worktree. Returns 0 when WORKTREE_NAME is not set. + */ +export function getWorktreePortOffset() { + const name = process.env.WORKTREE_NAME; + if (!name) return 0; + let hash = 0; + for (const c of name) hash = ((hash << 5) - hash + c.charCodeAt(0)) | 0; + return ((Math.abs(hash) % 49) + 1) * 100; +} + +/** MongoDB port for the current worktree. */ +export function getMongoPort() { + return 50000 + getWorktreePortOffset(); +} + +/** Azurite blob/queue/table ports for the current worktree. */ +export function getAzuritePorts() { + const offset = getWorktreePortOffset(); + return { + blob: 10000 + offset, + queue: 10001 + offset, + table: 10002 + offset, + }; +} + +/** + * Azurite connection string for worktree-specific ports. + * Returns `UseDevelopmentStorage=true` for the default worktree (port 10000). + */ +export function getAzuriteConnectionString() { + const ports = getAzuritePorts(); + if (ports.blob === 10000) return 'UseDevelopmentStorage=true'; + const accountName = getSetting('STORAGE_ACCOUNT_NAME'); + const accountKey = getSetting('STORAGE_ACCOUNT_KEY'); + if (!accountName || !accountKey) { + throw new Error('[worktree-ports] STORAGE_ACCOUNT_NAME and STORAGE_ACCOUNT_KEY must be set to build a worktree Azurite connection string'); + } + return [ + 'DefaultEndpointsProtocol=http', + `AccountName=${accountName}`, + `AccountKey=${accountKey}`, + `BlobEndpoint=http://127.0.0.1:${ports.blob}/${accountName}`, + `QueueEndpoint=http://127.0.0.1:${ports.queue}/${accountName}`, + `TableEndpoint=http://127.0.0.1:${ports.table}/${accountName}`, + ].join(';'); +} + +/** + * MongoDB connection string with the worktree-specific port patched in. + * Reads COSMOSDB_CONNECTION_STRING from env or local.settings.json and replaces + * the host:port segment. + */ +export function getMongoConnectionString() { + const base = getSetting('COSMOSDB_CONNECTION_STRING'); + if (!base) throw new Error('[worktree-ports] COSMOSDB_CONNECTION_STRING must be set'); + return base.replace(/127\.0\.0\.1:\d+/, `127.0.0.1:${getMongoPort()}`); +} diff --git a/knip.json b/knip.json index 035e1f57f..4a560cc9a 100644 --- a/knip.json +++ b/knip.json @@ -2,8 +2,9 @@ "$schema": "https://unpkg.com/knip@5/schema.json", "workspaces": { "apps/api": { - "entry": ["src/index.ts"], - "project": ["src/**/*.ts"] + "entry": ["src/index.ts", "start-*.mjs"], + "project": ["src/**/*.ts", "*.mjs"], + "ignoreDependencies": ["azurite"] }, "apps/ui-community": { "entry": ["src/main.tsx"], diff --git a/package.json b/package.json index 12aea8a26..c1757bc2c 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ "test": "turbo run test", "lint": "turbo run lint", "dev": "pnpm proxy:stop && pnpm proxy:start && turbo watch azurite dev --filter='./apps/*' --filter='./packages/*'", + "dev:portless": "WORKTREE_NAME=$(basename $PWD) pnpm proxy:ensure && WORKTREE_NAME=$(basename $PWD) turbo watch azurite dev:portless --filter='./apps/*' --filter='./packages/*'", "start": "turbo run build && concurrently --kill-others-on-fail \"pnpm run start:api\" \"pnpm run start:ui-community\"", "proxy:stop": "pnpm exec portless proxy stop || true", "proxy:start": "pnpm exec portless proxy start --https -p 1355", + "proxy:ensure": "pnpm exec portless proxy start --https -p 1355 || true", "format": "turbo run format", "format:check": "turbo run format:check", "format:staged": "biome check --write --staged --no-errors-on-unmatched", @@ -31,6 +33,7 @@ "test:coverage": "turbo run test:coverage", "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", "test:e2e": "turbo run test:e2e --filter=@ocom-verification/e2e-tests", + "test:e2e:portless": "WORKTREE_NAME=$(basename $PWD) turbo run test:e2e --filter=@ocom-verification/e2e-tests", "test:acceptance": "turbo run test:acceptance --filter=@ocom-verification/acceptance-api --filter=@ocom-verification/acceptance-ui", "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", "test:integration": "turbo run test:integration", @@ -77,7 +80,6 @@ "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "@vitest/coverage-istanbul": "catalog:", - "azurite": "^3.35.0", "chrome-devtools-mcp": "^0.21.0", "concurrently": "^9.1.2", "husky": "^9.1.7", diff --git a/packages/cellix/server-oauth2-mock-seedwork/package.json b/packages/cellix/server-oauth2-mock-seedwork/package.json index ef0015814..68da0838b 100644 --- a/packages/cellix/server-oauth2-mock-seedwork/package.json +++ b/packages/cellix/server-oauth2-mock-seedwork/package.json @@ -18,7 +18,7 @@ "test:watch": "vitest" }, "dependencies": { - "express": "^4.22.0", + "express": "^4.22.2", "express-rate-limit": "^8.5.1", "jose": "^5.9.6" }, diff --git a/packages/ocom-verification/e2e-tests/cucumber.js b/packages/ocom-verification/e2e-tests/cucumber.js index 339a66b11..51912f713 100644 --- a/packages/ocom-verification/e2e-tests/cucumber.js +++ b/packages/ocom-verification/e2e-tests/cucumber.js @@ -7,5 +7,5 @@ export default { formatOptions: { snippetInterface: 'async-await', }, - parallel: 1, + parallel: 0, }; diff --git a/packages/ocom-verification/e2e-tests/package.json b/packages/ocom-verification/e2e-tests/package.json index cbe99de57..6d3508bc6 100644 --- a/packages/ocom-verification/e2e-tests/package.json +++ b/packages/ocom-verification/e2e-tests/package.json @@ -5,7 +5,9 @@ "private": true, "type": "module", "scripts": { - "test:e2e": "NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", + "test:e2e": "pnpm run proxy:start && pnpm run test:e2e:run", + "test:e2e:run": "NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", + "proxy:start": "pnpm exec portless proxy start -p 1355", "playwright:install": "playwright install chromium", "clean": "rimraf dist reports target" }, diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts index b5db83c66..abc2b4638 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts @@ -15,8 +15,8 @@ const isPostAuthUrl = (url: URL) => !url.hostname.includes('mock-auth') && !url. * The app uses RequireAuth + react-oidc-context. When an unauthenticated * user hits a protected route, RequireAuth calls `signinRedirect()` which * navigates to the mock OAuth2 server's `/authorize` endpoint. The mock - * server auto-completes the flow (no login form) and redirects back with a - * code that the OIDC library exchanges for tokens. + * server redirects to `/login` (since userStore is configured). This + * function fills in the test user credentials and submits the form. */ export async function performOAuth2Login(page: Page): Promise { // Navigate to a protected route to trigger the OIDC signinRedirect flow. @@ -29,6 +29,16 @@ export async function performOAuth2Login(page: Page): Promise { // Navigation may be interrupted by OIDC redirect — this is expected } + // Wait for redirects to settle on either the login page or the app + await page.waitForLoadState('domcontentloaded', { timeout: 10_000 }).catch(() => undefined); + + // If the mock OAuth2 login form is shown, fill credentials and submit + if (page.url().includes('/login')) { + await page.fill('input[name="username"]', 'test@example.com'); + await page.fill('input[name="password"]', 'password'); + await page.click('button[type="submit"]'); + } + // Wait for the redirect chain to settle on an authenticated page await page.waitForURL(isPostAuthUrl, { timeout: 30_000 }); await page.waitForLoadState('networkidle'); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts index 6f2cca847..c096f6636 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts @@ -1,6 +1,14 @@ export { MongoDBTestServer } from '@ocom-verification/verification-shared/servers'; export { PortlessServer } from './portless-server.ts'; export { TestApiServer } from './test-api-server.ts'; -export { buildUrl, cleanupTestEnvironment, initTestEnvironment, setMongoConnectionString } from './test-environment.ts'; +export { TestCommunityViteServer } from './test-community-vite-server.ts'; +export { + buildUrl, + cleanupTestEnvironment, + initTestEnvironment, + mockOidcAudience, + mockOidcEndpoint, + mockOidcIssuer, + setMongoConnectionString, +} from './test-environment.ts'; export { TestOAuth2Server } from './test-oauth2-server.ts'; -export { TestViteServer } from './test-vite-server.ts'; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index 8b78c1c02..aea6c3ab1 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -1,37 +1,69 @@ import { type ChildProcess, spawn } from 'node:child_process'; +import type { TestServer } from '@ocom-verification/verification-shared/servers'; +import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { getPortlessPath } from './resolve-portless.ts'; /** * Abstract base class for portless-proxied servers. * Subclasses define the hostname, command, ready marker, and working directory. * The base class handles spawning via portless, readiness detection, and shutdown. + * + * This implements the TestServer interface for consistency with + * GraphQLTestServer (in-process), while providing subprocess isolation + * for full system tests. */ -export abstract class PortlessServer { +export abstract class PortlessServer implements TestServer { private process: ChildProcess | null = null; private startedByUs = false; + private readonly useDetachedProcessGroup = process.platform !== 'win32'; protected abstract get probeUrl(): string; protected abstract get readyMarker(): string; protected abstract get serverName(): string; - protected abstract get startupTimeoutMs(): number; protected abstract get spawnArgs(): string[]; protected abstract get cwd(): string; + + protected get executable(): string { + return getPortlessPath(); + } + + protected get probeRequestInit(): RequestInit { + return {}; + } + protected get extraEnv(): Record { return {}; } + protected isProbeHealthy(response: Response): boolean | Promise { + return response.ok; + } + + protected get startupTimeoutMs(): number { + return getTimeout('serverStartup'); + } + + abstract getUrl(): string; + + /** + * Check if server is already running (via health probe). + */ async isAlreadyRunning(): Promise { try { const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 3_000); - const res = await fetch(this.probeUrl, { signal: controller.signal }); + const probeTimeout = getTimeout('healthProbe'); + const timeout = setTimeout(() => controller.abort(), probeTimeout); + const res = await fetch(this.probeUrl, { ...this.probeRequestInit, signal: controller.signal }); clearTimeout(timeout); - return res.ok; + return await this.isProbeHealthy(res); } catch { return false; } } + /** + * Start the server subprocess and wait for it to be ready. + */ async start(): Promise { if (this.process || this.startedByUs) return; if (await this.isAlreadyRunning()) return; @@ -43,9 +75,10 @@ export abstract class PortlessServer { // Remove NODE_OPTIONS from child process to avoid tsx import issues delete env['NODE_OPTIONS']; - this.process = spawn(getPortlessPath(), this.spawnArgs, { + this.process = spawn(this.executable, this.spawnArgs, { cwd: this.cwd, env, + detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], }); this.startedByUs = true; @@ -53,6 +86,9 @@ export abstract class PortlessServer { await this.waitForReady(); } + /** + * Stop the server gracefully, with fallback to SIGKILL. + */ async stop(): Promise { if (!this.process || !this.startedByUs) return; @@ -60,13 +96,17 @@ export abstract class PortlessServer { this.process = null; this.startedByUs = false; - proc.kill('SIGTERM'); + // SIGINT lets portless run its cleanup branch — deregister the hostname from + // ~/.portless/routes.json before exiting. Fall back to SIGKILL after the + // shutdown timeout for anything that ignores SIGINT. + this.killProcess(proc, 'SIGINT'); + const shutdownTimeout = getTimeout('serverShutdown'); await new Promise((resolve) => { const timeout = setTimeout(() => { - proc.kill('SIGKILL'); + this.killProcess(proc, 'SIGKILL'); resolve(); - }, 10_000); + }, shutdownTimeout); proc.on('exit', () => { clearTimeout(timeout); @@ -87,16 +127,34 @@ export abstract class PortlessServer { return; } + const startupTimeout = this.startupTimeoutMs; const timeout = setTimeout(() => { - reject(new Error(`${this.serverName} did not start within ${this.startupTimeoutMs}ms`)); - }, this.startupTimeoutMs); + reject(new Error(`${this.serverName} did not start within ${startupTimeout}ms`)); + }, startupTimeout); let stderrOutput = ''; - + let ready = false; + + const resolveWhenReachable = () => { + if (ready) return; + ready = true; + + this.waitForProbeReady() + .then(() => { + clearTimeout(timeout); + resolve(); + }) + .catch((error: unknown) => { + clearTimeout(timeout); + reject(error); + }); + }; + + // stdout listener detects the readyMarker then waits for the probe to respond proc.stdout?.on('data', (data: Buffer) => { - if (data.toString().includes(this.readyMarker)) { - clearTimeout(timeout); - resolve(); + const text = data.toString(); + if (text.includes(this.readyMarker)) { + resolveWhenReachable(); } }); @@ -104,19 +162,36 @@ export abstract class PortlessServer { stderrOutput += data.toString(); }); - proc.on('error', (err) => { + proc.on('error', (err: Error) => { clearTimeout(timeout); - this.process = null; - this.startedByUs = false; reject(new Error(`${this.serverName} failed to start: ${err.message}`)); }); - proc.on('exit', (code) => { + proc.on('exit', (code, signal) => { clearTimeout(timeout); this.process = null; this.startedByUs = false; - reject(new Error(`${this.serverName} exited unexpectedly (code: ${code}). stderr: ${stderrOutput.slice(-2000)}`)); + reject(new Error(`${this.serverName} exited unexpectedly (code: ${code}, signal: ${signal}). stderr: ${stderrOutput.slice(-2000)}`)); }); }); } + + private async waitForProbeReady(): Promise { + const probeInterval = getTimeout('healthProbeInterval'); + while (!(await this.isAlreadyRunning())) { + await new Promise((resolve) => setTimeout(resolve, probeInterval)); + } + } + + private killProcess(proc: ChildProcess, signal: NodeJS.Signals): void { + if (this.useDetachedProcessGroup && proc.pid) { + try { + process.kill(-proc.pid, signal); + return; + } catch { + /* Fall back to killing the direct child below. */ + } + } + proc.kill(signal); + } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index 84a87174a..380249d50 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -1,7 +1,9 @@ import { execFileSync } from 'node:child_process'; import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { PortlessServer } from './portless-server.ts'; -import { buildUrl, getMongoConnectionString } from './test-environment.ts'; +import { buildUrl, getHostnames, getMongoConnectionString, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; + +const hostnames = getHostnames(); export class TestApiServer extends PortlessServer { override async start(): Promise { @@ -10,7 +12,7 @@ export class TestApiServer extends PortlessServer { const env = { ...process.env, }; - delete env.NODE_OPTIONS; + delete env['NODE_OPTIONS']; execFileSync('pnpm', ['run', 'predev'], { cwd: this.cwd, @@ -22,19 +24,35 @@ export class TestApiServer extends PortlessServer { } protected get probeUrl() { - return buildUrl('data-access.ownercommunity.localhost', '/api/graphql'); + return buildUrl(hostnames.api, '/api/graphql'); + } + + protected override get probeRequestInit(): RequestInit { + return { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: '{ __typename }' }), + }; } + + protected override async isProbeHealthy(response: Response): Promise { + if (!response.ok) return false; + try { + const data = (await response.json()) as { data?: { __typename?: string } }; + return data?.data?.__typename === 'Query'; + } catch { + return false; + } + } + protected get readyMarker() { return 'Functions:'; } protected get serverName() { return 'TestApiServer'; } - protected get startupTimeoutMs() { - return 120_000; - } protected get spawnArgs() { - return ['data-access.ownercommunity.localhost', 'node', 'start-dev.mjs']; + return [hostnames.api, 'node', 'start-dev.mjs']; } protected get cwd() { return apiSettings.apiDir; @@ -42,15 +60,24 @@ export class TestApiServer extends PortlessServer { protected override get extraEnv() { return { + NODE_ENV: 'development', languageWorkers__node__arguments: '', COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), - ACCOUNT_PORTAL_OIDC_ISSUER: apiSettings.accountPortalOidcIssuer, - ACCOUNT_PORTAL_OIDC_ENDPOINT: apiSettings.accountPortalOidcEndpoint, - VITE_COMMON_API_ENDPOINT: buildUrl('data-access.ownercommunity.localhost', '/api/graphql'), + COSMOSDB_DBNAME: apiSettings.cosmosDbName, + AZURE_STORAGE_CONNECTION_STRING: 'UseDevelopmentStorage=true', + ACCOUNT_PORTAL_OIDC_ISSUER: mockOidcIssuer, + ACCOUNT_PORTAL_OIDC_ENDPOINT: mockOidcEndpoint, + ACCOUNT_PORTAL_OIDC_AUDIENCE: mockOidcAudience, + ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER: 'true', + STAFF_PORTAL_OIDC_ISSUER: mockOidcIssuer, + STAFF_PORTAL_OIDC_ENDPOINT: mockOidcEndpoint, + STAFF_PORTAL_OIDC_AUDIENCE: mockOidcAudience, + STAFF_PORTAL_OIDC_IGNORE_ISSUER: 'true', + VITE_COMMON_API_ENDPOINT: buildUrl(hostnames.api, '/api/graphql'), }; } getUrl(): string { - return buildUrl('data-access.ownercommunity.localhost', '/api/graphql'); + return buildUrl(hostnames.api, '/api/graphql'); } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts new file mode 100644 index 000000000..8de00bfca --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts @@ -0,0 +1,40 @@ +import { apiSettings } from '@ocom-verification/verification-shared/settings'; +import { PortlessServer } from './portless-server.ts'; +import { buildUrl, getHostnames } from './test-environment.ts'; + +const hostnames = getHostnames(); + +/** + * Starts the community portal Vite dev server via portless. + * + * The `apps/ui-community/start-dev.mjs` script sets all VITE_* environment + * variables at runtime using portless-hostnames.mjs, so no extraEnv overrides + * are needed here beyond suppressing browser auto-launch. + */ +export class TestCommunityViteServer extends PortlessServer { + protected get probeUrl() { + return buildUrl(hostnames.uiCommunity); + } + protected get readyMarker() { + return 'ready in'; + } + protected get serverName() { + return 'TestCommunityViteServer'; + } + protected get spawnArgs() { + return [hostnames.uiCommunity, 'node', 'start-dev.mjs']; + } + protected get cwd() { + return apiSettings.uiCommunityDir; + } + protected override get extraEnv() { + return { + BROWSER: 'none', + NODE_ENV: 'development', + }; + } + + getUrl(): string { + return buildUrl(hostnames.uiCommunity); + } +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index 76c9a1d8d..4a8fbb54f 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -1,30 +1,48 @@ import { execFileSync } from 'node:child_process'; +import { buildPortlessUrl, getHostnames } from '@ocom-verification/verification-shared/settings'; import { getPortlessPath } from './resolve-portless.ts'; let proxyInitialized = false; let mongoConnectionString: string | undefined; +/** Module-level hostnames derived from .env files (matches dev:portless pattern). */ +const hostnames = getHostnames(); + +/** OIDC issuer URL for the community portal on the mock auth server. */ +export const mockOidcIssuer = buildPortlessUrl(hostnames.mockAuth, '/community'); + +/** JWKS endpoint used as the OIDC discovery / probe URL. */ +export const mockOidcEndpoint = `${mockOidcIssuer}/.well-known/jwks.json`; + +/** Audience claim expected in JWTs issued by the mock OIDC server. */ +export const mockOidcAudience = 'mock-client'; + +/** + * Prune orphaned portless route locks from previous test runs. + * The proxy itself is started by the `test:e2e` npm script before the + * Cucumber process spawns, so we only need to clean stale locks here. + */ export function initTestEnvironment() { if (proxyInitialized) return; - execFileSync(getPortlessPath(), ['proxy', 'start', '-p', '1355'], { - timeout: 15_000, + execFileSync(getPortlessPath(), ['prune'], { + timeout: 10_000, stdio: 'pipe', }); proxyInitialized = true; } -export function buildUrl(hostname: string, path = ''): string { - return `https://${hostname}:1355${path}`; -} +export { buildPortlessUrl as buildUrl, getHostnames }; export function setMongoConnectionString(connStr: string): void { mongoConnectionString = connStr; } export function getMongoConnectionString(): string { - if (!mongoConnectionString) throw new Error('MongoDB connection string not set. Start MongoDBTestServer first.'); + if (!mongoConnectionString) { + throw new Error('MongoDB connection string not set — call setMongoConnectionString() first'); + } return mongoConnectionString; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts index 319f80830..7d843f446 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts @@ -1,81 +1,33 @@ import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { PortlessServer } from './portless-server.ts'; -import { buildUrl } from './test-environment.ts'; +import { getHostnames, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; +const hostnames = getHostnames(); + +/** + * Starts the mock OAuth2/OIDC server via portless. + * + * Login is performed by the browser context in oauth2-login.ts rather than + * by programmatic token generation — this tests the real OIDC redirect flow. + */ export class TestOAuth2Server extends PortlessServer { protected get probeUrl() { - return apiSettings.accountPortalOidcEndpoint; + return mockOidcEndpoint; } protected get readyMarker() { - return 'Mock OAuth2 server running'; + return 'Registered OIDC config'; } protected get serverName() { return 'TestOAuth2Server'; } - protected get startupTimeoutMs() { - return 30_000; - } protected get spawnArgs() { - return ['mock-auth.ownercommunity.localhost', 'pnpm', 'exec', 'tsx', 'src/index.ts']; + return [hostnames.mockAuth, 'node', 'start-dev.mjs']; } protected get cwd() { return apiSettings.oauth2MockDir; } - private readonly testUser: { - email: string; - given_name: string; - family_name: string; - }; - - constructor(options?: { - testUser?: { - email?: string; - given_name?: string; - family_name?: string; - }; - }) { - super(); - this.testUser = { - email: options?.testUser?.email ?? 'alice@test.cellix.local', - given_name: options?.testUser?.given_name ?? 'Alice', - family_name: options?.testUser?.family_name ?? 'Test', - }; - } - - protected override get extraEnv() { - return { - EMAIL: this.testUser.email, - GIVEN_NAME: this.testUser.given_name, - FAMILY_NAME: this.testUser.family_name, - BASE_URL: buildUrl('mock-auth.ownercommunity.localhost'), - ALLOWED_REDIRECT_URI: buildUrl('ownercommunity.localhost', '/auth-redirect'), - CLIENT_ID: apiSettings.accountPortalOidcAudience, - }; - } - getUrl(): string { - return apiSettings.accountPortalOidcIssuer; - } - - async generateAccessToken(_audience = 'mock-client'): Promise { - const issuer = this.getUrl(); - const uiBaseUrl = buildUrl('ownercommunity.localhost'); - const redirectUri = `${uiBaseUrl}/auth-redirect`; - - const code = `mock-auth-code-${Buffer.from(redirectUri).toString('base64')}`; - - const response = await fetch(`${issuer}/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code, grant_type: 'authorization_code' }), - }); - - if (!response.ok) { - throw new Error(`Token request failed: ${response.status} ${await response.text()}`); - } - - const data = (await response.json()) as { access_token: string }; - return data.access_token; + return mockOidcIssuer; } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-vite-server.ts deleted file mode 100644 index 44ede2444..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-vite-server.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { apiSettings } from '@ocom-verification/verification-shared/settings'; -import { PortlessServer } from './portless-server.ts'; -import { buildUrl } from './test-environment.ts'; - -export class TestViteServer extends PortlessServer { - protected get probeUrl() { - return buildUrl('ownercommunity.localhost'); - } - protected get readyMarker() { - return 'ready in'; - } - protected get serverName() { - return 'TestViteServer'; - } - protected get startupTimeoutMs() { - return 60_000; - } - protected get spawnArgs() { - return ['ownercommunity.localhost', 'pnpm', 'exec', 'vite']; - } - protected get cwd() { - return apiSettings.uiDir; - } - - protected override get extraEnv() { - const uiBase = buildUrl('ownercommunity.localhost'); - const apiEndpoint = buildUrl('data-access.ownercommunity.localhost', '/api/graphql'); - - return { - BROWSER: 'none', - VITE_APP_UI_COMMUNITY_BASE_URL: uiBase, - VITE_AAD_B2C_ACCOUNT_AUTHORITY: apiSettings.accountPortalOidcIssuer, - VITE_AAD_B2C_REDIRECT_URI: `${uiBase}/auth-redirect`, - VITE_COMMON_API_ENDPOINT: apiEndpoint, - }; - } - - getUrl(): string { - return buildUrl('ownercommunity.localhost'); - } -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index d3d2c3ece..e91cca9b9 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -1,29 +1,41 @@ -import { apiSettings } from '@ocom-verification/verification-shared/settings'; -import { actors } from '@ocom-verification/verification-shared/test-data'; import playwright, { type Browser, type BrowserContext } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; import { performOAuth2Login } from './oauth2-login.ts'; -import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestOAuth2Server, TestViteServer } from './servers/index.ts'; +import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; let mongoDBServer: MongoDBTestServer | undefined; let oauth2Server: TestOAuth2Server | undefined; let apiServer: TestApiServer | undefined; -let viteServer: TestViteServer | undefined; +let viteServer: TestCommunityViteServer | undefined; let apiUrl: string | undefined; -let accessToken: string | undefined; let browser: Browser | undefined; let browserBaseUrl: string | undefined; let authenticatedBrowserContext: BrowserContext | undefined; let browseTheWeb: BrowseTheWeb | undefined; +let shutdownHandlersRegistered = false; + +function registerShutdownHandlers(): void { + if (shutdownHandlersRegistered) return; + shutdownHandlersRegistered = true; + + const shutdown = (signal: string) => { + void stopAll().finally(() => { + process.exit(signal === 'SIGINT' ? 130 : 143); + }); + }; + + process.once('SIGINT', () => shutdown('SIGINT')); + process.once('SIGTERM', () => shutdown('SIGTERM')); +} + export interface InfrastructureState { apiUrl: string | undefined; - accessToken: string | undefined; browseTheWeb: BrowseTheWeb | undefined; } export function getState(): InfrastructureState { - return { apiUrl, accessToken, browseTheWeb }; + return { apiUrl, browseTheWeb }; } export async function stopAll(): Promise { @@ -56,22 +68,16 @@ export async function stopAll(): Promise { } apiUrl = undefined; browserBaseUrl = undefined; - accessToken = undefined; cleanupTestEnvironment(); } export async function ensureE2EServers(): Promise { + registerShutdownHandlers(); initTestEnvironment(); // Phase 1: Start MongoDB and OAuth2 in parallel (no interdependency) mongoDBServer ??= new MongoDBTestServer(); - oauth2Server ??= new TestOAuth2Server({ - testUser: { - email: actors.CommunityOwner.email, - given_name: actors.CommunityOwner.givenName, - family_name: actors.CommunityOwner.familyName, - }, - }); + oauth2Server ??= new TestOAuth2Server(); const mongo = mongoDBServer; const oauth2 = oauth2Server; const phase1: Promise[] = []; @@ -83,9 +89,9 @@ export async function ensureE2EServers(): Promise { } if (phase1.length > 0) await Promise.all(phase1); - // Phase 2: Start API (needs MongoDB conn string), Vite (independent), and generate token (needs OAuth2) in parallel + // Phase 2: Start API (needs MongoDB conn string) and Vite (independent) in parallel apiServer ??= new TestApiServer(); - viteServer ??= new TestViteServer(); + viteServer ??= new TestCommunityViteServer(); const api = apiServer; const vite = viteServer; const phase2: Promise[] = []; @@ -99,13 +105,6 @@ export async function ensureE2EServers(): Promise { if (!vite.isRunning()) { phase2.push(vite.start()); } - if (!accessToken) { - phase2.push( - oauth2.generateAccessToken(apiSettings.accountPortalOidcAudience).then((token) => { - accessToken = token; - }), - ); - } if (phase2.length > 0) await Promise.all(phase2); browserBaseUrl = viteServer.getUrl(); diff --git a/packages/ocom-verification/verification-shared/src/servers/index.ts b/packages/ocom-verification/verification-shared/src/servers/index.ts index 585f779f2..32810914a 100644 --- a/packages/ocom-verification/verification-shared/src/servers/index.ts +++ b/packages/ocom-verification/verification-shared/src/servers/index.ts @@ -7,3 +7,4 @@ export { MongoDBTestServer, seedOwnerCommunityReferenceData, } from './test-mongodb-server.ts'; +export type { TestServer, TestServerOptions } from './test-server.interface.ts'; diff --git a/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts b/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts new file mode 100644 index 000000000..8b08f6b92 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts @@ -0,0 +1,40 @@ +/** + * Common interface for all test servers (in-process and subprocess). + * + * This abstraction allows acceptance-api and e2e tests to use + * consistent server lifecycle management patterns while choosing + * the appropriate implementation: + * + * - **In-process** (GraphQLTestServer): Fast, isolated, mocked services + * Best for: API acceptance tests, unit-like integration tests + * + * - **Subprocess** (PortlessServer): Full stack, realistic, real services + * Best for: E2E tests, full system integration tests + */ +export interface TestServer { + /** Start the server and return when ready */ + start(): Promise; + + /** Stop the server gracefully */ + stop(): Promise; + + /** Check if server is currently running */ + isRunning(): boolean; + + /** Get the server URL (throws if not running) */ + getUrl(): string; +} + +/** + * Configuration options for test server startup. + */ +export interface TestServerOptions { + /** Port to listen on (0 for random available port) */ + port?: number; + + /** Additional environment variables for subprocess servers */ + env?: Record; + + /** Timeout for server startup (defaults to centralized config) */ + startupTimeoutMs?: number; +} diff --git a/packages/ocom-verification/verification-shared/src/settings/index.ts b/packages/ocom-verification/verification-shared/src/settings/index.ts index 88ed046dd..68ac69337 100644 --- a/packages/ocom-verification/verification-shared/src/settings/index.ts +++ b/packages/ocom-verification/verification-shared/src/settings/index.ts @@ -1,4 +1,5 @@ export { apiSettings, uiSettings } from './local-settings.ts'; +export { buildPortlessUrl, getHostnames, PORTLESS_PORT } from './portless-settings.ts'; export { findWorkspaceRoot, readDotEnv, @@ -7,3 +8,4 @@ export { requireSetting, resolveWorkspacePath, } from './settings-utils.ts'; +export { getTimeout, type TimeoutKey, timeouts } from './timeout-settings.ts'; diff --git a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts index e1a03e801..c28d7a6f5 100644 --- a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts @@ -8,21 +8,40 @@ const uiEnvPath = resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env'); const apiValues = readJsonSettings(apiSettingsPath); const uiValues = readDotEnv(uiEnvPath); +/** + * Defaults for E2E/acceptance test settings when local.settings.json is absent + * (e.g. CI pipelines). All values are non-secret mock/localhost references used + * exclusively by the test harness — no real credentials are involved. + */ +const ciDefaults = { + COSMOSDB_CONNECTION_STRING: '', + COSMOSDB_DBNAME: 'owner-community', + COSMOSDB_PORT: '50000', + NODE_ENV: 'development', + ACCOUNT_PORTAL_OIDC_AUDIENCE: 'mock-client', + ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', + ACCOUNT_PORTAL_OIDC_ENDPOINT: 'https://mock-auth.ownercommunity.localhost:1355/community/.well-known/jwks.json', +} as const; + +function setting(key: keyof typeof ciDefaults): string { + return readSetting(apiValues, key, ciDefaults[key]) ?? ciDefaults[key]; +} + export const apiSettings = { - nodeEnv: readSetting(apiValues, 'NODE_ENV', 'development') ?? 'development', - isDevelopment: (readSetting(apiValues, 'NODE_ENV', 'development') ?? 'development') === 'development', + nodeEnv: setting('NODE_ENV'), + isDevelopment: setting('NODE_ENV') === 'development', - cosmosDbConnectionString: readSetting(apiValues, 'COSMOSDB_CONNECTION_STRING') ?? '', - cosmosDbName: readSetting(apiValues, 'COSMOSDB_DBNAME', 'owner-community') ?? 'owner-community', - cosmosDbPort: Number(readSetting(apiValues, 'COSMOSDB_PORT', '50000')), + cosmosDbConnectionString: setting('COSMOSDB_CONNECTION_STRING'), + cosmosDbName: setting('COSMOSDB_DBNAME'), + cosmosDbPort: Number(setting('COSMOSDB_PORT')), - accountPortalOidcIssuer: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ISSUER') ?? '', - accountPortalOidcEndpoint: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ENDPOINT') ?? '', - accountPortalOidcAudience: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_AUDIENCE', 'mock-client') ?? '', + accountPortalOidcIssuer: setting('ACCOUNT_PORTAL_OIDC_ISSUER'), + accountPortalOidcEndpoint: setting('ACCOUNT_PORTAL_OIDC_ENDPOINT'), + accountPortalOidcAudience: setting('ACCOUNT_PORTAL_OIDC_AUDIENCE'), apiDir: path.dirname(apiSettingsPath), oauth2MockDir: path.join(workspaceRoot, 'apps', 'server-oauth2-mock'), - uiDir: path.dirname(uiEnvPath), + uiCommunityDir: path.dirname(uiEnvPath), } as const; export const uiSettings = { diff --git a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts new file mode 100644 index 000000000..e084702e0 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts @@ -0,0 +1,65 @@ +/** + * Portless hostname derivation for test environments. + * + * Mirrors the logic of `build-pipeline/scripts/portless-hostnames.mjs` — + * hostnames are derived from the tracked .env files rather than hardcoded, + * and the `WORKTREE_NAME` suffix is applied automatically. + * + * This keeps test-environment code in sync with the dev:portless startup + * scripts without adding a cross-layer import to build-pipeline/. + */ + +import { findWorkspaceRoot, readDotEnv, resolveWorkspacePath } from './settings-utils.ts'; + +const PORTLESS_PORT = 1355; +const workspaceRoot = findWorkspaceRoot(); + +const uiCommunityEnv = readDotEnv(resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env')); +const uiStaffEnv = readDotEnv(resolveWorkspacePath(workspaceRoot, 'apps/ui-staff/.env')); + +function hostnameFromUrl(url: string): string { + try { + return new URL(url).hostname; + } catch { + return ''; + } +} + +/** Splice `.` in before `.localhost` for worktree isolation. */ +function applyWorktreeSuffix(hostname: string): string { + const wt = process.env['WORKTREE_NAME']; + if (!wt) return hostname; + return hostname.replace('.localhost', `.${wt}.localhost`); +} + +/** + * Returns all portless service hostnames scoped to the current worktree. + * Hostnames are derived from the tracked .env files — no names are hardcoded. + */ +export function getHostnames() { + const uiCommunity = hostnameFromUrl(uiCommunityEnv['VITE_APP_UI_COMMUNITY_BASE_URL'] ?? ''); + const api = hostnameFromUrl(uiCommunityEnv['VITE_COMMON_API_ENDPOINT'] ?? ''); + const mockAuth = hostnameFromUrl(uiCommunityEnv['VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'] ?? ''); + const uiStaff = hostnameFromUrl(uiStaffEnv['VITE_APP_UI_STAFF_AAD_REDIRECT_URI'] ?? ''); + + if (!uiCommunity || !api || !mockAuth) { + throw new Error( + 'portless-settings: could not derive hostnames from .env files. ' + 'Ensure apps/ui-community/.env is present with VITE_APP_UI_COMMUNITY_BASE_URL, ' + 'VITE_COMMON_API_ENDPOINT, and VITE_APP_UI_COMMUNITY_B2C_AUTHORITY.', + ); + } + + return { + uiCommunity: applyWorktreeSuffix(uiCommunity), + uiStaff: uiStaff ? applyWorktreeSuffix(uiStaff) : '', + api: applyWorktreeSuffix(api), + mockAuth: applyWorktreeSuffix(mockAuth), + docs: applyWorktreeSuffix(`docs.${uiCommunity}`), + }; +} + +/** Build a full portless HTTPS URL from a hostname and optional path. */ +export function buildPortlessUrl(hostname: string, path = ''): string { + return `https://${hostname}:${PORTLESS_PORT}${path}`; +} + +export { PORTLESS_PORT }; diff --git a/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts b/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts new file mode 100644 index 000000000..309ef7f3e --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts @@ -0,0 +1,48 @@ +/** + * Centralized timeout configuration for all verification test packages. + * + * These timeouts are intentionally generous to accommodate: + * - CI environments with limited resources + * - First-time server startup (cold starts) + * - Parallel test execution contention + */ +export const timeouts = { + /** Default scenario timeout (2 minutes) */ + scenario: 120_000, + + /** Server startup timeout (2 minutes) */ + serverStartup: 120_000, + + /** Server shutdown graceful period (10 seconds) */ + serverShutdown: 10_000, + + /** Health probe timeout (3 seconds) */ + healthProbe: 3_000, + + /** Health probe retry interval (500ms) */ + healthProbeInterval: 500, + + /** UI initialization timeout (30 seconds) */ + uiInit: 30_000, + + /** UI cleanup timeout (10 seconds) */ + uiCleanup: 10_000, +} as const; + +/** Type for timeout configuration keys */ +export type TimeoutKey = keyof typeof timeouts; + +/** + * Get timeout value with optional override from environment. + * Usage: TIMEOUT_SERVER_STARTUP=300000 npm test + */ +export function getTimeout(key: TimeoutKey): number { + const envOverride = process.env[`TIMEOUT_${key.toUpperCase()}`]; + if (envOverride) { + const parsed = Number.parseInt(envOverride, 10); + if (!Number.isNaN(parsed)) { + return parsed; + } + } + return timeouts[key]; +} diff --git a/packages/ocom/service-otel/package.json b/packages/ocom/service-otel/package.json index 18fb61910..0fe4c8bd5 100644 --- a/packages/ocom/service-otel/package.json +++ b/packages/ocom/service-otel/package.json @@ -35,7 +35,7 @@ "@opentelemetry/instrumentation-mongoose": "0.47.0", "@opentelemetry/sdk-logs": "0.57.2", "@opentelemetry/sdk-metrics": "1.30.1", - "@opentelemetry/sdk-node": "0.57.2", + "@opentelemetry/sdk-node": "0.217.0", "@opentelemetry/sdk-trace-node": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0464b416e..a16e5e4be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,7 +105,8 @@ overrides: rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 brace-expansion@1.1.12: 1.1.13 - brace-expansion@5.0.4: 5.0.5 + brace-expansion@5.0.4: 5.0.6 + brace-expansion@5.0.5: 5.0.6 diff@4.0.2: 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 @@ -116,7 +117,7 @@ overrides: svgo: ^3.3.3 yaml@2.8.2: 2.8.3 yauzl@3.2.0: 3.2.1 - qs: ^6.14.2 + qs: 6.15.2 ajv@^6: 6.14.0 lodash: 4.18.1 lodash-es: 4.18.1 @@ -126,7 +127,9 @@ overrides: webpack: ^5.105.4 webpack-dev-server: ^5.2.4 express-rate-limit: 8.5.1 - uuid: 14.0.0 + '@azure/ms-rest-js>uuid': ^3.4.0 + azurite>uuid: ^3.4.0 + ws@8.20.0: 8.20.1 playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 @@ -190,9 +193,6 @@ importers: '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.2(vitest@4.1.2) - azurite: - specifier: ^3.35.0 - version: 3.35.0 chrome-devtools-mcp: specifier: ^0.21.0 version: 0.21.0 @@ -290,6 +290,9 @@ importers: '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.2(vitest@4.1.2) + azurite: + specifier: ^3.35.0 + version: 3.35.0(@azure/core-client@1.10.1)(@types/node@24.10.1) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -430,7 +433,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../packages/cellix/ui-core @@ -454,7 +457,7 @@ importers: version: 6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) apollo-link-rest: specifier: ^0.9.0 - version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0) + version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) less: specifier: ^4.4.0 version: 4.4.2 @@ -527,7 +530,7 @@ importers: version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) storybook-addon-apollo-client: specifier: ^9.0.0 - version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) + version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) tailwindcss: specifier: ^3.4.17 version: 3.4.18(tsx@4.21.0)(yaml@2.8.3) @@ -545,7 +548,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../packages/cellix/ui-core @@ -581,7 +584,7 @@ importers: version: 6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) apollo-link-rest: specifier: ^0.9.0 - version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0) + version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) less: specifier: ^4.4.0 version: 4.4.2 @@ -894,11 +897,11 @@ importers: packages/cellix/server-oauth2-mock-seedwork: dependencies: express: - specifier: ^4.22.0 - version: 4.22.1 + specifier: ^4.22.2 + version: 4.22.2 express-rate-limit: specifier: 8.5.1 - version: 8.5.1(express@4.22.1) + version: 8.5.1(express@4.22.2) jose: specifier: ^5.9.6 version: 5.10.0 @@ -1698,8 +1701,8 @@ importers: specifier: 1.30.1 version: 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: 0.57.2 - version: 0.57.2(@opentelemetry/api@1.9.0) + specifier: 0.217.0 + version: 0.217.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': specifier: 1.30.1 version: 1.30.1(@opentelemetry/api@1.9.0) @@ -1764,7 +1767,7 @@ importers: version: 7.22.7(antd@6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -1855,7 +1858,7 @@ importers: version: 7.22.7(antd@6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2010,7 +2013,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2092,7 +2095,7 @@ importers: version: 6.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2171,7 +2174,7 @@ importers: version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) storybook-addon-apollo-client: specifier: ^9.0.0 - version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) + version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) typescript: specifier: 'catalog:' version: 6.0.3 @@ -2745,8 +2748,8 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - '@azure-rest/core-client@2.5.1': - resolution: {integrity: sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==} + '@azure-rest/core-client@2.6.0': + resolution: {integrity: sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ==} engines: {node: '>=20.0.0'} '@azure/abort-controller@1.1.0': @@ -2769,9 +2772,12 @@ packages: resolution: {integrity: sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==} engines: {node: '>=20.0.0'} - '@azure/core-http-compat@2.3.1': - resolution: {integrity: sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==} + '@azure/core-http-compat@2.4.0': + resolution: {integrity: sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==} engines: {node: '>=20.0.0'} + peerDependencies: + '@azure/core-client': ^1.10.0 + '@azure/core-rest-pipeline': ^1.22.0 '@azure/core-lro@2.7.2': resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} @@ -2815,9 +2821,9 @@ packages: resolution: {integrity: sha512-0q5DL4uyR0EZ4RXQKD8MadGH6zTIcloUoS/RVbCpNpej4pwte0xpqYxk8K97Py2RiuUvI7F4GXpoT4046VfufA==} engines: {node: '>=14.0.0'} - '@azure/keyvault-common@2.0.0': - resolution: {integrity: sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==} - engines: {node: '>=18.0.0'} + '@azure/keyvault-common@2.1.0': + resolution: {integrity: sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw==} + engines: {node: '>=20.0.0'} '@azure/keyvault-keys@4.10.0': resolution: {integrity: sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==} @@ -2846,9 +2852,9 @@ packages: resolution: {integrity: sha512-CO+SE4weOsfJf+C5LM8argzvotrXw252/ZU6SM2Tz63fEblhH1uuVaaO4ISYFuN4Q6BhTo7I3qIdi8ydUQCqhw==} engines: {node: '>=16'} - '@azure/opentelemetry-instrumentation-azure-sdk@1.0.0-beta.9': - resolution: {integrity: sha512-gNCFokEoQQEkhu2T8i1i+1iW2o9wODn2slu5tpqJmjV1W7qf9dxVv6GNXW1P1WC8wMga8BCc2t/oMhOK3iwRQg==} - engines: {node: '>=18.0.0'} + '@azure/opentelemetry-instrumentation-azure-sdk@1.0.0': + resolution: {integrity: sha512-Y8rZOIMXQY/GwNRL+uLVuwIn9aEa/KnnggyYUmFxC1MigmRJCNH5NxMmxKSpddXF9SW6Z1ijRd6Pptd2A5OhGw==} + engines: {node: '>=20.0.0'} '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} @@ -4806,8 +4812,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@js-joda/core@5.6.5': - resolution: {integrity: sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ==} + '@js-joda/core@5.7.0': + resolution: {integrity: sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==} '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} @@ -4902,6 +4908,14 @@ packages: resolution: {integrity: sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==} engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.211.0': + resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.217.0': + resolution: {integrity: sha512-Cdq0jW2lknrNfrAm92MyEAvpe2cRsKjdnQLHUL6xRA4IVUnsWx6P65E7NcUO0Y+L4w1Aee5iV8FvjSwd+lrs9A==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.52.1': resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==} engines: {node: '>=14'} @@ -4914,12 +4928,24 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@opentelemetry/configuration@0.217.0': + resolution: {integrity: sha512-xCtrYOhBqdy6ZOMfe0Oa73ZKF+2LMhoOv4L5vmwAHVvOXUg+V3fvKuEIr9ZyD0Ow+vxllEjWO6PV1wd0DOtyvw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks@1.30.1': resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/context-async-hooks@2.7.1': + resolution: {integrity: sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@1.25.1': resolution: {integrity: sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==} engines: {node: '>=14'} @@ -4944,69 +4970,69 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.57.2': - resolution: {integrity: sha512-eovEy10n3umjKJl2Ey6TLzikPE+W4cUQ4gCwgGP1RqzTGtgDra0WjIqdy29ohiUKfvmbiL3MndZww58xfIvyFw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-logs-otlp-grpc@0.217.0': + resolution: {integrity: sha512-vC5S0Dc+noxD86CVtNu1+awCHPA5Kewi1Sg23ps+9lh4YifwsKXh3pe4XTNEKtUJiAcjpJ5dqStGakLbrSE+YQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.57.2': - resolution: {integrity: sha512-0rygmvLcehBRp56NQVLSleJ5ITTduq/QfU7obOkyWgPpFHulwpw2LYTqNIz5TczKZuy5YY+5D3SDnXZL1tXImg==} - engines: {node: '>=14'} + '@opentelemetry/exporter-logs-otlp-http@0.217.0': + resolution: {integrity: sha512-KfLAdt1uilVE+3FxbgVnp2ZrzqbIawzcesnRoi+Kh9ckB5Ld5D8btUgoBvwTbdmuNx1j6b132Wsh72azq+pPNQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.57.2': - resolution: {integrity: sha512-ta0ithCin0F8lu9eOf4lEz9YAScecezCHkMMyDkvd9S7AnZNX5ikUmC5EQOQADU+oCcgo/qkQIaKcZvQ0TYKDw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-logs-otlp-proto@0.217.0': + resolution: {integrity: sha512-Se0GG/ZO24mQTlQj7zprR4pNI0nKe4lPDPBsuJmi6508b9TlZEuUd3EfyuHk6oJxzL7fGyDFYAbxNigQvRP2ZQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2': - resolution: {integrity: sha512-r70B8yKR41F0EC443b5CGB4rUaOMm99I5N75QQt6sHKxYDzSEc6gm48Diz1CI1biwa5tDPznpylTrywO/pT7qw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0': + resolution: {integrity: sha512-0GpJKnCoVaVA1rKBMVPHziznfOQlXgH72S9ktjBAF1AnAVPzX7vVEBGrhwiSxxHDAiefXk+J8znApsMb/K6Z3w==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.57.2': - resolution: {integrity: sha512-ttb9+4iKw04IMubjm3t0EZsYRNWr3kg44uUuzfo9CaccYlOh8cDooe4QObDUkvx9d5qQUrbEckhrWKfJnKhemA==} - engines: {node: '>=14'} + '@opentelemetry/exporter-metrics-otlp-http@0.217.0': + resolution: {integrity: sha512-1zkMzzhiNJdVmLxuwkltqWGw4fOOam47bqRxmuQNjyKJe/9NmY5cIrZ4kiQV7sVGxoOgT0ZvGUfLcjvtpC/b9Q==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.57.2': - resolution: {integrity: sha512-HX068Q2eNs38uf7RIkNN9Hl4Ynl+3lP0++KELkXMCpsCbFO03+0XNNZ1SkwxPlP9jrhQahsMPMkzNXpq3fKsnw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-metrics-otlp-proto@0.217.0': + resolution: {integrity: sha512-nfxt/KxVGFkjkO/M+58y1ugHu/dwPtxG4eYq0KApcQ7xk5CHzhdn+IuLZfDSvNDrJ3Uy5q++Fj/wbK7i8yryfQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.57.2': - resolution: {integrity: sha512-VqIqXnuxWMWE/1NatAGtB1PvsQipwxDcdG4RwA/umdBcW3/iOHp0uejvFHTRN2O78ZPged87ErJajyUBPUhlDQ==} - engines: {node: '>=14'} + '@opentelemetry/exporter-prometheus@0.217.0': + resolution: {integrity: sha512-U9MCXxJu0sBCh5aEkylYRR4xVIL8D1CW6dGwvYXbfFr0qveSorfD0XJchCAWoW6QfAAIcY/yxjf4Dj8OgkHBPw==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.57.2': - resolution: {integrity: sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-trace-otlp-grpc@0.217.0': + resolution: {integrity: sha512-fPZs2fw7veLH3pEKu8vSepUa2fQpAE2P7al6qU10aH9GrEJJ8YaPgsd5xON7by5rbcEVS71FOU2aWyK6nzB7VQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.57.2': - resolution: {integrity: sha512-sB/gkSYFu+0w2dVQ0PWY9fAMl172PKMZ/JrHkkW8dmjCL0CYkmXeE+ssqIL/yBUTPOvpLIpenX5T9RwXRBW/3g==} - engines: {node: '>=14'} + '@opentelemetry/exporter-trace-otlp-http@0.217.0': + resolution: {integrity: sha512-38YQoqtYjglz2GV94LGUN/djLvxtvGIQO68o6qAFPVshjmwSdX1F2i0c7vn3lEl1L5B/YqjB/bgKXaVx7KO+RQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.57.2': - resolution: {integrity: sha512-awDdNRMIwDvUtoRYxRhja5QYH6+McBLtoz1q9BeEsskhZcrGmH/V1fWpGx8n+Rc+542e8pJA6y+aullbIzQmlw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-trace-otlp-proto@0.217.0': + resolution: {integrity: sha512-nPV8gKHUiSuTZpQcnZU3/pBlK7crSyEGpZuh5MtWySB0vv6NNG0QvvfKitQt+Fc2Mc6qfyU54KlZcurwoTbrVg==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@1.30.1': - resolution: {integrity: sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA==} - engines: {node: '>=14'} + '@opentelemetry/exporter-zipkin@2.7.1': + resolution: {integrity: sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -5040,33 +5066,39 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.52.1': - resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} - engines: {node: '>=14'} + '@opentelemetry/instrumentation@0.211.0': + resolution: {integrity: sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.57.2': - resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} - engines: {node: '>=14'} + '@opentelemetry/instrumentation@0.217.0': + resolution: {integrity: sha512-24ucQMjz7Y34Kw3trbxL2ZrssbtgWnR+Clpaa+YdeWuuyH3Cvk23Q03PcQvqiZrDvt8AmQmjgg9v6Y9PHoxG7w==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.57.2': - resolution: {integrity: sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==} + '@opentelemetry/instrumentation@0.52.1': + resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.57.2': - resolution: {integrity: sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ==} - engines: {node: '>=14'} + '@opentelemetry/otlp-exporter-base@0.217.0': + resolution: {integrity: sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.57.2': - resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} - engines: {node: '>=14'} + '@opentelemetry/otlp-grpc-exporter-base@0.217.0': + resolution: {integrity: sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.217.0': + resolution: {integrity: sha512-MKK8UHKFUOGAvbZRWh90MhwHG+Fxm6OROBdjKPCF+HQobjuJ/Kuf8Chs8CR45X1aqotxrMj7OxTdsXe8sXuGVA==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -5076,24 +5108,42 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-b3@2.7.1': + resolution: {integrity: sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-jaeger@1.30.1': resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-jaeger@2.7.1': + resolution: {integrity: sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/resources@1.30.1': resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/resources@2.2.0': - resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} + '@opentelemetry/resources@2.7.1': + resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-logs@0.217.0': + resolution: {integrity: sha512-BB+PcHItcZDL63dPMW+mJvwN9rk37wuIDjRxbVlg6pPDvDR/7GL7UJHbGsllgoggOoTimsKgENaWPoGch/oE1A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.57.2': resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} engines: {node: '>=14'} @@ -5106,9 +5156,15 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-node@0.57.2': - resolution: {integrity: sha512-8BaeqZyN5sTuPBtAoY+UtKwXBdqyuRKmekN5bFzAO40CgbGzAxfTpiL3PBerT7rhZ7p2nBdq7FaMv/tBQgHE4A==} - engines: {node: '>=14'} + '@opentelemetry/sdk-metrics@2.7.1': + resolution: {integrity: sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.217.0': + resolution: {integrity: sha512-K/60pSv42+NQiZKy1pAH18nYDkxltsDV4O3SJ233J0E9raU1ksyL9gsKuS8p30bYBb4AMPCfDuutHQaHYpcv0Q==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -5118,8 +5174,8 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.2.0': - resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} + '@opentelemetry/sdk-trace-base@2.7.1': + resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -5130,8 +5186,14 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-web@2.2.0': - resolution: {integrity: sha512-x/LHsDBO3kfqaFx5qSzBljJ5QHsRXrvS4MybBDy1k7Svidb8ZyIPudWVzj3s5LpPkYZIgi9e+7tdsNCnptoelw==} + '@opentelemetry/sdk-trace-node@2.7.1': + resolution: {integrity: sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-web@2.7.1': + resolution: {integrity: sha512-K806OouCSOjMd8Nr7+ZCq3QT22tdAzzS/7h8vprfiKjkgFQ99/dvwU8d12WJANA6D5Qtme65hyBAqAu9CkQuxQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -6562,8 +6624,8 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} - '@types/readable-stream@4.0.22': - resolution: {integrity: sha512-/FFhJpfCLAPwAcN3mFycNUa77ddnr8jTgF5VmSNetaemWB2cIlfCA9t0YTM3JAT0wOcv8D4tjPo7pkDhK3EJIg==} + '@types/readable-stream@4.0.23': + resolution: {integrity: sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==} '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} @@ -7012,7 +7074,7 @@ packages: peerDependencies: '@apollo/client': '>=3' graphql: '>=0.11' - qs: ^6.14.2 + qs: 6.15.2 applicationinsights@2.9.8: resolution: {integrity: sha512-eB/EtAXJ6mDLLvHrtZj/7h31qUfnC2Npr2pHGqds5+1OP7BFLsn5us+HCkwTj7Q+1sHXujLphE5Cyvq5grtV6g==} @@ -7227,13 +7289,17 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - bl@6.1.5: - resolution: {integrity: sha512-XylDt2P3JBttAwLpORq/hOEX9eJzP0r6Voa46C/WVvad8D1J0jW5876txB8FnzKtbdnU6X4Y1vOEvC6PllJrDg==} + bl@6.1.6: + resolution: {integrity: sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==} body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@1.20.5: + resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -7262,8 +7328,8 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -7349,6 +7415,10 @@ packages: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -7480,6 +7550,9 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -8113,8 +8186,8 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dottie@2.0.6: - resolution: {integrity: sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==} + dottie@2.0.7: + resolution: {integrity: sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. dset@3.1.4: @@ -8198,8 +8271,8 @@ packages: error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} es-aggregate-error@1.0.14: @@ -8380,6 +8453,10 @@ packages: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + express@4.22.2: + resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} + engines: {node: '>= 0.10.0'} + extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -8765,7 +8842,7 @@ packages: crossws: ~0.3 graphql: ^15.10.1 || ^16 uWebSockets.js: ^20 - ws: ^8 + ws: 8.20.1 peerDependenciesMeta: '@fastify/websocket': optional: true @@ -8996,6 +9073,10 @@ packages: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + icss-utils@5.1.0: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -9041,6 +9122,13 @@ packages: import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + + import-in-the-middle@3.0.1: + resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} + engines: {node: '>=18'} + import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} @@ -9378,7 +9466,7 @@ packages: isomorphic-ws@5.0.0: resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: - ws: '*' + ws: 8.20.1 istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} @@ -9491,21 +9579,15 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - jsonwebtoken@9.0.2: - resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} - jwa@1.4.2: - resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} - jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - jws@3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} - - jws@4.0.0: - resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} jwt-decode@4.0.0: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} @@ -9776,10 +9858,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.3.3: - resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} - engines: {node: 20 || >=22} - lru-cache@11.3.5: resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} engines: {node: 20 || >=22} @@ -9787,12 +9865,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - - lru.min@1.1.3: - resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==} + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} luxon@3.7.2: @@ -10268,16 +10342,18 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - mysql2@3.15.3: - resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + mysql2@3.22.3: + resolution: {integrity: sha512-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA==} engines: {node: '>= 8.0'} + peerDependencies: + '@types/node': '>= 8' mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - named-placeholders@1.1.3: - resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} - engines: {node: '>=12.0.0'} + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} @@ -10665,8 +10741,8 @@ packages: pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - pg-connection-string@2.9.1: - resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + pg-connection-string@2.13.0: + resolution: {integrity: sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -11246,8 +11322,8 @@ packages: resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} engines: {node: '>=16.0.0'} - qs@6.15.0: - resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -11269,6 +11345,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} @@ -11547,6 +11627,10 @@ packages: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + require-like@0.1.2: resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==} @@ -11664,8 +11748,8 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} safe-buffer@5.1.2: @@ -11774,15 +11858,12 @@ packages: sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} - seq-queue@0.0.5: - resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} - sequelize-pool@7.1.0: resolution: {integrity: sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==} engines: {node: '>= 10.0.0'} - sequelize@6.37.7: - resolution: {integrity: sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==} + sequelize@6.37.8: + resolution: {integrity: sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==} engines: {node: '>=10.0.0'} peerDependencies: ibm_db: '*' @@ -12031,9 +12112,9 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - sqlstring@2.3.3: - resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} - engines: {node: '>= 0.6'} + sql-escaper@1.3.3: + resolution: {integrity: sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==} + engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} srcset@4.0.0: resolution: {integrity: sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==} @@ -12741,8 +12822,27 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - uuid@14.0.0: - resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@11.0.5: + resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==} + hasBin: true + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-to-istanbul@9.3.0: @@ -12756,8 +12856,8 @@ packages: resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} engines: {node: ^20.17.0 || >=22.9.0} - validator@13.15.23: - resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} + validator@13.15.35: + resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} engines: {node: '>= 0.10'} value-equal@1.0.1: @@ -13024,8 +13124,8 @@ packages: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} - winston@3.18.3: - resolution: {integrity: sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==} + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} engines: {node: '>= 12.0.0'} wkx@0.5.0: @@ -13061,8 +13161,8 @@ packages: utf-8-validate: optional: true - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -13456,7 +13556,7 @@ snapshots: dependencies: graphql: 16.12.0 - '@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) '@wry/caches': 1.0.1 @@ -13473,7 +13573,7 @@ snapshots: tslib: 2.8.1 zen-observable-ts: 1.2.5 optionalDependencies: - graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.0) + graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.1) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: @@ -13522,9 +13622,9 @@ snapshots: finalhandler: 2.1.1 graphql: 16.12.0 loglevel: 1.9.2 - lru-cache: 11.3.3 + lru-cache: 11.3.5 negotiator: 1.0.0 - uuid: 14.0.0 + uuid: 11.1.1 whatwg-mimetype: 4.0.0 transitivePeerDependencies: - supports-color @@ -13606,7 +13706,7 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 - '@azure-rest/core-client@2.5.1': + '@azure-rest/core-client@2.6.0': dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.10.1 @@ -13653,13 +13753,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/core-http-compat@2.3.1': + '@azure/core-http-compat@2.4.0(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.22.2)': dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-client': 1.10.1 '@azure/core-rest-pipeline': 1.22.2 - transitivePeerDependencies: - - supports-color '@azure/core-lro@2.7.2': dependencies: @@ -13738,18 +13836,18 @@ snapshots: '@azure/msal-browser': 3.30.0 '@azure/msal-node': 2.16.3 events: 3.3.0 - jws: 4.0.0 + jws: 4.0.1 open: 8.4.2 stoppable: 1.1.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/keyvault-common@2.0.0': + '@azure/keyvault-common@2.1.0': dependencies: + '@azure-rest/core-client': 2.6.0 '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.10.1 - '@azure/core-client': 1.10.1 '@azure/core-rest-pipeline': 1.22.2 '@azure/core-tracing': 1.3.1 '@azure/core-util': 1.13.1 @@ -13758,21 +13856,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/keyvault-keys@4.10.0': + '@azure/keyvault-keys@4.10.0(@azure/core-client@1.10.1)': dependencies: - '@azure-rest/core-client': 2.5.1 + '@azure-rest/core-client': 2.6.0 '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.10.1 - '@azure/core-http-compat': 2.3.1 + '@azure/core-http-compat': 2.4.0(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.22.2) '@azure/core-lro': 2.7.2 '@azure/core-paging': 1.6.2 '@azure/core-rest-pipeline': 1.22.2 '@azure/core-tracing': 1.3.1 '@azure/core-util': 1.13.1 - '@azure/keyvault-common': 2.0.0 + '@azure/keyvault-common': 2.1.0 '@azure/logger': 1.3.0 tslib: 2.8.1 transitivePeerDependencies: + - '@azure/core-client' - supports-color '@azure/logger@1.3.0': @@ -13807,7 +13906,7 @@ snapshots: tough-cookie: 2.5.0 tslib: 1.14.1 tunnel: 0.0.6 - uuid: 14.0.0 + uuid: 3.4.0 xml2js: 0.4.23 transitivePeerDependencies: - debug @@ -13822,17 +13921,17 @@ snapshots: '@azure/msal-node@2.16.3': dependencies: '@azure/msal-common': 14.16.1 - jsonwebtoken: 9.0.2 - uuid: 14.0.0 + jsonwebtoken: 9.0.3 + uuid: 8.3.2 - '@azure/opentelemetry-instrumentation-azure-sdk@1.0.0-beta.9': + '@azure/opentelemetry-instrumentation-azure-sdk@1.0.0': dependencies: '@azure/core-tracing': 1.3.1 '@azure/logger': 1.3.0 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.200.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-web': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-web': 2.7.1(@opentelemetry/api@1.9.0) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -15085,14 +15184,14 @@ snapshots: '@types/uuid': 10.0.0 class-transformer: 0.5.1 reflect-metadata: 0.2.2 - uuid: 14.0.0 + uuid: 10.0.0 '@cucumber/messages@27.2.0': dependencies: '@types/uuid': 10.0.0 class-transformer: 0.5.1 reflect-metadata: 0.2.2 - uuid: 14.0.0 + uuid: 11.0.5 '@cucumber/messages@32.2.0': dependencies: @@ -16405,10 +16504,10 @@ snapshots: '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@whatwg-node/disposablestack': 0.0.6 graphql: 16.12.0 - graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.0) - isomorphic-ws: 5.0.0(ws@8.20.0) + graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.1) + isomorphic-ws: 5.0.0(ws@8.20.1) tslib: 2.8.1 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - '@fastify/websocket' - bufferutil @@ -16436,9 +16535,9 @@ snapshots: '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@types/ws': 8.18.1 graphql: 16.12.0 - isomorphic-ws: 5.0.0(ws@8.20.0) + isomorphic-ws: 5.0.0(ws@8.20.1) tslib: 2.8.1 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -16610,10 +16709,10 @@ snapshots: '@whatwg-node/fetch': 0.10.13 '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.12.0 - isomorphic-ws: 5.0.0(ws@8.20.0) + isomorphic-ws: 5.0.0(ws@8.20.1) sync-fetch: 0.6.0-2 tslib: 2.8.1 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - '@fastify/websocket' - '@types/node' @@ -16730,7 +16829,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@js-joda/core@5.6.5': {} + '@js-joda/core@5.7.0': {} '@js-sdsl/ordered-map@4.4.2': {} @@ -16845,6 +16944,14 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.211.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api-logs@0.217.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.52.1': dependencies: '@opentelemetry/api': 1.9.0 @@ -16855,10 +16962,20 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@opentelemetry/configuration@0.217.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + yaml: 2.8.3 + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -16879,110 +16996,111 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-prometheus@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin@1.30.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-zipkin@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 '@opentelemetry/instrumentation-dataloader@0.17.0(@opentelemetry/api@1.9.0)': dependencies: @@ -17028,22 +17146,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.52.1 - '@types/shimmer': 1.2.0 - import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 - semver: 7.7.4 - shimmer: 1.2.1 + '@opentelemetry/api-logs': 0.211.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/api-logs': 0.217.0 + import-in-the-middle: 3.0.1 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.52.1 '@types/shimmer': 1.2.0 import-in-the-middle: 1.15.0 require-in-the-middle: 7.5.2 @@ -17052,29 +17176,29 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-grpc-exporter-base@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) protobufjs: 7.5.8 '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': @@ -17082,21 +17206,39 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3@2.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger@2.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.28.0 - '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/sdk-logs@0.217.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.0)': @@ -17112,29 +17254,40 @@ snapshots: '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-node@0.217.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/configuration': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 transitivePeerDependencies: - supports-color @@ -17145,11 +17298,11 @@ snapshots: '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.28.0 - '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 '@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.0)': @@ -17162,11 +17315,18 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) semver: 7.7.4 - '@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-node@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-web@2.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions@1.25.1': {} @@ -18589,7 +18749,7 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/readable-stream@4.0.22': + '@types/readable-stream@4.0.23': dependencies: '@types/node': 24.10.1 @@ -18745,7 +18905,7 @@ snapshots: sirv: 3.0.2 tinyrainbow: 3.1.0 vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - msw @@ -18763,7 +18923,7 @@ snapshots: sirv: 3.0.2 tinyrainbow: 3.1.0 vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - msw @@ -19180,17 +19340,17 @@ snapshots: normalize-path: 3.0.0 picomatch: 4.0.4 - apollo-link-rest@0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0): + apollo-link-rest@0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2): dependencies: - '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) graphql: 16.12.0 - qs: 6.15.0 + qs: 6.15.2 applicationinsights@2.9.8: dependencies: '@azure/core-auth': 1.7.2 '@azure/core-rest-pipeline': 1.16.3 - '@azure/opentelemetry-instrumentation-azure-sdk': 1.0.0-beta.9 + '@azure/opentelemetry-instrumentation-azure-sdk': 1.0.0 '@microsoft/applicationinsights-web-snippet': 1.0.1 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) @@ -19245,7 +19405,7 @@ snapshots: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 @@ -19327,7 +19487,7 @@ snapshots: transitivePeerDependencies: - debug - azurite@3.35.0: + azurite@3.35.0(@azure/core-client@1.10.1)(@types/node@24.10.1): dependencies: '@azure/ms-rest-js': 1.11.2 applicationinsights: 2.9.8 @@ -19337,22 +19497,24 @@ snapshots: express: 4.22.1 fs-extra: 11.3.2 glob-to-regexp: 0.4.1 - jsonwebtoken: 9.0.2 + jsonwebtoken: 9.0.3 lokijs: 1.5.12 morgan: 1.10.1 multistream: 2.1.1 - mysql2: 3.15.3 + mysql2: 3.22.3(@types/node@24.10.1) rimraf: 3.0.2 - sequelize: 6.37.7(mysql2@3.15.3)(tedious@16.7.1) + sequelize: 6.37.8(mysql2@3.22.3(@types/node@24.10.1))(tedious@16.7.1(@azure/core-client@1.10.1)) stoppable: 1.1.0 - tedious: 16.7.1 + tedious: 16.7.1(@azure/core-client@1.10.1) to-readable-stream: 2.1.0 tslib: 2.8.1 uri-templates: 0.2.0 - uuid: 14.0.0 - winston: 3.18.3 + uuid: 3.4.0 + winston: 3.19.0 xml2js: 0.6.2 transitivePeerDependencies: + - '@azure/core-client' + - '@types/node' - applicationinsights-native-metrics - debug - ibm_db @@ -19433,9 +19595,9 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - bl@6.1.5: + bl@6.1.6: dependencies: - '@types/readable-stream': 4.0.22 + '@types/readable-stream': 4.0.23 buffer: 6.0.3 inherits: 2.0.4 readable-stream: 4.7.0 @@ -19450,13 +19612,30 @@ snapshots: http-errors: 2.0.0 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.15.0 + qs: 6.15.2 raw-body: 2.5.2 type-is: 1.6.18 unpipe: 1.0.0 transitivePeerDependencies: - supports-color + body-parser@1.20.5: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -19465,7 +19644,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.0 on-finished: 2.4.1 - qs: 6.15.0 + qs: 6.15.2 raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: @@ -19511,7 +19690,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.5: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -19611,6 +19790,13 @@ snapshots: get-intrinsic: 1.3.0 set-function-length: 1.2.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -19756,6 +19942,8 @@ snapshots: cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.2.0: {} + class-transformer@0.5.1: {} classnames@2.5.1: {} @@ -20363,7 +20551,7 @@ snapshots: dotenv@16.6.1: {} - dottie@2.0.6: {} + dottie@2.0.7: {} dset@3.1.4: {} @@ -20433,7 +20621,7 @@ snapshots: dependencies: stackframe: 1.3.4 - es-abstract@1.24.0: + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 @@ -20475,7 +20663,7 @@ snapshots: object.assign: 4.1.7 own-keys: 1.0.1 regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 + safe-array-concat: 1.1.4 safe-push-apply: 1.0.0 safe-regex-test: 1.1.0 set-proto: 1.0.0 @@ -20494,7 +20682,7 @@ snapshots: dependencies: define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-errors: 1.3.0 function-bind: 1.1.2 globalthis: 1.0.4 @@ -20712,9 +20900,9 @@ snapshots: expect-type@1.3.0: {} - express-rate-limit@8.5.1(express@4.22.1): + express-rate-limit@8.5.1(express@4.22.2): dependencies: - express: 4.22.1 + express: 4.22.2 ip-address: 10.2.0 express@4.22.1: @@ -20740,7 +20928,43 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.13 proxy-addr: 2.0.7 - qs: 6.15.0 + qs: 6.15.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@4.22.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.5 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.7 + qs: 6.15.2 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.0 @@ -21205,11 +21429,11 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 - graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0): + graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1): dependencies: graphql: 16.12.0 optionalDependencies: - ws: 8.20.0 + ws: 8.20.1 graphql@14.7.0: dependencies: @@ -21527,6 +21751,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + icss-utils@5.1.0(postcss@8.5.10): dependencies: postcss: 8.5.10 @@ -21560,6 +21788,20 @@ snapshots: cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 + import-in-the-middle@2.0.6: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + + import-in-the-middle@3.0.1: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + import-lazy@4.0.0: {} imurmurhash@0.1.4: {} @@ -21850,9 +22092,9 @@ snapshots: isobject@3.0.1: {} - isomorphic-ws@5.0.0(ws@8.20.0): + isomorphic-ws@5.0.0(ws@8.20.1): dependencies: - ws: 8.20.0 + ws: 8.20.1 istanbul-lib-coverage@3.2.2: {} @@ -21948,7 +22190,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.20.0 + ws: 8.20.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -21991,9 +22233,9 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonwebtoken@9.0.2: + jsonwebtoken@9.0.3: dependencies: - jws: 3.2.2 + jws: 4.0.1 lodash.includes: 4.3.0 lodash.isboolean: 3.0.3 lodash.isinteger: 4.0.4 @@ -22004,24 +22246,13 @@ snapshots: ms: 2.1.3 semver: 7.7.4 - jwa@1.4.2: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - jws@3.2.2: - dependencies: - jwa: 1.4.2 - safe-buffer: 5.2.1 - - jws@4.0.0: + jws@4.0.1: dependencies: jwa: 2.0.1 safe-buffer: 5.2.1 @@ -22266,17 +22497,13 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.3.3: {} - lru-cache@11.3.5: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 - lru-cache@7.18.3: {} - - lru.min@1.1.3: {} + lru.min@1.1.4: {} luxon@3.7.2: {} @@ -22883,7 +23110,7 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 minimatch@3.1.5: dependencies: @@ -23023,17 +23250,17 @@ snapshots: mute-stream@0.0.8: {} - mysql2@3.15.3: + mysql2@3.22.3(@types/node@24.10.1): dependencies: + '@types/node': 24.10.1 aws-ssl-profiles: 1.1.2 denque: 2.1.0 generate-function: 2.3.1 - iconv-lite: 0.7.0 + iconv-lite: 0.7.2 long: 5.3.2 - lru.min: 1.1.3 - named-placeholders: 1.1.3 - seq-queue: 0.0.5 - sqlstring: 2.3.3 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + sql-escaper: 1.3.3 mz@2.7.0: dependencies: @@ -23041,9 +23268,9 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - named-placeholders@1.1.3: + named-placeholders@1.1.6: dependencies: - lru-cache: 7.18.3 + lru.min: 1.1.4 nanoid@3.3.11: {} @@ -23466,7 +23693,7 @@ snapshots: pend@1.2.0: {} - pg-connection-string@2.9.1: {} + pg-connection-string@2.13.0: {} picocolors@1.1.1: {} @@ -24080,7 +24307,7 @@ snapshots: pvutils@1.1.5: {} - qs@6.15.0: + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -24099,6 +24326,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -24321,7 +24555,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 @@ -24498,6 +24732,13 @@ snapshots: transitivePeerDependencies: - supports-color + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + require-like@0.1.2: {} requires-port@1.0.0: {} @@ -24636,9 +24877,9 @@ snapshots: dependencies: tslib: 2.8.1 - safe-array-concat@1.1.3: + safe-array-concat@1.1.4: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 get-intrinsic: 1.3.0 has-symbols: 1.1.0 @@ -24750,31 +24991,29 @@ snapshots: tslib: 2.8.1 upper-case-first: 2.0.2 - seq-queue@0.0.5: {} - sequelize-pool@7.1.0: {} - sequelize@6.37.7(mysql2@3.15.3)(tedious@16.7.1): + sequelize@6.37.8(mysql2@3.22.3(@types/node@24.10.1))(tedious@16.7.1(@azure/core-client@1.10.1)): dependencies: '@types/debug': 4.1.12 '@types/validator': 13.15.10 debug: 4.4.3(supports-color@8.1.1) - dottie: 2.0.6 + dottie: 2.0.7 inflection: 1.13.4 lodash: 4.18.1 moment: 2.30.1 moment-timezone: 0.5.48 - pg-connection-string: 2.9.1 + pg-connection-string: 2.13.0 retry-as-promised: 7.1.1 semver: 7.7.4 sequelize-pool: 7.1.0 toposort-class: 1.0.1 - uuid: 14.0.0 - validator: 13.15.23 + uuid: 8.3.2 + validator: 13.15.35 wkx: 0.5.0 optionalDependencies: - mysql2: 3.15.3 - tedious: 16.7.1 + mysql2: 3.22.3(@types/node@24.10.1) + tedious: 16.7.1(@azure/core-client@1.10.1) transitivePeerDependencies: - supports-color @@ -24965,7 +25204,7 @@ snapshots: sockjs@0.3.24: dependencies: faye-websocket: 0.11.4 - uuid: 14.0.0 + uuid: 8.3.2 websocket-driver: 0.7.4 sort-css-media-queries@2.2.0: {} @@ -25034,7 +25273,7 @@ snapshots: sprintf-js@1.1.3: {} - sqlstring@2.3.3: {} + sql-escaper@1.3.3: {} srcset@4.0.0: {} @@ -25067,9 +25306,9 @@ snapshots: stoppable@1.1.0: {} - storybook-addon-apollo-client@9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0): + storybook-addon-apollo-client@9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0): dependencies: - '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) graphql: 16.12.0 react: 19.2.0 @@ -25086,7 +25325,7 @@ snapshots: esbuild-register: 3.6.0(esbuild@0.25.12) recast: 0.23.11 semver: 7.7.4 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - '@testing-library/dom' - bufferutil @@ -25133,7 +25372,7 @@ snapshots: call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 @@ -25312,12 +25551,12 @@ snapshots: - bare-abort-controller - react-native-b4a - tedious@16.7.1: + tedious@16.7.1(@azure/core-client@1.10.1): dependencies: '@azure/identity': 3.4.2 - '@azure/keyvault-keys': 4.10.0 - '@js-joda/core': 5.6.5 - bl: 6.1.5 + '@azure/keyvault-keys': 4.10.0(@azure/core-client@1.10.1) + '@js-joda/core': 5.7.0 + bl: 6.1.6 es-aggregate-error: 1.0.14 iconv-lite: 0.6.3 js-md4: 0.3.2 @@ -25326,6 +25565,7 @@ snapshots: node-abort-controller: 3.1.1 sprintf-js: 1.1.3 transitivePeerDependencies: + - '@azure/core-client' - supports-color terser-webpack-plugin@5.3.14(esbuild@0.27.4)(webpack@5.105.4(esbuild@0.27.4)): @@ -25763,7 +26003,15 @@ snapshots: utils-merge@1.0.1: {} - uuid@14.0.0: {} + uuid@10.0.0: {} + + uuid@11.0.5: {} + + uuid@11.1.1: {} + + uuid@3.4.0: {} + + uuid@8.3.2: {} v8-to-istanbul@9.3.0: dependencies: @@ -25778,7 +26026,7 @@ snapshots: validate-npm-package-name@7.0.2: {} - validator@13.15.23: {} + validator@13.15.35: {} value-equal@1.0.1: {} @@ -25990,7 +26238,7 @@ snapshots: sockjs: 0.3.24 spdy: 4.0.2 webpack-dev-middleware: 7.4.5(webpack@5.105.4(esbuild@0.27.4)) - ws: 8.20.0 + ws: 8.20.1 optionalDependencies: webpack: 5.105.4(esbuild@0.27.4) transitivePeerDependencies: @@ -26153,7 +26401,7 @@ snapshots: readable-stream: 3.6.2 triple-beam: 1.4.1 - winston@3.18.3: + winston@3.19.0: dependencies: '@colors/colors': 1.6.0 '@dabh/diagnostics': 2.0.8 @@ -26200,7 +26448,7 @@ snapshots: ws@7.5.10: {} - ws@8.20.0: {} + ws@8.20.1: {} wsl-utils@0.1.0: dependencies: @@ -26210,18 +26458,18 @@ snapshots: xml-js@1.6.11: dependencies: - sax: 1.4.3 + sax: 1.5.0 xml-name-validator@5.0.0: {} xml2js@0.4.23: dependencies: - sax: 1.4.3 + sax: 1.5.0 xmlbuilder: 11.0.1 xml2js@0.6.2: dependencies: - sax: 1.4.3 + sax: 1.5.0 xmlbuilder: 11.0.1 xmlbuilder@11.0.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ac9ea4615..63e1e511e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -65,7 +65,8 @@ overrides: rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 'brace-expansion@1.1.12': 1.1.13 - 'brace-expansion@5.0.4': 5.0.5 + 'brace-expansion@5.0.4': 5.0.6 + 'brace-expansion@5.0.5': 5.0.6 'diff@4.0.2': 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 @@ -76,7 +77,7 @@ overrides: svgo: ^3.3.3 'yaml@2.8.2': 2.8.3 'yauzl@3.2.0': 3.2.1 - qs: ^6.14.2 + qs: 6.15.2 'ajv@^6': 6.14.0 lodash: 4.18.1 lodash-es: 4.18.1 @@ -86,7 +87,9 @@ overrides: webpack: ^5.105.4 webpack-dev-server: ^5.2.4 express-rate-limit: 8.5.1 - uuid: 14.0.0 + '@azure/ms-rest-js>uuid': '^3.4.0' + 'azurite>uuid': '^3.4.0' + 'ws@8.20.0': 8.20.1 playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 diff --git a/turbo.json b/turbo.json index 0ef199ae0..8bef61034 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,7 @@ { "$schema": "https://turbo.build/schema.json", "ui": "tui", + "globalPassThroughEnv": ["WORKTREE_NAME"], "futureFlags": { "affectedUsingTaskInputs": true, "watchUsingTaskInputs": true @@ -101,6 +102,12 @@ "cache": false, "persistent": true }, + "dev:portless": { + "description": "Starts dev servers with worktree-scoped portless hostnames for git worktree isolation", + "dependsOn": ["^build"], + "cache": false, + "persistent": true + }, "azurite": { "description": "Starts the Azurite storage emulator", "cache": false, From b4db123ea16af23cb539ea8de5ce0db682489477 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 21 May 2026 10:27:20 -0400 Subject: [PATCH 03/12] fixes for e2e tests along with overall pattern cleanup --- apps/api/package.json | 4 +- apps/api/start-dev.mjs | 16 +- apps/api/turbo.json | 2 +- apps/docs/package.json | 2 +- apps/docs/turbo.json | 2 +- apps/server-mongodb-memory-mock/package.json | 2 +- apps/server-mongodb-memory-mock/turbo.json | 2 +- apps/server-oauth2-mock/package.json | 2 +- apps/server-oauth2-mock/start-dev.mjs | 18 +- apps/server-oauth2-mock/turbo.json | 2 +- apps/ui-community/package.json | 2 +- apps/ui-community/start-dev.mjs | 19 +- apps/ui-community/turbo.json | 2 +- apps/ui-staff/package.json | 2 +- apps/ui-staff/start-dev.mjs | 16 +- .../scripts/portless-hostnames.d.mts | 13 + build-pipeline/scripts/worktree-ports.d.mts | 5 + package.json | 4 +- .../ocom-verification/e2e-tests/cucumber.js | 2 + .../ocom-verification/e2e-tests/package.json | 4 +- .../src/shared/support/oauth2-login.ts | 6 +- .../shared/support/servers/e2e-defaults.ts | 65 +++++ .../src/shared/support/servers/index.ts | 2 +- .../shared/support/servers/portless-server.ts | 13 +- .../shared/support/servers/test-api-server.ts | 53 ++-- .../support/servers/test-azurite-server.ts | 105 +++++++ .../support/servers/test-environment.ts | 19 +- .../shared/support/shared-infrastructure.ts | 20 +- .../verification-shared/src/servers/index.ts | 2 +- .../src/servers/test-mongodb-server.ts | 3 +- .../src/servers/test-server.interface.ts | 26 +- .../src/settings/local-settings.ts | 31 +-- .../src/settings/portless-settings.ts | 68 +---- pnpm-lock.yaml | 258 ++++++++++++++++-- readme.md | 2 - turbo.json | 2 +- 36 files changed, 566 insertions(+), 230 deletions(-) create mode 100644 build-pipeline/scripts/portless-hostnames.d.mts create mode 100644 build-pipeline/scripts/worktree-ports.d.mts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts diff --git a/apps/api/package.json b/apps/api/package.json index 6041983b2..122cb1ca1 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,8 +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:portless": "pnpm run prepare:deploy && pnpm run sync-local-settings", - "dev:portless": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.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", diff --git a/apps/api/start-dev.mjs b/apps/api/start-dev.mjs index 978b8744b..9dc871786 100644 --- a/apps/api/start-dev.mjs +++ b/apps/api/start-dev.mjs @@ -22,15 +22,19 @@ const childEnv = { // 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 (e.g. e2e harness with a MongoMemoryServer port) can override +// any individual value via process.env before invoking this script. 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.COSMOSDB_CONNECTION_STRING = getMongoConnectionString(); - childEnv.AZURE_STORAGE_CONNECTION_STRING = getAzuriteConnectionString(); - childEnv.AzureWebJobsStorage = getAzuriteConnectionString(); + 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 = ''; + childEnv.languageWorkers__node__arguments ??= ''; } const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort, '--cors', '*'], { diff --git a/apps/api/turbo.json b/apps/api/turbo.json index 0f908f3c8..6487a447c 100644 --- a/apps/api/turbo.json +++ b/apps/api/turbo.json @@ -12,7 +12,7 @@ "interruptible": true, "inputs": [] }, - "dev:portless": { + "dev:worktree": { "dependsOn": ["build"], "interruptible": true, "inputs": [] diff --git a/apps/docs/package.json b/apps/docs/package.json index 10766a652..9e8348ac1 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -5,7 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "dev": "pnpm exec portless docs.ownercommunity.localhost --force node start-dev.mjs", - "dev:portless": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.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", diff --git a/apps/docs/turbo.json b/apps/docs/turbo.json index d39e3fdc8..304f1bb57 100644 --- a/apps/docs/turbo.json +++ b/apps/docs/turbo.json @@ -7,7 +7,7 @@ "interruptible": false, "inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"] }, - "dev:portless": { + "dev:worktree": { "dependsOn": [], "persistent": true, "interruptible": false, diff --git a/apps/server-mongodb-memory-mock/package.json b/apps/server-mongodb-memory-mock/package.json index 2372d4635..ed4a774d3 100644 --- a/apps/server-mongodb-memory-mock/package.json +++ b/apps/server-mongodb-memory-mock/package.json @@ -12,7 +12,7 @@ "format:check": "biome format .", "start": "node dist/index.js", "dev": "tsx src/index.ts", - "dev:portless": "node start-mongo.mjs" + "dev:worktree": "node start-mongo.mjs" }, "dependencies": { "@cellix/server-mongodb-memory-mock-seedwork": "workspace:*", diff --git a/apps/server-mongodb-memory-mock/turbo.json b/apps/server-mongodb-memory-mock/turbo.json index b0f2f18a9..5aea8cad2 100644 --- a/apps/server-mongodb-memory-mock/turbo.json +++ b/apps/server-mongodb-memory-mock/turbo.json @@ -8,7 +8,7 @@ "interruptible": true, "inputs": [] }, - "dev:portless": { + "dev:worktree": { "dependsOn": ["build"], "persistent": true, "interruptible": true, diff --git a/apps/server-oauth2-mock/package.json b/apps/server-oauth2-mock/package.json index bc92782ff..7201d84b9 100644 --- a/apps/server-oauth2-mock/package.json +++ b/apps/server-oauth2-mock/package.json @@ -12,7 +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:portless": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "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" diff --git a/apps/server-oauth2-mock/start-dev.mjs b/apps/server-oauth2-mock/start-dev.mjs index b6434763a..2bd4f4b1e 100644 --- a/apps/server-oauth2-mock/start-dev.mjs +++ b/apps/server-oauth2-mock/start-dev.mjs @@ -2,17 +2,19 @@ import { spawn } from 'node:child_process'; import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; -const hostnames = getHostnames(); +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: { - ...process.env, - BASE_URL: buildPortlessUrl(hostnames.mockAuth), - // Override redirect URIs so portal-discovery picks up worktree-scoped URLs - VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'), - VITE_APP_UI_STAFF_AAD_REDIRECT_URI: buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'), - }, + env: childEnv, }); child.on('exit', (code, signal) => { diff --git a/apps/server-oauth2-mock/turbo.json b/apps/server-oauth2-mock/turbo.json index 325758390..2596ed01a 100644 --- a/apps/server-oauth2-mock/turbo.json +++ b/apps/server-oauth2-mock/turbo.json @@ -10,7 +10,7 @@ "persistent": true, "inputs": ["$TURBO_DEFAULT$", "../**/mock-oidc*.json"] }, - "dev:portless": { + "dev:worktree": { "dependsOn": ["build"], "interruptible": true, "persistent": true, diff --git a/apps/ui-community/package.json b/apps/ui-community/package.json index 86bc14f1d..ac0f7010d 100644 --- a/apps/ui-community/package.json +++ b/apps/ui-community/package.json @@ -10,7 +10,7 @@ "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", "dev": "pnpm exec portless ownercommunity.localhost --force vite", - "dev:portless": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "dev:worktree": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs index a13661535..5226fb736 100644 --- a/apps/ui-community/start-dev.mjs +++ b/apps/ui-community/start-dev.mjs @@ -2,17 +2,20 @@ import { spawn } from 'node:child_process'; import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; -const hostnames = getHostnames(); +const childEnv = { ...process.env }; + +// Worktree-scoped overrides; plain `dev` leaves .env as the source of truth. +if (process.env.WORKTREE_NAME) { + const hostnames = getHostnames(); + childEnv.VITE_APP_UI_COMMUNITY_B2C_AUTHORITY = buildPortlessUrl(hostnames.mockAuth, '/community'); + childEnv.VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI = buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'); + childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); + childEnv.VITE_APP_UI_COMMUNITY_BASE_URL = buildPortlessUrl(hostnames.uiCommunity); +} const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { stdio: 'inherit', - env: { - ...process.env, - VITE_APP_UI_COMMUNITY_B2C_AUTHORITY: buildPortlessUrl(hostnames.mockAuth, '/community'), - VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'), - VITE_COMMON_API_ENDPOINT: buildPortlessUrl(hostnames.api, '/api/graphql'), - VITE_APP_UI_COMMUNITY_BASE_URL: buildPortlessUrl(hostnames.uiCommunity), - }, + env: childEnv, }); child.on('exit', (code, signal) => { diff --git a/apps/ui-community/turbo.json b/apps/ui-community/turbo.json index e253b58b7..b7c32402e 100644 --- a/apps/ui-community/turbo.json +++ b/apps/ui-community/turbo.json @@ -7,7 +7,7 @@ "interruptible": false, "inputs": [".env", "package.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] }, - "dev:portless": { + "dev:worktree": { "dependsOn": ["^build"], "persistent": true, "interruptible": false, diff --git a/apps/ui-staff/package.json b/apps/ui-staff/package.json index bd35eaf13..9b8757b90 100644 --- a/apps/ui-staff/package.json +++ b/apps/ui-staff/package.json @@ -10,7 +10,7 @@ "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", "dev": "pnpm exec portless staff.ownercommunity.localhost --force vite", - "dev:portless": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "dev:worktree": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs index 4b656ce70..eaa17cdb8 100644 --- a/apps/ui-staff/start-dev.mjs +++ b/apps/ui-staff/start-dev.mjs @@ -2,16 +2,18 @@ import { spawn } from 'node:child_process'; import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; -const hostnames = getHostnames(); +const childEnv = { ...process.env }; + +if (process.env.WORKTREE_NAME) { + const hostnames = getHostnames(); + childEnv.VITE_APP_UI_STAFF_AAD_AUTHORITY = buildPortlessUrl(hostnames.mockAuth, '/staff'); + childEnv.VITE_APP_UI_STAFF_AAD_REDIRECT_URI = buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'); + childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); +} const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { stdio: 'inherit', - env: { - ...process.env, - VITE_APP_UI_STAFF_AAD_AUTHORITY: buildPortlessUrl(hostnames.mockAuth, '/staff'), - VITE_APP_UI_STAFF_AAD_REDIRECT_URI: buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'), - VITE_COMMON_API_ENDPOINT: buildPortlessUrl(hostnames.api, '/api/graphql'), - }, + env: childEnv, }); child.on('exit', (code, signal) => { diff --git a/build-pipeline/scripts/portless-hostnames.d.mts b/build-pipeline/scripts/portless-hostnames.d.mts new file mode 100644 index 000000000..139d75e94 --- /dev/null +++ b/build-pipeline/scripts/portless-hostnames.d.mts @@ -0,0 +1,13 @@ +export const PORTLESS_PORT: number; + +export interface PortlessHostnames { + uiCommunity: string; + uiStaff: string; + api: string; + mockAuth: string; + docs: string; +} + +export function getHostnames(): PortlessHostnames; + +export function buildPortlessUrl(hostname: string, path?: string): string; diff --git a/build-pipeline/scripts/worktree-ports.d.mts b/build-pipeline/scripts/worktree-ports.d.mts new file mode 100644 index 000000000..c2cbdf86e --- /dev/null +++ b/build-pipeline/scripts/worktree-ports.d.mts @@ -0,0 +1,5 @@ +export function getWorktreePortOffset(): number; +export function getMongoPort(): number; +export function getAzuritePorts(): { blob: number; queue: number; table: number }; +export function getAzuriteConnectionString(): string; +export function getMongoConnectionString(): string; diff --git a/package.json b/package.json index c1757bc2c..3dc82ec64 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test": "turbo run test", "lint": "turbo run lint", "dev": "pnpm proxy:stop && pnpm proxy:start && turbo watch azurite dev --filter='./apps/*' --filter='./packages/*'", - "dev:portless": "WORKTREE_NAME=$(basename $PWD) pnpm proxy:ensure && WORKTREE_NAME=$(basename $PWD) turbo watch azurite dev:portless --filter='./apps/*' --filter='./packages/*'", + "dev:worktree": "WORKTREE_NAME=$(basename $PWD) pnpm proxy:ensure && WORKTREE_NAME=$(basename $PWD) turbo watch azurite dev:worktree --filter='./apps/*' --filter='./packages/*'", "start": "turbo run build && concurrently --kill-others-on-fail \"pnpm run start:api\" \"pnpm run start:ui-community\"", "proxy:stop": "pnpm exec portless proxy stop || true", "proxy:start": "pnpm exec portless proxy start --https -p 1355", @@ -33,7 +33,7 @@ "test:coverage": "turbo run test:coverage", "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", "test:e2e": "turbo run test:e2e --filter=@ocom-verification/e2e-tests", - "test:e2e:portless": "WORKTREE_NAME=$(basename $PWD) turbo run test:e2e --filter=@ocom-verification/e2e-tests", + "test:e2e:worktree": "WORKTREE_NAME=$(basename $PWD) turbo run test:e2e --filter=@ocom-verification/e2e-tests", "test:acceptance": "turbo run test:acceptance --filter=@ocom-verification/acceptance-api --filter=@ocom-verification/acceptance-ui", "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", "test:integration": "turbo run test:integration", diff --git a/packages/ocom-verification/e2e-tests/cucumber.js b/packages/ocom-verification/e2e-tests/cucumber.js index 51912f713..e548d91e5 100644 --- a/packages/ocom-verification/e2e-tests/cucumber.js +++ b/packages/ocom-verification/e2e-tests/cucumber.js @@ -7,5 +7,7 @@ export default { formatOptions: { snippetInterface: 'async-await', }, + // Disable parallel workers — the shared portless proxy and per-worktree port + // scheme make parallel browsers contend for the same hostnames. parallel: 0, }; diff --git a/packages/ocom-verification/e2e-tests/package.json b/packages/ocom-verification/e2e-tests/package.json index 6d3508bc6..cbe99de57 100644 --- a/packages/ocom-verification/e2e-tests/package.json +++ b/packages/ocom-verification/e2e-tests/package.json @@ -5,9 +5,7 @@ "private": true, "type": "module", "scripts": { - "test:e2e": "pnpm run proxy:start && pnpm run test:e2e:run", - "test:e2e:run": "NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", - "proxy:start": "pnpm exec portless proxy start -p 1355", + "test:e2e": "NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", "playwright:install": "playwright install chromium", "clean": "rimraf dist reports target" }, diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts index abc2b4638..3bc2fccf8 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts @@ -1,3 +1,4 @@ +import { actors } from '@ocom-verification/verification-shared/test-data'; import { type Actor, Interaction, the } from '@serenity-js/core'; import type { Page } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; @@ -32,9 +33,10 @@ export async function performOAuth2Login(page: Page): Promise { // Wait for redirects to settle on either the login page or the app await page.waitForLoadState('domcontentloaded', { timeout: 10_000 }).catch(() => undefined); - // If the mock OAuth2 login form is shown, fill credentials and submit + // If the mock OAuth2 login form is shown, fill credentials and submit. + // CommunityOwner is defined in mock-oidc.users.json with password "password". if (page.url().includes('/login')) { - await page.fill('input[name="username"]', 'test@example.com'); + await page.fill('input[name="username"]', actors.CommunityOwner.email); await page.fill('input[name="password"]', 'password'); await page.click('button[type="submit"]'); } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts new file mode 100644 index 000000000..6680d32a8 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts @@ -0,0 +1,65 @@ +// biome-ignore format: keep the public Azurite dev key split across chunks for static analysis. + +/** + * Non-secret environment defaults for the e2e harness. + * + * Used when `apps/api/local.settings.json` is absent (typical in CI). Every + * value here is either a public Azurite development credential or a mock-only + * constant — none of them are valid in any real environment, and they are + * never applied outside the e2e harness. + * + * Worktree-scoped values (OIDC URLs, mongo/azurite ports, connection strings) + * are computed at runtime by `build-pipeline/scripts/portless-hostnames.mjs` + * and `worktree-ports.mjs`; they intentionally do NOT live here. + */ + +const AZURITE_ACCOUNT_KEY = [ + 'Eby8vdM02xNOcqFlqUwJPLlm', + 'EtlCDXJ1OUzFT50uSRZ6IFs', + 'uFq2UVErCz4I6tq/K1SZFP', + 'TOtr/KBHBeksoGMGw==', +].join(''); + +const E2E_API_DEFAULTS = { + FUNCTIONS_WORKER_RUNTIME: 'node', + NODE_ENV: 'development', + CONFIG_VERSION: '3.0', + + ACCOUNT_PORTAL_OIDC_AUDIENCE: 'mock-client', + ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER: 'true', + STAFF_PORTAL_OIDC_AUDIENCE: 'mock-client', + STAFF_PORTAL_OIDC_IGNORE_ISSUER: 'true', + + COSMOSDB_DBNAME: 'owner-community', + + // Well-known Azurite dev account — documented in Azurite's README, not a secret. + STORAGE_ACCOUNT_NAME: 'devstoreaccount1', + STORAGE_ACCOUNT_KEY: AZURITE_ACCOUNT_KEY, +} as const; + +/** + * Apply e2e defaults to `process.env` for any key not already set. A pre-existing + * env var (from the developer's shell or a copied local.settings.json) always wins. + * Spawned child processes inherit `process.env`, so this propagates to azurite, + * the api function host, and the oauth2 mock without any per-server plumbing. + */ +export function applyE2EDefaultsToEnv(): void { + for (const [key, value] of Object.entries(E2E_API_DEFAULTS)) { + process.env[key] ??= value; + } +} + +/** + * Returns a shallow copy of `process.env` with `NODE_OPTIONS` removed. + * + * Cucumber runs with `NODE_OPTIONS='--import tsx/esm'` so that TypeScript + * source is executed directly. Child processes spawned by the test harness + * (Azurite, portless, func) are plain JavaScript and do not have `tsx` on + * their resolution path, so inheriting `NODE_OPTIONS` causes an immediate + * crash. Callers can spread additional overrides on top of the result. + */ +export function spawnEnv(overrides: Record = {}): NodeJS.ProcessEnv { + const env = { ...process.env, ...overrides }; + delete env['NODE_OPTIONS']; + return env; +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts index c096f6636..25d4dffc6 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts @@ -1,12 +1,12 @@ export { MongoDBTestServer } from '@ocom-verification/verification-shared/servers'; export { PortlessServer } from './portless-server.ts'; export { TestApiServer } from './test-api-server.ts'; +export { TestAzuriteServer } from './test-azurite-server.ts'; export { TestCommunityViteServer } from './test-community-vite-server.ts'; export { buildUrl, cleanupTestEnvironment, initTestEnvironment, - mockOidcAudience, mockOidcEndpoint, mockOidcIssuer, setMongoConnectionString, diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index aea6c3ab1..6ff6ace04 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -1,6 +1,7 @@ import { type ChildProcess, spawn } from 'node:child_process'; import type { TestServer } from '@ocom-verification/verification-shared/servers'; import { getTimeout } from '@ocom-verification/verification-shared/settings'; +import { spawnEnv } from './e2e-defaults.ts'; import { getPortlessPath } from './resolve-portless.ts'; /** @@ -68,16 +69,9 @@ export abstract class PortlessServer implements TestServer { if (this.process || this.startedByUs) return; if (await this.isAlreadyRunning()) return; - const env = { - ...process.env, - ...this.extraEnv, - }; - // Remove NODE_OPTIONS from child process to avoid tsx import issues - delete env['NODE_OPTIONS']; - this.process = spawn(this.executable, this.spawnArgs, { cwd: this.cwd, - env, + env: spawnEnv(this.extraEnv), detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -159,7 +153,8 @@ export abstract class PortlessServer implements TestServer { }); proc.stderr?.on('data', (data: Buffer) => { - stderrOutput += data.toString(); + const text = data.toString(); + stderrOutput += text; }); proc.on('error', (err: Error) => { diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index 380249d50..a92d3f893 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -1,25 +1,44 @@ import { execFileSync } from 'node:child_process'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; import { apiSettings } from '@ocom-verification/verification-shared/settings'; +import { spawnEnv } from './e2e-defaults.ts'; import { PortlessServer } from './portless-server.ts'; -import { buildUrl, getHostnames, getMongoConnectionString, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; +import { buildUrl, getHostnames, getMongoConnectionString } from './test-environment.ts'; const hostnames = getHostnames(); +/** + * Spawns the api dev server the same way `pnpm dev:worktree` does. The + * worktree-aware overrides (OIDC URLs, Azurite connection, etc.) all come + * from apps/api/start-dev.mjs — we only inject the dynamic MongoDB + * connection string from MongoMemoryServer here. + */ export class TestApiServer extends PortlessServer { override async start(): Promise { - // Mirror the app's real dev bootstrap so deploy assets and local settings - // stay in sync with recent package-script changes. - const env = { - ...process.env, - }; - delete env['NODE_OPTIONS']; - - execFileSync('pnpm', ['run', 'predev'], { + // Mirror the `predev:worktree` lifecycle hook so deploy/ and local.settings.json + // stay in sync — start-dev.mjs is invoked directly here, bypassing pnpm's pre-hook. + execFileSync('pnpm', ['run', 'predev:worktree'], { cwd: this.cwd, - env, + env: spawnEnv(), stdio: 'pipe', }); + // Patch deploy/local.settings.json with e2e-specific values. + // Azure Functions loads local.settings.json values into the worker env, + // overriding any parent-process env vars. We must patch the file so the + // worker gets the MongoMemoryServer connection string (random port) and + // the inspector is disabled (port 5858 may already be in use). + const settingsPath = join(this.cwd, 'deploy', 'local.settings.json'); + try { + const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); + settings.Values.COSMOSDB_CONNECTION_STRING = getMongoConnectionString(); + settings.Values['languageWorkers__node__arguments'] = ''; + writeFileSync(settingsPath, JSON.stringify(settings, null, '\t')); + } catch { + /* best-effort — file may not exist in CI */ + } + await super.start(); } @@ -59,21 +78,9 @@ export class TestApiServer extends PortlessServer { } protected override get extraEnv() { + // start-dev.mjs handles the rest via WORKTREE_NAME + `??=` fallbacks. return { - NODE_ENV: 'development', - languageWorkers__node__arguments: '', COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), - COSMOSDB_DBNAME: apiSettings.cosmosDbName, - AZURE_STORAGE_CONNECTION_STRING: 'UseDevelopmentStorage=true', - ACCOUNT_PORTAL_OIDC_ISSUER: mockOidcIssuer, - ACCOUNT_PORTAL_OIDC_ENDPOINT: mockOidcEndpoint, - ACCOUNT_PORTAL_OIDC_AUDIENCE: mockOidcAudience, - ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER: 'true', - STAFF_PORTAL_OIDC_ISSUER: mockOidcIssuer, - STAFF_PORTAL_OIDC_ENDPOINT: mockOidcEndpoint, - STAFF_PORTAL_OIDC_AUDIENCE: mockOidcAudience, - STAFF_PORTAL_OIDC_IGNORE_ISSUER: 'true', - VITE_COMMON_API_ENDPOINT: buildUrl(hostnames.api, '/api/graphql'), }; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts new file mode 100644 index 000000000..d43d9efd3 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts @@ -0,0 +1,105 @@ +import { type ChildProcess, spawn } from 'node:child_process'; +import net from 'node:net'; +import { join } from 'node:path'; +import type { TestServer } from '@ocom-verification/verification-shared/servers'; +import { apiSettings, getTimeout } from '@ocom-verification/verification-shared/settings'; +import { getAzuritePorts } from '../../../../../../../build-pipeline/scripts/worktree-ports.mjs'; +import { spawnEnv } from './e2e-defaults.ts'; + +/** + * Starts Azurite via apps/api/start-azurite.mjs. The script itself short-circuits + * if the blob port is already listening, so concurrent worktrees and re-runs are + * safe. We track the spawned process only when we started it ourselves. + */ +export class TestAzuriteServer implements TestServer { + private process: ChildProcess | null = null; + private startedByUs = false; + private readonly useDetachedProcessGroup = process.platform !== 'win32'; + + private get blobPort(): number { + return getAzuritePorts().blob; + } + + async start(): Promise { + if (this.process || this.startedByUs) return; + if (await isPortListening(this.blobPort)) return; + + const binDir = join(apiSettings.apiDir, 'node_modules', '.bin'); + + this.process = spawn('node', ['start-azurite.mjs'], { + cwd: apiSettings.apiDir, + env: spawnEnv({ PATH: `${binDir}:${process.env['PATH'] ?? ''}` }), + detached: this.useDetachedProcessGroup, + stdio: ['ignore', 'pipe', 'pipe'], + }); + this.startedByUs = true; + + await this.waitForReady(); + } + + async stop(): Promise { + if (!this.process || !this.startedByUs) return; + + const proc = this.process; + this.process = null; + this.startedByUs = false; + + killProcess(proc, 'SIGTERM', this.useDetachedProcessGroup); + + await new Promise((resolve) => { + const timeout = setTimeout(() => { + killProcess(proc, 'SIGKILL', this.useDetachedProcessGroup); + resolve(); + }, getTimeout('serverShutdown')); + + proc.on('exit', () => { + clearTimeout(timeout); + resolve(); + }); + }); + } + + isRunning(): boolean { + return this.process !== null; + } + + getUrl(): string { + return `http://127.0.0.1:${this.blobPort}`; + } + + private async waitForReady(): Promise { + const deadline = Date.now() + getTimeout('serverStartup'); + const interval = getTimeout('healthProbeInterval'); + while (Date.now() < deadline) { + if (await isPortListening(this.blobPort)) return; + await new Promise((resolve) => setTimeout(resolve, interval)); + } + throw new Error(`TestAzuriteServer: blob port ${this.blobPort} did not start within timeout`); + } +} + +function isPortListening(port: number): Promise { + return new Promise((resolve) => { + const socket = net.createConnection({ port, host: '127.0.0.1' }); + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('error', () => { + socket.destroy(); + resolve(false); + }); + }); +} + +function killProcess(proc: ChildProcess, signal: NodeJS.Signals, useGroup: boolean): void { + if (useGroup && proc.pid) { + try { + process.kill(-proc.pid, signal); + return; + } catch { + /* Fall back to killing the direct child. */ + } + } + proc.kill(signal); +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index 4a8fbb54f..6550beb7f 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -1,11 +1,12 @@ import { execFileSync } from 'node:child_process'; import { buildPortlessUrl, getHostnames } from '@ocom-verification/verification-shared/settings'; +import { applyE2EDefaultsToEnv } from './e2e-defaults.ts'; import { getPortlessPath } from './resolve-portless.ts'; let proxyInitialized = false; let mongoConnectionString: string | undefined; -/** Module-level hostnames derived from .env files (matches dev:portless pattern). */ +/** Module-level hostnames derived from .env files (matches dev:worktree pattern). */ const hostnames = getHostnames(); /** OIDC issuer URL for the community portal on the mock auth server. */ @@ -14,19 +15,19 @@ export const mockOidcIssuer = buildPortlessUrl(hostnames.mockAuth, '/community') /** JWKS endpoint used as the OIDC discovery / probe URL. */ export const mockOidcEndpoint = `${mockOidcIssuer}/.well-known/jwks.json`; -/** Audience claim expected in JWTs issued by the mock OIDC server. */ -export const mockOidcAudience = 'mock-client'; - /** - * Prune orphaned portless route locks from previous test runs. - * The proxy itself is started by the `test:e2e` npm script before the - * Cucumber process spawns, so we only need to clean stale locks here. + * Apply e2e env defaults (so CI runs without local.settings.json) and ensure + * the portless proxy is running. The `proxy start` invocation is idempotent — + * if another worktree or dev session already started it on the shared port, + * this returns immediately. */ export function initTestEnvironment() { if (proxyInitialized) return; - execFileSync(getPortlessPath(), ['prune'], { - timeout: 10_000, + applyE2EDefaultsToEnv(); + + execFileSync(getPortlessPath(), ['proxy', 'start', '--https', '-p', '1355'], { + timeout: 15_000, stdio: 'pipe', }); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index e91cca9b9..7a793aa96 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -1,9 +1,10 @@ import playwright, { type Browser, type BrowserContext } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; import { performOAuth2Login } from './oauth2-login.ts'; -import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; +import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestAzuriteServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; let mongoDBServer: MongoDBTestServer | undefined; +let azuriteServer: TestAzuriteServer | undefined; let oauth2Server: TestOAuth2Server | undefined; let apiServer: TestApiServer | undefined; let viteServer: TestCommunityViteServer | undefined; @@ -66,6 +67,10 @@ export async function stopAll(): Promise { await mongoDBServer.stop().catch(() => undefined); mongoDBServer = undefined; } + if (azuriteServer) { + await azuriteServer.stop().catch(() => undefined); + azuriteServer = undefined; + } apiUrl = undefined; browserBaseUrl = undefined; cleanupTestEnvironment(); @@ -75,14 +80,23 @@ export async function ensureE2EServers(): Promise { registerShutdownHandlers(); initTestEnvironment(); - // Phase 1: Start MongoDB and OAuth2 in parallel (no interdependency) + // Phase 1: Start MongoDB, Azurite, and OAuth2 in parallel (no interdependency) mongoDBServer ??= new MongoDBTestServer(); + azuriteServer ??= new TestAzuriteServer(); oauth2Server ??= new TestOAuth2Server(); const mongo = mongoDBServer; + const azurite = azuriteServer; const oauth2 = oauth2Server; const phase1: Promise[] = []; if (!mongo.isRunning()) { - phase1.push(mongo.start().then(() => setMongoConnectionString(mongo.getConnectionString()))); + phase1.push( + mongo.start().then(() => { + setMongoConnectionString(mongo.getConnectionString()); + }), + ); + } + if (!azurite.isRunning()) { + phase1.push(azurite.start()); } if (!oauth2.isRunning()) { phase1.push(oauth2.start()); diff --git a/packages/ocom-verification/verification-shared/src/servers/index.ts b/packages/ocom-verification/verification-shared/src/servers/index.ts index 32810914a..4313727fb 100644 --- a/packages/ocom-verification/verification-shared/src/servers/index.ts +++ b/packages/ocom-verification/verification-shared/src/servers/index.ts @@ -7,4 +7,4 @@ export { MongoDBTestServer, seedOwnerCommunityReferenceData, } from './test-mongodb-server.ts'; -export type { TestServer, TestServerOptions } from './test-server.interface.ts'; +export type { TestServer } from './test-server.interface.ts'; diff --git a/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts b/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts index 37937e616..ca57fa2b9 100644 --- a/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts +++ b/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts @@ -6,6 +6,7 @@ import { getAllMockUsers } from '../test-data/index.ts'; const MONGO_BINARY_VERSION = '7.0.14'; const DEFAULT_DB_NAME = 'owner-community-test'; const MAX_REPLSET_START_ATTEMPTS = 5; +type MongoMemoryReplSetConfig = NonNullable[0]>; export type MongoDBSeedDataFunction = (connectionString: string, dbName: string) => Promise; @@ -122,7 +123,7 @@ export class MongoDBTestServer { return this.serviceMongoose !== null; } - private async createReplicaSetWithRetry(config: Parameters[0]): Promise { + private async createReplicaSetWithRetry(config: MongoMemoryReplSetConfig): Promise { let lastError: unknown; for (let attempt = 1; attempt <= MAX_REPLSET_START_ATTEMPTS; attempt += 1) { diff --git a/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts b/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts index 8b08f6b92..16e024f23 100644 --- a/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts +++ b/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts @@ -1,15 +1,7 @@ /** * Common interface for all test servers (in-process and subprocess). - * - * This abstraction allows acceptance-api and e2e tests to use - * consistent server lifecycle management patterns while choosing - * the appropriate implementation: - * - * - **In-process** (GraphQLTestServer): Fast, isolated, mocked services - * Best for: API acceptance tests, unit-like integration tests - * - * - **Subprocess** (PortlessServer): Full stack, realistic, real services - * Best for: E2E tests, full system integration tests + * Implemented by GraphQLTestServer (in-process), PortlessServer (subprocess + * via the portless proxy), and TestAzuriteServer. */ export interface TestServer { /** Start the server and return when ready */ @@ -24,17 +16,3 @@ export interface TestServer { /** Get the server URL (throws if not running) */ getUrl(): string; } - -/** - * Configuration options for test server startup. - */ -export interface TestServerOptions { - /** Port to listen on (0 for random available port) */ - port?: number; - - /** Additional environment variables for subprocess servers */ - env?: Record; - - /** Timeout for server startup (defaults to centralized config) */ - startupTimeoutMs?: number; -} diff --git a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts index c28d7a6f5..8e9929f78 100644 --- a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts @@ -8,36 +8,9 @@ const uiEnvPath = resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env'); const apiValues = readJsonSettings(apiSettingsPath); const uiValues = readDotEnv(uiEnvPath); -/** - * Defaults for E2E/acceptance test settings when local.settings.json is absent - * (e.g. CI pipelines). All values are non-secret mock/localhost references used - * exclusively by the test harness — no real credentials are involved. - */ -const ciDefaults = { - COSMOSDB_CONNECTION_STRING: '', - COSMOSDB_DBNAME: 'owner-community', - COSMOSDB_PORT: '50000', - NODE_ENV: 'development', - ACCOUNT_PORTAL_OIDC_AUDIENCE: 'mock-client', - ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', - ACCOUNT_PORTAL_OIDC_ENDPOINT: 'https://mock-auth.ownercommunity.localhost:1355/community/.well-known/jwks.json', -} as const; - -function setting(key: keyof typeof ciDefaults): string { - return readSetting(apiValues, key, ciDefaults[key]) ?? ciDefaults[key]; -} - export const apiSettings = { - nodeEnv: setting('NODE_ENV'), - isDevelopment: setting('NODE_ENV') === 'development', - - cosmosDbConnectionString: setting('COSMOSDB_CONNECTION_STRING'), - cosmosDbName: setting('COSMOSDB_DBNAME'), - cosmosDbPort: Number(setting('COSMOSDB_PORT')), - - accountPortalOidcIssuer: setting('ACCOUNT_PORTAL_OIDC_ISSUER'), - accountPortalOidcEndpoint: setting('ACCOUNT_PORTAL_OIDC_ENDPOINT'), - accountPortalOidcAudience: setting('ACCOUNT_PORTAL_OIDC_AUDIENCE'), + cosmosDbConnectionString: readSetting(apiValues, 'COSMOSDB_CONNECTION_STRING', '') ?? '', + cosmosDbName: readSetting(apiValues, 'COSMOSDB_DBNAME', 'owner-community') ?? 'owner-community', apiDir: path.dirname(apiSettingsPath), oauth2MockDir: path.join(workspaceRoot, 'apps', 'server-oauth2-mock'), diff --git a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts index e084702e0..405f1802e 100644 --- a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts @@ -1,65 +1,7 @@ /** - * Portless hostname derivation for test environments. - * - * Mirrors the logic of `build-pipeline/scripts/portless-hostnames.mjs` — - * hostnames are derived from the tracked .env files rather than hardcoded, - * and the `WORKTREE_NAME` suffix is applied automatically. - * - * This keeps test-environment code in sync with the dev:portless startup - * scripts without adding a cross-layer import to build-pipeline/. + * Re-export of the canonical portless hostname helpers used by both the + * dev:worktree scripts and the E2E test harness. Keeping the .mjs file as + * the single source of truth means there is exactly one place that derives + * hostnames from .env and applies the WORKTREE_NAME suffix. */ - -import { findWorkspaceRoot, readDotEnv, resolveWorkspacePath } from './settings-utils.ts'; - -const PORTLESS_PORT = 1355; -const workspaceRoot = findWorkspaceRoot(); - -const uiCommunityEnv = readDotEnv(resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env')); -const uiStaffEnv = readDotEnv(resolveWorkspacePath(workspaceRoot, 'apps/ui-staff/.env')); - -function hostnameFromUrl(url: string): string { - try { - return new URL(url).hostname; - } catch { - return ''; - } -} - -/** Splice `.` in before `.localhost` for worktree isolation. */ -function applyWorktreeSuffix(hostname: string): string { - const wt = process.env['WORKTREE_NAME']; - if (!wt) return hostname; - return hostname.replace('.localhost', `.${wt}.localhost`); -} - -/** - * Returns all portless service hostnames scoped to the current worktree. - * Hostnames are derived from the tracked .env files — no names are hardcoded. - */ -export function getHostnames() { - const uiCommunity = hostnameFromUrl(uiCommunityEnv['VITE_APP_UI_COMMUNITY_BASE_URL'] ?? ''); - const api = hostnameFromUrl(uiCommunityEnv['VITE_COMMON_API_ENDPOINT'] ?? ''); - const mockAuth = hostnameFromUrl(uiCommunityEnv['VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'] ?? ''); - const uiStaff = hostnameFromUrl(uiStaffEnv['VITE_APP_UI_STAFF_AAD_REDIRECT_URI'] ?? ''); - - if (!uiCommunity || !api || !mockAuth) { - throw new Error( - 'portless-settings: could not derive hostnames from .env files. ' + 'Ensure apps/ui-community/.env is present with VITE_APP_UI_COMMUNITY_BASE_URL, ' + 'VITE_COMMON_API_ENDPOINT, and VITE_APP_UI_COMMUNITY_B2C_AUTHORITY.', - ); - } - - return { - uiCommunity: applyWorktreeSuffix(uiCommunity), - uiStaff: uiStaff ? applyWorktreeSuffix(uiStaff) : '', - api: applyWorktreeSuffix(api), - mockAuth: applyWorktreeSuffix(mockAuth), - docs: applyWorktreeSuffix(`docs.${uiCommunity}`), - }; -} - -/** Build a full portless HTTPS URL from a hostname and optional path. */ -export function buildPortlessUrl(hostname: string, path = ''): string { - return `https://${hostname}:${PORTLESS_PORT}${path}`; -} - -export { PORTLESS_PORT }; +export { buildPortlessUrl, getHostnames, PORTLESS_PORT } from '../../../../../build-pipeline/scripts/portless-hostnames.mjs'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a16e5e4be..8b33c6e41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -948,25 +948,25 @@ importers: version: link:../config-vitest '@chromatic-com/storybook': specifier: ^4.1.1 - version: 4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-a11y': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-docs': specifier: ^9.1.3 - version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-onboarding': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) '@storybook/react': specifier: ^9.1.9 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) + version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) '@storybook/react-vite': specifier: ^9.1.3 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -978,10 +978,10 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitest/browser': specifier: ^4.1.2 - version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/browser-playwright': specifier: ^4.1.2 - version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.2(vitest@4.1.2) @@ -999,13 +999,13 @@ importers: version: 6.0.1 storybook: specifier: 'catalog:' - version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 'catalog:' version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/acceptance-api: dependencies: @@ -14761,6 +14761,18 @@ snapshots: '@blazediff/core@1.9.1': {} + '@chromatic-com/storybook@4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + '@neoconfetti/react': 1.0.0 + chromatic: 13.3.4 + filesize: 10.1.6 + jsonfile: 6.2.0 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + strip-ansi: 7.1.2 + transitivePeerDependencies: + - '@chromatic-com/cypress' + - '@chromatic-com/playwright' + '@chromatic-com/storybook@4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@neoconfetti/react': 1.0.0 @@ -15089,7 +15101,7 @@ snapshots: '@cucumber/gherkin-utils': 11.0.0 '@cucumber/html-formatter': 23.0.0(@cucumber/messages@32.2.0) '@cucumber/junit-xml-formatter': 0.13.3(@cucumber/messages@32.2.0) - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) '@cucumber/messages': 32.2.0 '@cucumber/pretty-formatter': 1.0.1(@cucumber/cucumber@12.8.1)(@cucumber/messages@32.2.0) '@cucumber/tag-expressions': 9.1.0 @@ -15125,7 +15137,7 @@ snapshots: '@cucumber/gherkin-streams@6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0))(@cucumber/messages@32.2.0)': dependencies: '@cucumber/gherkin': 38.0.0 - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) '@cucumber/messages': 32.2.0 commander: 14.0.0 source-map-support: 0.5.21 @@ -15174,9 +15186,9 @@ snapshots: luxon: 3.7.2 xmlbuilder: 15.1.1 - '@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0)': + '@cucumber/message-streams@4.1.1(@cucumber/messages@32.3.1)': dependencies: - '@cucumber/messages': 32.2.0 + '@cucumber/messages': 32.3.1 mime: 3.0.0 '@cucumber/messages@26.0.1': @@ -16796,6 +16808,15 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + glob: 10.5.0 + magic-string: 0.30.21 + react-docgen-typescript: 2.4.0(typescript@6.0.3) + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + optionalDependencies: + typescript: 6.0.3 + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: glob: 10.5.0 @@ -18279,12 +18300,31 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@storybook/addon-a11y@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + '@storybook/global': 5.0.0 + axe-core: 4.11.0 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/addon-a11y@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.0 storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/addon-docs@9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) + '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + '@storybook/addon-docs@9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) @@ -18298,6 +18338,10 @@ snapshots: transitivePeerDependencies: - '@types/react' + '@storybook/addon-onboarding@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/addon-onboarding@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18318,6 +18362,22 @@ snapshots: - react - react-dom + '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + prompts: 2.4.2 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ts-dedent: 2.2.0 + optionalDependencies: + '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/runner': 4.1.2 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - react + - react-dom + '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': dependencies: '@storybook/global': 5.0.0 @@ -18334,6 +18394,13 @@ snapshots: - react - react-dom + '@storybook/builder-vite@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ts-dedent: 2.2.0 + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@storybook/builder-vite@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) @@ -18341,6 +18408,11 @@ snapshots: ts-dedent: 2.2.0 vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@storybook/csf-plugin@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + unplugin: 1.16.1 + '@storybook/csf-plugin@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18353,12 +18425,38 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@storybook/react-dom-shim@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/react-dom-shim@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/react-vite@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@rollup/pluginutils': 5.3.0 + '@storybook/builder-vite': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/react': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) + find-up: 7.0.0 + magic-string: 0.30.21 + react: 19.2.0 + react-docgen: 8.0.2 + react-dom: 19.2.0(react@19.2.0) + resolve: 1.22.11 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + tsconfig-paths: 4.2.0 + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + '@storybook/react-vite@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18379,6 +18477,16 @@ snapshots: - supports-color - typescript + '@storybook/react@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + optionalDependencies: + typescript: 6.0.3 + '@storybook/react@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)': dependencies: '@storybook/global': 5.0.0 @@ -18882,6 +18990,19 @@ snapshots: - vite optional: true + '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + dependencies: + '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + playwright: 1.59.0 + tinyrainbow: 3.1.0 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': dependencies: '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) @@ -18913,6 +19034,23 @@ snapshots: - vite optional: true + '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': dependencies: '@blazediff/core': 1.9.1 @@ -18942,7 +19080,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color @@ -18963,6 +19101,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 + '@vitest/mocker@3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.2.4 @@ -18979,6 +19125,14 @@ snapshots: optionalDependencies: vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.2 @@ -25312,6 +25466,28 @@ snapshots: graphql: 16.12.0 react: 19.2.0 + storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@storybook/global': 5.0.0 + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/spy': 3.2.4 + better-opn: 3.0.2 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + recast: 0.23.11 + semver: 7.7.4 + ws: 8.20.1 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - msw + - supports-color + - utf-8-validate + - vite + storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@storybook/global': 5.0.0 @@ -26069,6 +26245,26 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.10 + rolldown: 1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.1 + esbuild: 0.25.12 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.4.2 + terser: 5.44.1 + tsx: 4.21.0 + yaml: 2.8.3 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -26119,6 +26315,36 @@ snapshots: transitivePeerDependencies: - msw + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 24.10.1 + '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + jsdom: 26.1.0 + transitivePeerDependencies: + - msw + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.2 diff --git a/readme.md b/readme.md index c64caa156..c05851acc 100644 --- a/readme.md +++ b/readme.md @@ -26,8 +26,6 @@ Our Docusaurus website will help you get started in running and contributing to ## Developer usage - - - Full local dev (builds, starts the portless HTTPS proxy, starts Azurite, and runs the app-level dev servers): ```bash diff --git a/turbo.json b/turbo.json index 8bef61034..c1988720e 100644 --- a/turbo.json +++ b/turbo.json @@ -102,7 +102,7 @@ "cache": false, "persistent": true }, - "dev:portless": { + "dev:worktree": { "description": "Starts dev servers with worktree-scoped portless hostnames for git worktree isolation", "dependsOn": ["^build"], "cache": false, From 2d54c579b05fb58beca45b5f5d5d3519e1e037cc Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 26 May 2026 15:15:56 -0400 Subject: [PATCH 04/12] overall improvements to workspace files, along with implementation of settings modes, for e2e, so we have alternate between settings files --- .gitignore | 1 - .snyk | 5 + apps/api/local-settings.e2e.json | 29 + apps/api/package.json | 4 +- apps/api/scripts/sync-local-settings.mjs | 59 ++ apps/api/start-azurite.mjs | 30 +- apps/api/start-dev.mjs | 18 +- apps/api/turbo.json | 2 +- apps/docs/start-dev.mjs | 11 +- .../start-mongo.mjs | 18 +- apps/server-oauth2-mock/start-dev.mjs | 12 +- apps/ui-community/.env.e2e | 9 + apps/ui-community/start-dev.mjs | 20 +- apps/ui-community/turbo.json | 4 +- apps/ui-staff/.env.e2e | 8 + apps/ui-staff/start-dev.mjs | 20 +- biome.json | 2 +- build-pipeline/scripts/dev-process-exit.mjs | 8 - .../scripts/portless-hostnames.d.mts | 13 - build-pipeline/scripts/worktree-ports.d.mts | 5 - knip.json | 16 +- package.json | 2 +- .../mock-application-services.ts | 2 +- .../shared/support/shared-infrastructure.ts | 20 +- .../src/shared/support/ui/jsdom-setup.ts | 2 +- .../archunit-tests/src/validate-env-names.cjs | 11 + .../src/shared/support/servers/app-paths.ts | 9 + .../support/servers/child-process-env.ts | 14 + .../shared/support/servers/e2e-defaults.ts | 65 -- .../shared/support/servers/portless-server.ts | 2 +- .../shared/support/servers/test-api-server.ts | 46 +- .../support/servers/test-azurite-server.ts | 52 +- .../servers/test-community-vite-server.ts | 5 +- .../support/servers/test-environment.ts | 6 +- .../support/servers/test-oauth2-server.ts | 4 +- .../ocom-verification/e2e-tests/turbo.json | 28 +- .../verification-shared/src/settings/index.ts | 9 - .../src/settings/local-settings.ts | 24 - .../src/settings/portless-settings.ts | 22 +- .../src/settings/settings-utils.ts | 93 --- packages/ocom/service-otel/package.json | 2 +- pnpm-lock.yaml | 678 +++++------------- pnpm-workspace.yaml | 2 + scripts/local-dev/dev-process-exit.mjs | 40 ++ scripts/local-dev/port-ready.mjs | 45 ++ .../local-dev}/portless-hostnames.mjs | 30 +- .../local-dev}/worktree-ports.mjs | 42 +- sonar-project.properties | 2 +- 48 files changed, 636 insertions(+), 915 deletions(-) create mode 100644 apps/api/local-settings.e2e.json create mode 100644 apps/api/scripts/sync-local-settings.mjs create mode 100644 apps/ui-community/.env.e2e create mode 100644 apps/ui-staff/.env.e2e delete mode 100644 build-pipeline/scripts/dev-process-exit.mjs delete mode 100644 build-pipeline/scripts/portless-hostnames.d.mts delete mode 100644 build-pipeline/scripts/worktree-ports.d.mts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts delete mode 100644 packages/ocom-verification/verification-shared/src/settings/local-settings.ts delete mode 100644 packages/ocom-verification/verification-shared/src/settings/settings-utils.ts create mode 100644 scripts/local-dev/dev-process-exit.mjs create mode 100644 scripts/local-dev/port-ready.mjs rename {build-pipeline/scripts => scripts/local-dev}/portless-hostnames.mjs (81%) rename {build-pipeline/scripts => scripts/local-dev}/worktree-ports.mjs (77%) diff --git a/.gitignore b/.gitignore index 9bce6ceaf..33786a672 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,3 @@ apps/api/deploy/ # Build-time evidence artifacts (generated) build-artifacts/ - diff --git a/.snyk b/.snyk index 02a4e4b14..b473a7757 100644 --- a/.snyk +++ b/.snyk @@ -91,3 +91,8 @@ 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' diff --git a/apps/api/local-settings.e2e.json b/apps/api/local-settings.e2e.json new file mode 100644 index 000000000..174c23534 --- /dev/null +++ b/apps/api/local-settings.e2e.json @@ -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": "*" + } +} diff --git a/apps/api/package.json b/apps/api/package.json index 122cb1ca1..3e940dba6 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -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:e2e": "pnpm run prepare:deploy && node scripts/sync-local-settings.mjs e2e", + "dev:e2e": "pnpm exec portless data-access.ownercommunity${WORKTREE_NAME:+.${WORKTREE_NAME}}.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", @@ -23,7 +25,7 @@ "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');\"", + "sync-local-settings": "node scripts/sync-local-settings.mjs", "azurite": "node start-azurite.mjs" }, "dependencies": { diff --git a/apps/api/scripts/sync-local-settings.mjs b/apps/api/scripts/sync-local-settings.mjs new file mode 100644 index 000000000..57b89d37a --- /dev/null +++ b/apps/api/scripts/sync-local-settings.mjs @@ -0,0 +1,59 @@ +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]; +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; +} diff --git a/apps/api/start-azurite.mjs b/apps/api/start-azurite.mjs index ac3dff4e4..cc253db13 100644 --- a/apps/api/start-azurite.mjs +++ b/apps/api/start-azurite.mjs @@ -1,28 +1,14 @@ import { spawn } from 'node:child_process'; -import net from 'node:net'; -import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; -import { getAzuritePorts } from '../../build-pipeline/scripts/worktree-ports.mjs'; +import { isGracefulInterruptExit } from '../../scripts/local-dev/dev-process-exit.mjs'; +import { isPortListening, waitForPort } from '../../scripts/local-dev/port-ready.mjs'; +import { getAzuritePorts } from '../../scripts/local-dev/worktree-ports.mjs'; const ports = getAzuritePorts(); const worktreeName = process.env.WORKTREE_NAME ?? ''; const storageSuffix = worktreeName ? `-${worktreeName}` : ''; -function isPortListening(port) { - return new Promise((resolve) => { - const socket = net.createConnection({ port, host: '127.0.0.1' }); - socket.once('connect', () => { - socket.destroy(); - resolve(true); - }); - socket.once('error', () => { - socket.destroy(); - resolve(false); - }); - }); -} - if (await isPortListening(ports.blob)) { - console.log(`[azurite] already running (blob port ${ports.blob}), skipping`); + console.log(`[azurite] ready (blob port ${ports.blob})`); process.exit(0); } @@ -49,6 +35,14 @@ for (const proc of procs) { }); } +if (await waitForPort(ports.blob, { timeoutMs: 30_000 })) { + console.log(`[azurite] ready (blob port ${ports.blob})`); +} else { + console.error(`[azurite] blob port ${ports.blob} did not become ready within 30000ms`); + for (const p of procs) p.kill(); + process.exit(1); +} + process.on('SIGINT', () => { for (const p of procs) p.kill('SIGINT'); }); diff --git a/apps/api/start-dev.mjs b/apps/api/start-dev.mjs index 9dc871786..22b30e1c7 100644 --- a/apps/api/start-dev.mjs +++ b/apps/api/start-dev.mjs @@ -1,9 +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 { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; -import { getAzuriteConnectionString, getMongoConnectionString } from '../../build-pipeline/scripts/worktree-ports.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; @@ -22,8 +22,7 @@ const childEnv = { // 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 (e.g. e2e harness with a MongoMemoryServer port) can override -// any individual value via process.env before invoking this script. +// 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'); @@ -42,11 +41,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); diff --git a/apps/api/turbo.json b/apps/api/turbo.json index 6487a447c..75ff6794b 100644 --- a/apps/api/turbo.json +++ b/apps/api/turbo.json @@ -4,7 +4,7 @@ "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": { diff --git a/apps/docs/start-dev.mjs b/apps/docs/start-dev.mjs index cb09162a8..8180032bb 100644 --- a/apps/docs/start-dev.mjs +++ b/apps/docs/start-dev.mjs @@ -1,5 +1,5 @@ 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'; @@ -7,11 +7,4 @@ const child = spawn('docusaurus', ['start', '--port', port, '--host', '127.0.0.1 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); diff --git a/apps/server-mongodb-memory-mock/start-mongo.mjs b/apps/server-mongodb-memory-mock/start-mongo.mjs index 1b39ae1ef..8d15a147e 100644 --- a/apps/server-mongodb-memory-mock/start-mongo.mjs +++ b/apps/server-mongodb-memory-mock/start-mongo.mjs @@ -1,22 +1,8 @@ -import net from 'node:net'; -import { getMongoPort } from '../../build-pipeline/scripts/worktree-ports.mjs'; +import { isPortListening } from '../../scripts/local-dev/port-ready.mjs'; +import { getMongoPort } from '../../scripts/local-dev/worktree-ports.mjs'; const MONGO_PORT = getMongoPort(); -function isPortListening(port) { - return new Promise((resolve) => { - const socket = net.createConnection({ port, host: '127.0.0.1' }); - socket.once('connect', () => { - socket.destroy(); - resolve(true); - }); - socket.once('error', () => { - socket.destroy(); - resolve(false); - }); - }); -} - if (await isPortListening(MONGO_PORT)) { console.log(`[mongo-mock] already running on port ${MONGO_PORT}, skipping`); process.exit(0); diff --git a/apps/server-oauth2-mock/start-dev.mjs b/apps/server-oauth2-mock/start-dev.mjs index 2bd4f4b1e..3f9d0f985 100644 --- a/apps/server-oauth2-mock/start-dev.mjs +++ b/apps/server-oauth2-mock/start-dev.mjs @@ -1,6 +1,6 @@ import { spawn } from 'node:child_process'; -import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; -import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; +import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; const childEnv = { ...process.env }; @@ -17,10 +17,4 @@ const child = spawn('tsx', ['src/index.ts'], { env: childEnv, }); -child.on('exit', (code, signal) => { - if (isGracefulInterruptExit(signal, code)) { - process.exitCode = 0; - return; - } - process.exitCode = code ?? 1; -}); +forwardChildExit(child); diff --git a/apps/ui-community/.env.e2e b/apps/ui-community/.env.e2e new file mode 100644 index 000000000..35b7f837a --- /dev/null +++ b/apps/ui-community/.env.e2e @@ -0,0 +1,9 @@ +# E2E defaults. Vite loads this via `vite --mode e2e`; +# process env supplied by local shells or CI still has precedence. +NODE_ENV=development +VITE_APP_UI_COMMUNITY_B2C_AUTHORITY=https://mock-auth.ownercommunity.localhost:1355/community +VITE_APP_UI_COMMUNITY_B2C_CLIENTID=mock-client +VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI=https://ownercommunity.localhost:1355/auth-redirect +VITE_APP_UI_COMMUNITY_B2C_SCOPES=openid +VITE_COMMON_API_ENDPOINT=https://data-access.ownercommunity.localhost:1355/api/graphql +VITE_APP_UI_COMMUNITY_BASE_URL=https://ownercommunity.localhost:1355 diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs index 5226fb736..62c79117f 100644 --- a/apps/ui-community/start-dev.mjs +++ b/apps/ui-community/start-dev.mjs @@ -1,6 +1,6 @@ import { spawn } from 'node:child_process'; -import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; -import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; +import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; const childEnv = { ...process.env }; @@ -13,15 +13,15 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_APP_UI_COMMUNITY_BASE_URL = buildPortlessUrl(hostnames.uiCommunity); } -const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { +const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; +const viteMode = process.env.VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); +if (viteMode) { + viteArgs.push('--mode', viteMode); +} + +const child = spawn('vite', viteArgs, { stdio: 'inherit', env: childEnv, }); -child.on('exit', (code, signal) => { - if (isGracefulInterruptExit(signal, code)) { - process.exitCode = 0; - return; - } - process.exitCode = code ?? 1; -}); +forwardChildExit(child); diff --git a/apps/ui-community/turbo.json b/apps/ui-community/turbo.json index b7c32402e..e49eac493 100644 --- a/apps/ui-community/turbo.json +++ b/apps/ui-community/turbo.json @@ -5,13 +5,13 @@ "dependsOn": ["^build"], "persistent": true, "interruptible": false, - "inputs": [".env", "package.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] + "inputs": [".env", ".env.*", "package.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] }, "dev:worktree": { "dependsOn": ["^build"], "persistent": true, "interruptible": false, - "inputs": [".env", "package.json", "start-dev.mjs", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] + "inputs": [".env", ".env.*", "package.json", "start-dev.mjs", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] } } } diff --git a/apps/ui-staff/.env.e2e b/apps/ui-staff/.env.e2e new file mode 100644 index 000000000..4eec69940 --- /dev/null +++ b/apps/ui-staff/.env.e2e @@ -0,0 +1,8 @@ +# E2E defaults. Vite loads this via `vite --mode e2e`; +# process env supplied by local shells or CI still has precedence. +NODE_ENV=development +VITE_APP_UI_STAFF_AAD_AUTHORITY=https://mock-auth.ownercommunity.localhost:1355/staff +VITE_APP_UI_STAFF_AAD_CLIENTID=mock-client +VITE_APP_UI_STAFF_AAD_REDIRECT_URI=https://staff.ownercommunity.localhost:1355/auth-redirect +VITE_APP_UI_STAFF_AAD_SCOPES=openid +VITE_COMMON_API_ENDPOINT=https://data-access.ownercommunity.localhost:1355/api/graphql diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs index eaa17cdb8..0ef5c7673 100644 --- a/apps/ui-staff/start-dev.mjs +++ b/apps/ui-staff/start-dev.mjs @@ -1,6 +1,6 @@ import { spawn } from 'node:child_process'; -import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; -import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; +import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; const childEnv = { ...process.env }; @@ -11,15 +11,15 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); } -const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { +const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; +const viteMode = process.env.VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); +if (viteMode) { + viteArgs.push('--mode', viteMode); +} + +const child = spawn('vite', viteArgs, { stdio: 'inherit', env: childEnv, }); -child.on('exit', (code, signal) => { - if (isGracefulInterruptExit(signal, code)) { - process.exitCode = 0; - return; - } - process.exitCode = code ?? 1; -}); +forwardChildExit(child); diff --git a/biome.json b/biome.json index 2ed46c509..e14913384 100644 --- a/biome.json +++ b/biome.json @@ -166,7 +166,7 @@ }, { "includes": ["**/src/**/*.ts"], "javascript": { "globals": [] } }, { - "includes": ["build-pipeline/scripts/**/*.{c|m}js"], + "includes": ["build-pipeline/scripts/**/*.{c|m}js", "scripts/local-dev/**/*.mjs"], "linter": { "rules": { "style": { "noCommonJs": "off" } } } }, { diff --git a/build-pipeline/scripts/dev-process-exit.mjs b/build-pipeline/scripts/dev-process-exit.mjs deleted file mode 100644 index 04eabd5a4..000000000 --- a/build-pipeline/scripts/dev-process-exit.mjs +++ /dev/null @@ -1,8 +0,0 @@ -const INTERRUPT_SIGNALS = new Set(['SIGINT', 'SIGTERM', 'SIGQUIT']); -const INTERRUPT_EXIT_CODES = new Set([130, 143]); - -export const isInterruptSignal = (signal) => Boolean(signal && INTERRUPT_SIGNALS.has(signal)); - -export const isInterruptExitCode = (code) => Number.isInteger(code) && INTERRUPT_EXIT_CODES.has(code); - -export const isGracefulInterruptExit = (signal, code) => isInterruptSignal(signal) || isInterruptExitCode(code); diff --git a/build-pipeline/scripts/portless-hostnames.d.mts b/build-pipeline/scripts/portless-hostnames.d.mts deleted file mode 100644 index 139d75e94..000000000 --- a/build-pipeline/scripts/portless-hostnames.d.mts +++ /dev/null @@ -1,13 +0,0 @@ -export const PORTLESS_PORT: number; - -export interface PortlessHostnames { - uiCommunity: string; - uiStaff: string; - api: string; - mockAuth: string; - docs: string; -} - -export function getHostnames(): PortlessHostnames; - -export function buildPortlessUrl(hostname: string, path?: string): string; diff --git a/build-pipeline/scripts/worktree-ports.d.mts b/build-pipeline/scripts/worktree-ports.d.mts deleted file mode 100644 index c2cbdf86e..000000000 --- a/build-pipeline/scripts/worktree-ports.d.mts +++ /dev/null @@ -1,5 +0,0 @@ -export function getWorktreePortOffset(): number; -export function getMongoPort(): number; -export function getAzuritePorts(): { blob: number; queue: number; table: number }; -export function getAzuriteConnectionString(): string; -export function getMongoConnectionString(): string; diff --git a/knip.json b/knip.json index 4a560cc9a..130ffcb7f 100644 --- a/knip.json +++ b/knip.json @@ -98,7 +98,21 @@ } }, "ignoreWorkspaces": ["packages/cellix/config-typescript"], - "ignore": ["build-pipeline/scripts/**", "**/fixtures/**", "**/*.test.ts", "**/*.spec.ts", "**/*.stories.tsx", "**/dist/**", "**/coverage/**", "**/__tests__/**", "**/tests/**", ".agents/**", ".github/**", "portless.config.cjs"], + "ignore": [ + "build-pipeline/scripts/**", + "scripts/local-dev/**", + "**/fixtures/**", + "**/*.test.ts", + "**/*.spec.ts", + "**/*.stories.tsx", + "**/dist/**", + "**/coverage/**", + "**/__tests__/**", + "**/tests/**", + ".agents/**", + ".github/**", + "portless.config.cjs" + ], "ignoreIssues": { "codegen.yml": ["unlisted"] }, diff --git a/package.json b/package.json index 3dc82ec64..46e1f3dd3 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "sonar:pr": "export PR_NUMBER=$(node build-pipeline/scripts/get-pr-number.cjs) && sonar-scanner -Dsonar.pullrequest.key=$PR_NUMBER -Dsonar.pullrequest.branch=$(git branch --show-current) -Dsonar.pullrequest.base=main", "sonar:pr-windows": "for /f %i in ('node build-pipeline/scripts/get-pr-number.cjs') do set PR_NUMBER=%i && sonar-scanner -Dsonar.pullrequest.key=%PR_NUMBER% -Dsonar.pullrequest.branch=%BRANCH_NAME% -Dsonar.pullrequest.base=main", "check-sonar": "node build-pipeline/scripts/check-sonar-quality-gate.cjs", - "verify": "pnpm run format:check && pnpm run test:arch && pnpm run test:coverage:merge && pnpm run knip && pnpm run audit && pnpm run snyk && pnpm run sonar:pr && pnpm run check-sonar", + "verify": "pnpm run format:check && pnpm run test:arch && pnpm run test:coverage:merge && pnpm run test:e2e:worktree && pnpm run knip && pnpm run audit && pnpm run snyk && pnpm run sonar:pr && pnpm run check-sonar", "knip": "knip", "snyk": "pnpm run snyk:test && pnpm run snyk:code", "snyk:report": "pnpm run snyk:monitor && pnpm run snyk:code:report", diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts b/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts index 7e774dc2c..ca8096e34 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts +++ b/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts @@ -33,7 +33,7 @@ function createNoOpApolloServerService(): ServiceApolloServer; + } as unknown as ServiceApolloServer>; } export function createMockApplicationServicesFactory(serviceMongoose: ServiceMongoose): ApplicationServicesFactory { diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts index cdc91878a..bbcfd73b0 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts @@ -1,10 +1,8 @@ import { GraphQLTestServer, MongoDBTestServer } from '@ocom-verification/verification-shared/servers'; -import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { createMockApplicationServicesFactory } from './application-services/index.ts'; // Shared infrastructure — persists across scenarios within a single test run let mongoDBServer: MongoDBTestServer | undefined; -let mongoSeeded = false; let graphQLServer: GraphQLTestServer | undefined; let apiUrl: string | undefined; @@ -26,27 +24,13 @@ export async function stopAll(): Promise { mongoDBServer = undefined; } apiUrl = undefined; - mongoSeeded = false; } -async function ensureMongoDBServer(options?: { port?: number; dbName?: string }): Promise { +async function ensureMongoDBServer(): Promise { if (mongoDBServer) return mongoDBServer; - const connectionString = options?.port ? apiSettings.cosmosDbConnectionString : ''; - - if (connectionString && (await MongoDBTestServer.isReachable(connectionString))) { - if (!mongoSeeded) { - await MongoDBTestServer.seedData(connectionString, options?.dbName ?? apiSettings.cosmosDbName); - mongoSeeded = true; - } - mongoDBServer = new MongoDBTestServer(); - await mongoDBServer.start(options); - return mongoDBServer; - } - mongoDBServer = new MongoDBTestServer(); - await mongoDBServer.start(options); - mongoSeeded = true; + await mongoDBServer.start(); return mongoDBServer; } diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts index 349b3edee..5fc3e8f2f 100644 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts +++ b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts @@ -11,7 +11,7 @@ const dom = new JSDOM('
', { url: 'http://localhost:3000', pretendToBeVisual: true, }); -const domGlobal = (dom as unknown as Record)['window']; +const domGlobal = dom['window'] as unknown as Window & typeof globalThis; // biome-ignore lint/suspicious/noExplicitAny: attaching browser globals requires dynamic property assignment const g = globalThis as any; diff --git a/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs b/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs index 827e78164..2097f480c 100644 --- a/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs +++ b/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs @@ -8,6 +8,7 @@ const SKIP_DIRS = new Set(['node_modules', '.turbo', 'build-artifacts', 'dist', // Single source of truth for known portal identifiers (ADR-0031). // Keep this list in sync with apps/docs/docs/portals/PORTAL_REGISTRY.md. const CANONICAL_PORTALS = ['UI_COMMUNITY', 'UI_STAFF']; +const VITE_RESERVED_VARS = new Set(['VITE_MODE']); function walkDir(dir, fileList = []) { const files = fs.readdirSync(dir, { withFileTypes: true }); @@ -319,6 +320,16 @@ function validateEnvNames(options = {}) { for (const m of matches) { const variable = m.match; const relPath = `${path.relative(rootDir, filePath)}:${getLineNumber(newlineOffsets, m.index)}`; + if (VITE_RESERVED_VARS.has(variable)) { + results.push({ + variable, + status: 'compliant', + portal: 'BUILD', + ownerGroup: 'ocm-platform', + location: relPath, + }); + continue; + } if (!variable.startsWith('VITE_APP_') && !variable.startsWith('VITE_COMMON_')) { results.push({ variable, diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts new file mode 100644 index 000000000..b0ebf360f --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts @@ -0,0 +1,9 @@ +import { fileURLToPath } from 'node:url'; + +const workspaceRootUrl = new URL('../../../../../../../', import.meta.url); + +export const appPaths = { + apiDir: fileURLToPath(new URL('apps/api/', workspaceRootUrl)), + oauth2MockDir: fileURLToPath(new URL('apps/server-oauth2-mock/', workspaceRootUrl)), + uiCommunityDir: fileURLToPath(new URL('apps/ui-community/', workspaceRootUrl)), +} as const; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts new file mode 100644 index 000000000..51e02949c --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts @@ -0,0 +1,14 @@ +/** + * Returns a shallow copy of `process.env` with `NODE_OPTIONS` removed. + * + * Cucumber runs with `NODE_OPTIONS='--import tsx/esm'` so that TypeScript + * source is executed directly. Child processes spawned by the test harness + * (Azurite, portless, func) are plain JavaScript and do not have `tsx` on + * their resolution path, so inheriting `NODE_OPTIONS` causes an immediate + * crash. Callers can spread additional overrides on top of the result. + */ +export function spawnEnv(overrides: Record = {}): NodeJS.ProcessEnv { + const env = { ...process.env, ...overrides }; + delete env['NODE_OPTIONS']; + return env; +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts deleted file mode 100644 index 6680d32a8..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts +++ /dev/null @@ -1,65 +0,0 @@ -// biome-ignore format: keep the public Azurite dev key split across chunks for static analysis. - -/** - * Non-secret environment defaults for the e2e harness. - * - * Used when `apps/api/local.settings.json` is absent (typical in CI). Every - * value here is either a public Azurite development credential or a mock-only - * constant — none of them are valid in any real environment, and they are - * never applied outside the e2e harness. - * - * Worktree-scoped values (OIDC URLs, mongo/azurite ports, connection strings) - * are computed at runtime by `build-pipeline/scripts/portless-hostnames.mjs` - * and `worktree-ports.mjs`; they intentionally do NOT live here. - */ - -const AZURITE_ACCOUNT_KEY = [ - 'Eby8vdM02xNOcqFlqUwJPLlm', - 'EtlCDXJ1OUzFT50uSRZ6IFs', - 'uFq2UVErCz4I6tq/K1SZFP', - 'TOtr/KBHBeksoGMGw==', -].join(''); - -const E2E_API_DEFAULTS = { - FUNCTIONS_WORKER_RUNTIME: 'node', - NODE_ENV: 'development', - CONFIG_VERSION: '3.0', - - ACCOUNT_PORTAL_OIDC_AUDIENCE: 'mock-client', - ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER: 'true', - STAFF_PORTAL_OIDC_AUDIENCE: 'mock-client', - STAFF_PORTAL_OIDC_IGNORE_ISSUER: 'true', - - COSMOSDB_DBNAME: 'owner-community', - - // Well-known Azurite dev account — documented in Azurite's README, not a secret. - STORAGE_ACCOUNT_NAME: 'devstoreaccount1', - STORAGE_ACCOUNT_KEY: AZURITE_ACCOUNT_KEY, -} as const; - -/** - * Apply e2e defaults to `process.env` for any key not already set. A pre-existing - * env var (from the developer's shell or a copied local.settings.json) always wins. - * Spawned child processes inherit `process.env`, so this propagates to azurite, - * the api function host, and the oauth2 mock without any per-server plumbing. - */ -export function applyE2EDefaultsToEnv(): void { - for (const [key, value] of Object.entries(E2E_API_DEFAULTS)) { - process.env[key] ??= value; - } -} - -/** - * Returns a shallow copy of `process.env` with `NODE_OPTIONS` removed. - * - * Cucumber runs with `NODE_OPTIONS='--import tsx/esm'` so that TypeScript - * source is executed directly. Child processes spawned by the test harness - * (Azurite, portless, func) are plain JavaScript and do not have `tsx` on - * their resolution path, so inheriting `NODE_OPTIONS` causes an immediate - * crash. Callers can spread additional overrides on top of the result. - */ -export function spawnEnv(overrides: Record = {}): NodeJS.ProcessEnv { - const env = { ...process.env, ...overrides }; - delete env['NODE_OPTIONS']; - return env; -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index 6ff6ace04..a4dcfaa4b 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -1,7 +1,7 @@ import { type ChildProcess, spawn } from 'node:child_process'; import type { TestServer } from '@ocom-verification/verification-shared/servers'; import { getTimeout } from '@ocom-verification/verification-shared/settings'; -import { spawnEnv } from './e2e-defaults.ts'; +import { spawnEnv } from './child-process-env.ts'; import { getPortlessPath } from './resolve-portless.ts'; /** diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index a92d3f893..3a45a7176 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -1,8 +1,4 @@ -import { execFileSync } from 'node:child_process'; -import { readFileSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { apiSettings } from '@ocom-verification/verification-shared/settings'; -import { spawnEnv } from './e2e-defaults.ts'; +import { appPaths } from './app-paths.ts'; import { PortlessServer } from './portless-server.ts'; import { buildUrl, getHostnames, getMongoConnectionString } from './test-environment.ts'; @@ -10,38 +6,10 @@ const hostnames = getHostnames(); /** * Spawns the api dev server the same way `pnpm dev:worktree` does. The - * worktree-aware overrides (OIDC URLs, Azurite connection, etc.) all come - * from apps/api/start-dev.mjs — we only inject the dynamic MongoDB - * connection string from MongoMemoryServer here. + * Azure Functions runtime reads deploy/local.settings.json, which is generated + * from the committed e2e local settings plus runtime-only test values. */ export class TestApiServer extends PortlessServer { - override async start(): Promise { - // Mirror the `predev:worktree` lifecycle hook so deploy/ and local.settings.json - // stay in sync — start-dev.mjs is invoked directly here, bypassing pnpm's pre-hook. - execFileSync('pnpm', ['run', 'predev:worktree'], { - cwd: this.cwd, - env: spawnEnv(), - stdio: 'pipe', - }); - - // Patch deploy/local.settings.json with e2e-specific values. - // Azure Functions loads local.settings.json values into the worker env, - // overriding any parent-process env vars. We must patch the file so the - // worker gets the MongoMemoryServer connection string (random port) and - // the inspector is disabled (port 5858 may already be in use). - const settingsPath = join(this.cwd, 'deploy', 'local.settings.json'); - try { - const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); - settings.Values.COSMOSDB_CONNECTION_STRING = getMongoConnectionString(); - settings.Values['languageWorkers__node__arguments'] = ''; - writeFileSync(settingsPath, JSON.stringify(settings, null, '\t')); - } catch { - /* best-effort — file may not exist in CI */ - } - - await super.start(); - } - protected get probeUrl() { return buildUrl(hostnames.api, '/api/graphql'); } @@ -70,15 +38,17 @@ export class TestApiServer extends PortlessServer { protected get serverName() { return 'TestApiServer'; } + protected override get executable() { + return 'pnpm'; + } protected get spawnArgs() { - return [hostnames.api, 'node', 'start-dev.mjs']; + return ['run', 'dev:e2e']; } protected get cwd() { - return apiSettings.apiDir; + return appPaths.apiDir; } protected override get extraEnv() { - // start-dev.mjs handles the rest via WORKTREE_NAME + `??=` fallbacks. return { COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), }; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts index d43d9efd3..47558526a 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts @@ -1,10 +1,24 @@ import { type ChildProcess, spawn } from 'node:child_process'; -import net from 'node:net'; import { join } from 'node:path'; import type { TestServer } from '@ocom-verification/verification-shared/servers'; -import { apiSettings, getTimeout } from '@ocom-verification/verification-shared/settings'; -import { getAzuritePorts } from '../../../../../../../build-pipeline/scripts/worktree-ports.mjs'; -import { spawnEnv } from './e2e-defaults.ts'; +import { getTimeout } from '@ocom-verification/verification-shared/settings'; +import { appPaths } from './app-paths.ts'; +import { spawnEnv } from './child-process-env.ts'; + +interface PortReadyModule { + isPortListening(port: number): Promise; + waitForPort(port: number, options?: { timeoutMs?: number; intervalMs?: number }): Promise; +} + +interface WorktreePortsModule { + getAzuritePorts(): { blob: number; queue: number; table: number }; +} + +const portReadyModuleUrl = new URL('../../../../../../../scripts/local-dev/port-ready.mjs', import.meta.url).href; +const worktreePortsModuleUrl = new URL('../../../../../../../scripts/local-dev/worktree-ports.mjs', import.meta.url).href; + +const { isPortListening, waitForPort } = (await import(portReadyModuleUrl)) as PortReadyModule; +const { getAzuritePorts } = (await import(worktreePortsModuleUrl)) as WorktreePortsModule; /** * Starts Azurite via apps/api/start-azurite.mjs. The script itself short-circuits @@ -24,10 +38,10 @@ export class TestAzuriteServer implements TestServer { if (this.process || this.startedByUs) return; if (await isPortListening(this.blobPort)) return; - const binDir = join(apiSettings.apiDir, 'node_modules', '.bin'); + const binDir = join(appPaths.apiDir, 'node_modules', '.bin'); this.process = spawn('node', ['start-azurite.mjs'], { - cwd: apiSettings.apiDir, + cwd: appPaths.apiDir, env: spawnEnv({ PATH: `${binDir}:${process.env['PATH'] ?? ''}` }), detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], @@ -68,30 +82,16 @@ export class TestAzuriteServer implements TestServer { } private async waitForReady(): Promise { - const deadline = Date.now() + getTimeout('serverStartup'); - const interval = getTimeout('healthProbeInterval'); - while (Date.now() < deadline) { - if (await isPortListening(this.blobPort)) return; - await new Promise((resolve) => setTimeout(resolve, interval)); + const ready = await waitForPort(this.blobPort, { + timeoutMs: getTimeout('serverStartup'), + intervalMs: getTimeout('healthProbeInterval'), + }); + if (!ready) { + throw new Error(`TestAzuriteServer: blob port ${this.blobPort} did not start within timeout`); } - throw new Error(`TestAzuriteServer: blob port ${this.blobPort} did not start within timeout`); } } -function isPortListening(port: number): Promise { - return new Promise((resolve) => { - const socket = net.createConnection({ port, host: '127.0.0.1' }); - socket.once('connect', () => { - socket.destroy(); - resolve(true); - }); - socket.once('error', () => { - socket.destroy(); - resolve(false); - }); - }); -} - function killProcess(proc: ChildProcess, signal: NodeJS.Signals, useGroup: boolean): void { if (useGroup && proc.pid) { try { diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts index 8de00bfca..2251dba3c 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts @@ -1,4 +1,4 @@ -import { apiSettings } from '@ocom-verification/verification-shared/settings'; +import { appPaths } from './app-paths.ts'; import { PortlessServer } from './portless-server.ts'; import { buildUrl, getHostnames } from './test-environment.ts'; @@ -25,12 +25,13 @@ export class TestCommunityViteServer extends PortlessServer { return [hostnames.uiCommunity, 'node', 'start-dev.mjs']; } protected get cwd() { - return apiSettings.uiCommunityDir; + return appPaths.uiCommunityDir; } protected override get extraEnv() { return { BROWSER: 'none', NODE_ENV: 'development', + VITE_MODE: 'e2e', }; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index 6550beb7f..d9fa97a66 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -1,6 +1,5 @@ import { execFileSync } from 'node:child_process'; import { buildPortlessUrl, getHostnames } from '@ocom-verification/verification-shared/settings'; -import { applyE2EDefaultsToEnv } from './e2e-defaults.ts'; import { getPortlessPath } from './resolve-portless.ts'; let proxyInitialized = false; @@ -16,16 +15,13 @@ export const mockOidcIssuer = buildPortlessUrl(hostnames.mockAuth, '/community') export const mockOidcEndpoint = `${mockOidcIssuer}/.well-known/jwks.json`; /** - * Apply e2e env defaults (so CI runs without local.settings.json) and ensure - * the portless proxy is running. The `proxy start` invocation is idempotent — + * Ensure the portless proxy is running. The `proxy start` invocation is idempotent, * if another worktree or dev session already started it on the shared port, * this returns immediately. */ export function initTestEnvironment() { if (proxyInitialized) return; - applyE2EDefaultsToEnv(); - execFileSync(getPortlessPath(), ['proxy', 'start', '--https', '-p', '1355'], { timeout: 15_000, stdio: 'pipe', diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts index 7d843f446..d21f0e090 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts @@ -1,4 +1,4 @@ -import { apiSettings } from '@ocom-verification/verification-shared/settings'; +import { appPaths } from './app-paths.ts'; import { PortlessServer } from './portless-server.ts'; import { getHostnames, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; @@ -24,7 +24,7 @@ export class TestOAuth2Server extends PortlessServer { return [hostnames.mockAuth, 'node', 'start-dev.mjs']; } protected get cwd() { - return apiSettings.oauth2MockDir; + return appPaths.oauth2MockDir; } getUrl(): string { diff --git a/packages/ocom-verification/e2e-tests/turbo.json b/packages/ocom-verification/e2e-tests/turbo.json index 4a199c38c..a5adb33a1 100644 --- a/packages/ocom-verification/e2e-tests/turbo.json +++ b/packages/ocom-verification/e2e-tests/turbo.json @@ -3,12 +3,36 @@ "tasks": { "test:e2e": { "dependsOn": ["^build"], - "inputs": ["src/**/*.ts", "cucumber.js", "package.json"], + "inputs": [ + "src/**/*.ts", + "config/**", + "$TURBO_ROOT$/apps/api/local-settings.e2e.json", + "$TURBO_ROOT$/apps/api/scripts/sync-local-settings.mjs", + "$TURBO_ROOT$/apps/ui-community/.env", + "$TURBO_ROOT$/apps/ui-community/.env.e2e", + "$TURBO_ROOT$/apps/ui-staff/.env", + "$TURBO_ROOT$/apps/ui-staff/.env.e2e", + "$TURBO_ROOT$/scripts/local-dev/**", + "cucumber.js", + "package.json" + ], "cache": false }, "test:serenity": { "dependsOn": ["^build"], - "inputs": ["src/**/*.ts", "cucumber.js", "package.json"], + "inputs": [ + "src/**/*.ts", + "config/**", + "$TURBO_ROOT$/apps/api/local-settings.e2e.json", + "$TURBO_ROOT$/apps/api/scripts/sync-local-settings.mjs", + "$TURBO_ROOT$/apps/ui-community/.env", + "$TURBO_ROOT$/apps/ui-community/.env.e2e", + "$TURBO_ROOT$/apps/ui-staff/.env", + "$TURBO_ROOT$/apps/ui-staff/.env.e2e", + "$TURBO_ROOT$/scripts/local-dev/**", + "cucumber.js", + "package.json" + ], "cache": false } } diff --git a/packages/ocom-verification/verification-shared/src/settings/index.ts b/packages/ocom-verification/verification-shared/src/settings/index.ts index 68ac69337..5969fa475 100644 --- a/packages/ocom-verification/verification-shared/src/settings/index.ts +++ b/packages/ocom-verification/verification-shared/src/settings/index.ts @@ -1,11 +1,2 @@ -export { apiSettings, uiSettings } from './local-settings.ts'; export { buildPortlessUrl, getHostnames, PORTLESS_PORT } from './portless-settings.ts'; -export { - findWorkspaceRoot, - readDotEnv, - readJsonSettings, - readSetting, - requireSetting, - resolveWorkspacePath, -} from './settings-utils.ts'; export { getTimeout, type TimeoutKey, timeouts } from './timeout-settings.ts'; diff --git a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts deleted file mode 100644 index 8e9929f78..000000000 --- a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts +++ /dev/null @@ -1,24 +0,0 @@ -import path from 'node:path'; -import { findWorkspaceRoot, readDotEnv, readJsonSettings, readSetting, requireSetting, resolveWorkspacePath } from './settings-utils.ts'; - -const workspaceRoot = findWorkspaceRoot(); -const apiSettingsPath = resolveWorkspacePath(workspaceRoot, 'apps/api/local.settings.json'); -const uiEnvPath = resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env'); - -const apiValues = readJsonSettings(apiSettingsPath); -const uiValues = readDotEnv(uiEnvPath); - -export const apiSettings = { - cosmosDbConnectionString: readSetting(apiValues, 'COSMOSDB_CONNECTION_STRING', '') ?? '', - cosmosDbName: readSetting(apiValues, 'COSMOSDB_DBNAME', 'owner-community') ?? 'owner-community', - - apiDir: path.dirname(apiSettingsPath), - oauth2MockDir: path.join(workspaceRoot, 'apps', 'server-oauth2-mock'), - uiCommunityDir: path.dirname(uiEnvPath), -} as const; - -export const uiSettings = { - baseUrl: requireSetting(uiValues, 'VITE_APP_UI_COMMUNITY_BASE_URL', 'VITE_APP_UI_COMMUNITY_BASE_URL is required in .env'), - - graphqlEndpoint: requireSetting(uiValues, 'VITE_COMMON_API_ENDPOINT', 'VITE_COMMON_API_ENDPOINT is required in .env'), -} as const; diff --git a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts index 405f1802e..a77b96e19 100644 --- a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts @@ -1,7 +1,25 @@ /** - * Re-export of the canonical portless hostname helpers used by both the + * Runtime bridge to the canonical portless hostname helpers used by both the * dev:worktree scripts and the E2E test harness. Keeping the .mjs file as * the single source of truth means there is exactly one place that derives * hostnames from .env and applies the WORKTREE_NAME suffix. */ -export { buildPortlessUrl, getHostnames, PORTLESS_PORT } from '../../../../../build-pipeline/scripts/portless-hostnames.mjs'; +interface PortlessHostnames { + uiCommunity: string; + uiStaff: string; + api: string; + mockAuth: string; + docs: string; +} + +interface PortlessHostnamesModule { + PORTLESS_PORT: number; + getHostnames(): PortlessHostnames; + buildPortlessUrl(hostname: string, path?: string): string; +} + +const portlessHostnamesModuleUrl = new URL('../../../../../scripts/local-dev/portless-hostnames.mjs', import.meta.url).href; + +const { buildPortlessUrl, getHostnames, PORTLESS_PORT } = (await import(portlessHostnamesModuleUrl)) as PortlessHostnamesModule; + +export { buildPortlessUrl, getHostnames, PORTLESS_PORT }; diff --git a/packages/ocom-verification/verification-shared/src/settings/settings-utils.ts b/packages/ocom-verification/verification-shared/src/settings/settings-utils.ts deleted file mode 100644 index c51a891eb..000000000 --- a/packages/ocom-verification/verification-shared/src/settings/settings-utils.ts +++ /dev/null @@ -1,93 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const defaultStartDir = fileURLToPath(new URL('.', import.meta.url)); - -/** - * Walks up from `startDir` looking for pnpm-workspace.yaml to locate the - * monorepo root. Projects that use a different workspace marker should pass a - * custom `markerFile`. - */ -export function findWorkspaceRoot(startDir = defaultStartDir, markerFile = 'pnpm-workspace.yaml'): string { - let dir = startDir; - - while (dir !== path.dirname(dir)) { - if (fs.existsSync(path.join(dir, markerFile))) { - return dir; - } - dir = path.dirname(dir); - } - - throw new Error(`Could not find workspace root (${markerFile})`); -} - -/** - * Reads an Azure Functions-style settings file ({ "Values": { ... } }) and - * returns the Values map. Returns `{}` if the file doesn't exist. - */ -export function readJsonSettings(filePath: string): Record { - if (!fs.existsSync(filePath)) { - return {}; - } - const raw = fs.readFileSync(filePath, 'utf-8'); - const parsed = JSON.parse(raw) as { Values?: Record }; - return parsed.Values ?? {}; -} - -/** - * Reads a simple `KEY=VALUE` .env file. Ignores blank lines and `#` comments. - * Returns `{}` if the file doesn't exist. - */ -export function readDotEnv(filePath: string): Record { - if (!fs.existsSync(filePath)) { - return {}; - } - - const lines = fs.readFileSync(filePath, 'utf-8').split('\n'); - const result: Record = {}; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) { - continue; - } - - const eqIndex = trimmed.indexOf('='); - if (eqIndex === -1) { - continue; - } - - result[trimmed.slice(0, eqIndex)] = trimmed.slice(eqIndex + 1); - } - - return result; -} - -/** - * Resolves a target path against the workspace root unless it's already - * absolute. - */ -export function resolveWorkspacePath(workspaceRoot: string, targetPath: string): string { - return path.isAbsolute(targetPath) ? targetPath : path.join(workspaceRoot, targetPath); -} - -/** - * Returns the value for `key`, or `defaultValue` if absent/empty. Use this - * when an optional setting has a reasonable default. - */ -export function readSetting(values: Record, key: string, defaultValue?: string): string | undefined { - return values[key] ?? defaultValue; -} - -/** - * Returns the value for `key`. Throws `errorMessage` if the value is missing - * or empty. Use this when a setting is required for the test run to work. - */ -export function requireSetting(values: Record, key: string, errorMessage: string): string { - const value = values[key]; - if (value) { - return value; - } - throw new Error(errorMessage); -} diff --git a/packages/ocom/service-otel/package.json b/packages/ocom/service-otel/package.json index 0fe4c8bd5..18fb61910 100644 --- a/packages/ocom/service-otel/package.json +++ b/packages/ocom/service-otel/package.json @@ -35,7 +35,7 @@ "@opentelemetry/instrumentation-mongoose": "0.47.0", "@opentelemetry/sdk-logs": "0.57.2", "@opentelemetry/sdk-metrics": "1.30.1", - "@opentelemetry/sdk-node": "0.217.0", + "@opentelemetry/sdk-node": "0.57.2", "@opentelemetry/sdk-trace-node": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b33c6e41..929e94059 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,8 @@ overrides: ip-address: ^10.1.1 fast-uri: ^3.1.2 '@babel/plugin-transform-modules-systemjs': 7.29.4 + shell-quote@<1.8.4: 1.8.4 + '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 patchedDependencies: '@azure/functions@4.11.0': 69772ce521bf6df67d814ff4f419f19b5e966a41c4ce80b5938143ad628e5645 @@ -948,25 +950,25 @@ importers: version: link:../config-vitest '@chromatic-com/storybook': specifier: ^4.1.1 - version: 4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-a11y': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-docs': specifier: ^9.1.3 - version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-onboarding': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) '@storybook/react': specifier: ^9.1.9 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) + version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) '@storybook/react-vite': specifier: ^9.1.3 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -978,10 +980,10 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitest/browser': specifier: ^4.1.2 - version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/browser-playwright': specifier: ^4.1.2 - version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.2(vitest@4.1.2) @@ -999,13 +1001,13 @@ importers: version: 6.0.1 storybook: specifier: 'catalog:' - version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 'catalog:' version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/acceptance-api: dependencies: @@ -1701,8 +1703,8 @@ importers: specifier: 1.30.1 version: 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: 0.217.0 - version: 0.217.0(@opentelemetry/api@1.9.0) + specifier: 0.57.2 + version: 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': specifier: 1.30.1 version: 1.30.1(@opentelemetry/api@1.9.0) @@ -4912,10 +4914,6 @@ packages: resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==} engines: {node: '>=8.0.0'} - '@opentelemetry/api-logs@0.217.0': - resolution: {integrity: sha512-Cdq0jW2lknrNfrAm92MyEAvpe2cRsKjdnQLHUL6xRA4IVUnsWx6P65E7NcUO0Y+L4w1Aee5iV8FvjSwd+lrs9A==} - engines: {node: '>=8.0.0'} - '@opentelemetry/api-logs@0.52.1': resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==} engines: {node: '>=14'} @@ -4928,24 +4926,12 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/configuration@0.217.0': - resolution: {integrity: sha512-xCtrYOhBqdy6ZOMfe0Oa73ZKF+2LMhoOv4L5vmwAHVvOXUg+V3fvKuEIr9ZyD0Ow+vxllEjWO6PV1wd0DOtyvw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks@1.30.1': resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/context-async-hooks@2.7.1': - resolution: {integrity: sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@1.25.1': resolution: {integrity: sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==} engines: {node: '>=14'} @@ -4970,39 +4956,39 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.217.0': - resolution: {integrity: sha512-vC5S0Dc+noxD86CVtNu1+awCHPA5Kewi1Sg23ps+9lh4YifwsKXh3pe4XTNEKtUJiAcjpJ5dqStGakLbrSE+YQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2': + resolution: {integrity: sha512-eovEy10n3umjKJl2Ey6TLzikPE+W4cUQ4gCwgGP1RqzTGtgDra0WjIqdy29ohiUKfvmbiL3MndZww58xfIvyFw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.217.0': - resolution: {integrity: sha512-KfLAdt1uilVE+3FxbgVnp2ZrzqbIawzcesnRoi+Kh9ckB5Ld5D8btUgoBvwTbdmuNx1j6b132Wsh72azq+pPNQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-logs-otlp-http@0.57.2': + resolution: {integrity: sha512-0rygmvLcehBRp56NQVLSleJ5ITTduq/QfU7obOkyWgPpFHulwpw2LYTqNIz5TczKZuy5YY+5D3SDnXZL1tXImg==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.217.0': - resolution: {integrity: sha512-Se0GG/ZO24mQTlQj7zprR4pNI0nKe4lPDPBsuJmi6508b9TlZEuUd3EfyuHk6oJxzL7fGyDFYAbxNigQvRP2ZQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-logs-otlp-proto@0.57.2': + resolution: {integrity: sha512-ta0ithCin0F8lu9eOf4lEz9YAScecezCHkMMyDkvd9S7AnZNX5ikUmC5EQOQADU+oCcgo/qkQIaKcZvQ0TYKDw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0': - resolution: {integrity: sha512-0GpJKnCoVaVA1rKBMVPHziznfOQlXgH72S9ktjBAF1AnAVPzX7vVEBGrhwiSxxHDAiefXk+J8znApsMb/K6Z3w==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2': + resolution: {integrity: sha512-r70B8yKR41F0EC443b5CGB4rUaOMm99I5N75QQt6sHKxYDzSEc6gm48Diz1CI1biwa5tDPznpylTrywO/pT7qw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.217.0': - resolution: {integrity: sha512-1zkMzzhiNJdVmLxuwkltqWGw4fOOam47bqRxmuQNjyKJe/9NmY5cIrZ4kiQV7sVGxoOgT0ZvGUfLcjvtpC/b9Q==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-metrics-otlp-http@0.57.2': + resolution: {integrity: sha512-ttb9+4iKw04IMubjm3t0EZsYRNWr3kg44uUuzfo9CaccYlOh8cDooe4QObDUkvx9d5qQUrbEckhrWKfJnKhemA==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.217.0': - resolution: {integrity: sha512-nfxt/KxVGFkjkO/M+58y1ugHu/dwPtxG4eYq0KApcQ7xk5CHzhdn+IuLZfDSvNDrJ3Uy5q++Fj/wbK7i8yryfQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-metrics-otlp-proto@0.57.2': + resolution: {integrity: sha512-HX068Q2eNs38uf7RIkNN9Hl4Ynl+3lP0++KELkXMCpsCbFO03+0XNNZ1SkwxPlP9jrhQahsMPMkzNXpq3fKsnw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -5012,27 +4998,27 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.217.0': - resolution: {integrity: sha512-fPZs2fw7veLH3pEKu8vSepUa2fQpAE2P7al6qU10aH9GrEJJ8YaPgsd5xON7by5rbcEVS71FOU2aWyK6nzB7VQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-trace-otlp-grpc@0.57.2': + resolution: {integrity: sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.217.0': - resolution: {integrity: sha512-38YQoqtYjglz2GV94LGUN/djLvxtvGIQO68o6qAFPVshjmwSdX1F2i0c7vn3lEl1L5B/YqjB/bgKXaVx7KO+RQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-trace-otlp-http@0.57.2': + resolution: {integrity: sha512-sB/gkSYFu+0w2dVQ0PWY9fAMl172PKMZ/JrHkkW8dmjCL0CYkmXeE+ssqIL/yBUTPOvpLIpenX5T9RwXRBW/3g==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.217.0': - resolution: {integrity: sha512-nPV8gKHUiSuTZpQcnZU3/pBlK7crSyEGpZuh5MtWySB0vv6NNG0QvvfKitQt+Fc2Mc6qfyU54KlZcurwoTbrVg==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-trace-otlp-proto@0.57.2': + resolution: {integrity: sha512-awDdNRMIwDvUtoRYxRhja5QYH6+McBLtoz1q9BeEsskhZcrGmH/V1fWpGx8n+Rc+542e8pJA6y+aullbIzQmlw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.7.1': - resolution: {integrity: sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-zipkin@1.30.1': + resolution: {integrity: sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -5072,33 +5058,33 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.217.0': - resolution: {integrity: sha512-24ucQMjz7Y34Kw3trbxL2ZrssbtgWnR+Clpaa+YdeWuuyH3Cvk23Q03PcQvqiZrDvt8AmQmjgg9v6Y9PHoxG7w==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/instrumentation@0.52.1': + resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.52.1': - resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.217.0': - resolution: {integrity: sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/otlp-exporter-base@0.57.2': + resolution: {integrity: sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.217.0': - resolution: {integrity: sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/otlp-grpc-exporter-base@0.57.2': + resolution: {integrity: sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.217.0': - resolution: {integrity: sha512-MKK8UHKFUOGAvbZRWh90MhwHG+Fxm6OROBdjKPCF+HQobjuJ/Kuf8Chs8CR45X1aqotxrMj7OxTdsXe8sXuGVA==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/otlp-transformer@0.57.2': + resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -5108,24 +5094,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-b3@2.7.1': - resolution: {integrity: sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@1.30.1': resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.7.1': - resolution: {integrity: sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/resources@1.30.1': resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} engines: {node: '>=14'} @@ -5138,12 +5112,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.217.0': - resolution: {integrity: sha512-BB+PcHItcZDL63dPMW+mJvwN9rk37wuIDjRxbVlg6pPDvDR/7GL7UJHbGsllgoggOoTimsKgENaWPoGch/oE1A==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-logs@0.57.2': resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} engines: {node: '>=14'} @@ -5162,9 +5130,9 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.217.0': - resolution: {integrity: sha512-K/60pSv42+NQiZKy1pAH18nYDkxltsDV4O3SJ233J0E9raU1ksyL9gsKuS8p30bYBb4AMPCfDuutHQaHYpcv0Q==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/sdk-node@0.57.2': + resolution: {integrity: sha512-8BaeqZyN5sTuPBtAoY+UtKwXBdqyuRKmekN5bFzAO40CgbGzAxfTpiL3PBerT7rhZ7p2nBdq7FaMv/tBQgHE4A==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -5186,12 +5154,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.7.1': - resolution: {integrity: sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-web@2.7.1': resolution: {integrity: sha512-K806OouCSOjMd8Nr7+ZCq3QT22tdAzzS/7h8vprfiKjkgFQ99/dvwU8d12WJANA6D5Qtme65hyBAqAu9CkQuxQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -9125,10 +9087,6 @@ packages: import-in-the-middle@2.0.6: resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} - import-in-the-middle@3.0.1: - resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} - engines: {node: '>=18'} - import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} @@ -11958,8 +11916,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.3: - resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} engines: {node: '>= 0.4'} shimmer@1.2.1: @@ -14761,18 +14719,6 @@ snapshots: '@blazediff/core@1.9.1': {} - '@chromatic-com/storybook@4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - '@neoconfetti/react': 1.0.0 - chromatic: 13.3.4 - filesize: 10.1.6 - jsonfile: 6.2.0 - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - strip-ansi: 7.1.2 - transitivePeerDependencies: - - '@chromatic-com/cypress' - - '@chromatic-com/playwright' - '@chromatic-com/storybook@4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@neoconfetti/react': 1.0.0 @@ -15101,7 +15047,7 @@ snapshots: '@cucumber/gherkin-utils': 11.0.0 '@cucumber/html-formatter': 23.0.0(@cucumber/messages@32.2.0) '@cucumber/junit-xml-formatter': 0.13.3(@cucumber/messages@32.2.0) - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) '@cucumber/messages': 32.2.0 '@cucumber/pretty-formatter': 1.0.1(@cucumber/cucumber@12.8.1)(@cucumber/messages@32.2.0) '@cucumber/tag-expressions': 9.1.0 @@ -15137,7 +15083,7 @@ snapshots: '@cucumber/gherkin-streams@6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0))(@cucumber/messages@32.2.0)': dependencies: '@cucumber/gherkin': 38.0.0 - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) '@cucumber/messages': 32.2.0 commander: 14.0.0 source-map-support: 0.5.21 @@ -15186,9 +15132,9 @@ snapshots: luxon: 3.7.2 xmlbuilder: 15.1.1 - '@cucumber/message-streams@4.1.1(@cucumber/messages@32.3.1)': + '@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0)': dependencies: - '@cucumber/messages': 32.3.1 + '@cucumber/messages': 32.2.0 mime: 3.0.0 '@cucumber/messages@26.0.1': @@ -16285,7 +16231,7 @@ snapshots: listr2: 4.0.5 log-symbols: 4.1.0 micromatch: 4.0.8 - shell-quote: 1.8.3 + shell-quote: 1.8.4 string-env-interpolation: 1.0.1 ts-log: 2.2.7 tslib: 2.8.1 @@ -16808,15 +16754,6 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - glob: 10.5.0 - magic-string: 0.30.21 - react-docgen-typescript: 2.4.0(typescript@6.0.3) - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - optionalDependencies: - typescript: 6.0.3 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: glob: 10.5.0 @@ -16969,10 +16906,6 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs@0.217.0': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs@0.52.1': dependencies: '@opentelemetry/api': 1.9.0 @@ -16983,20 +16916,10 @@ snapshots: '@opentelemetry/api@1.9.0': {} - '@opentelemetry/configuration@0.217.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - yaml: 2.8.3 - '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -17017,66 +16940,66 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus@0.217.0(@opentelemetry/api@1.9.0)': dependencies: @@ -17086,42 +17009,42 @@ snapshots: '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin@2.7.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-zipkin@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 '@opentelemetry/instrumentation-dataloader@0.17.0(@opentelemetry/api@1.9.0)': dependencies: @@ -17176,19 +17099,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - import-in-the-middle: 3.0.1 - require-in-the-middle: 8.0.1 + '@opentelemetry/api-logs': 0.52.1 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + semver: 7.7.4 + shimmer: 1.2.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.52.1 + '@opentelemetry/api-logs': 0.57.2 '@types/shimmer': 1.2.0 import-in-the-middle: 1.15.0 require-in-the-middle: 7.5.2 @@ -17197,29 +17123,29 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-grpc-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) protobufjs: 7.5.8 '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': @@ -17227,21 +17153,11 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3@2.7.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger@2.7.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -17254,14 +17170,6 @@ snapshots: '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/sdk-logs@0.217.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -17281,34 +17189,29 @@ snapshots: '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-node@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - '@opentelemetry/configuration': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/exporter-trace-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 transitivePeerDependencies: - supports-color @@ -17336,13 +17239,6 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) semver: 7.7.4 - '@opentelemetry/sdk-trace-node@2.7.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-web@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -18300,31 +18196,12 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-a11y@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - '@storybook/global': 5.0.0 - axe-core: 4.11.0 - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/addon-a11y@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.0 storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/addon-docs@9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) - '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) - '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ts-dedent: 2.2.0 - transitivePeerDependencies: - - '@types/react' - '@storybook/addon-docs@9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) @@ -18338,10 +18215,6 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@storybook/addon-onboarding@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/addon-onboarding@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18362,22 +18235,6 @@ snapshots: - react - react-dom - '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': - dependencies: - '@storybook/global': 5.0.0 - '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - prompts: 2.4.2 - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ts-dedent: 2.2.0 - optionalDependencies: - '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/runner': 4.1.2 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - transitivePeerDependencies: - - react - - react-dom - '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': dependencies: '@storybook/global': 5.0.0 @@ -18394,13 +18251,6 @@ snapshots: - react - react-dom - '@storybook/builder-vite@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ts-dedent: 2.2.0 - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@storybook/builder-vite@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) @@ -18408,11 +18258,6 @@ snapshots: ts-dedent: 2.2.0 vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@storybook/csf-plugin@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - unplugin: 1.16.1 - '@storybook/csf-plugin@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18425,38 +18270,12 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - '@storybook/react-dom-shim@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/react-dom-shim@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/react-vite@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@rollup/pluginutils': 5.3.0 - '@storybook/builder-vite': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/react': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) - find-up: 7.0.0 - magic-string: 0.30.21 - react: 19.2.0 - react-docgen: 8.0.2 - react-dom: 19.2.0(react@19.2.0) - resolve: 1.22.11 - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - tsconfig-paths: 4.2.0 - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - rollup - - supports-color - - typescript - '@storybook/react-vite@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18477,16 +18296,6 @@ snapshots: - supports-color - typescript - '@storybook/react@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)': - dependencies: - '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - optionalDependencies: - typescript: 6.0.3 - '@storybook/react@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)': dependencies: '@storybook/global': 5.0.0 @@ -18990,19 +18799,6 @@ snapshots: - vite optional: true - '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': - dependencies: - '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - playwright: 1.59.0 - tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': dependencies: '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) @@ -19034,23 +18830,6 @@ snapshots: - vite optional: true - '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': - dependencies: - '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/utils': 4.1.2 - magic-string: 0.30.21 - pngjs: 7.0.0 - sirv: 3.0.2 - tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.1 - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': dependencies: '@blazediff/core': 1.9.1 @@ -19080,7 +18859,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color @@ -19101,14 +18880,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.2.4 @@ -19125,14 +18896,6 @@ snapshots: optionalDependencies: vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@vitest/spy': 4.1.2 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.2 @@ -20250,7 +20013,7 @@ snapshots: dependencies: chalk: 4.1.2 rxjs: 7.8.2 - shell-quote: 1.8.3 + shell-quote: 1.8.4 supports-color: 8.1.1 tree-kill: 1.2.2 yargs: 17.7.2 @@ -21949,13 +21712,6 @@ snapshots: cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 - import-in-the-middle@3.0.1: - dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 2.2.0 - module-details-from-path: 1.0.4 - import-lazy@4.0.0: {} imurmurhash@0.1.4: {} @@ -22456,7 +22212,7 @@ snapshots: launch-editor@2.12.0: dependencies: picocolors: 1.1.1 - shell-quote: 1.8.3 + shell-quote: 1.8.4 less@4.4.2: dependencies: @@ -25256,7 +25012,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.3: {} + shell-quote@1.8.4: {} shimmer@1.2.1: {} @@ -25466,28 +25222,6 @@ snapshots: graphql: 16.12.0 react: 19.2.0 - storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - '@storybook/global': 5.0.0 - '@testing-library/jest-dom': 6.9.1 - '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/spy': 3.2.4 - better-opn: 3.0.2 - esbuild: 0.25.12 - esbuild-register: 3.6.0(esbuild@0.25.12) - recast: 0.23.11 - semver: 7.7.4 - ws: 8.20.1 - transitivePeerDependencies: - - '@testing-library/dom' - - bufferutil - - msw - - supports-color - - utf-8-validate - - vite - storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@storybook/global': 5.0.0 @@ -26245,26 +25979,6 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.10 - rolldown: 1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.10.1 - esbuild: 0.25.12 - fsevents: 2.3.3 - jiti: 2.6.1 - less: 4.4.2 - terser: 5.44.1 - tsx: 4.21.0 - yaml: 2.8.3 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -26315,36 +26029,6 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - tinyrainbow: 3.1.0 - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@types/node': 24.10.1 - '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - jsdom: 26.1.0 - transitivePeerDependencies: - - msw - vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 63e1e511e..5abbacdb1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -97,6 +97,8 @@ overrides: ip-address: ^10.1.1 fast-uri: ^3.1.2 '@babel/plugin-transform-modules-systemjs': 7.29.4 + 'shell-quote@<1.8.4': 1.8.4 + '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 patchedDependencies: '@azure/functions@4.11.0': patches/@azure__functions@4.11.0.patch diff --git a/scripts/local-dev/dev-process-exit.mjs b/scripts/local-dev/dev-process-exit.mjs new file mode 100644 index 000000000..d762c4d5e --- /dev/null +++ b/scripts/local-dev/dev-process-exit.mjs @@ -0,0 +1,40 @@ +/** @typedef {import('node:child_process').ChildProcess} ChildProcess */ + +const INTERRUPT_SIGNALS = new Set(['SIGINT', 'SIGTERM', 'SIGQUIT']); +const INTERRUPT_EXIT_CODES = new Set([130, 143]); + +/** + * @param {NodeJS.Signals | null | undefined} signal + * @returns {boolean} + */ +export const isInterruptSignal = (signal) => Boolean(signal && INTERRUPT_SIGNALS.has(signal)); + +/** + * @param {number | null | undefined} code + * @returns {boolean} + */ +export const isInterruptExitCode = (code) => Number.isInteger(code) && INTERRUPT_EXIT_CODES.has(/** @type {number} */ (code)); + +/** + * @param {NodeJS.Signals | null | undefined} signal + * @param {number | null | undefined} code + * @returns {boolean} + */ +export const isGracefulInterruptExit = (signal, code) => isInterruptSignal(signal) || isInterruptExitCode(code); + +/** + * Wires a spawned dev child process to forward its exit status to the parent, + * treating Turbo's interrupt signals as graceful exits. Every `start-dev.mjs` + * runner ends with the same handler, so this is the single source of truth. + * @param {ChildProcess} child + * @returns {void} + */ +export function forwardChildExit(child) { + child.on('exit', (code, signal) => { + if (isGracefulInterruptExit(signal, code)) { + process.exitCode = 0; + return; + } + process.exitCode = code ?? 1; + }); +} diff --git a/scripts/local-dev/port-ready.mjs b/scripts/local-dev/port-ready.mjs new file mode 100644 index 000000000..a24930f35 --- /dev/null +++ b/scripts/local-dev/port-ready.mjs @@ -0,0 +1,45 @@ +import net from 'node:net'; + +/** + * @typedef {object} WaitForPortOptions + * @property {number} [timeoutMs] + * @property {number} [intervalMs] + */ + +/** + * Resolves true if a TCP listener is accepting connections on the given port + * on 127.0.0.1. Used by the dev launchers to short-circuit when a service is + * already running (Azurite, Mongo) and by the e2e harness to wait for ports + * to come up. + * @param {number} port + * @returns {Promise} + */ +export function isPortListening(port) { + return new Promise((resolve) => { + const socket = net.createConnection({ port, host: '127.0.0.1' }); + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('error', () => { + socket.destroy(); + resolve(false); + }); + }); +} + +/** + * Polls `isPortListening(port)` until it resolves true or the timeout elapses. + * Resolves true on success, false on timeout — callers decide how to react. + * @param {number} port + * @param {WaitForPortOptions} [options] + * @returns {Promise} + */ +export async function waitForPort(port, { timeoutMs = 30_000, intervalMs = 250 } = {}) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (await isPortListening(port)) return true; + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + return false; +} diff --git a/build-pipeline/scripts/portless-hostnames.mjs b/scripts/local-dev/portless-hostnames.mjs similarity index 81% rename from build-pipeline/scripts/portless-hostnames.mjs rename to scripts/local-dev/portless-hostnames.mjs index f5df73258..b2e9deb01 100644 --- a/build-pipeline/scripts/portless-hostnames.mjs +++ b/scripts/local-dev/portless-hostnames.mjs @@ -19,12 +19,27 @@ import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +/** + * @typedef {Record} DotEnvValues + * @typedef {object} PortlessHostnames + * @property {string} uiCommunity + * @property {string} uiStaff + * @property {string} api + * @property {string} mockAuth + * @property {string} docs + */ + const PORTLESS_PORT = 1355; const scriptDir = fileURLToPath(new URL('.', import.meta.url)); const workspaceRoot = resolve(scriptDir, '../..'); +/** + * @param {string} filePath + * @returns {DotEnvValues} + */ function readDotEnv(filePath) { if (!existsSync(filePath)) return {}; + /** @type {DotEnvValues} */ const result = {}; for (const line of readFileSync(filePath, 'utf-8').split('\n')) { const trimmed = line.trim(); @@ -36,6 +51,10 @@ function readDotEnv(filePath) { return result; } +/** + * @param {string} url + * @returns {string | null} + */ function hostnameFrom(url) { try { return new URL(url).hostname; @@ -44,7 +63,12 @@ function hostnameFrom(url) { } } -/** Splice `.` in before `.localhost` in an existing hostname. */ +/** + * Splice `.` in before `.localhost` in an existing hostname. + * @param {string} hostname + * @param {string} worktreeName + * @returns {string} + */ function applyWorktreeSuffix(hostname, worktreeName) { if (!worktreeName) return hostname; return hostname.replace('.localhost', `.${worktreeName}.localhost`); @@ -54,6 +78,7 @@ function applyWorktreeSuffix(hostname, worktreeName) { * Returns all service hostnames scoped to the current worktree (if any). * Hostname shapes are read from the tracked .env files — no names are * hardcoded in this module. + * @returns {PortlessHostnames} */ export function getHostnames() { const uiEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-community/.env')); @@ -80,6 +105,9 @@ export function getHostnames() { /** * Builds a full portless-proxied URL for the given hostname and optional path. + * @param {string} hostname + * @param {string} [path] + * @returns {string} */ export function buildPortlessUrl(hostname, path = '') { return `https://${hostname}:${PORTLESS_PORT}${path}`; diff --git a/build-pipeline/scripts/worktree-ports.mjs b/scripts/local-dev/worktree-ports.mjs similarity index 77% rename from build-pipeline/scripts/worktree-ports.mjs rename to scripts/local-dev/worktree-ports.mjs index 9a86b183c..25d843cbf 100644 --- a/build-pipeline/scripts/worktree-ports.mjs +++ b/scripts/local-dev/worktree-ports.mjs @@ -2,6 +2,11 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +/** + * @typedef {Record} SettingsValues + * @typedef {{ blob: number, queue: number, table: number }} AzuritePorts + */ + /** * Worktree-scoped port computation for service isolation. * @@ -19,17 +24,30 @@ import { fileURLToPath } from 'node:url'; const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); const apiLocalSettingsPaths = [path.join(workspaceRoot, 'apps', 'api', 'deploy', 'local.settings.json'), path.join(workspaceRoot, 'apps', 'api', 'local.settings.json')]; +/** @type {SettingsValues | undefined} */ let apiLocalSettingsValues; -function getSetting(name) { - return process.env[name] ?? getApiLocalSetting(name); +/** + * @param {string} name + * @param {SettingsValues} [values] + * @returns {string | undefined} + */ +function getSetting(name, values) { + return process.env[name] ?? values?.[name] ?? getApiLocalSetting(name); } +/** + * @param {string} name + * @returns {string | undefined} + */ function getApiLocalSetting(name) { apiLocalSettingsValues ??= readApiLocalSettingsValues(); return apiLocalSettingsValues[name]; } +/** + * @returns {SettingsValues} + */ function readApiLocalSettingsValues() { for (const settingsPath of apiLocalSettingsPaths) { if (!fs.existsSync(settingsPath)) continue; @@ -42,6 +60,7 @@ function readApiLocalSettingsValues() { /** * Returns a deterministic port offset in the range [100, 4900] (step 100) * for the current worktree. Returns 0 when WORKTREE_NAME is not set. + * @returns {number} */ export function getWorktreePortOffset() { const name = process.env.WORKTREE_NAME; @@ -51,12 +70,18 @@ export function getWorktreePortOffset() { return ((Math.abs(hash) % 49) + 1) * 100; } -/** MongoDB port for the current worktree. */ +/** + * MongoDB port for the current worktree. + * @returns {number} + */ export function getMongoPort() { return 50000 + getWorktreePortOffset(); } -/** Azurite blob/queue/table ports for the current worktree. */ +/** + * Azurite blob/queue/table ports for the current worktree. + * @returns {AzuritePorts} + */ export function getAzuritePorts() { const offset = getWorktreePortOffset(); return { @@ -69,12 +94,14 @@ export function getAzuritePorts() { /** * Azurite connection string for worktree-specific ports. * Returns `UseDevelopmentStorage=true` for the default worktree (port 10000). + * @param {SettingsValues} [values] + * @returns {string} */ -export function getAzuriteConnectionString() { +export function getAzuriteConnectionString(values) { const ports = getAzuritePorts(); if (ports.blob === 10000) return 'UseDevelopmentStorage=true'; - const accountName = getSetting('STORAGE_ACCOUNT_NAME'); - const accountKey = getSetting('STORAGE_ACCOUNT_KEY'); + const accountName = getSetting('STORAGE_ACCOUNT_NAME', values); + const accountKey = getSetting('STORAGE_ACCOUNT_KEY', values); if (!accountName || !accountKey) { throw new Error('[worktree-ports] STORAGE_ACCOUNT_NAME and STORAGE_ACCOUNT_KEY must be set to build a worktree Azurite connection string'); } @@ -92,6 +119,7 @@ export function getAzuriteConnectionString() { * MongoDB connection string with the worktree-specific port patched in. * Reads COSMOSDB_CONNECTION_STRING from env or local.settings.json and replaces * the host:port segment. + * @returns {string} */ export function getMongoConnectionString() { const base = getSetting('COSMOSDB_CONNECTION_STRING'); diff --git a/sonar-project.properties b/sonar-project.properties index 141305444..0ede0a4b7 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -77,7 +77,7 @@ sonar.test.inclusions=**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/* sonar.exclusions=**/*.config.ts,**/tsconfig.json,**/.storybook/**,**/*.stories.ts,**/*.test.ts,**/*.test.tsx,**/*.generated.ts,**/*.generated.tsx,**/*.d.ts,**/dist/**,**/deploy/**,**/coverage/**,apps/docs/src/test/**,packages/ocom/domain/tests/**,packages/cellix/server-oauth2-mock-seedwork/**,packages/cellix/server-mongodb-memory-mock-seedwork/** # Coverage exclusions -sonar.coverage.exclusions=**/*.config.ts,**/tsconfig.json,**/.storybook/**,**/*.stories.ts,**/*.stories.tsx,**/*.test.ts,**/*.test.tsx,**/*.generated.ts,**/*.generated.tsx,**/*.d.ts,**/dist/**,**/deploy/**,**/coverage/**,apps/docs/src/test/**,build-pipeline/scripts/**,packages/ocom/domain/tests/**,packages/cellix/server-oauth2-mock-seedwork/**,packages/cellix/server-mongodb-memory-mock-seedwork/**,packages/ocom/data-sources-mongoose-models/**,packages/ocom/graphql/src/schema/builder/schema-builder.ts,apps/api/src/index.ts,apps/api/src/service-config/**,packages/cellix/archunit-tests/**,packages/ocom-verification/archunit-tests/**,packages/cellix/ui-core/**,apps/ui-community/**,packages/ocom/ui-shared/src/components/organisms/header/index.tsx +sonar.coverage.exclusions=**/*.config.ts,**/tsconfig.json,**/.storybook/**,**/*.stories.ts,**/*.stories.tsx,**/*.test.ts,**/*.test.tsx,**/*.generated.ts,**/*.generated.tsx,**/*.d.ts,**/dist/**,**/deploy/**,**/coverage/**,apps/docs/src/test/**,build-pipeline/scripts/**,scripts/local-dev/**,packages/ocom/domain/tests/**,packages/cellix/server-oauth2-mock-seedwork/**,packages/cellix/server-mongodb-memory-mock-seedwork/**,packages/ocom/data-sources-mongoose-models/**,packages/ocom/graphql/src/schema/builder/schema-builder.ts,apps/api/src/index.ts,apps/api/src/service-config/**,packages/cellix/archunit-tests/**,packages/ocom-verification/archunit-tests/**,packages/cellix/ui-core/**,apps/ui-community/**,packages/ocom/ui-shared/src/components/organisms/header/index.tsx # CPD (code duplication) exclusions sonar.cpd.exclusions=**/*.test.ts,**/*.generated.ts,**/*.generated.tsx,packages/cellix/archunit-tests/src/test-suites/**,packages/cellix/archunit-tests/src/fixtures/** From 6c415ea3221aa9e8175ab576ee270484de5fcb37 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 26 May 2026 15:56:09 -0400 Subject: [PATCH 05/12] small fixes for build file issue --- .../src/shared/support/servers/app-paths.ts | 10 ++- .../src/shared/support/servers/port-ready.ts | 29 +++++++ .../support/servers/resolve-portless.ts | 6 +- .../support/servers/test-azurite-server.ts | 17 +--- .../shared/support/servers/worktree-ports.ts | 22 +++++ .../src/settings/portless-settings.ts | 82 ++++++++++++++++--- 6 files changed, 132 insertions(+), 34 deletions(-) create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/worktree-ports.ts diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts index b0ebf360f..d12c15632 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts @@ -1,9 +1,11 @@ +import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -const workspaceRootUrl = new URL('../../../../../../../', import.meta.url); +const currentDir = dirname(fileURLToPath(import.meta.url)); +const workspaceRoot = resolve(currentDir, '../../../../../../..'); export const appPaths = { - apiDir: fileURLToPath(new URL('apps/api/', workspaceRootUrl)), - oauth2MockDir: fileURLToPath(new URL('apps/server-oauth2-mock/', workspaceRootUrl)), - uiCommunityDir: fileURLToPath(new URL('apps/ui-community/', workspaceRootUrl)), + apiDir: resolve(workspaceRoot, 'apps/api'), + oauth2MockDir: resolve(workspaceRoot, 'apps/server-oauth2-mock'), + uiCommunityDir: resolve(workspaceRoot, 'apps/ui-community'), } as const; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts new file mode 100644 index 000000000..548862d56 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts @@ -0,0 +1,29 @@ +import net from 'node:net'; + +interface WaitForPortOptions { + timeoutMs?: number; + intervalMs?: number; +} + +export function isPortListening(port: number): Promise { + return new Promise((resolve) => { + const socket = net.createConnection({ port, host: '127.0.0.1' }); + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('error', () => { + socket.destroy(); + resolve(false); + }); + }); +} + +export async function waitForPort(port: number, { timeoutMs = 30_000, intervalMs = 250 }: WaitForPortOptions = {}): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (await isPortListening(port)) return true; + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + return false; +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/resolve-portless.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/resolve-portless.ts index 8c5cb9333..42c7f7827 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/resolve-portless.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/resolve-portless.ts @@ -1,14 +1,14 @@ import { existsSync } from 'node:fs'; -import { resolve } from 'node:path'; +import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -const currentDir = fileURLToPath(new URL('.', import.meta.url)); +const currentDir = dirname(fileURLToPath(import.meta.url)); +const workspaceRoot = resolve(currentDir, '../../../../../../..'); let resolvedPath: string | undefined; export function getPortlessPath(): string { if (!resolvedPath) { - const workspaceRoot = resolve(currentDir, '../../../../../../..'); const localBin = resolve(workspaceRoot, 'node_modules/.bin/portless'); if (existsSync(localBin)) { resolvedPath = localBin; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts index 47558526a..2c8e92b0b 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts @@ -4,21 +4,8 @@ import type { TestServer } from '@ocom-verification/verification-shared/servers' import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { appPaths } from './app-paths.ts'; import { spawnEnv } from './child-process-env.ts'; - -interface PortReadyModule { - isPortListening(port: number): Promise; - waitForPort(port: number, options?: { timeoutMs?: number; intervalMs?: number }): Promise; -} - -interface WorktreePortsModule { - getAzuritePorts(): { blob: number; queue: number; table: number }; -} - -const portReadyModuleUrl = new URL('../../../../../../../scripts/local-dev/port-ready.mjs', import.meta.url).href; -const worktreePortsModuleUrl = new URL('../../../../../../../scripts/local-dev/worktree-ports.mjs', import.meta.url).href; - -const { isPortListening, waitForPort } = (await import(portReadyModuleUrl)) as PortReadyModule; -const { getAzuritePorts } = (await import(worktreePortsModuleUrl)) as WorktreePortsModule; +import { isPortListening, waitForPort } from './port-ready.ts'; +import { getAzuritePorts } from './worktree-ports.ts'; /** * Starts Azurite via apps/api/start-azurite.mjs. The script itself short-circuits diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/worktree-ports.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/worktree-ports.ts new file mode 100644 index 000000000..29f367936 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/worktree-ports.ts @@ -0,0 +1,22 @@ +interface AzuritePorts { + blob: number; + queue: number; + table: number; +} + +export function getWorktreePortOffset(): number { + const name = process.env['WORKTREE_NAME']; + if (!name) return 0; + let hash = 0; + for (const c of name) hash = ((hash << 5) - hash + c.charCodeAt(0)) | 0; + return ((Math.abs(hash) % 49) + 1) * 100; +} + +export function getAzuritePorts(): AzuritePorts { + const offset = getWorktreePortOffset(); + return { + blob: 10000 + offset, + queue: 10001 + offset, + table: 10002 + offset, + }; +} diff --git a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts index a77b96e19..892c540d8 100644 --- a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts @@ -1,9 +1,7 @@ -/** - * Runtime bridge to the canonical portless hostname helpers used by both the - * dev:worktree scripts and the E2E test harness. Keeping the .mjs file as - * the single source of truth means there is exactly one place that derives - * hostnames from .env and applies the WORKTREE_NAME suffix. - */ +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + interface PortlessHostnames { uiCommunity: string; uiStaff: string; @@ -12,14 +10,74 @@ interface PortlessHostnames { docs: string; } -interface PortlessHostnamesModule { - PORTLESS_PORT: number; - getHostnames(): PortlessHostnames; - buildPortlessUrl(hostname: string, path?: string): string; +type DotEnvValues = Record; + +const PORTLESS_PORT = 1355; +const currentDir = dirname(fileURLToPath(import.meta.url)); +const workspaceRoot = resolve(currentDir, '../../../../..'); +const uiCommunityEnvPath = resolve(workspaceRoot, 'apps/ui-community/.env'); +const uiStaffEnvPath = resolve(workspaceRoot, 'apps/ui-staff/.env'); + +function buildPortlessUrl(hostname: string, path = ''): string { + return `https://${hostname}:${PORTLESS_PORT}${path}`; +} + +function getHostnames(): PortlessHostnames { + const uiCommunityEnv = readDotEnv(uiCommunityEnvPath); + const uiStaffEnv = readDotEnv(uiStaffEnvPath); + + const hostnames = { + uiCommunity: requireHostname(uiCommunityEnv, 'VITE_APP_UI_COMMUNITY_BASE_URL', uiCommunityEnvPath), + uiStaff: requireHostname(uiStaffEnv, 'VITE_APP_UI_STAFF_AAD_REDIRECT_URI', uiStaffEnvPath), + api: requireHostname(uiCommunityEnv, 'VITE_COMMON_API_ENDPOINT', uiCommunityEnvPath), + mockAuth: requireHostname(uiCommunityEnv, 'VITE_APP_UI_COMMUNITY_B2C_AUTHORITY', uiCommunityEnvPath), + }; + + return applyWorktreeSuffixes(hostnames, process.env['WORKTREE_NAME'] ?? ''); } -const portlessHostnamesModuleUrl = new URL('../../../../../scripts/local-dev/portless-hostnames.mjs', import.meta.url).href; +function readDotEnv(filePath: string): DotEnvValues { + if (!existsSync(filePath)) return {}; + const result: DotEnvValues = {}; + for (const line of readFileSync(filePath, 'utf-8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) continue; + result[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); + } + return result; +} -const { buildPortlessUrl, getHostnames, PORTLESS_PORT } = (await import(portlessHostnamesModuleUrl)) as PortlessHostnamesModule; +function hostnameFrom(url: string): string | null { + try { + return new URL(url).hostname; + } catch { + return null; + } +} + +function requireHostname(values: DotEnvValues, key: string, filePath: string): string { + const hostname = hostnameFrom(values[key] ?? ''); + if (!hostname) { + throw new Error(`portless-settings: could not derive hostname from ${key} in ${filePath}`); + } + return hostname; +} + +function applyWorktreeSuffixes(hostnames: Omit, worktreeName: string): PortlessHostnames { + return { + uiCommunity: applyWorktreeSuffix(hostnames.uiCommunity, worktreeName), + uiStaff: applyWorktreeSuffix(hostnames.uiStaff, worktreeName), + api: applyWorktreeSuffix(hostnames.api, worktreeName), + mockAuth: applyWorktreeSuffix(hostnames.mockAuth, worktreeName), + docs: applyWorktreeSuffix(`docs.${hostnames.uiCommunity}`, worktreeName), + }; +} + +function applyWorktreeSuffix(hostname: string, worktreeName: string): string { + if (!worktreeName) return hostname; + return hostname.replace('.localhost', `.${worktreeName}.localhost`); +} export { buildPortlessUrl, getHostnames, PORTLESS_PORT }; From 395bfcd4a6441536226dd1a18a2a5d011d4aaf31 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 27 May 2026 15:01:04 -0400 Subject: [PATCH 06/12] small improvements based on my comments, simplified overcomplicated logic --- apps/api/start-azurite.mjs | 17 +--- .../start-mongo.mjs | 6 -- apps/ui-community/start-dev.mjs | 2 +- apps/ui-staff/start-dev.mjs | 2 +- package.json | 1 + .../archunit-tests/src/validate-env-names.cjs | 11 --- .../support/servers/child-process-env.ts | 14 +--- .../src/shared/support/servers/port-ready.ts | 29 ------- .../shared/support/servers/portless-server.ts | 77 ++----------------- .../shared/support/servers/test-api-server.ts | 22 ------ .../support/servers/test-azurite-server.ts | 60 +++++++++++---- .../servers/test-community-vite-server.ts | 5 +- .../support/servers/test-environment.ts | 21 +++++ .../support/servers/test-oauth2-server.ts | 5 +- .../shared/support/shared-infrastructure.ts | 17 ---- .../ocom-verification/e2e-tests/turbo.json | 4 - .../src/settings/portless-settings.ts | 43 +++-------- scripts/local-dev/dev-process-exit.mjs | 17 +--- scripts/local-dev/port-ready.mjs | 45 ----------- scripts/local-dev/portless-hostnames.mjs | 45 ++++------- 20 files changed, 106 insertions(+), 337 deletions(-) delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts delete mode 100644 scripts/local-dev/port-ready.mjs diff --git a/apps/api/start-azurite.mjs b/apps/api/start-azurite.mjs index cc253db13..eb2f6146b 100644 --- a/apps/api/start-azurite.mjs +++ b/apps/api/start-azurite.mjs @@ -1,17 +1,11 @@ import { spawn } from 'node:child_process'; import { isGracefulInterruptExit } from '../../scripts/local-dev/dev-process-exit.mjs'; -import { isPortListening, waitForPort } from '../../scripts/local-dev/port-ready.mjs'; import { getAzuritePorts } from '../../scripts/local-dev/worktree-ports.mjs'; const ports = getAzuritePorts(); const worktreeName = process.env.WORKTREE_NAME ?? ''; const storageSuffix = worktreeName ? `-${worktreeName}` : ''; -if (await isPortListening(ports.blob)) { - console.log(`[azurite] ready (blob port ${ports.blob})`); - process.exit(0); -} - const blobDir = `../../__blobstorage__${storageSuffix}`; const queueDir = `../../__queuestorage__${storageSuffix}`; const tableDir = `../../__tablestorage__${storageSuffix}`; @@ -22,6 +16,8 @@ const procs = [ spawn('azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', tableDir], { stdio: 'inherit' }), ]; +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) => { @@ -34,15 +30,6 @@ for (const proc of procs) { process.exit(code ?? 1); }); } - -if (await waitForPort(ports.blob, { timeoutMs: 30_000 })) { - console.log(`[azurite] ready (blob port ${ports.blob})`); -} else { - console.error(`[azurite] blob port ${ports.blob} did not become ready within 30000ms`); - for (const p of procs) p.kill(); - process.exit(1); -} - process.on('SIGINT', () => { for (const p of procs) p.kill('SIGINT'); }); diff --git a/apps/server-mongodb-memory-mock/start-mongo.mjs b/apps/server-mongodb-memory-mock/start-mongo.mjs index 8d15a147e..30aa60095 100644 --- a/apps/server-mongodb-memory-mock/start-mongo.mjs +++ b/apps/server-mongodb-memory-mock/start-mongo.mjs @@ -1,13 +1,7 @@ -import { isPortListening } from '../../scripts/local-dev/port-ready.mjs'; import { getMongoPort } from '../../scripts/local-dev/worktree-ports.mjs'; const MONGO_PORT = getMongoPort(); -if (await isPortListening(MONGO_PORT)) { - console.log(`[mongo-mock] already running on port ${MONGO_PORT}, skipping`); - process.exit(0); -} - // Not running — start it via tsx with the worktree-scoped port const { default: { spawn }, diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs index 62c79117f..7767c64f9 100644 --- a/apps/ui-community/start-dev.mjs +++ b/apps/ui-community/start-dev.mjs @@ -14,7 +14,7 @@ if (process.env.WORKTREE_NAME) { } const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; -const viteMode = process.env.VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); +const viteMode = process.env.E2E_VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); if (viteMode) { viteArgs.push('--mode', viteMode); } diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs index 0ef5c7673..8232af298 100644 --- a/apps/ui-staff/start-dev.mjs +++ b/apps/ui-staff/start-dev.mjs @@ -12,7 +12,7 @@ if (process.env.WORKTREE_NAME) { } const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; -const viteMode = process.env.VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); +const viteMode = process.env.E2E_VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); if (viteMode) { viteArgs.push('--mode', viteMode); } diff --git a/package.json b/package.json index 46e1f3dd3..e1fabbbbd 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", "test:e2e": "turbo run test:e2e --filter=@ocom-verification/e2e-tests", "test:e2e:worktree": "WORKTREE_NAME=$(basename $PWD) turbo run test:e2e --filter=@ocom-verification/e2e-tests", + "test:e2e:workspace": "WORKTREE_NAME=$(basename $PWD) turbo run test:e2e --filter=@ocom-verification/e2e-tests", "test:acceptance": "turbo run test:acceptance --filter=@ocom-verification/acceptance-api --filter=@ocom-verification/acceptance-ui", "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", "test:integration": "turbo run test:integration", diff --git a/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs b/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs index 2097f480c..827e78164 100644 --- a/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs +++ b/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs @@ -8,7 +8,6 @@ const SKIP_DIRS = new Set(['node_modules', '.turbo', 'build-artifacts', 'dist', // Single source of truth for known portal identifiers (ADR-0031). // Keep this list in sync with apps/docs/docs/portals/PORTAL_REGISTRY.md. const CANONICAL_PORTALS = ['UI_COMMUNITY', 'UI_STAFF']; -const VITE_RESERVED_VARS = new Set(['VITE_MODE']); function walkDir(dir, fileList = []) { const files = fs.readdirSync(dir, { withFileTypes: true }); @@ -320,16 +319,6 @@ function validateEnvNames(options = {}) { for (const m of matches) { const variable = m.match; const relPath = `${path.relative(rootDir, filePath)}:${getLineNumber(newlineOffsets, m.index)}`; - if (VITE_RESERVED_VARS.has(variable)) { - results.push({ - variable, - status: 'compliant', - portal: 'BUILD', - ownerGroup: 'ocm-platform', - location: relPath, - }); - continue; - } if (!variable.startsWith('VITE_APP_') && !variable.startsWith('VITE_COMMON_')) { results.push({ variable, diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts index 51e02949c..1ce570cd1 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts @@ -1,14 +1,4 @@ -/** - * Returns a shallow copy of `process.env` with `NODE_OPTIONS` removed. - * - * Cucumber runs with `NODE_OPTIONS='--import tsx/esm'` so that TypeScript - * source is executed directly. Child processes spawned by the test harness - * (Azurite, portless, func) are plain JavaScript and do not have `tsx` on - * their resolution path, so inheriting `NODE_OPTIONS` causes an immediate - * crash. Callers can spread additional overrides on top of the result. - */ export function spawnEnv(overrides: Record = {}): NodeJS.ProcessEnv { - const env = { ...process.env, ...overrides }; - delete env['NODE_OPTIONS']; - return env; + const { NODE_OPTIONS: _ignored, ...baseEnv } = process.env; + return { ...baseEnv, ...overrides }; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts deleted file mode 100644 index 548862d56..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts +++ /dev/null @@ -1,29 +0,0 @@ -import net from 'node:net'; - -interface WaitForPortOptions { - timeoutMs?: number; - intervalMs?: number; -} - -export function isPortListening(port: number): Promise { - return new Promise((resolve) => { - const socket = net.createConnection({ port, host: '127.0.0.1' }); - socket.once('connect', () => { - socket.destroy(); - resolve(true); - }); - socket.once('error', () => { - socket.destroy(); - resolve(false); - }); - }); -} - -export async function waitForPort(port: number, { timeoutMs = 30_000, intervalMs = 250 }: WaitForPortOptions = {}): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (await isPortListening(port)) return true; - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } - return false; -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index a4dcfaa4b..c5e8aff5f 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -16,9 +16,7 @@ import { getPortlessPath } from './resolve-portless.ts'; export abstract class PortlessServer implements TestServer { private process: ChildProcess | null = null; private startedByUs = false; - private readonly useDetachedProcessGroup = process.platform !== 'win32'; - protected abstract get probeUrl(): string; protected abstract get readyMarker(): string; protected abstract get serverName(): string; protected abstract get spawnArgs(): string[]; @@ -28,51 +26,25 @@ export abstract class PortlessServer implements TestServer { return getPortlessPath(); } - protected get probeRequestInit(): RequestInit { - return {}; - } - protected get extraEnv(): Record { return {}; } - protected isProbeHealthy(response: Response): boolean | Promise { - return response.ok; - } - protected get startupTimeoutMs(): number { return getTimeout('serverStartup'); } abstract getUrl(): string; - /** - * Check if server is already running (via health probe). - */ - async isAlreadyRunning(): Promise { - try { - const controller = new AbortController(); - const probeTimeout = getTimeout('healthProbe'); - const timeout = setTimeout(() => controller.abort(), probeTimeout); - const res = await fetch(this.probeUrl, { ...this.probeRequestInit, signal: controller.signal }); - clearTimeout(timeout); - return await this.isProbeHealthy(res); - } catch { - return false; - } - } - /** * Start the server subprocess and wait for it to be ready. */ async start(): Promise { if (this.process || this.startedByUs) return; - if (await this.isAlreadyRunning()) return; this.process = spawn(this.executable, this.spawnArgs, { cwd: this.cwd, env: spawnEnv(this.extraEnv), - detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], }); this.startedByUs = true; @@ -90,15 +62,12 @@ export abstract class PortlessServer implements TestServer { this.process = null; this.startedByUs = false; - // SIGINT lets portless run its cleanup branch — deregister the hostname from - // ~/.portless/routes.json before exiting. Fall back to SIGKILL after the - // shutdown timeout for anything that ignores SIGINT. - this.killProcess(proc, 'SIGINT'); + proc.kill('SIGINT'); const shutdownTimeout = getTimeout('serverShutdown'); await new Promise((resolve) => { const timeout = setTimeout(() => { - this.killProcess(proc, 'SIGKILL'); + proc.kill('SIGKILL'); resolve(); }, shutdownTimeout); @@ -129,26 +98,12 @@ export abstract class PortlessServer implements TestServer { let stderrOutput = ''; let ready = false; - const resolveWhenReachable = () => { - if (ready) return; - ready = true; - - this.waitForProbeReady() - .then(() => { - clearTimeout(timeout); - resolve(); - }) - .catch((error: unknown) => { - clearTimeout(timeout); - reject(error); - }); - }; - - // stdout listener detects the readyMarker then waits for the probe to respond proc.stdout?.on('data', (data: Buffer) => { const text = data.toString(); - if (text.includes(this.readyMarker)) { - resolveWhenReachable(); + if (!ready && text.includes(this.readyMarker)) { + ready = true; + clearTimeout(timeout); + resolve(); } }); @@ -163,6 +118,7 @@ export abstract class PortlessServer implements TestServer { }); proc.on('exit', (code, signal) => { + if (ready) return; clearTimeout(timeout); this.process = null; this.startedByUs = false; @@ -170,23 +126,4 @@ export abstract class PortlessServer implements TestServer { }); }); } - - private async waitForProbeReady(): Promise { - const probeInterval = getTimeout('healthProbeInterval'); - while (!(await this.isAlreadyRunning())) { - await new Promise((resolve) => setTimeout(resolve, probeInterval)); - } - } - - private killProcess(proc: ChildProcess, signal: NodeJS.Signals): void { - if (this.useDetachedProcessGroup && proc.pid) { - try { - process.kill(-proc.pid, signal); - return; - } catch { - /* Fall back to killing the direct child below. */ - } - } - proc.kill(signal); - } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index 3a45a7176..601df0683 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -10,28 +10,6 @@ const hostnames = getHostnames(); * from the committed e2e local settings plus runtime-only test values. */ export class TestApiServer extends PortlessServer { - protected get probeUrl() { - return buildUrl(hostnames.api, '/api/graphql'); - } - - protected override get probeRequestInit(): RequestInit { - return { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query: '{ __typename }' }), - }; - } - - protected override async isProbeHealthy(response: Response): Promise { - if (!response.ok) return false; - try { - const data = (await response.json()) as { data?: { __typename?: string } }; - return data?.data?.__typename === 'Query'; - } catch { - return false; - } - } - protected get readyMarker() { return 'Functions:'; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts index 2c8e92b0b..9f3c8f28b 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts @@ -4,13 +4,12 @@ import type { TestServer } from '@ocom-verification/verification-shared/servers' import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { appPaths } from './app-paths.ts'; import { spawnEnv } from './child-process-env.ts'; -import { isPortListening, waitForPort } from './port-ready.ts'; import { getAzuritePorts } from './worktree-ports.ts'; /** - * Starts Azurite via apps/api/start-azurite.mjs. The script itself short-circuits - * if the blob port is already listening, so concurrent worktrees and re-runs are - * safe. We track the spawned process only when we started it ourselves. + * Starts Azurite via apps/api/start-azurite.mjs. + * If ports are already bound (EADDRINUSE), we treat that as an existing + * reusable instance for this worktree. */ export class TestAzuriteServer implements TestServer { private process: ChildProcess | null = null; @@ -23,19 +22,19 @@ export class TestAzuriteServer implements TestServer { async start(): Promise { if (this.process || this.startedByUs) return; - if (await isPortListening(this.blobPort)) return; const binDir = join(appPaths.apiDir, 'node_modules', '.bin'); + const { PATH: pathValue = '' } = process.env; this.process = spawn('node', ['start-azurite.mjs'], { cwd: appPaths.apiDir, - env: spawnEnv({ PATH: `${binDir}:${process.env['PATH'] ?? ''}` }), + env: spawnEnv({ PATH: `${binDir}:${pathValue}` }), detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], }); this.startedByUs = true; - await this.waitForReady(); + await this.waitForStartedMarker(); } async stop(): Promise { @@ -68,14 +67,47 @@ export class TestAzuriteServer implements TestServer { return `http://127.0.0.1:${this.blobPort}`; } - private async waitForReady(): Promise { - const ready = await waitForPort(this.blobPort, { - timeoutMs: getTimeout('serverStartup'), - intervalMs: getTimeout('healthProbeInterval'), + private waitForStartedMarker(): Promise { + return new Promise((resolve, reject) => { + const proc = this.process; + if (!proc) { + reject(new Error('TestAzuriteServer process not started')); + return; + } + + const timeout = setTimeout(() => { + reject(new Error(`TestAzuriteServer did not emit start marker within ${getTimeout('serverStartup')}ms`)); + }, getTimeout('serverStartup')); + + let stderrOutput = ''; + + proc.stdout?.on('data', (data: Buffer) => { + if (data.toString().includes('[azurite] started')) { + clearTimeout(timeout); + resolve(); + } + }); + + proc.stderr?.on('data', (data: Buffer) => { + stderrOutput += data.toString(); + }); + + proc.on('error', (error: Error) => { + clearTimeout(timeout); + reject(new Error(`TestAzuriteServer failed to start: ${error.message}`)); + }); + + proc.on('exit', (code, signal) => { + clearTimeout(timeout); + if (stderrOutput.includes('EADDRINUSE')) { + this.process = null; + this.startedByUs = false; + resolve(); + return; + } + reject(new Error(`TestAzuriteServer exited unexpectedly (code: ${code}, signal: ${signal}). stderr: ${stderrOutput.slice(-2000)}`)); + }); }); - if (!ready) { - throw new Error(`TestAzuriteServer: blob port ${this.blobPort} did not start within timeout`); - } } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts index 2251dba3c..c6a26237a 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts @@ -12,9 +12,6 @@ const hostnames = getHostnames(); * are needed here beyond suppressing browser auto-launch. */ export class TestCommunityViteServer extends PortlessServer { - protected get probeUrl() { - return buildUrl(hostnames.uiCommunity); - } protected get readyMarker() { return 'ready in'; } @@ -31,7 +28,7 @@ export class TestCommunityViteServer extends PortlessServer { return { BROWSER: 'none', NODE_ENV: 'development', - VITE_MODE: 'e2e', + E2E_VITE_MODE: 'e2e', }; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index d9fa97a66..067b085c9 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -1,10 +1,15 @@ import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { buildPortlessUrl, getHostnames } from '@ocom-verification/verification-shared/settings'; import { getPortlessPath } from './resolve-portless.ts'; let proxyInitialized = false; let mongoConnectionString: string | undefined; +loadE2EEnvDefaults(); + /** Module-level hostnames derived from .env files (matches dev:worktree pattern). */ const hostnames = getHostnames(); @@ -47,3 +52,19 @@ export function cleanupTestEnvironment(): void { proxyInitialized = false; mongoConnectionString = undefined; } + +function loadE2EEnvDefaults(): void { + const currentDir = dirname(fileURLToPath(import.meta.url)); + const workspaceRoot = resolve(currentDir, '../../../../../../..'); + for (const filePath of [resolve(workspaceRoot, 'apps/ui-community/.env.e2e'), resolve(workspaceRoot, 'apps/ui-staff/.env.e2e')]) { + if (!existsSync(filePath)) continue; + for (const line of readFileSync(filePath, 'utf-8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const idx = trimmed.indexOf('='); + if (idx === -1) continue; + const key = trimmed.slice(0, idx); + process.env[key] ??= trimmed.slice(idx + 1); + } + } +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts index d21f0e090..f0c1b65c7 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts @@ -1,6 +1,6 @@ import { appPaths } from './app-paths.ts'; import { PortlessServer } from './portless-server.ts'; -import { getHostnames, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; +import { getHostnames, mockOidcIssuer } from './test-environment.ts'; const hostnames = getHostnames(); @@ -11,9 +11,6 @@ const hostnames = getHostnames(); * by programmatic token generation — this tests the real OIDC redirect flow. */ export class TestOAuth2Server extends PortlessServer { - protected get probeUrl() { - return mockOidcEndpoint; - } protected get readyMarker() { return 'Registered OIDC config'; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index 7a793aa96..e42eb0d26 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -14,22 +14,6 @@ let browserBaseUrl: string | undefined; let authenticatedBrowserContext: BrowserContext | undefined; let browseTheWeb: BrowseTheWeb | undefined; -let shutdownHandlersRegistered = false; - -function registerShutdownHandlers(): void { - if (shutdownHandlersRegistered) return; - shutdownHandlersRegistered = true; - - const shutdown = (signal: string) => { - void stopAll().finally(() => { - process.exit(signal === 'SIGINT' ? 130 : 143); - }); - }; - - process.once('SIGINT', () => shutdown('SIGINT')); - process.once('SIGTERM', () => shutdown('SIGTERM')); -} - export interface InfrastructureState { apiUrl: string | undefined; browseTheWeb: BrowseTheWeb | undefined; @@ -77,7 +61,6 @@ export async function stopAll(): Promise { } export async function ensureE2EServers(): Promise { - registerShutdownHandlers(); initTestEnvironment(); // Phase 1: Start MongoDB, Azurite, and OAuth2 in parallel (no interdependency) diff --git a/packages/ocom-verification/e2e-tests/turbo.json b/packages/ocom-verification/e2e-tests/turbo.json index a5adb33a1..f677b6e40 100644 --- a/packages/ocom-verification/e2e-tests/turbo.json +++ b/packages/ocom-verification/e2e-tests/turbo.json @@ -8,9 +8,7 @@ "config/**", "$TURBO_ROOT$/apps/api/local-settings.e2e.json", "$TURBO_ROOT$/apps/api/scripts/sync-local-settings.mjs", - "$TURBO_ROOT$/apps/ui-community/.env", "$TURBO_ROOT$/apps/ui-community/.env.e2e", - "$TURBO_ROOT$/apps/ui-staff/.env", "$TURBO_ROOT$/apps/ui-staff/.env.e2e", "$TURBO_ROOT$/scripts/local-dev/**", "cucumber.js", @@ -25,9 +23,7 @@ "config/**", "$TURBO_ROOT$/apps/api/local-settings.e2e.json", "$TURBO_ROOT$/apps/api/scripts/sync-local-settings.mjs", - "$TURBO_ROOT$/apps/ui-community/.env", "$TURBO_ROOT$/apps/ui-community/.env.e2e", - "$TURBO_ROOT$/apps/ui-staff/.env", "$TURBO_ROOT$/apps/ui-staff/.env.e2e", "$TURBO_ROOT$/scripts/local-dev/**", "cucumber.js", diff --git a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts index 892c540d8..9415b0a64 100644 --- a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts @@ -1,7 +1,3 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - interface PortlessHostnames { uiCommunity: string; uiStaff: string; @@ -10,43 +6,22 @@ interface PortlessHostnames { docs: string; } -type DotEnvValues = Record; - const PORTLESS_PORT = 1355; -const currentDir = dirname(fileURLToPath(import.meta.url)); -const workspaceRoot = resolve(currentDir, '../../../../..'); -const uiCommunityEnvPath = resolve(workspaceRoot, 'apps/ui-community/.env'); -const uiStaffEnvPath = resolve(workspaceRoot, 'apps/ui-staff/.env'); function buildPortlessUrl(hostname: string, path = ''): string { return `https://${hostname}:${PORTLESS_PORT}${path}`; } function getHostnames(): PortlessHostnames { - const uiCommunityEnv = readDotEnv(uiCommunityEnvPath); - const uiStaffEnv = readDotEnv(uiStaffEnvPath); - const hostnames = { - uiCommunity: requireHostname(uiCommunityEnv, 'VITE_APP_UI_COMMUNITY_BASE_URL', uiCommunityEnvPath), - uiStaff: requireHostname(uiStaffEnv, 'VITE_APP_UI_STAFF_AAD_REDIRECT_URI', uiStaffEnvPath), - api: requireHostname(uiCommunityEnv, 'VITE_COMMON_API_ENDPOINT', uiCommunityEnvPath), - mockAuth: requireHostname(uiCommunityEnv, 'VITE_APP_UI_COMMUNITY_B2C_AUTHORITY', uiCommunityEnvPath), + uiCommunity: requireHostname('VITE_APP_UI_COMMUNITY_BASE_URL'), + uiStaff: requireHostname('VITE_APP_UI_STAFF_AAD_REDIRECT_URI'), + api: requireHostname('VITE_COMMON_API_ENDPOINT'), + mockAuth: requireHostname('VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'), }; + const { WORKTREE_NAME: worktreeName = '' } = process.env; - return applyWorktreeSuffixes(hostnames, process.env['WORKTREE_NAME'] ?? ''); -} - -function readDotEnv(filePath: string): DotEnvValues { - if (!existsSync(filePath)) return {}; - const result: DotEnvValues = {}; - for (const line of readFileSync(filePath, 'utf-8').split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const eqIdx = trimmed.indexOf('='); - if (eqIdx === -1) continue; - result[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); - } - return result; + return applyWorktreeSuffixes(hostnames, worktreeName); } function hostnameFrom(url: string): string | null { @@ -57,10 +32,10 @@ function hostnameFrom(url: string): string | null { } } -function requireHostname(values: DotEnvValues, key: string, filePath: string): string { - const hostname = hostnameFrom(values[key] ?? ''); +function requireHostname(key: string): string { + const hostname = hostnameFrom(process.env[key] ?? ''); if (!hostname) { - throw new Error(`portless-settings: could not derive hostname from ${key} in ${filePath}`); + throw new Error(`portless-settings: required env var ${key} is missing or invalid`); } return hostname; } diff --git a/scripts/local-dev/dev-process-exit.mjs b/scripts/local-dev/dev-process-exit.mjs index d762c4d5e..ed0232915 100644 --- a/scripts/local-dev/dev-process-exit.mjs +++ b/scripts/local-dev/dev-process-exit.mjs @@ -1,26 +1,11 @@ /** @typedef {import('node:child_process').ChildProcess} ChildProcess */ -const INTERRUPT_SIGNALS = new Set(['SIGINT', 'SIGTERM', 'SIGQUIT']); -const INTERRUPT_EXIT_CODES = new Set([130, 143]); - -/** - * @param {NodeJS.Signals | null | undefined} signal - * @returns {boolean} - */ -export const isInterruptSignal = (signal) => Boolean(signal && INTERRUPT_SIGNALS.has(signal)); - -/** - * @param {number | null | undefined} code - * @returns {boolean} - */ -export const isInterruptExitCode = (code) => Number.isInteger(code) && INTERRUPT_EXIT_CODES.has(/** @type {number} */ (code)); - /** * @param {NodeJS.Signals | null | undefined} signal * @param {number | null | undefined} code * @returns {boolean} */ -export const isGracefulInterruptExit = (signal, code) => isInterruptSignal(signal) || isInterruptExitCode(code); +export const isGracefulInterruptExit = (signal, code) => signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGQUIT' || code === 130 || code === 143; /** * Wires a spawned dev child process to forward its exit status to the parent, diff --git a/scripts/local-dev/port-ready.mjs b/scripts/local-dev/port-ready.mjs deleted file mode 100644 index a24930f35..000000000 --- a/scripts/local-dev/port-ready.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import net from 'node:net'; - -/** - * @typedef {object} WaitForPortOptions - * @property {number} [timeoutMs] - * @property {number} [intervalMs] - */ - -/** - * Resolves true if a TCP listener is accepting connections on the given port - * on 127.0.0.1. Used by the dev launchers to short-circuit when a service is - * already running (Azurite, Mongo) and by the e2e harness to wait for ports - * to come up. - * @param {number} port - * @returns {Promise} - */ -export function isPortListening(port) { - return new Promise((resolve) => { - const socket = net.createConnection({ port, host: '127.0.0.1' }); - socket.once('connect', () => { - socket.destroy(); - resolve(true); - }); - socket.once('error', () => { - socket.destroy(); - resolve(false); - }); - }); -} - -/** - * Polls `isPortListening(port)` until it resolves true or the timeout elapses. - * Resolves true on success, false on timeout — callers decide how to react. - * @param {number} port - * @param {WaitForPortOptions} [options] - * @returns {Promise} - */ -export async function waitForPort(port, { timeoutMs = 30_000, intervalMs = 250 } = {}) { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (await isPortListening(port)) return true; - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } - return false; -} diff --git a/scripts/local-dev/portless-hostnames.mjs b/scripts/local-dev/portless-hostnames.mjs index b2e9deb01..976c82e92 100644 --- a/scripts/local-dev/portless-hostnames.mjs +++ b/scripts/local-dev/portless-hostnames.mjs @@ -1,20 +1,3 @@ -/** - * Shared portless hostname computation for git worktree isolation. - * - * Hostnames are derived from the tracked .env files, so this module contains - * no hardcoded service names. When `WORKTREE_NAME` is set the worktree suffix - * is spliced in before `.localhost`, giving each worktree its own subdomain - * on the shared proxy port. - * - * Default (no worktree): - * ownercommunity.localhost (read from VITE_APP_UI_COMMUNITY_BASE_URL) - * data-access.ownercommunity.localhost (read from VITE_COMMON_API_ENDPOINT) - * - * With WORKTREE_NAME=feature-a: - * ownercommunity.feature-a.localhost - * data-access.ownercommunity.feature-a.localhost - */ - import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -64,42 +47,40 @@ function hostnameFrom(url) { } /** - * Splice `.` in before `.localhost` in an existing hostname. - * @param {string} hostname - * @param {string} worktreeName - * @returns {string} + * @param {string} key + * @param {DotEnvValues} values + * @returns {string | null} */ +function hostnameFor(key, values) { + return hostnameFrom(process.env[key] ?? values[key] ?? ''); +} + function applyWorktreeSuffix(hostname, worktreeName) { if (!worktreeName) return hostname; return hostname.replace('.localhost', `.${worktreeName}.localhost`); } -/** - * Returns all service hostnames scoped to the current worktree (if any). - * Hostname shapes are read from the tracked .env files — no names are - * hardcoded in this module. - * @returns {PortlessHostnames} - */ export function getHostnames() { const uiEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-community/.env')); const staffEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-staff/.env')); const wt = process.env.WORKTREE_NAME ?? ''; - const uiCommunity = hostnameFrom(uiEnv['VITE_APP_UI_COMMUNITY_BASE_URL'] ?? ''); - const api = hostnameFrom(uiEnv['VITE_COMMON_API_ENDPOINT'] ?? ''); - const mockAuth = hostnameFrom(uiEnv['VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'] ?? ''); - const uiStaff = hostnameFrom(staffEnv['VITE_APP_UI_STAFF_AAD_REDIRECT_URI'] ?? ''); + const uiCommunity = hostnameFor('VITE_APP_UI_COMMUNITY_BASE_URL', uiEnv); + const api = hostnameFor('VITE_COMMON_API_ENDPOINT', uiEnv); + const mockAuth = hostnameFor('VITE_APP_UI_COMMUNITY_B2C_AUTHORITY', uiEnv); + const uiStaff = hostnameFor('VITE_APP_UI_STAFF_AAD_REDIRECT_URI', staffEnv); if (!uiCommunity || !api || !mockAuth || !uiStaff) { throw new Error('portless-hostnames: could not derive all hostnames from .env files. ' + 'Ensure apps/ui-community/.env and apps/ui-staff/.env are present.'); } + const docs = `docs.${uiCommunity}`; return { uiCommunity: applyWorktreeSuffix(uiCommunity, wt), uiStaff: applyWorktreeSuffix(uiStaff, wt), api: applyWorktreeSuffix(api, wt), mockAuth: applyWorktreeSuffix(mockAuth, wt), - docs: applyWorktreeSuffix(`docs.${uiCommunity}`, wt), + docs: applyWorktreeSuffix(docs, wt), }; } From 15226024a8afe0f57020abe2cd6704de14003e00 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 28 May 2026 13:47:27 -0400 Subject: [PATCH 07/12] retrigger build --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 56b87aea2..526b5e88f 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,7 @@ Domain-driven architecture for Azure Functions with GraphQL/REST, MongoDB (Mongo ## Introduction + [Getting Started](https://developers.cellixjs.org/docs/intro): Our Docusaurus website will help you get started in running and contributing to CellixJS From 843f3d06746402e22b67a4861ae38811b2b0766f Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 28 May 2026 14:22:44 -0400 Subject: [PATCH 08/12] sourcery suggestions pt1 --- apps/server-mongodb-memory-mock/start-mongo.mjs | 11 ++++------- apps/ui-community/start-dev.mjs | 5 ++++- apps/ui-staff/start-dev.mjs | 5 ++++- scripts/local-dev/worktree-ports.mjs | 4 +++- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/server-mongodb-memory-mock/start-mongo.mjs b/apps/server-mongodb-memory-mock/start-mongo.mjs index 30aa60095..cead8110f 100644 --- a/apps/server-mongodb-memory-mock/start-mongo.mjs +++ b/apps/server-mongodb-memory-mock/start-mongo.mjs @@ -1,15 +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(); -// Not running — start it via tsx with the worktree-scoped port -const { - default: { spawn }, -} = await import('node:child_process'); const child = spawn('tsx', ['src/index.ts'], { stdio: 'inherit', env: { ...process.env, PORT: String(MONGO_PORT) }, }); -child.on('exit', (code) => { - process.exit(code ?? 1); -}); + +forwardChildExit(child); diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs index 02965f584..ff5f6dc34 100644 --- a/apps/ui-community/start-dev.mjs +++ b/apps/ui-community/start-dev.mjs @@ -13,7 +13,10 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_APP_UI_COMMUNITY_BASE_URL = buildPortlessUrl(hostnames.uiCommunity); } -const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; +const viteArgs = ['--host', process.env.HOST ?? '127.0.0.1']; +if (process.env.PORT) { + viteArgs.push('--port', process.env.PORT); +} const viteMode = process.env.E2E_VITE_MODE ?? (isE2E() || process.env.TF_BUILD ? 'e2e' : undefined); if (viteMode) { viteArgs.push('--mode', viteMode); diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs index 776cabcea..23265c8c1 100644 --- a/apps/ui-staff/start-dev.mjs +++ b/apps/ui-staff/start-dev.mjs @@ -11,7 +11,10 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); } -const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; +const viteArgs = ['--host', process.env.HOST ?? '127.0.0.1']; +if (process.env.PORT) { + viteArgs.push('--port', process.env.PORT); +} const viteMode = process.env.E2E_VITE_MODE ?? (isE2E() || process.env.TF_BUILD ? 'e2e' : undefined); if (viteMode) { viteArgs.push('--mode', viteMode); diff --git a/scripts/local-dev/worktree-ports.mjs b/scripts/local-dev/worktree-ports.mjs index 25d843cbf..de11700d6 100644 --- a/scripts/local-dev/worktree-ports.mjs +++ b/scripts/local-dev/worktree-ports.mjs @@ -124,5 +124,7 @@ export function getAzuriteConnectionString(values) { export function getMongoConnectionString() { const base = getSetting('COSMOSDB_CONNECTION_STRING'); if (!base) throw new Error('[worktree-ports] COSMOSDB_CONNECTION_STRING must be set'); - return base.replace(/127\.0\.0\.1:\d+/, `127.0.0.1:${getMongoPort()}`); + const url = new URL(base); + url.port = String(getMongoPort()); + return url.toString(); } From c184d921660274ca936caf22f3beb7e74caf4f80 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 28 May 2026 15:02:12 -0400 Subject: [PATCH 09/12] sourcery suggestions reound 2 - reusability --- apps/api/start-azurite.mjs | 17 +++++++++++++---- apps/ui-community/start-dev.mjs | 14 ++------------ apps/ui-staff/start-dev.mjs | 14 ++------------ scripts/local-dev/vite-dev-args.mjs | 26 ++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 28 deletions(-) create mode 100644 scripts/local-dev/vite-dev-args.mjs diff --git a/apps/api/start-azurite.mjs b/apps/api/start-azurite.mjs index eb2f6146b..8cb110b31 100644 --- a/apps/api/start-azurite.mjs +++ b/apps/api/start-azurite.mjs @@ -10,11 +10,20 @@ const blobDir = `../../__blobstorage__${storageSuffix}`; const queueDir = `../../__queuestorage__${storageSuffix}`; const tableDir = `../../__tablestorage__${storageSuffix}`; -const procs = [ - spawn('azurite-blob', ['--silent', '--blobPort', String(ports.blob), '--location', blobDir], { stdio: 'inherit' }), - spawn('azurite-queue', ['--silent', '--queuePort', String(ports.queue), '--location', queueDir], { stdio: 'inherit' }), - spawn('azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', tableDir], { stdio: 'inherit' }), +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})`); diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs index ff5f6dc34..233be3229 100644 --- a/apps/ui-community/start-dev.mjs +++ b/apps/ui-community/start-dev.mjs @@ -1,6 +1,7 @@ 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'; +import { buildViteArgs } from '../../scripts/local-dev/vite-dev-args.mjs'; const childEnv = { ...process.env }; @@ -13,14 +14,7 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_APP_UI_COMMUNITY_BASE_URL = buildPortlessUrl(hostnames.uiCommunity); } -const viteArgs = ['--host', process.env.HOST ?? '127.0.0.1']; -if (process.env.PORT) { - viteArgs.push('--port', process.env.PORT); -} -const viteMode = process.env.E2E_VITE_MODE ?? (isE2E() || process.env.TF_BUILD ? 'e2e' : undefined); -if (viteMode) { - viteArgs.push('--mode', viteMode); -} +const viteArgs = buildViteArgs({ host: process.env.HOST, port: process.env.PORT }); const child = spawn('vite', viteArgs, { stdio: 'inherit', @@ -28,7 +22,3 @@ const child = spawn('vite', viteArgs, { }); forwardChildExit(child); - -function isE2E() { - return ['1', 'true', 'yes'].includes((process.env.E2E ?? '').toLowerCase()); -} diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs index 23265c8c1..c18f42dcf 100644 --- a/apps/ui-staff/start-dev.mjs +++ b/apps/ui-staff/start-dev.mjs @@ -1,6 +1,7 @@ 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'; +import { buildViteArgs } from '../../scripts/local-dev/vite-dev-args.mjs'; const childEnv = { ...process.env }; @@ -11,14 +12,7 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); } -const viteArgs = ['--host', process.env.HOST ?? '127.0.0.1']; -if (process.env.PORT) { - viteArgs.push('--port', process.env.PORT); -} -const viteMode = process.env.E2E_VITE_MODE ?? (isE2E() || process.env.TF_BUILD ? 'e2e' : undefined); -if (viteMode) { - viteArgs.push('--mode', viteMode); -} +const viteArgs = buildViteArgs({ host: process.env.HOST, port: process.env.PORT }); const child = spawn('vite', viteArgs, { stdio: 'inherit', @@ -26,7 +20,3 @@ const child = spawn('vite', viteArgs, { }); forwardChildExit(child); - -function isE2E() { - return ['1', 'true', 'yes'].includes((process.env.E2E ?? '').toLowerCase()); -} diff --git a/scripts/local-dev/vite-dev-args.mjs b/scripts/local-dev/vite-dev-args.mjs new file mode 100644 index 000000000..dcc39bc77 --- /dev/null +++ b/scripts/local-dev/vite-dev-args.mjs @@ -0,0 +1,26 @@ +/** + * @param {NodeJS.ProcessEnv} [env] + * @returns {boolean} + */ +export function isE2E(env = process.env) { + return ['1', 'true', 'yes'].includes((env.E2E ?? '').toLowerCase()); +} + +/** + * @param {{ host?: string; port?: string; env?: NodeJS.ProcessEnv }} [options] + * @returns {string[]} + */ +export function buildViteArgs(options = {}) { + const { host = '127.0.0.1', port, env = process.env } = options; + const args = ['--host', host]; + if (port) { + args.push('--port', port); + } + + const viteMode = env.E2E_VITE_MODE ?? (isE2E(env) || env.TF_BUILD ? 'e2e' : undefined); + if (viteMode) { + args.push('--mode', viteMode); + } + + return args; +} From fabfb36ef0ccad7249847af165d51f50f3b6d8b6 Mon Sep 17 00:00:00 2001 From: jasonmorais <120504390+jasonmorais@users.noreply.github.com> Date: Thu, 28 May 2026 15:11:04 -0400 Subject: [PATCH 10/12] Update readme.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 526b5e88f..886223cc6 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ Domain-driven architecture for Azure Functions with GraphQL/REST, MongoDB (Mongo [Getting Started](https://developers.cellixjs.org/docs/intro): -Our Docusaurus website will help you get started in running and contributing to CellixJS +Our Docusaurus website will help you get started with running and contributing to CellixJS ## Project Status From 0b7647786ab2266a74c146af4815b17951c21776 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 28 May 2026 16:01:20 -0400 Subject: [PATCH 11/12] undo tsc to tsgo --- packages/cellix/archunit-tests/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cellix/archunit-tests/package.json b/packages/cellix/archunit-tests/package.json index 3c1e6ba9c..96dcca505 100644 --- a/packages/cellix/archunit-tests/package.json +++ b/packages/cellix/archunit-tests/package.json @@ -43,8 +43,8 @@ }, "scripts": { "prebuild": "biome lint", - "build": "tsc --build", - "watch": "tsc --watch", + "build": "tsgo --build", + "watch": "tsgo --watch", "test": "vitest run", "test:arch": "vitest run", "test:coverage": "pnpm run test", From 110f771e1b4c97998cd42593012703f016bf534a Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Fri, 29 May 2026 15:49:13 -0400 Subject: [PATCH 12/12] fix serenity pattern - use task pattern instead of interaction --- .../authentication/abilities/header-types.ts | 5 + .../step-definitions/header-login.steps.ts | 11 +- .../tasks/click-header-sign-in.ts | 14 ++ .../tasks/click-header-sign-in.ts | 18 +- .../community/tasks/create-community.ts | 20 ++- .../authentication/abilities/header-types.ts | 7 + .../step-definitions/header-login.steps.ts | 49 +----- .../tasks/click-header-sign-in.ts | 46 +++++ .../community/tasks/create-community.ts | 166 +++++++++--------- .../src/shared/support/oauth2-login.ts | 47 ++--- .../verification-shared/package.json | 2 + .../verification-shared/src/serenity/index.ts | 1 + .../src/serenity/task-step.ts | 14 ++ pnpm-lock.yaml | 35 ++-- pnpm-workspace.yaml | 2 +- 15 files changed, 244 insertions(+), 193 deletions(-) create mode 100644 packages/ocom-verification/acceptance-api/src/contexts/authentication/abilities/header-types.ts create mode 100644 packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts create mode 100644 packages/ocom-verification/e2e-tests/src/contexts/authentication/abilities/header-types.ts create mode 100644 packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts create mode 100644 packages/ocom-verification/verification-shared/src/serenity/index.ts create mode 100644 packages/ocom-verification/verification-shared/src/serenity/task-step.ts diff --git a/packages/ocom-verification/acceptance-api/src/contexts/authentication/abilities/header-types.ts b/packages/ocom-verification/acceptance-api/src/contexts/authentication/abilities/header-types.ts new file mode 100644 index 000000000..c9b39119a --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/authentication/abilities/header-types.ts @@ -0,0 +1,5 @@ +export interface HeaderApiNotes { + identityProviderUnreachable: boolean; + signinRedirectInvoked: boolean; + fallbackTriggered: boolean; +} diff --git a/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts index 8ab66301e..d204f57f9 100644 --- a/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts +++ b/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -1,11 +1,7 @@ import { Given, Then, When } from '@cucumber/cucumber'; import { actorCalled, notes } from '@serenity-js/core'; - -interface HeaderApiNotes { - identityProviderUnreachable: boolean; - signinRedirectInvoked: boolean; - fallbackTriggered: boolean; -} +import type { HeaderApiNotes } from '../abilities/header-types.ts'; +import { ClickHeaderSignIn } from '../tasks/click-header-sign-in.ts'; let lastActorName = 'Alex'; @@ -32,8 +28,7 @@ Given('the identity provider is unreachable', async () => { When('{word} chooses to sign in', async (actorName: string) => { lastActorName = actorName; const actor = actorCalled(actorName); - const unreachable = await actor.answer(notes().get('identityProviderUnreachable')); - await actor.attemptsTo(notes().set('signinRedirectInvoked', !unreachable), notes().set('fallbackTriggered', unreachable)); + await actor.attemptsTo(ClickHeaderSignIn()); }); Then('{word} is taken to the sign-in flow', async (actorName: string) => { diff --git a/packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts b/packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts new file mode 100644 index 000000000..b0e46f773 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts @@ -0,0 +1,14 @@ +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { type Activity, type Actor, notes, Task } from '@serenity-js/core'; +import type { HeaderApiNotes } from '../abilities/header-types.ts'; + +export const ClickHeaderSignIn = () => + Task.where( + '#actor chooses to sign in through the authentication API', + new TaskStep('#actor requests the sign-in redirect state', async (serenityActor) => { + const actor = serenityActor as Actor; + const unreachable = await actor.answer(notes().get('identityProviderUnreachable')); + + await actor.attemptsTo(notes().set('signinRedirectInvoked', !unreachable), notes().set('fallbackTriggered', unreachable)); + }) as Activity, + ); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts index 8b634c058..5b36010c4 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts @@ -1,6 +1,7 @@ import { HomePage, type UiHomePage } from '@ocom-verification/verification-shared/pages'; import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; -import { Interaction } from '@serenity-js/core'; +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { type Activity, Task } from '@serenity-js/core'; async function flushAsync(): Promise { await new Promise((resolve) => { @@ -9,10 +10,13 @@ async function flushAsync(): Promise { } export const ClickHeaderSignIn = (container: HTMLElement) => - Interaction.where('#actor clicks the sign-in button on the home page', async () => { - const adapter = new JsdomPageAdapter(container); - const page: UiHomePage = new HomePage(adapter); + Task.where( + '#actor clicks the sign-in button on the home page', + new TaskStep('#actor clicks the sign-in button', async () => { + const adapter = new JsdomPageAdapter(container); + const page: UiHomePage = new HomePage(adapter); - await page.clickSignIn(); - await flushAsync(); - }); + await page.clickSignIn(); + await flushAsync(); + }) as Activity, + ); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts index 2c2683b3e..320723c11 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts @@ -1,6 +1,7 @@ import { CommunityPage, type UiCommunityPage } from '@ocom-verification/verification-shared/pages'; import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; -import { Interaction } from '@serenity-js/core'; +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { type Activity, Task } from '@serenity-js/core'; async function flushAsync(): Promise { await new Promise((resolve) => { @@ -12,12 +13,15 @@ async function flushAsync(): Promise { } export const CreateCommunity = (container: HTMLElement, name: string) => - Interaction.where(`#actor fills community name "${name}" and submits`, async () => { - const adapter = new JsdomPageAdapter(container); - const page: UiCommunityPage = new CommunityPage(adapter); + Task.where( + `#actor fills community name "${name}" and submits`, + new TaskStep(`#actor submits community name "${name}"`, async () => { + const adapter = new JsdomPageAdapter(container); + const page: UiCommunityPage = new CommunityPage(adapter); - await page.fillName(name); - await page.clickCreate(); + await page.fillName(name); + await page.clickCreate(); - await flushAsync(); - }); + await flushAsync(); + }) as Activity, + ); diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/abilities/header-types.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/abilities/header-types.ts new file mode 100644 index 000000000..b9ad079eb --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/abilities/header-types.ts @@ -0,0 +1,7 @@ +export interface HeaderE2ENotes { + signinRedirectInvoked: boolean; + fallbackTriggered: boolean; + postLoginUrl: string; +} + +export type HeaderE2ESite = 'community' | 'staff'; diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts index ee45019e4..998ec4dcd 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -3,18 +3,12 @@ import { actorCalled, notes } from '@serenity-js/core'; import type { BrowserContext, Page } from 'playwright'; import * as infra from '../../../shared/support/shared-infrastructure.ts'; import type { CellixE2EWorld } from '../../../world.ts'; - -interface HeaderE2ENotes { - signinRedirectInvoked: boolean; - fallbackTriggered: boolean; - postLoginUrl: string; -} - -type Site = 'community' | 'staff'; +import type { HeaderE2ENotes, HeaderE2ESite } from '../abilities/header-types.ts'; +import { ClickHeaderSignIn } from '../tasks/click-header-sign-in.ts'; interface HeaderE2EState { actorName: string; - site: Site; + site: HeaderE2ESite; identityProviderUnreachable: boolean; context?: BrowserContext; page?: Page; @@ -29,7 +23,7 @@ function getHeaderState(world: HeaderE2EWorld): HeaderE2EState { return world.__headerState; } -function setHeaderState(world: HeaderE2EWorld, actorName: string, site: Site): HeaderE2EState { +function setHeaderState(world: HeaderE2EWorld, actorName: string, site: HeaderE2ESite): HeaderE2EState { const state: HeaderE2EState = { actorName, site, identityProviderUnreachable: false }; world.__headerState = state; return state; @@ -63,15 +57,10 @@ Given('the identity provider is unreachable', function (this: HeaderE2EWorld) { getHeaderState(this).identityProviderUnreachable = true; }); -// Credentials from apps/ui-{portal}/mock-oidc.users.json -const portalCredentials: Record = { - community: { username: 'test@example.com', password: 'password' }, - staff: { username: 'staff@ownercommunity.onmicrosoft.com', password: 'password' }, -}; - When('{word} chooses to sign in', async function (this: HeaderE2EWorld, actorName: string) { const s = getHeaderState(this); s.actorName = actorName; + const actor = actorCalled(actorName); const { browser } = infra.getState(); if (!browser) throw new Error('Browser not launched'); @@ -93,33 +82,7 @@ When('{word} chooses to sign in', async function (this: HeaderE2EWorld, actorNam const page = await context.newPage(); s.page = page; - // Navigate to site root — the unauthenticated home page is visible - await page.goto('/', { waitUntil: 'networkidle', timeout: 30_000 }); - - // Click the sign-in button on the home page - const signInButton = page.getByRole('button', { name: /Log In|Sign In/i }); - await signInButton.click(); - - if (s.identityProviderUnreachable) { - // IdP is blocked — the app should handle the error gracefully. - // Wait for error handling to settle, then leave the page open for Then to inspect. - await page.waitForTimeout(2000); - } else { - // Wait for redirect to mock-auth login form - await page.waitForURL((url) => url.hostname.includes('mock-auth'), { timeout: 15_000 }); - - // Complete the login form with portal-specific credentials - const creds = portalCredentials[s.site]; - if (page.url().includes('/login')) { - await page.fill('input[name="username"]', creds.username); - await page.fill('input[name="password"]', creds.password); - await page.click('button[type="submit"]'); - } - - // Wait for the redirect chain to settle back on the portal - await page.waitForURL((url) => !url.hostname.includes('mock-auth') && !url.pathname.includes('auth-redirect'), { timeout: 30_000 }); - await page.waitForLoadState('networkidle'); - } + await actor.attemptsTo(ClickHeaderSignIn(page, s.site, s.identityProviderUnreachable)); }); Then('{word} is taken to the sign-in flow', async function (this: HeaderE2EWorld, actorName: string) { diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts new file mode 100644 index 000000000..bdf690de1 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts @@ -0,0 +1,46 @@ +import { type E2EHomePage, HomePage } from '@ocom-verification/verification-shared/pages'; +import { PlaywrightPageAdapter } from '@ocom-verification/verification-shared/pages/playwright'; +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { type Activity, type Actor, notes, Task } from '@serenity-js/core'; +import type { Page } from 'playwright'; +import type { HeaderE2ENotes, HeaderE2ESite } from '../abilities/header-types.ts'; + +const portalCredentials: Record = { + community: { username: 'test@example.com', password: 'password' }, + staff: { username: 'staff@ownercommunity.onmicrosoft.com', password: 'password' }, +}; + +const isPostAuthUrl = (url: URL) => !url.hostname.includes('mock-auth') && !url.pathname.includes('auth-redirect'); + +export const ClickHeaderSignIn = (page: Page, site: HeaderE2ESite, identityProviderUnreachable: boolean) => + Task.where( + '#actor clicks the sign-in button on the home page', + new TaskStep('#actor clicks the sign-in button on the home page', async (serenityActor) => { + const actor = serenityActor as Actor; + + await page.goto('/', { waitUntil: 'networkidle', timeout: 30_000 }); + + const adapter = new PlaywrightPageAdapter(page); + const homePage: E2EHomePage = new HomePage(adapter); + await homePage.clickSignIn(); + + if (identityProviderUnreachable) { + await page.waitForTimeout(2_000); + await actor.attemptsTo(notes().set('fallbackTriggered', true), notes().set('signinRedirectInvoked', false), notes().set('postLoginUrl', page.url())); + return; + } + + await page.waitForURL((url) => url.hostname.includes('mock-auth'), { timeout: 15_000 }); + + const creds = portalCredentials[site]; + if (page.url().includes('/login')) { + await page.fill('input[name="username"]', creds.username); + await page.fill('input[name="password"]', creds.password); + await page.click('button[type="submit"]'); + } + + await page.waitForURL(isPostAuthUrl, { timeout: 30_000 }); + await page.waitForLoadState('networkidle'); + await actor.attemptsTo(notes().set('signinRedirectInvoked', true), notes().set('fallbackTriggered', false), notes().set('postLoginUrl', page.url())); + }) as Activity, + ); diff --git a/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts index f24fb78f4..1f67af90e 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts @@ -1,6 +1,7 @@ import { CommunityPage, type E2ECommunityPage } from '@ocom-verification/verification-shared/pages'; import { PlaywrightPageAdapter } from '@ocom-verification/verification-shared/pages/playwright'; -import { type Actor, Interaction, notes, the } from '@serenity-js/core'; +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { type Activity, type Actor, notes, Task, the } from '@serenity-js/core'; import type { Response } from 'playwright'; import { BrowseTheWeb } from '../../../shared/abilities/browse-the-web.ts'; import type { CommunityE2ENotes } from '../abilities/community-types.ts'; @@ -63,83 +64,86 @@ const graphqlErrors = (payload: { errors?: Array<{ message?: string }> } | null) * Creates a community through the browser UI. */ export const CreateCommunity = (name: string) => - Interaction.where(the`#actor creates community "${name}" via UI`, async (serenityActor) => { - const actor = serenityActor as unknown as Actor; - const { page } = BrowseTheWeb.withActor(actor); - await page.goto('/community/accounts/create-community', { - waitUntil: 'networkidle', - }); - - const adapter = new PlaywrightPageAdapter(page); - const communityPage: E2ECommunityPage = new CommunityPage(adapter); - - await communityPage.fillName(name); - - const createMutationResponse = page.waitForResponse(hasGraphqlOperation(createCommunityOperationName), { timeout: 15_000 }).catch(() => null); - const communityListResponse = page.waitForResponse(hasGraphqlOperation(communityListOperationName), { timeout: 15_000 }).catch(() => null); - const memberListResponse = page.waitForResponse(hasGraphqlOperation(memberListOperationName), { timeout: 15_000 }).catch(() => null); - - await communityPage.clickCreate(); - - await communityPage.firstValidationError.waitFor({ state: 'visible', timeout: 750 }).catch(() => undefined); - const validationError = await communityPage.firstValidationError.isVisible().catch(() => false); - if (validationError) { - const errorText = await communityPage.firstValidationError.textContent(); - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', errorText || 'Validation error')); - return; - } - - const mutationResponse = await createMutationResponse; - if (!mutationResponse) { - await communityPage.errorToast.waitFor({ state: 'visible', timeout: 1_000 }).catch(() => undefined); - const hasErrorToast = await communityPage.errorToast.isVisible().catch(() => false); - const errorText = hasErrorToast ? await communityPage.errorToast.textContent() : null; - const message = errorText || `No ${createCommunityOperationName} GraphQL response was received`; - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); - throw new Error(message); - } - - const payload = selectGraphqlPayload((await mutationResponse.json().catch(() => null)) as CommunityCreateGraphqlPayload | CommunityCreateGraphqlPayload[] | null, (data) => Boolean(data?.communityCreate)); - const graphqlError = graphqlErrors(payload); - const mutationResult = payload?.data?.communityCreate; - const mutationError = mutationResult?.status?.errorMessage ?? graphqlError; - const createdName = mutationResult?.community?.name ?? null; - - if (!mutationResponse.ok || graphqlError || mutationResult?.status?.success !== true || (createdName !== null && createdName !== name)) { - const message = - mutationError || - (mutationResult?.status?.success !== true - ? `${createCommunityOperationName} did not report success: ${JSON.stringify(payload)}` - : createdName !== name - ? `Expected created community name "${name}" but GraphQL returned "${createdName ?? 'null'}"` - : `Community create GraphQL request failed with HTTP ${mutationResponse.status()}`); - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); - throw new Error(message); - } - - const listResponse = await communityListResponse; - const listPayload = listResponse - ? selectGraphqlPayload((await listResponse.json().catch(() => null)) as CommunityListGraphqlPayload | CommunityListGraphqlPayload[] | null, (data) => data?.communitiesForCurrentEndUser !== undefined) - : null; - const listGraphqlError = graphqlErrors(listPayload); - const listContainsCreatedCommunity = listPayload?.data?.communitiesForCurrentEndUser?.some((community) => community.name === name) ?? false; - if (!listResponse?.ok() || listGraphqlError || !listContainsCreatedCommunity) { - const message = listGraphqlError || `Expected "${name}" in ${communityListOperationName} response after creation`; - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); - throw new Error(message); - } - - const membersResponse = await memberListResponse; - const membersPayload = membersResponse - ? selectGraphqlPayload((await membersResponse.json().catch(() => null)) as CommunityListGraphqlPayload | CommunityListGraphqlPayload[] | null, (data) => data?.membersForCurrentEndUser !== undefined) - : null; - const membersGraphqlError = graphqlErrors(membersPayload); - if (!membersResponse?.ok() || membersGraphqlError) { - const message = membersGraphqlError || `${memberListOperationName} did not complete successfully after creation`; - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); - throw new Error(message); - } - - await page.getByRole('cell', { name, exact: true }).first().waitFor({ state: 'visible', timeout: 5_000 }); - await actor.attemptsTo(notes().set('communityName', name), notes().set('communityCreated', true), notes().set('errorMessage', null)); - }); + Task.where( + the`#actor creates community "${name}" via UI`, + new TaskStep(`#actor submits community "${name}" through the browser UI`, async (actor) => { + const serenityActor = actor as Actor; + const { page } = BrowseTheWeb.withActor(serenityActor); + await page.goto('/community/accounts/create-community', { + waitUntil: 'networkidle', + }); + + const adapter = new PlaywrightPageAdapter(page); + const communityPage: E2ECommunityPage = new CommunityPage(adapter); + + await communityPage.fillName(name); + + const createMutationResponse = page.waitForResponse(hasGraphqlOperation(createCommunityOperationName), { timeout: 15_000 }).catch(() => null); + const communityListResponse = page.waitForResponse(hasGraphqlOperation(communityListOperationName), { timeout: 15_000 }).catch(() => null); + const memberListResponse = page.waitForResponse(hasGraphqlOperation(memberListOperationName), { timeout: 15_000 }).catch(() => null); + + await communityPage.clickCreate(); + + await communityPage.firstValidationError.waitFor({ state: 'visible', timeout: 750 }).catch(() => undefined); + const validationError = await communityPage.firstValidationError.isVisible().catch(() => false); + if (validationError) { + const errorText = await communityPage.firstValidationError.textContent(); + await serenityActor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', errorText || 'Validation error')); + return; + } + + const mutationResponse = await createMutationResponse; + if (!mutationResponse) { + await communityPage.errorToast.waitFor({ state: 'visible', timeout: 1_000 }).catch(() => undefined); + const hasErrorToast = await communityPage.errorToast.isVisible().catch(() => false); + const errorText = hasErrorToast ? await communityPage.errorToast.textContent() : null; + const message = errorText || `No ${createCommunityOperationName} GraphQL response was received`; + await serenityActor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + const payload = selectGraphqlPayload((await mutationResponse.json().catch(() => null)) as CommunityCreateGraphqlPayload | CommunityCreateGraphqlPayload[] | null, (data) => Boolean(data?.communityCreate)); + const graphqlError = graphqlErrors(payload); + const mutationResult = payload?.data?.communityCreate; + const mutationError = mutationResult?.status?.errorMessage ?? graphqlError; + const createdName = mutationResult?.community?.name ?? null; + + if (!mutationResponse.ok || graphqlError || mutationResult?.status?.success !== true || (createdName !== null && createdName !== name)) { + const message = + mutationError || + (mutationResult?.status?.success !== true + ? `${createCommunityOperationName} did not report success: ${JSON.stringify(payload)}` + : createdName !== name + ? `Expected created community name "${name}" but GraphQL returned "${createdName ?? 'null'}"` + : `Community create GraphQL request failed with HTTP ${mutationResponse.status()}`); + await serenityActor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + const listResponse = await communityListResponse; + const listPayload = listResponse + ? selectGraphqlPayload((await listResponse.json().catch(() => null)) as CommunityListGraphqlPayload | CommunityListGraphqlPayload[] | null, (data) => data?.communitiesForCurrentEndUser !== undefined) + : null; + const listGraphqlError = graphqlErrors(listPayload); + const listContainsCreatedCommunity = listPayload?.data?.communitiesForCurrentEndUser?.some((community) => community.name === name) ?? false; + if (!listResponse?.ok() || listGraphqlError || !listContainsCreatedCommunity) { + const message = listGraphqlError || `Expected "${name}" in ${communityListOperationName} response after creation`; + await serenityActor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + const membersResponse = await memberListResponse; + const membersPayload = membersResponse + ? selectGraphqlPayload((await membersResponse.json().catch(() => null)) as CommunityListGraphqlPayload | CommunityListGraphqlPayload[] | null, (data) => data?.membersForCurrentEndUser !== undefined) + : null; + const membersGraphqlError = graphqlErrors(membersPayload); + if (!membersResponse?.ok() || membersGraphqlError) { + const message = membersGraphqlError || `${memberListOperationName} did not complete successfully after creation`; + await serenityActor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + await page.getByRole('cell', { name, exact: true }).first().waitFor({ state: 'visible', timeout: 5_000 }); + await serenityActor.attemptsTo(notes().set('communityName', name), notes().set('communityCreated', true), notes().set('errorMessage', null)); + }) as Activity, + ); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts index 675ee96db..92911f16d 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts @@ -1,5 +1,6 @@ +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; import { actors } from '@ocom-verification/verification-shared/test-data'; -import { type Actor, Interaction, the } from '@serenity-js/core'; +import { type Activity, type Actor, Task, the } from '@serenity-js/core'; import type { Page } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; @@ -47,32 +48,34 @@ export async function performOAuth2Login(page: Page): Promise { } /** - * Screenplay Interaction — confirms the actor is authenticated. + * Screenplay Task — confirms the actor is authenticated. * * The browser context is pre-authenticated by {@link performOAuth2Login} - * during server setup. This interaction navigates to a protected route and + * during server setup. This task navigates to a protected route and * verifies the page loads without being kicked to the auth provider. */ export const OAuth2Login = (_email?: string, _password?: string) => - Interaction.where(the`#actor logs in via OAuth2`, async (serenityActor) => { - const actor = serenityActor as unknown as Actor; - const { page } = BrowseTheWeb.withActor(actor); + Task.where( + the`#actor logs in via OAuth2`, + new TaskStep('#actor confirms the OAuth2 session is active', async (actor) => { + const { page } = BrowseTheWeb.withActor(actor as Actor); - // Session tokens live in sessionStorage from pre-auth. - try { - await page.goto('/community/accounts', { - waitUntil: 'networkidle', - timeout: 30_000, - }); - } catch { - // Navigation may be interrupted by OIDC redirect on first access - } + // Session tokens live in sessionStorage from pre-auth. + try { + await page.goto('/community/accounts', { + waitUntil: 'networkidle', + timeout: 30_000, + }); + } catch { + // Navigation may be interrupted by OIDC redirect on first access + } - if (page.url().includes('/login')) { - await page.fill('input[name="username"]', actors.CommunityOwner.email); - await page.fill('input[name="password"]', 'password'); - await page.click('button[type="submit"]'); - } + if (page.url().includes('/login')) { + await page.fill('input[name="username"]', actors.CommunityOwner.email); + await page.fill('input[name="password"]', 'password'); + await page.click('button[type="submit"]'); + } - await page.waitForURL(isPostAuthUrl, { timeout: 30_000 }); - }); + await page.waitForURL(isPostAuthUrl, { timeout: 30_000 }); + }) as Activity, + ); diff --git a/packages/ocom-verification/verification-shared/package.json b/packages/ocom-verification/verification-shared/package.json index 13c3822aa..9d4803c01 100644 --- a/packages/ocom-verification/verification-shared/package.json +++ b/packages/ocom-verification/verification-shared/package.json @@ -10,6 +10,7 @@ "./formatters": "./src/formatters/index.ts", "./servers": "./src/servers/index.ts", "./settings": "./src/settings/index.ts", + "./serenity": "./src/serenity/index.ts", "./pages": "./src/pages/index.ts", "./pages/jsdom": "./src/pages/adapters/jsdom-adapter.ts", "./pages/playwright": "./src/pages/adapters/playwright-adapter.ts" @@ -20,6 +21,7 @@ "@ocom/service-mongoose": "workspace:*", "@cucumber/cucumber": "catalog:", "@cucumber/messages": "catalog:", + "@serenity-js/core": "catalog:", "@ocom/graphql": "workspace:*", "@ocom/application-services": "workspace:*", "@testing-library/react": "^16.3.0", diff --git a/packages/ocom-verification/verification-shared/src/serenity/index.ts b/packages/ocom-verification/verification-shared/src/serenity/index.ts new file mode 100644 index 000000000..54ba8c2e5 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/serenity/index.ts @@ -0,0 +1 @@ +export * from './task-step.ts'; diff --git a/packages/ocom-verification/verification-shared/src/serenity/task-step.ts b/packages/ocom-verification/verification-shared/src/serenity/task-step.ts new file mode 100644 index 000000000..b11ff0a82 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/serenity/task-step.ts @@ -0,0 +1,14 @@ +import { Task } from '@serenity-js/core'; + +export class TaskStep extends Task { + constructor( + description: string, + private readonly action: (actor: unknown) => Promise, + ) { + super(description); + } + + performAs(actor: unknown): Promise { + return this.action(actor); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68e8e6f12..3be5c052c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,7 +101,7 @@ catalogs: version: 4.1.2 overrides: - axios: 1.15.2 + axios: 1.16.0 follow-redirects: ^1.16.0 vite: 8.0.5 jiti: 2.6.1 @@ -1253,6 +1253,9 @@ importers: '@ocom/service-mongoose': specifier: workspace:* version: link:../../ocom/service-mongoose + '@serenity-js/core': + specifier: 'catalog:' + version: 3.42.2 '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -7222,8 +7225,8 @@ packages: resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} engines: {node: '>=4'} - axios@1.15.2: - resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==} + axios@1.16.0: + resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} azurite@3.35.0: resolution: {integrity: sha512-GzKmi+/5U0baNRjEEVtBMLpLuIKEJ0uSh0VWBzOI4qe4f5ziJyoZQmcTO7QhxZTF6+rphj7TZS3PtJY7uiiacA==} @@ -13347,18 +13350,6 @@ packages: write-file-atomic@3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.20.1: resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} @@ -14100,7 +14091,7 @@ snapshots: '@azure/ms-rest-js@1.11.2': dependencies: '@azure/core-auth': 1.10.1 - axios: 1.15.2 + axios: 1.16.0 form-data: 2.5.5 tough-cookie: 2.5.0 tslib: 1.14.1 @@ -18360,7 +18351,7 @@ snapshots: dependencies: '@serenity-js/core': 3.42.2 agent-base: 7.1.4 - axios: 1.15.2 + axios: 1.16.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 11.3.5 @@ -18375,7 +18366,7 @@ snapshots: '@serenity-js/core': 3.42.2 '@serenity-js/rest': 3.42.2 ansi-regex: 5.0.1 - axios: 1.15.2 + axios: 1.16.0 chalk: 4.1.2 find-java-home: 2.0.0 progress: 2.0.3 @@ -18426,7 +18417,7 @@ snapshots: '@sonar/scan@4.3.2': dependencies: adm-zip: 0.5.16 - axios: 1.15.2 + axios: 1.16.0 commander: 13.1.0 fs-extra: 11.3.2 hpagent: 1.2.0 @@ -19657,7 +19648,7 @@ snapshots: axe-core@4.11.0: {} - axios@1.15.2: + axios@1.16.0: dependencies: follow-redirects: 1.16.0(debug@4.4.3) form-data: 4.0.5 @@ -19670,7 +19661,7 @@ snapshots: '@azure/ms-rest-js': 1.11.2 applicationinsights: 2.9.8 args: 5.0.3 - axios: 1.15.2 + axios: 1.16.0 etag: 1.8.1 express: 4.22.2 fs-extra: 11.3.2 @@ -26870,8 +26861,6 @@ snapshots: signal-exit: 3.0.7 typedarray-to-buffer: 3.1.5 - ws@7.5.10: {} - ws@8.20.1: {} wsl-utils@0.1.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cc09ee7ba..9238556fd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -59,7 +59,7 @@ allowBuilds: snyk: true overrides: - axios: 1.15.2 + axios: 1.16.0 follow-redirects: ^1.16.0 vite: "catalog:" jiti: 2.6.1