Skip to content

Commit 142c01c

Browse files
os-trumpclaude
andauthored
fix(plugin-auth): register the MCP resource so RFC 8707 authorize can succeed (#16780)
* test(plugin-auth): make the MCP OAuth resource check falsifiable The predecessor check asserted `opts.validAudiences` on the options object captured from a mocked `oauthProvider`. The provider never consumes that object, so the assertion was green whether or not the installed version read the option -- and 1.7.2 does not read it at all. An assertion that cannot fail is indistinguishable from one that passed. Replace it with checks whose subject is what the REAL provider does: - an option-surface liveness scan over the INSTALLED provider dist, carrying a two-way control so a 0-hit reading is a measurement rather than silence; - an end-to-end block that boots a real authorization server from the exact options AuthManager produces and drives discovery -> DCR -> `authorize?resource=<mcp url>` -> consent -> token; - a guard that the per-client resource check stays ON, so satisfying the flow by switching a security check off turns this red instead. This commit is deliberately red: it is the reproduction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37 * fix(plugin-auth): register the MCP resource so RFC 8707 authorize can succeed `@better-auth/oauth-provider` 1.7.2 resolves a requested `resource` from the `oauthResource` table and, with `enforcePerClientResources` at its `true` default, requires the client to be linked in `oauthClientResource`. Neither row was ever written, so every MCP client that sends `resource=` was refused at `/oauth2/authorize` with `invalid_target: requested resource <mcp url> is not configured`. No token could be minted on 17.3.0. Route (a): declare the resource rather than relax the check. - `resources: [mcpResourceUrl]` seeds the sys_oauth_resource row from the provider's own `init`, idempotently and `insertOnly`, so an admin's later policy edits survive a restart. - `clientRegistrationDefaultResources: [mcpResourceUrl]` links every newly registered client inside the DCR transaction -- the only place the link can happen, since a client registers anonymously about a second before login. - `enforcePerClientResources` stays at its `true` default. A client with no link row is still refused, and a test asserts that. Two dead options removed. Neither `validAudiences` nor `silenceWarnings` occurs anywhere in the installed `@better-auth/oauth-provider` or `better-auth` (0 hits each, against positive controls that fire), and the `oauthAuthServerConfig` notice `silenceWarnings` claimed to suppress no longer exists in 1.7.2 either. A field that is passed and read by nobody looks like configuration and enforces nothing -- that is how this defect survived a version bump, so the new option-surface liveness check refuses any such field rather than allowlisting these two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37 * fix(plugin-auth): settle auth plugin init, and key the dev in-memory fallback by modelName Registering the MCP resource made the oauth-provider seed a `sys_oauth_resource` row from its plugin `init` — the first write this package ever performs during better-auth construction. Two latent boot-path defects became reachable as soon as it did, and both are fixed here: * `betterAuth()` returns synchronously and runs plugin `init` behind `auth.$context`, so anything a plugin does at init was a promise nobody held. A failure escaped as an UNHANDLED REJECTION (fatal to the process by default) and, in tests, as a boot write racing its engine teardown. `createAuthInstance` now awaits `$context`, making the seed part of "the instance is ready" and a boot failure a rejection of the call that asked for it. * The no-`dataEngine` fallback handed better-auth no `database` at all, which makes it build an in-memory store keyed by the schema KEY while every read resolves by `modelName`. Measured on better-auth 1.7.2: every renamed model — `user`/`sys_user` included, not just the oauth ones — answered "Model <name> not found" on that path. The fallback now builds the store itself, keyed the way the adapter reads it. Production is unaffected: it returns the ObjectQL adapter factory above this branch. The pin that asserted `database === undefined` is replaced rather than edited — it pinned exactly the branch this removes, and it read the value we passed rather than what that value does. Its successor drives the factory and asks the adapter for a renamed model. Two suites had their measurement windows corrected, not their assertions weakened: the sign-up refusal test now drains boot writes before arming its insert recorder, and the membership-policy double stops filing every insert as a membership regardless of which object it named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37 * test(plugin-auth): add the wrong-resource negative control; grade the changeset minor Contract review of PR #16780 returned CHANGES REQUIRED on two points. F1 — the changeset level. The PR declares `Clause-②: yes` (the accept set of `/oauth2/authorize` grows) while grading `@objectstack/plugin-auth` `patch`. The maintainer's 2026-09-04 ruling (decision batch #35, the WHICH LEVEL prose in `pr-automation.yml`) settles the order between that and "a bug fix in a released package takes `patch`": a purely additive widening of a published package's public surface — "a new accepted key or value" — takes at least `minor`, and the commit type never lowers the bump below what the act requires. Graded `minor`. F2 — the missing negative control. The body claimed "a request naming any other resource is still refused exactly as before" and nothing tested it: `authorizeWithResource`'s `resource` parameter was never varied. Without that control a green suite cannot tell "the MCP resource is registered" from "resource checking is off" — the same axis as this card's original defect, where the assertion read the options we passed rather than what the provider does. The control is written as a DIFFERENTIAL against the real provider: one run, one booted AS, one DCR client, one session, and two authorize requests that differ only in `resource`. The registered MCP resource must reach consent; an identifier that was never registered must answer `invalid_target`. That shape reddens from both sides — remove the registration and the granted half fails, seed the second resource and link clients to it and the refused half does — where a bare refusal assertion would stay green under either. It asserts nothing about the options object; the resource inventory it checks at the end is read out of the AS's own store. A second control covers the token leg: a code bound to the MCP resource at authorize, redeemed with a `resource` the grant never carried, must be refused `invalid_target` and mint nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 613bfbd commit 142c01c

7 files changed

Lines changed: 703 additions & 51 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/plugin-auth": minor
3+
---
4+
5+
MCP OAuth can complete again: the MCP resource is registered as an RFC 8707 resource and DCR-registered clients are linked to it, so `authorize?resource=<mcp url>` no longer answers `invalid_target`.
6+
7+
On 17.3.0 no MCP client could ever obtain a token. `plugin-auth` configured `@better-auth/oauth-provider` with `validAudiences: [authIssuer, mcpResourceUrl]`, an option the pinned 1.7.2 does not read — the string does not occur once in its dist. In 1.7.2 a requested `resource` is resolved from the `oauthResource` table (`sys_oauth_resource`) and `enforcePerClientResources` defaults to `true`, so the client must also be linked in `oauthClientResource` (`sys_oauth_client_resource`). Neither row was ever written, so every client that sends `resource=` — Claude Code does — was refused at `/oauth2/authorize` with `invalid_target: requested resource <mcp url> is not configured`. Discovery, dynamic client registration and the login page all worked; the flow died one step before consent.
8+
9+
- **`resources: [mcpResourceUrl]`** seeds the `sys_oauth_resource` row from the provider's own `init`. Seeding is idempotent and defaults to `insertOnly`, so an administrator's later edits to the row's token policy are never reverted by a restart.
10+
- **`clientRegistrationDefaultResources: [mcpResourceUrl]`** links each newly registered client to that resource inside the DCR transaction. This is the only place the link can be made: a client registers anonymously about one second before the browser login, leaving no window for an administrator to insert the row by hand.
11+
- **`enforcePerClientResources` is left at its `true` default.** The per-client linkage check stays on — the fix makes the link exist rather than switching the check off. A client with no link row is still refused with `invalid_target`, and a test asserts that.
12+
- **`validAudiences` is removed.** It was passed and read by nobody, which is precisely how the defect survived a version bump: it looked like configuration and enforced nothing.
13+
14+
Two boot-path defects the resource seed uncovered are fixed in the same change, because seeding is the first thing this package ever wrote from a plugin `init`:
15+
16+
- **`getAuthInstance()` now settles better-auth's plugin `init` hooks before it resolves.** `betterAuth()` returns synchronously and runs those hooks behind `auth.$context`, so a failure inside one had no catcher and escaped as an unhandled rejection — which Node terminates the process for by default. A boot failure now rejects the call that asked for the instance.
17+
- **The no-`dataEngine` development fallback builds its own in-memory adapter instead of letting better-auth build one.** better-auth 1.7.2 keys that store by the schema *key* while every read resolves by `modelName`, so on that path every model this package renames was unreachable — `user`/`sys_user` as much as `oauthResource`/`sys_oauth_resource` — answering `Model <name> not found`. Production never took this branch (it uses the ObjectQL adapter); development and tests did.
18+
19+
No configuration change is required. Deployments that already ran 17.3.0 get the resource row on the next boot; MCP clients that failed to connect need to reconnect so a fresh registration picks up the link.

packages/plugins/plugin-auth/src/auth-manager.mcp-oauth-resource.test.ts

Lines changed: 523 additions & 0 deletions
Large diffs are not rendered by default.

packages/plugins/plugin-auth/src/auth-manager.mcp-oauth.test.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77
* Token verification tests use REAL jose-signed JWTs against a locally
88
* generated JWKS (mocked `getApi().getJwks`), so the crypto path — signature,
99
* issuer, audience, expiry — is exercised for real, fail-closed on each axis.
10-
* The full discovery → DCR → PKCE browser flow is covered end-to-end against
11-
* a live dev server (see the PR's verification notes); better-auth's own
12-
* endpoint behavior is not re-tested here.
10+
*
11+
* ⚠️ The `oauthProvider plugin wiring` block below reads the options object
12+
* this package passes to a MOCKED `oauthProvider`. That subject can only
13+
* answer "did we pass X", never "does the provider honour X" — so ⛔ never
14+
* assert protocol behaviour here. Anything whose truth depends on what the
15+
* installed provider DOES belongs in auth-manager.mcp-oauth-resource.test.ts,
16+
* which boots the real provider and drives the flow end to end.
1317
*/
1418

1519
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
@@ -272,7 +276,7 @@ describe('verifyMcpAccessToken (local JWKS verification, fail-closed)', () => {
272276
});
273277
});
274278

