Skip to content
Merged
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
54 changes: 54 additions & 0 deletions packages/core/src/node/__tests__/context-auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { ResolvedConfig } from 'vite'
import process from 'node:process'
import { afterEach, describe, expect, it } from 'vitest'
import { createDevToolsContext } from '../context'
import '@vitejs/devtools-kit'

function createConfig(options: {
command?: 'serve' | 'build'
clientAuth?: boolean
} = {}): ResolvedConfig {
return {
root: process.cwd(),
command: options.command ?? 'serve',
plugins: [],
devtools: options.clientAuth === undefined
? undefined
: { config: { clientAuth: options.clientAuth } },
} as unknown as ResolvedConfig
}

describe('createDevToolsContext auth registration', () => {
afterEach(() => {
delete process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH
})

it('registers the interactive-auth handshake when client auth is enabled', async () => {
const ctx = await createDevToolsContext(createConfig())

expect(ctx.rpc.definitions.has('anonymous:devframe:auth')).toBe(true)
})

it('skips the interactive-auth handshake in build mode (regression #539)', async () => {
const ctx = await createDevToolsContext(createConfig({ command: 'build' }))

// Left unregistered so devframe's `auth: false` auto-trust shim (armed
// by `createDevToolsHub`) can install its own noop handler and mark the
// session trusted — see `isClientAuthDisabled`.
expect(ctx.rpc.definitions.has('anonymous:devframe:auth')).toBe(false)
})

it('skips the interactive-auth handshake when `devtools.clientAuth` is false (regression #539)', async () => {
const ctx = await createDevToolsContext(createConfig({ clientAuth: false }))

expect(ctx.rpc.definitions.has('anonymous:devframe:auth')).toBe(false)
})

it('skips the interactive-auth handshake when VITE_DEVTOOLS_DISABLE_CLIENT_AUTH=true (regression #539)', async () => {
process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH = 'true'

const ctx = await createDevToolsContext(createConfig())

expect(ctx.rpc.definitions.has('anonymous:devframe:auth')).toBe(false)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ vi.mock('../ui', () => ({

vi.mock('../auth-handler', () => ({
getAuthHandler: () => ({ rpcFunctions: [] }),
isClientAuthDisabled: () => false,
}))

function fakeContext(opts: { viteServer?: boolean } = {}): ViteDevToolsNodeContext {
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/node/auth-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import process from 'node:process'
import { createInteractiveAuth } from 'devframe/recipes/interactive-auth'

export type DevToolsAuthHandler = ReturnType<typeof createInteractiveAuth>
Expand All @@ -23,3 +24,20 @@ export function getAuthHandler(context: ViteDevToolsNodeContext): DevToolsAuthHa
}
return handler
}

/**
* Whether the interactive OTP gate should stay off for this context — a
* build snapshot (nothing live to authorize against), an explicit
* `devtools: { clientAuth: false }`, or the `VITE_DEVTOOLS_DISABLE_CLIENT_AUTH`
* escape-hatch env var. Shared between `createDevToolsContext` (which must
* skip registering the interactive-auth RPC functions so devframe's
* `auth: false` auto-trust shim can register `anonymous:devframe:auth`
* itself) and `createDevToolsHub` (which feeds the same intent to
* `initHub`'s transport-level `auth` option) — both need to agree, or the
* client's session never gets marked trusted.
*/
export function isClientAuthDisabled(context: ViteDevToolsNodeContext): boolean {
return context.mode === 'build'
|| context.viteConfig.devtools?.config?.clientAuth === false
|| process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH === 'true'
}
15 changes: 11 additions & 4 deletions packages/core/src/node/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ResolvedConfig, ViteDevServer } from 'vite'
import { createKitContext, createViteDevToolsHost } from '@vitejs/devtools-kit/node'
import { createDebug } from 'obug'
import { DEVTOOLS_ASSETS_BASE, dirAssets } from '../dirs'
import { getAuthHandler } from './auth-handler'
import { getAuthHandler, isClientAuthDisabled } from './auth-handler'
import { diagnostics } from './diagnostics'
import { builtinRpcDeclarations } from './rpc'

Expand Down Expand Up @@ -71,9 +71,16 @@ export async function createDevToolsContext(
// recipe: registers the `anonymous:devframe:auth` / `:exchange` handshake
// and the `devframe:auth:revoke` self-revoke. The resolver gate and the
// one-time-code banner are wired up by `initHub`'s `auth` option (same
// handler) in `createDevToolsHub`.
for (const fn of getAuthHandler(context).rpcFunctions)
rpcHost.register(fn)
// handler) in `createDevToolsHub`. Skipped entirely when the client-auth
// gate is disabled — leaving `anonymous:devframe:auth` unregistered lets
// devframe's `auth: false` auto-trust shim (armed by `createDevToolsHub`
// passing `auth: false` to `initHub`) register its own noop handler and
// mark sessions trusted, instead of the interactive handler winning the
// race and leaving every session stuck untrusted.
if (!isClientAuthDisabled(context)) {
for (const fn of getAuthHandler(context).rpcFunctions)
rpcHost.register(fn)
}

// Vite-specific built-in server commands.
context.commands.register({
Expand Down
9 changes: 4 additions & 5 deletions packages/core/src/node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import type { ViteDevToolsHost } from '@vitejs/devtools-kit/node'
import type { Server as NodeHttpServer } from 'node:http'
import type { DevToolsConfig } from './config'
import type { ViteDevToolsUiOptions } from './ui'
import process from 'node:process'
import { initHub } from '@devframes/hub/initiate'
import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub'
import { DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants'
import { getAuthHandler } from './auth-handler'
import { getAuthHandler, isClientAuthDisabled } from './auth-handler'
import { createViteDevToolsUi } from './ui'

export interface CreateDevToolsHubOptions {
Expand Down Expand Up @@ -53,9 +52,9 @@ export async function createDevToolsHub(options: CreateDevToolsHubOptions): Prom

// Mirror the WS trust posture the bespoke transport used: skip the OTP gate
// in build snapshots, when the user opts out, or via the escape-hatch env.
const authDisabled = context.mode === 'build'
|| context.viteConfig.devtools?.config?.clientAuth === false
|| process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH === 'true'
// Must agree with `createDevToolsContext`'s registration guard (same
// helper) — see `isClientAuthDisabled` for why.
const authDisabled = isClientAuthDisabled(context)

// Vite's published types bundle a frozen `DevToolsConfig` snapshot, so a
// field added here isn't visible through `config` until Vite re-vendors it.
Expand Down
Loading