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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}),
Comment on lines +224 to +233
}),
}),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
})
})
31 changes: 29 additions & 2 deletions apps/api/src/common/providers/openfeature-posthog.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, boolean | string | number>
}

export class OpenFeaturePostHogProvider implements Provider {
Expand All @@ -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<string, boolean | string | number>

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 ?? {})
Comment on lines +47 to +48

if (config.apiKey) {
try {
Expand Down Expand Up @@ -92,8 +108,10 @@ export class OpenFeaturePostHogProvider implements Provider {
context: EvaluationContext,
logger: Logger,
): Promise<ResolutionDetails<T>> {
// 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,
Expand Down Expand Up @@ -147,8 +165,17 @@ export class OpenFeaturePostHogProvider implements Provider {
context: EvaluationContext,
logger: Logger,
): Promise<ResolutionDetails<any>> {
// 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,
Expand Down
16 changes: 16 additions & 0 deletions apps/api/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
}
Comment on lines +49 to +54

config.watchOptions = {
ignored: /node_modules/,
}
Expand Down
67 changes: 60 additions & 7 deletions apps/dashboard/src/components/PostHogProviderWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<string, boolean> = {
[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)
Expand All @@ -35,20 +53,55 @@ function PostHogConsentSync() {
export const PostHogProviderWrapper: FC<PostHogProviderWrapperProps> = ({ 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 (
<PostHogProvider
apiKey="phc_local_dev_no_op"
options={{
// api_host is required by posthog-js but never used because
// capturing is opted out and flags are bootstrapped below.
api_host: 'https://localhost.invalid',
bootstrap: { featureFlags: LOCAL_DEV_FEATURE_FLAG_DEFAULTS },
advanced_disable_feature_flags: true, // skip /decide network call
autocapture: false,
capture_pageview: false,
capture_pageleave: false,
opt_out_capturing_by_default: true,
disable_session_recording: true,
loaded: (posthog) => posthog.opt_out_capturing(),
Comment on lines +79 to +92
}}
>
{children}
</PostHogProvider>
)
}

return (
<PostHogProvider
apiKey={config.posthog.apiKey}
apiKey={config.posthog!.apiKey}
options={{
api_host: config.posthog.host,
api_host: config.posthog!.host,
opt_out_capturing_by_default: true,
cookieless_mode: 'on_reject',
persistence: 'localStorage',
Expand Down
3 changes: 2 additions & 1 deletion apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Are @types/node-forgeversion 1.3.14 types compatible with node-forge version 1.4.0, and does a@types/node-forge version 1.4.x exist?

💡 Result:

As of June 10, 2026, there is no version 1.4.x of @types/node-forge; the latest available version is 1.3.14 [1][2][3]. Regarding compatibility, @types/node-forge version 1.3.14 is generally used with node-forge version 1.4.0, though users may encounter minor type-related incompatibilities depending on their specific implementation. For example, some development teams have found it necessary to cast certain objects or handle widened types (such as pki.PrivateKey) explicitly when upgrading to node-forge 1.4.0 while using the 1.3.14 types [4][5].

Citations:


@types/node-forge 1.3.14 vs node-forge 1.4.0: acceptable but keep an eye on minor type drift

@types/node-forge has no 1.4.x release line—the latest version is 1.3.14. So using ^1.3.14 alongside node-forge ^1.4.0 is the expected/latest pairing; minor type mismatches may still happen depending on usage, but this is not a versioning bug in the dependency declaration itself.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/package.json` at line 253, The package.json entry for `@types/node-forge`
(^1.3.14) alongside node-forge (^1.4.0) is acceptable but may produce minor type
drift; leave the dependency versions as-is and add a short comment or repository
note to monitor for type mismatches, or add a package-lock/yarn.lock update step
to ensure consistent installs; reference the dependency names
"`@types/node-forge`" and "node-forge" when making the note or adding an
actionable TODO so reviewers can easily find and revisit it later.

"@types/nodemailer": "^6.4.17",
"@types/passport-http-bearer": "^1.0.41",
"@types/passport-jwt": "^4.0.1",
Expand Down
Loading