275-
describe('oauthProvider plugin wiring (DCR + scopes + audiences)', () => {
279+
describe('oauthProvider plugin wiring (DCR + scopes — options we pass, not behaviour)', () => {
276280
async function capturePluginOpts(env: Record<string, string>): Promise<any> {
277281
for (const [k, v] of Object.entries(env)) process.env[k] = v;
278282
(betterAuth as any).mockImplementation((config: any) => ({ handler: vi.fn(), api: {}, _cfg: config }));
@@ -297,19 +301,13 @@ describe('oauthProvider plugin wiring (DCR + scopes + audiences)', () => {
297301
expect(opts.allowUnauthenticatedClientRegistration).toBe(true);
298302
for (const scope of MCP_OAUTH_SCOPES) expect(opts.scopes).toContain(scope);
299303
expect(opts.scopes).toEqual(expect.arrayContaining(['openid', 'profile', 'email', 'offline_access']));
300-
// RFC 8707: the MCP resource must be a valid audience or token minting fails.
301-
expect(opts.validAudiences).toContain('https://acme.example.com/api/v1/mcp');
302-
expect(opts.validAudiences).toContain('https://acme.example.com/api/v1/auth');
303-
});
304-
305-
it('silences the false-positive oauthAuthServerConfig warning (#3420)', async () => {
306-
// registerOidcDiscoveryRoutes mounts /.well-known/oauth-authorization-server
307-
// (and the /api/v1/auth path-insertion variant) at the issuer ROOT ourselves,
308-
// so better-auth's boot-time "Please ensure … exists" reminder is a false
309-
// positive. It must be silenced via the documented option, or the stock
310-
// showcase prints it (twice) on every `os dev`. Regression guard for the fix.
311-
const opts = await capturePluginOpts({ OS_MCP_SERVER_ENABLED: 'true' });
312-
expect(opts.silenceWarnings).toEqual({ oauthAuthServerConfig: true });
304+
// ⛔ RFC 8707 audience binding is NOT asserted here. The subject available
305+
// in this describe block is the options object we passed in, and the
306+
// provider never consumes it — an assertion on it is green whether or not
307+
// the installed version reads the option, which is exactly how a dead
308+
// `validAudiences` survived a version bump. The refutable form lives in
309+
// auth-manager.mcp-oauth-resource.test.ts, which boots the REAL provider
310+
// and drives `authorize?resource=<mcp url>` to a minted token.
313311
});
314312

315313
it('OS_OIDC_DCR_ENABLED=false forces DCR off even with MCP on', async () => {

packages/plugins/plugin-auth/src/auth-manager.test.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,15 @@ describe('AuthManager', () => {
333333
}));
334334
});
335335

336-
it('should return undefined (in-memory fallback) when no dataEngine is provided', async () => {
336+
// ⛔ This used to assert `database === undefined` — "let better-auth build
337+
// its own in-memory store". That store is keyed by the schema KEY while
338+
// every read resolves by `modelName`, so on better-auth 1.7.2 EVERY model
339+
// this package renames (`user`/`sys_user`, `oauthResource`/
340+
// `sys_oauth_resource`, …) was unreachable on that path with
341+
// "Model <name> not found". The old assertion could not see that: it read
342+
// the value we passed, never what the value does. This one drives the
343+
// factory and asks the adapter for a renamed model.
344+
it('the no-dataEngine fallback yields an adapter that can resolve a RENAMED model', async () => {
337345
let capturedConfig: any;
338346
(betterAuth as any).mockImplementation((config: any) => {
339347
capturedConfig = config;
@@ -348,9 +356,17 @@ describe('AuthManager', () => {
348356
});
349357

350358
await manager.getAuthInstance();
351-
352-
expect(capturedConfig.database).toBeUndefined();
353359
warnSpy.mockRestore();
360+
361+
// An AdapterFactory, not `undefined`.
362+
expect(typeof capturedConfig.database).toBe('function');
363+
364+
const adapter = capturedConfig.database(capturedConfig);
365+
// `sys_user` is `user` renamed via `modelName`; a store keyed by the
366+
// schema key answers "Model sys_user not found" here instead of `null`.
367+
await expect(
368+
adapter.findOne({ model: 'sys_user', where: [{ field: 'id', value: 'nobody' }] }),
369+
).resolves.toBeNull();
354370
});
355371
});
356372

packages/plugins/plugin-auth/src/auth-manager.ts

Lines changed: 99 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,7 +1256,7 @@ export class AuthManager {
12561256
basePath: this.configuredBasePath(),
12571257

12581258
// Database adapter configuration
1259-
database: this.createDatabaseConfig(),
1259+
database: await this.createDatabaseConfig(),
12601260

12611261
// Model/field mapping: camelCase (better-auth) → snake_case (ObjectStack)
12621262
// These declarations tell better-auth the actual table/column names used
@@ -2417,7 +2417,27 @@ export class AuthManager {
24172417
} : {}),
24182418
};
24192419

2420-
return betterAuth(betterAuthConfig);
2420+
const auth = betterAuth(betterAuthConfig);
2421+
2422+
// ⛔ Do not return before better-auth's plugin `init` hooks have settled.
2423+
//
2424+
// `betterAuth()` returns synchronously and runs those hooks behind
2425+
// `auth.$context`, so anything a plugin does at init is a promise NOBODY
2426+
// holds. That was harmless while init did no I/O. It stopped being harmless
2427+
// when the oauth-provider began seeding the RFC 8707 `sys_oauth_resource`
2428+
// row from its own `init`: a failure there had no catcher and surfaced as
2429+
// an UNHANDLED REJECTION — which Node terminates the process for by default
2430+
// — and, in tests, as a boot write racing its engine's teardown and failing
2431+
// with "No driver available for object 'sys_oauth_resource'" long after the
2432+
// test that triggered it had passed.
2433+
//
2434+
// Awaiting it here makes the seed part of "the instance is ready": a boot
2435+
// failure now rejects THIS call, where callers can see and handle it,
2436+
// instead of escaping the stack. `$context` is absent when better-auth is
2437+
// mocked, and `await undefined` is a no-op, so this is safe on that path.
2438+
await (auth as { $context?: Promise<unknown> } | undefined)?.$context;
2439+
2440+
return auth;
24212441
}
24222442

24232443
/**
@@ -3463,34 +3483,54 @@ export class AuthManager {
34633483
loginPage: this.getConsolePageUrl('/login'),
34643484
consentPage: this.getConsolePageUrl('/oauth/consent'),
34653485
schema: buildOauthProviderPluginSchema(),
3466-
// better-auth's oauth-provider cannot see the well-known documents we
3467-
// mount ourselves at the issuer ROOT (RFC 8414 §3 requires them there,
3468-
// not under the auth basePath) — registerOidcDiscoveryRoutes serves
3469-
// /.well-known/oauth-authorization-server AND the path-insertion variant
3470-
// (`…/api/v1/auth`) the notice names. Its "Please ensure … exists"
3471-
// reminder is therefore a false positive on every stock example.
3472-
//
3473-
// #3420 root cause of the DOUBLE print: the notice fires in the
3474-
// oauth-provider plugin's `init(ctx)`, which better-auth runs once per
3475-
// `betterAuth()` construction — and the instance is built more than once
3476-
// at boot (an initial lazy build, then a rebuild once boot-time auth
3477-
// *settings* are applied — applyConfigPatch() nulls the cached instance
3478-
// so the next request rebuilds with the new policy). Gating the emitter
3479-
// here silences the one requirement we've already satisfied across every
3480-
// build path, independent of how many times auth is constructed, so an
3481-
// official dev boot stays warning-free.
3482-
silenceWarnings: { oauthAuthServerConfig: true },
3486+
// ⛔ No `silenceWarnings` here. It was added for the #3420 double
3487+
// print of oauth-provider's "Please ensure /.well-known/… exists"
3488+
// notice — a false positive, because registerOidcDiscoveryRoutes
3489+
// mounts those documents at the issuer ROOT where RFC 8414 §3 requires
3490+
// them. The pinned 1.7.2 emits no such notice: neither the option name
3491+
// nor the `oauthAuthServerConfig` key nor the notice text occurs
3492+
// anywhere in `@better-auth/oauth-provider` or `better-auth`, so the
3493+
// option silenced nothing and was the same dead-option shape as the
3494+
// `validAudiences` defect below. If a future bump reintroduces the
3495+
// notice, re-add the silencer with a fresh reading — do NOT restore it
3496+
// on the strength of this comment.
34833497
// ── MCP OAuth track (#2698) ────────────────────────────────
34843498
// Coarse tool-family scopes for the platform's own MCP endpoint,
34853499
// advertised alongside the standard OIDC scopes. Names are
34863500
// single-sourced in @objectstack/spec so AS / resource server /
34873501
// tool layer cannot drift.
34883502
scopes: ['openid', 'profile', 'email', 'offline_access', ...MCP_OAUTH_SCOPES],
34893503
// MCP clients bind tokens to the resource via RFC 8707
3490-
// (`resource=<mcp url>`); the AS only mints audiences it knows.
3491-
// The auth base (better-auth's default audience) stays valid for
3492-
// plain OIDC SSO flows.
3493-
validAudiences: [this.getAuthIssuer(), this.getMcpResourceUrl()],
3504+
// (`resource=<mcp url>`). In @better-auth/oauth-provider 1.7.2 a
3505+
// requested `resource` is resolved from the `oauthResource` table
3506+
// (`sys_oauth_resource`) — a miss is refused at /oauth2/authorize with
3507+
// `invalid_target: requested resource <id> is not configured` — and
3508+
// `enforcePerClientResources` defaults to TRUE, so the client must
3509+
// additionally be linked in `oauthClientResource`
3510+
// (`sys_oauth_client_resource`). Both rows have to exist before the
3511+
// first Connect, so both are declared here:
3512+
//
3513+
// • `resources` seeds the sys_oauth_resource row from the plugin's
3514+
// own `init` (idempotent, `resourceSeedMode: "insertOnly"` by
3515+
// default, so an admin's later CRUD edits are never reverted);
3516+
// • `clientRegistrationDefaultResources` links every newly
3517+
// registered client to it inside the DCR transaction — a client
3518+
// that registers anonymously one second before the login cannot
3519+
// be linked by an admin in between.
3520+
//
3521+
// ⛔ `enforcePerClientResources` is deliberately NOT passed: the
3522+
// per-client linkage check stays at its `true` default. The fix makes
3523+
// the link happen; it does not switch the check off. A client with no
3524+
// link row is still refused, and
3525+
// auth-manager.mcp-oauth-resource.test.ts asserts exactly that.
3526+
//
3527+
// ⛔ Do not reintroduce `validAudiences`: 1.7.2 reads no such option
3528+
// (0 occurrences in its dist), and audience validation now runs
3529+
// through the resource table instead. A field that is passed and read
3530+
// by nobody looks like configuration and enforces nothing — that is
3531+
// how this defect survived a version bump.
3532+
resources: [this.getMcpResourceUrl()],
3533+
clientRegistrationDefaultResources: [this.getMcpResourceUrl()],
34943534
// RFC 7591 Dynamic Client Registration. `allowUnauthenticated…` is
34953535
// required: MCP clients register BEFORE any user is logged in (the
34963536
// whole point of the self-serve flow). Registration is rate-limited
@@ -3765,7 +3805,7 @@ export class AuthManager {
37653805
* silently. We therefore wrap the ObjectQL adapter in a factory function
37663806
* so it is correctly recognised as a `DBAdapterInstance`.
37673807
*/
3768-
private createDatabaseConfig(): any {
3808+
private async createDatabaseConfig(): Promise<any> {
37693809
// Use ObjectQL adapter factory if dataEngine is provided
37703810
if (this.config.dataEngine) {
37713811
// createObjectQLAdapterFactory returns an AdapterFactory
@@ -3783,9 +3823,41 @@ export class AuthManager {
37833823
'Please provide a dataEngine instance (e.g., ObjectQL) in AuthManagerOptions.'
37843824
);
37853825

3786-
// Return a minimal in-memory configuration as fallback
3787-
// This allows the system to work in development/testing without a real database
3788-
return undefined; // better-auth will use its default in-memory adapter
3826+
// ⛔ NOT `undefined`, and ⛔ do not "simplify" it back to that.
3827+
//
3828+
// Handing better-auth no `database` makes it build its own in-memory store
3829+
// in `getBaseAdapter`, and that store is keyed by the schema KEY while
3830+
// every read resolves by `modelName`. Measured on better-auth 1.7.2:
3831+
//
3832+
// getAuthTables(options) -> { oauthResource: { modelName: 'sys_oauth_resource' }, … }
3833+
// its memoryDB -> { oauthResource: [] } // keyed by KEY
3834+
// the adapter then asks -> 'sys_oauth_resource' // resolved by modelName
3835+
// => Error: Model sys_oauth_resource not found
3836+
//
3837+
// So on that path EVERY model this package renames is unreachable —
3838+
// `user`/`sys_user` included. It stayed invisible for as long as nothing
3839+
// touched a renamed model during boot; the RFC 8707 resource seed does,
3840+
// from the oauth-provider plugin's `init`, where the throw surfaces as an
3841+
// UNHANDLED REJECTION rather than a failed request.
3842+
//
3843+
// Keying the store by `modelName` is what the adapter actually reads, so
3844+
// this fixes the dev/test fallback instead of working around it. Production
3845+
// never reaches this branch — it returns the ObjectQL factory above.
3846+
//
3847+
// The import is dynamic on purpose (the rest of better-auth is loaded the
3848+
// same way here); `createAuthInstance` awaits this method, and better-auth
3849+
// then calls the returned factory SYNCHRONOUSLY, so the module has to be
3850+
// resolved before we hand it over, not inside it.
3851+
const [{ memoryAdapter }, { getAuthTables }] = await Promise.all([
3852+
import('better-auth/adapters/memory'),
3853+
import('@better-auth/core/db'),
3854+
]);
3855+
return (options: any) => {
3856+
const tables = getAuthTables(options) as Record<string, { modelName?: string }>;
3857+
const db: Record<string, unknown[]> = {};
3858+
for (const [key, table] of Object.entries(tables)) db[table?.modelName ?? key] = [];
3859+
return memoryAdapter(db)(options);
3860+
};
37893861
}
37903862

37913863
/**

0 commit comments

Comments
 (0)