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
93 changes: 93 additions & 0 deletions packages/devframe/src/adapters/__tests__/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,99 @@ describe('adapters/dev', () => {
}
})

it('the `auth: false` option overrides a gated `def.cli.auth` (hosted adapter defers to the host)', async () => {
const devframe = defineDevframe({
id: 'devframe-auth-option-off',
name: 'Auth Option Off',
version: '0.0.0',
packageName: 'devframe-test',
homepage: 'https://example.test',
description: 'Test devframe.',
// Standalone would gate; a hosted caller passes `auth: false` to defer.
cli: { auth: true },
setup: (ctx: DevframeNodeContext) => {
ctx.rpc.register({ name: 'test:probe', type: 'query', handler: () => 'ok' })
},
})
const host = '127.0.0.1'
const port = await getPort({ port: 19440, host })
const handle = await createDevServer(devframe, { host, port, openBrowser: false, auth: false })

try {
const client = connectWsClient(host, port)
const handshake = await client.$call('anonymous:devframe:auth' as any, HANDSHAKE)
expect(handshake).toEqual({ isTrusted: true })
await expect(client.$call('test:probe' as any)).resolves.toBe('ok')
client.$close()
}
finally {
await handle.close()
}
})

it('the `auth: true` option forces the gate on even when `def.cli.auth` is false', async () => {
const devframe = defineDevframe({
id: 'devframe-auth-option-on',
name: 'Auth Option On',
version: '0.0.0',
packageName: 'devframe-test',
homepage: 'https://example.test',
description: 'Test devframe.',
cli: { auth: false },
setup: (ctx: DevframeNodeContext) => {
ctx.rpc.register({ name: 'test:probe', type: 'query', handler: () => 'ok' })
},
})
const host = '127.0.0.1'
const port = await getPort({ port: 19450, host })
// Silence the default OTP stdout banner for the test run.
const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
const handle = await createDevServer(devframe, { host, port, openBrowser: false, auth: true })

try {
const client = connectWsClient(host, port)
// Gated: an empty handshake token is refused until a code is exchanged.
const handshake = await client.$call('anonymous:devframe:auth' as any, HANDSHAKE)
expect(handshake).toEqual({ isTrusted: false })
const code = getTempAuthCode()
const exchange = await client.$call('anonymous:devframe:auth:exchange' as any, { code, ua: 'test', origin: 'http://localhost' }) as { authToken: string | null }
expect(exchange.authToken).toBeTruthy()
client.$close()
}
finally {
spy.mockRestore()
await handle.close()
}
})

it('the `--no-auth` flag forces the gate off even when the `auth: true` option opts in', async () => {
const devframe = defineDevframe({
id: 'devframe-auth-flag-wins',
name: 'Auth Flag Wins',
version: '0.0.0',
packageName: 'devframe-test',
homepage: 'https://example.test',
description: 'Test devframe.',
setup: (ctx: DevframeNodeContext) => {
ctx.rpc.register({ name: 'test:probe', type: 'query', handler: () => 'ok' })
},
})
const host = '127.0.0.1'
const port = await getPort({ port: 19460, host })
const handle = await createDevServer(devframe, { host, port, openBrowser: false, auth: true, flags: { auth: false } })

try {
const client = connectWsClient(host, port)
const handshake = await client.$call('anonymous:devframe:auth' as any, HANDSHAKE)
expect(handshake).toEqual({ isTrusted: true })
await expect(client.$call('test:probe' as any)).resolves.toBe('ok')
client.$close()
}
finally {
await handle.close()
}
})

