Problem
@onderwijsin/nuxt-directus-client@0.11.0 regressed the Nuxt/Nitro runtime boundary and can make consuming applications fail during SSR with:
Package import specifier "#nitro-internal-virtual/storage" is not defined in package
...
ERR_PACKAGE_IMPORT_NOT_DEFINED
We have hit this class of failure before. Nitro runtime helpers such as useStorage() are valid inside Nitro-owned server runtime, but their implementation must not become part of the Nuxt application runtime graph. If a Nuxt SSR plugin statically imports Nitro-only implementation code, Nitro's generated virtual imports can leak into the consumer bundle and fail at runtime.
This is a release-blocking regression in 0.11.0.
Root cause
The regression was introduced in PR #286 / commit 1f5e6f5 while making SSR auth hydration resilient to transient refresh failures.
src/runtime/auth/app/ssr-session-plugin.ts now imports implementation code from auth/server/*:
import { isTransientDirectusRefreshError } from "../server/refresh";
import { getDirectusSessionSnapshot } from "../server/session";
The first import creates this dependency chain:
auth/app/ssr-session-plugin.ts
-> auth/server/refresh.ts
-> auth/server/refresh-coordinator/index.ts
-> nitropack/runtime
-> #nitro-internal-virtual/storage
refresh-coordinator/index.ts imports useStorage() from nitropack/runtime, so the Nuxt SSR application graph ends up pulling in Nitro's storage runtime.
The asset-cache/pruning code also legitimately uses useStorage(), but it is not the primary cause of this failure. The concrete leak is the Nuxt SSR auth plugin importing the Nitro-only refresh implementation graph.
The second import (auth/server/session.ts) does not currently trigger this exact storage error, but it crosses the same architectural boundary and should be removed as part of the fix. auth/app/** should not depend directly on auth/server/** implementation code.
Existing architecture we should preserve
We already have the correct boundary: Nitro owns request authentication and exposes it through event.context.directusAuth.
The Nitro plugin currently installs a lazy request-scoped resolver:
event.context.directusAuth = {
resolve() {
return ...;
}
};
This boundary should remain the only way the Nuxt SSR plugin asks Nitro to resolve authentication/session state.
The important distinction is:
- Nuxt application runtime may consume a typed request-context contract.
- Nitro server runtime may import
nitropack/runtime, useStorage(), refresh coordination, sealed-session utilities, etc.
- Nuxt application runtime must not statically import those Nitro implementation files.
Proposed fix
Extend the existing DirectusRequestAuthContext with a hydration-specific operation rather than importing refresh/session helpers directly from the Nuxt plugin.
For example:
export interface DirectusRequestAuthContext {
readonly resolve: () => Promise<DirectusRequestAuthState>;
readonly resolveSnapshot: () => Promise<DirectusSessionSnapshot | null>;
}
resolve()
Keep the current strict semantics:
- refresh when required;
- return usable request credentials and the token-free snapshot;
- propagate transient refresh failures;
- clear/resolve terminal authentication failures according to the existing auth contract.
This is the boundary used by authenticated upstream Directus requests and must not silently fall back to expired credentials.
resolveSnapshot()
Implement this entirely inside the Nitro-owned auth plugin/runtime:
- Call the refresh-aware resolver.
- On success, return its snapshot.
- If and only if the error is an explicitly classified transient Directus refresh failure, read the trusted local sealed-session snapshot without refreshing.
- Propagate all other errors.
The Nuxt SSR plugin should then become conceptually:
if (event) {
session.value = (await event.context.directusAuth?.resolveSnapshot()) ?? null;
}
It should no longer import either:
auth/server/refresh.ts
auth/server/session.ts
This keeps the transient fallback behavior introduced in #286 while restoring the runtime boundary previously fixed in #215/#250.
Why not just move isTransientDirectusRefreshError()?
Moving the classifier to a shared file would fix the immediate storage import chain, but it would leave the underlying architecture wrong: a Nuxt application plugin would still know about and import Nitro-side auth implementation details.
The fix should restore the boundary, not just make the current transitive import graph happen to be safe.
Likewise, replacing useStorage() with another Nitro import inside the refresh coordinator is not the right fix. useStorage() belongs there; the problem is that the file is reachable from the Nuxt application graph.
Regression coverage
The current test suite misses this for two reasons.
Unit test masks the import graph
ssr-session-plugin.test.ts mocks both auth/server/refresh and auth/server/session, so it never loads the transitive Nitro runtime graph that breaks real consumers.
Refactor this test to mock/test the request-context contract instead. The Nuxt SSR plugin test should not need to mock Nitro implementation modules at all.
Packed external consumer does not enable auth
The packed external-consumer Directus fixture currently has:
Therefore auth/app/ssr-session-plugin.ts is never registered in the exact integration test designed to catch published-package runtime-boundary problems.
Add packed-consumer coverage with Directus auth enabled.
The fixture should configure at least:
- a valid dummy
sessionSecret;
- a
directus-auth-refresh memory storage mount;
- an SSR page/request that causes the authenticated SSR plugin to load and render.
The packed consumer test must fail if the response/build/runtime contains either:
ERR_PACKAGE_IMPORT_NOT_DEFINED
#nitro-internal-virtual/storage
Do not limit the regression assertion to the asset route. We need coverage that actually loads the auth-enabled SSR application graph.
Repository-wide boundary guard
While fixing this issue, inspect the Directus client runtime for the same pattern:
runtime/**/app/** -> runtime/**/server/**
Do not broadly refactor unrelated code, but remove any equivalent direct application-to-Nitro implementation dependency that can make server-only Nitro runtime imports reachable from Nuxt app runtime.
It is acceptable and expected for Nitro handlers, Nitro plugins, tasks, and server utilities to use nitropack/runtime and useStorage().
The invariant is about graph ownership, not banning Nitro imports.
Acceptance criteria
Non-goals
- Replacing or removing Nitro
useStorage() from legitimate Nitro-owned runtime code.
- Redesigning refresh coordination.
- Changing transient/terminal refresh semantics beyond relocating ownership behind the request-context boundary.
- Refactoring unrelated Directus runtime organization.
Expected outcome
The public Nuxt package may still contain Nitro-specific implementation files, but Nuxt application runtime must never statically traverse into them. Nitro owns storage, refresh coordination and sealed-session internals; the Nuxt SSR layer consumes only the narrow request-scoped auth contract exposed on the H3 event.
Problem
@onderwijsin/nuxt-directus-client@0.11.0regressed the Nuxt/Nitro runtime boundary and can make consuming applications fail during SSR with:We have hit this class of failure before. Nitro runtime helpers such as
useStorage()are valid inside Nitro-owned server runtime, but their implementation must not become part of the Nuxt application runtime graph. If a Nuxt SSR plugin statically imports Nitro-only implementation code, Nitro's generated virtual imports can leak into the consumer bundle and fail at runtime.This is a release-blocking regression in
0.11.0.Root cause
The regression was introduced in PR #286 / commit
1f5e6f5while making SSR auth hydration resilient to transient refresh failures.src/runtime/auth/app/ssr-session-plugin.tsnow imports implementation code fromauth/server/*:The first import creates this dependency chain:
refresh-coordinator/index.tsimportsuseStorage()fromnitropack/runtime, so the Nuxt SSR application graph ends up pulling in Nitro's storage runtime.The asset-cache/pruning code also legitimately uses
useStorage(), but it is not the primary cause of this failure. The concrete leak is the Nuxt SSR auth plugin importing the Nitro-only refresh implementation graph.The second import (
auth/server/session.ts) does not currently trigger this exact storage error, but it crosses the same architectural boundary and should be removed as part of the fix.auth/app/**should not depend directly onauth/server/**implementation code.Existing architecture we should preserve
We already have the correct boundary: Nitro owns request authentication and exposes it through
event.context.directusAuth.The Nitro plugin currently installs a lazy request-scoped resolver:
This boundary should remain the only way the Nuxt SSR plugin asks Nitro to resolve authentication/session state.
The important distinction is:
nitropack/runtime,useStorage(), refresh coordination, sealed-session utilities, etc.Proposed fix
Extend the existing
DirectusRequestAuthContextwith a hydration-specific operation rather than importing refresh/session helpers directly from the Nuxt plugin.For example:
resolve()Keep the current strict semantics:
This is the boundary used by authenticated upstream Directus requests and must not silently fall back to expired credentials.
resolveSnapshot()Implement this entirely inside the Nitro-owned auth plugin/runtime:
The Nuxt SSR plugin should then become conceptually:
It should no longer import either:
This keeps the transient fallback behavior introduced in #286 while restoring the runtime boundary previously fixed in #215/#250.
Why not just move
isTransientDirectusRefreshError()?Moving the classifier to a shared file would fix the immediate storage import chain, but it would leave the underlying architecture wrong: a Nuxt application plugin would still know about and import Nitro-side auth implementation details.
The fix should restore the boundary, not just make the current transitive import graph happen to be safe.
Likewise, replacing
useStorage()with another Nitro import inside the refresh coordinator is not the right fix.useStorage()belongs there; the problem is that the file is reachable from the Nuxt application graph.Regression coverage
The current test suite misses this for two reasons.
Unit test masks the import graph
ssr-session-plugin.test.tsmocks bothauth/server/refreshandauth/server/session, so it never loads the transitive Nitro runtime graph that breaks real consumers.Refactor this test to mock/test the request-context contract instead. The Nuxt SSR plugin test should not need to mock Nitro implementation modules at all.
Packed external consumer does not enable auth
The packed external-consumer Directus fixture currently has:
Therefore
auth/app/ssr-session-plugin.tsis never registered in the exact integration test designed to catch published-package runtime-boundary problems.Add packed-consumer coverage with Directus auth enabled.
The fixture should configure at least:
sessionSecret;directus-auth-refreshmemory storage mount;The packed consumer test must fail if the response/build/runtime contains either:
Do not limit the regression assertion to the asset route. We need coverage that actually loads the auth-enabled SSR application graph.
Repository-wide boundary guard
While fixing this issue, inspect the Directus client runtime for the same pattern:
Do not broadly refactor unrelated code, but remove any equivalent direct application-to-Nitro implementation dependency that can make server-only Nitro runtime imports reachable from Nuxt app runtime.
It is acceptable and expected for Nitro handlers, Nitro plugins, tasks, and server utilities to use
nitropack/runtimeanduseStorage().The invariant is about graph ownership, not banning Nitro imports.
Acceptance criteria
ERR_PACKAGE_IMPORT_NOT_DEFINED/#nitro-internal-virtual/storageduring SSR.auth/app/ssr-session-plugin.tshas no direct imports fromauth/server/*implementation files.event.context.directusAuthboundary owns the hydration fallback behavior.ERR_PACKAGE_IMPORT_NOT_DEFINEDand#nitro-internal-virtual/storage.Non-goals
useStorage()from legitimate Nitro-owned runtime code.Expected outcome
The public Nuxt package may still contain Nitro-specific implementation files, but Nuxt application runtime must never statically traverse into them. Nitro owns storage, refresh coordination and sealed-session internals; the Nuxt SSR layer consumes only the narrow request-scoped auth contract exposed on the H3 event.