From 64089877bf7cd111ed5e5124fff7ebd598c53fc2 Mon Sep 17 00:00:00 2001 From: lile Date: Tue, 9 Jun 2026 21:06:37 +0800 Subject: [PATCH 1/3] chore(deps): pin typeorm to 0.3.28 and add @types/node-forge typeorm 0.3.29 widened the DISTINCT-pagination trigger to include .limit(), crashing quoted-orderBy lifecycle crons; pin to the last good 0.3.28. node-forge is used directly, so declare its types as a direct devDependency. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/package.json b/apps/package.json index 27b4ed2fa..c4b244bc4 100644 --- a/apps/package.json +++ b/apps/package.json @@ -197,7 +197,7 @@ "tailwind-scrollbar": "^4.0.2", "tailwindcss-animate": "^1.0.7", "tar": "^7.5.11", - "typeorm": "^0.3.20", + "typeorm": "0.3.28", "usehooks-ts": "^3.1.1", "uuid": "^14.0.0", "vaul": "^1.1.2", @@ -250,6 +250,7 @@ "@types/jest": "30.0.0", "@types/multer": "^1.4.12", "@types/node": "^22.13.4", + "@types/node-forge": "^1.3.14", "@types/nodemailer": "^6.4.17", "@types/passport-http-bearer": "^1.0.41", "@types/passport-jwt": "^4.0.1", From 555067398c269776ac6616ee6835ff780dfe4742 Mon Sep 17 00:00:00 2001 From: lile Date: Tue, 9 Jun 2026 21:06:52 +0800 Subject: [PATCH 2/3] fix(api): skip dev-incompatible webpack plugins under NODE_ENV=development `nx serve api` triggers the base build, which crashes in @nx/webpack's GeneratePackageJsonPlugin in this workspace and fails type-checking on a pre-existing @opentelemetry/otlp-exporter-base version skew (a type-only mismatch in tracing.ts; the exporters work at runtime). Drop both plugins only when NODE_ENV=development so the API builds/serves locally; production keeps the pruned deploy package.json and full type-checking. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/webpack.config.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/api/webpack.config.js b/apps/api/webpack.config.js index 62929cc1e..e4373d759 100644 --- a/apps/api/webpack.config.js +++ b/apps/api/webpack.config.js @@ -37,6 +37,22 @@ module.exports = composePlugins( } config.mode = process.env.NODE_ENV + // Local dev only (NODE_ENV=development, set by infra-local's stack-up via + // apps/.env). `nx serve api` triggers the base `api:build` target, which + // would otherwise (a) crash inside @nx/webpack's GeneratePackageJsonPlugin + // in this workspace and (b) fail type-checking on the pre-existing + // @opentelemetry/otlp-exporter-base version skew in the lockfile (a + // type-only mismatch in tracing.ts — the OTLP exporters work at runtime). + // Drop both plugins so the API builds and serves locally. Production + // (NODE_ENV=production) keeps both: the pruned deploy package.json and + // full type-checking. + if (process.env.NODE_ENV === 'development') { + const devSkipPlugins = new Set(['GeneratePackageJsonPlugin', 'ForkTsCheckerWebpackPlugin']) + config.plugins = (config.plugins || []).filter( + (plugin) => !devSkipPlugins.has(plugin?.constructor?.name), + ) + } + config.watchOptions = { ignored: /node_modules/, } From c77b87d05adb6b5202e4948adf4beaba29ad9686 Mon Sep 17 00:00:00 2001 From: lile Date: Tue, 9 Jun 2026 21:09:12 +0800 Subject: [PATCH 3/3] fix(api,dashboard): fail-closed feature-flag bootstrap for PostHog-less envs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When PostHog isn't configured, @RequireFlagsEnabled routes default to false (e.g. POST /regions, GET /runners return 404) and the dashboard's gated UI (Create Sandbox, etc.) goes blank. Add an optional bootstrapFlags map the provider/wrapper consult ONLY when PostHog has no apiKey AND NODE_ENV is not production — a prod deploy without a key still fails closed (gated routes stay hidden), enforced at both the injection site and inside the provider as defense-in-depth. Covered by openfeature-posthog.provider.spec.ts. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/src/app.module.ts | 19 ++++++ .../openfeature-posthog.provider.spec.ts | 43 ++++++++++++ .../providers/openfeature-posthog.provider.ts | 31 ++++++++- .../src/components/PostHogProviderWrapper.tsx | 67 +++++++++++++++++-- 4 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 apps/api/src/common/providers/openfeature-posthog.provider.spec.ts diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 49e81c1e4..669fddf96 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -212,6 +212,25 @@ import { BoxliteRestModule } from './boxlite-rest/boxlite-rest.module' host: process.env.POSTHOG_HOST, }, apiKey: process.env.POSTHOG_API_KEY, + // Local-dev bootstrap. Only consulted when PostHog isn't configured + // (i.e. no POSTHOG_API_KEY) AND never in production — a prod deploy + // without a PostHog key must fail closed (gated routes stay hidden), + // not fail open. Gated here at the injection site; the provider also + // refuses bootstrap in production as defense-in-depth. + // + // All true → matches the "team-internal full-access" rollout state. + // Mirrors the dashboard-side defaults in PostHogProviderWrapper.tsx + // (LOCAL_DEV_FEATURE_FLAG_DEFAULTS). + ...(process.env.NODE_ENV !== 'production' && { + bootstrapFlags: { + organization_infrastructure: true, + organization_experiments: true, + dashboard_playground: true, + dashboard_webhooks: true, + 'dashboard_create-sandbox': true, + sandbox_spending: true, + }, + }), }), }), ], diff --git a/apps/api/src/common/providers/openfeature-posthog.provider.spec.ts b/apps/api/src/common/providers/openfeature-posthog.provider.spec.ts new file mode 100644 index 000000000..0b2a7fffc --- /dev/null +++ b/apps/api/src/common/providers/openfeature-posthog.provider.spec.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Daytona Platforms Inc. + * Modified by BoxLite AI, 2025-2026 + * SPDX-License-Identifier: AGPL-3.0 + */ + +import type { EvaluationContext, Logger } from '@openfeature/server-sdk' +import { OpenFeaturePostHogProvider } from './openfeature-posthog.provider' + +/** + * Guards the production fail-closed gate: bootstrap flags are a local-dev + * convenience and must NEVER resolve `true` in production when PostHog is + * unconfigured — otherwise a prod deploy without POSTHOG_API_KEY would expose + * flag-gated routes (fail open). See app.module.ts injection-site gate. + */ +describe('OpenFeaturePostHogProvider — production bootstrap gate', () => { + const FLAG = 'dashboard_create-sandbox' + const originalNodeEnv = process.env.NODE_ENV + const logger: Logger = { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} } + const context: EvaluationContext = {} + + afterEach(() => { + process.env.NODE_ENV = originalNodeEnv + }) + + it('honors bootstrap flags outside production when PostHog is unconfigured', async () => { + process.env.NODE_ENV = 'development' + const provider = new OpenFeaturePostHogProvider({ bootstrapFlags: { [FLAG]: true } }) + + const result = await provider.resolveBooleanEvaluation(FLAG, false, context, logger) + + expect(result.value).toBe(true) + }) + + it('ignores bootstrap flags in production — fails closed to the call-site default', async () => { + process.env.NODE_ENV = 'production' + const provider = new OpenFeaturePostHogProvider({ bootstrapFlags: { [FLAG]: true } }) + + const result = await provider.resolveBooleanEvaluation(FLAG, false, context, logger) + + expect(result.value).toBe(false) + }) +}) diff --git a/apps/api/src/common/providers/openfeature-posthog.provider.ts b/apps/api/src/common/providers/openfeature-posthog.provider.ts index 3cfbffbb1..3ecf482b6 100644 --- a/apps/api/src/common/providers/openfeature-posthog.provider.ts +++ b/apps/api/src/common/providers/openfeature-posthog.provider.ts @@ -16,6 +16,14 @@ export interface OpenFeaturePostHogProviderConfig { clientOptions?: PostHogOptions /** Whether to evaluate flags locally (default: false) */ evaluateLocally?: boolean + /** + * Local-dev bootstrap values used when PostHog isn't configured (no apiKey). + * Mirrors the dashboard's `PostHogProviderWrapper` bootstrap pattern. + * Without these, `@RequireFlagsEnabled([X])` decorators always fail because + * each call-site's `defaultValue` is `false` — so e.g. POST /regions and + * GET /runners return 404 ("hidden route") in local dev. Map key = flag name. + */ + bootstrapFlags?: Record } export class OpenFeaturePostHogProvider implements Provider { @@ -26,10 +34,18 @@ export class OpenFeaturePostHogProvider implements Provider { private readonly client?: PostHog private readonly evaluateLocally: boolean private readonly isConfigured: boolean + private readonly bootstrapFlags: Record constructor(config: OpenFeaturePostHogProviderConfig = {}) { this.evaluateLocally = config.evaluateLocally ?? false this.isConfigured = !!config.apiKey + // Bootstrap flags are a local-dev convenience only. Hard-refuse them in + // production so an unconfigured prod (no PostHog key) falls through to each + // call-site's `defaultValue` (fail closed) instead of resolving them true + // (fail open). Defense-in-depth: the injection site (app.module) also skips + // passing them in production. + this.bootstrapFlags = + process.env.NODE_ENV === 'production' ? {} : (config.bootstrapFlags ?? {}) if (config.apiKey) { try { @@ -92,8 +108,10 @@ export class OpenFeaturePostHogProvider implements Provider { context: EvaluationContext, logger: Logger, ): Promise> { - // If PostHog is not configured, return default value + // Bootstrap-aware (mirror evaluateFlag's behavior for object resolution). if (!this.isConfigured || !this.client) { + // Object flags are rare in our usage; not added to bootstrapFlags map + // (typed as boolean | string | number). Fall through to default. logger.debug(`PostHog not configured, returning default value for flag ${flagKey}`) return { value: defaultValue, @@ -147,8 +165,17 @@ export class OpenFeaturePostHogProvider implements Provider { context: EvaluationContext, logger: Logger, ): Promise> { - // If PostHog is not configured, return default value + // If PostHog is not configured, prefer bootstrap value if present, then + // fall back to call-site defaultValue. if (!this.isConfigured || !this.client) { + if (Object.prototype.hasOwnProperty.call(this.bootstrapFlags, flagKey)) { + const bootstrap = this.bootstrapFlags[flagKey] + logger.debug(`PostHog not configured, using bootstrap value for flag ${flagKey}: ${bootstrap}`) + return { + value: bootstrap, + reason: StandardResolutionReasons.STATIC, + } + } logger.debug(`PostHog not configured, returning default value for flag ${flagKey}`) return { value: defaultValue, diff --git a/apps/dashboard/src/components/PostHogProviderWrapper.tsx b/apps/dashboard/src/components/PostHogProviderWrapper.tsx index 9f75f1bed..78735a964 100644 --- a/apps/dashboard/src/components/PostHogProviderWrapper.tsx +++ b/apps/dashboard/src/components/PostHogProviderWrapper.tsx @@ -6,6 +6,7 @@ import { useConfig } from '@/hooks/useConfig' import { usePrivacyConsentStore } from '@/hooks/usePrivacyConsent' +import { FeatureFlags } from '@/enums/FeatureFlags' import { usePostHog } from 'posthog-js/react' import { PostHogProvider } from 'posthog-js/react' import { FC, ReactNode, useEffect } from 'react' @@ -14,6 +15,23 @@ interface PostHogProviderWrapperProps { children: ReactNode } +// Local-dev defaults for the feature flags that gate UI surfaces. When +// PostHog isn't configured (no API key in /api/config — typical local dev), +// we mount PostHogProvider in a no-network mode with these bootstrap values +// so flag-gated components (e.g. CreateSandboxSheet, which short-circuits to +// `return null` if `dashboard_create-sandbox` is falsy) still render. +// +// All true → matches the "team-internal full-access" rollout state. Flip an +// entry to false to simulate prod gating one off. +const LOCAL_DEV_FEATURE_FLAG_DEFAULTS: Record = { + [FeatureFlags.DASHBOARD_CREATE_SANDBOX]: true, + [FeatureFlags.DASHBOARD_PLAYGROUND]: true, + [FeatureFlags.DASHBOARD_WEBHOOKS]: true, + [FeatureFlags.ORGANIZATION_INFRASTRUCTURE]: true, + [FeatureFlags.ORGANIZATION_EXPERIMENTS]: true, + [FeatureFlags.SANDBOX_SPENDING]: true, +} + function PostHogConsentSync() { const posthog = usePostHog() const analytics = usePrivacyConsentStore((state) => state.preferences.analytics) @@ -35,20 +53,55 @@ function PostHogConsentSync() { export const PostHogProviderWrapper: FC = ({ children }) => { const config = useConfig() - if (!config.posthog) { - return children - } + const posthogConfigured = !!(config.posthog?.apiKey && config.posthog?.host) - if (!config.posthog?.apiKey || !config.posthog?.host) { + if (config.posthog && !posthogConfigured) { + // Server sent a posthog block but it's incomplete — likely operator typo. console.error('Invalid PostHog configuration') - return children + } + + if (!posthogConfigured) { + // Production must never bootstrap flags to `true` (fail-open). If PostHog + // isn't configured in a prod build, render without a provider so every + // flag resolves falsy (fail-closed) — matching the pre-bootstrap behavior. + // `import.meta.env.PROD` is Vite's production-build equivalent of + // NODE_ENV==='production'; infra-local runs `vite` dev (PROD=false), so it + // still gets the local-dev bootstrap below. + if (import.meta.env.PROD) { + return children + } + + // Local-dev / unconfigured path. Mount PostHogProvider with a stub key + + // bootstrap feature flags so `useFeatureFlagEnabled(...)` returns the + // defaults above. All network behavior disabled: no /decide call (we + // pre-load flags via bootstrap), no event capture, no autocapture. + return ( + posthog.opt_out_capturing(), + }} + > + {children} + + ) } return (