it('resolveDevServerPort honors def.cli.port as the preferred default', async () => {
const preferred = await getPort({ port: 19500, host: '127.0.0.1' })
const devframe = defineDevframe({
Expand Down
22 changes: 20 additions & 2 deletions packages/devframe/src/adapters/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ export interface CreateDevServerOptions {
* sources; a string opens that relative path.
*/
openBrowser?: boolean | string
/**
* Override how authentication resolves, taking precedence over
* `def.cli?.auth`. Pass `false` to skip the gate entirely (the standard
* choice for a **hosted** deployment where the host manages auth — see
* {@link viteDevBridge}); a {@link DevframeAuthHandler} to install a custom
* scheme; or `true` to force devframe's interactive OTP gate on. When
* omitted, auth resolves from `flags.auth` / `def.cli?.auth` (the standalone
* default: gated). The `--no-auth` flag (`flags.auth === false`) still forces
* the gate off regardless of this option.
*/
auth?: boolean | DevframeAuthHandler
/**
* Expose a route-based MCP server on the dev server (Streamable-HTTP).
* Overrides `def.cli?.mcp`; `undefined` falls through to it. `false`
Expand Down Expand Up @@ -212,8 +223,15 @@ export async function createDevServer(
// interactive OTP handler and print its code + magic-link banner once the
// server is listening (a gate is useless without surfacing the code). A
// `false` (including the `--no-auth` flag) opts out; a handler object is
// passed straight through to `startHttpAndWs`.
const authOption = flags.auth === false ? false : def.cli?.auth
// passed straight through to `startHttpAndWs`. An explicit `options.auth`
// wins over the definition default — a hosted adapter (e.g. `viteDevBridge`)
// passes `false` so the plugin's own gate never fires and the host owns auth
// — but the `--no-auth` flag can still force the gate off.
const authOption = flags.auth === false
? false
: options.auth !== undefined
? options.auth
: def.cli?.auth
let authHandler: DevframeAuthHandler | undefined
let resolvedAuth: boolean | DevframeAuthHandler
if (authOption === false) {
Expand Down
23 changes: 23 additions & 0 deletions packages/devframe/src/helpers/vite.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { DevframeAuthHandler } from '../node/auth/handler'
import type { DevframeDefinition } from '../types/devframe'
import { serveStaticNodeMiddleware } from 'devframe/utils/serve-static'
import { resolve } from 'pathe'
Expand Down Expand Up @@ -36,6 +37,19 @@ export interface ViteDevBridgeOptions {
/** Flag bag forwarded to `def.setup(ctx, { flags })`. */
flags?: Record<string, unknown>
}
/**
* Whether the bridged devframe runs its own auth gate. This is a **hosted**
* adapter — the devframe shares the host app's origin and the host owns
* authentication — so it defaults to `false`: the plugin's own gate never
* fires and its `cli.auth` default is ignored (matching devframe's
* hosted-deployment contract). Pass `true` to force devframe's interactive
* OTP gate on, or a {@link DevframeAuthHandler} to install a custom scheme.
* Only applies in bridge mode (`devMiddleware`); the static-mount mode
* starts no RPC server.
*
* @default false
*/
auth?: boolean | DevframeAuthHandler
}

export interface DevframeVitePlugin {
Expand All @@ -62,6 +76,12 @@ export interface DevframeVitePlugin {
* host-served SPA can discover the WS endpoint via
* {@link connectDevframe}.
*
* As a hosted adapter the bridge defers authentication to the host: its
* side-car RPC server runs with the plugin's own auth gate **off** by
* default (ignoring `def.cli?.auth`), so a plugin mounted this way never
* triggers its standalone OTP prompt. Opt back in per-mount with
* `options.auth` (`true` for devframe's interactive gate, or a handler).
*
* Use bridge mode when integrating with frameworks that own the SPA
* (Nuxt, Astro, SolidStart, plain Vite apps). For the all-in-one
* `dev` / `build` / `mcp` shell, reach for {@link createCac} instead.
Expand Down Expand Up @@ -103,6 +123,9 @@ export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptio
port,
flags: mw.flags,
openBrowser: false,
// Hosted adapter: the host owns auth, so the bridged devframe's own
// gate stays off unless the caller explicitly opts back in.
auth: options.auth ?? false,
})
}
catch (e) {
Expand Down
2 changes: 1 addition & 1 deletion plugins/og/src/spa/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default defineConfig({
plugins: [
vue(),
UnoCSS(),
ogVitePlugin({ devMiddleware: true, base: '/', auth: false }),
ogVitePlugin({ devMiddleware: true, base: '/' }),
],
optimizeDeps: { exclude: ['@antfu/design'] },
build: {
Expand Down
16 changes: 8 additions & 8 deletions plugins/og/src/vite.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { DevframeVitePlugin, ViteDevBridgeOptions } from 'devframe/helpers/vite'
import { viteDevBridge } from 'devframe/helpers/vite'
import ogDevframe, { createOgDevframe } from './index'
import ogDevframe from './index'

export type { ViteDevBridgeOptions }

export interface OgVitePluginOptions extends ViteDevBridgeOptions {
/** Override standalone trust for the bridge; useful for local SPA development. */
auth?: boolean
}
export type OgVitePluginOptions = ViteDevBridgeOptions

/**
* Mount the OG image preview into an existing Vite dev server. As a hosted
* adapter it defers authentication to the host, so the bridged devframe's own
* gate stays off by default — opt back in with `{ auth: true }`.
*/
export function ogVitePlugin(options: OgVitePluginOptions = {}): DevframeVitePlugin {
const { auth, ...bridgeOptions } = options
const definition = auth === undefined ? ogDevframe : createOgDevframe({ auth })
return viteDevBridge(definition, bridgeOptions)
return viteDevBridge(ogDevframe, options)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/**
* Generated by tsnapi — public API snapshot of `@devframes/plugin-og/vite`
*/
// #region Interfaces
export interface OgVitePluginOptions extends ViteDevBridgeOptions {
auth?: boolean;
}
// #region Types
export type OgVitePluginOptions = ViteDevBridgeOptions;
// #endregion

// #region Functions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface CreateDevServerOptions {
ws?: DevframeWsOptions;
app?: H3;
openBrowser?: boolean | string;
auth?: boolean | DevframeAuthHandler;
mcp?: boolean | McpRouteOptions;
onReady?: (_: {
origin: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ViteDevBridgeOptions {
host?: string;
flags?: Record<string, unknown>;
};
auth?: boolean | DevframeAuthHandler;
}
// #endregion

Expand Down
Loading