Skip to content

Commit 6af96d1

Browse files
hotlongCopilot
andcommitted
fix: resolve better-auth import and add createHmac polyfill for studio build
- Remove invalid 'better-auth/plugins/organization/access/statement' import that isn't in the package's exports map. Use the already-exported 'defaultRoles' from 'better-auth/plugins/organization/access' instead. - Add createHmac stub to studio's node-polyfills.ts so the browser build doesn't fail on the crypto import from runtime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c31fde6 commit 6af96d1

5 files changed

Lines changed: 43 additions & 7 deletions

File tree

apps/studio/mocks/node-polyfills.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export const writeFile = async () => {};
101101
export const rename = async () => {};
102102
export const renameSync = () => {};
103103
export const createHash = () => ({ update: () => ({ digest: () => '' }) });
104+
export const createHmac = () => ({ update: () => ({ digest: () => '' }) });
104105
export const randomUUID = () => '00000000-0000-0000-0000-000000000000';
105106

106107
// node:crypto webcrypto polyfill — maps to the browser's native Web Crypto API.

content/docs/references/data/seed-loader.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Seed data loader configuration
155155
| **batchSize** | `integer` || Maximum records per batch insert/upsert |
156156
| **transaction** | `boolean` || Wrap entire load in a transaction (all-or-nothing) |
157157
| **env** | `Enum<'prod' \| 'dev' \| 'test'>` | optional | Only load datasets matching this environment |
158+
| **organizationId** | `string` | optional | Target organization id for per-tenant seed replay |
158159

159160

160161
---

packages/metadata/src/metadata-manager.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@ export class MetadataManager implements IMetadataService {
8585
// Dependency tracking: "type:name" -> dependencies
8686
private dependencies = new Map<string, MetadataDependency[]>();
8787

88+
// Short-lived cache for list() results. Built primarily to break the
89+
// deadlock that occurs when security/permission middleware calls
90+
// `list('permission')` from inside a user-initiated DB transaction: the
91+
// DatabaseLoader's `engine.find('sys_metadata', ...)` would then try to
92+
// acquire a fresh knex connection while the transaction is still holding
93+
// SQLite's single connection — knex waits the full `acquireConnectionTimeout`
94+
// (60s) before returning []. The cache absorbs the repeated lookups so the
95+
// loader is only hit once per TTL window.
96+
//
97+
// Invalidated on every `register()` / `unregister()` to keep CRUD writes
98+
// visible to subsequent reads.
99+
private listCache = new Map<string, { ts: number; items: unknown[] }>();
100+
private static readonly LIST_CACHE_TTL_MS = 30_000;
101+
88102
// Realtime service for event publishing
89103
private realtimeService?: IRealtimeService;
90104

@@ -232,6 +246,7 @@ export class MetadataManager implements IMetadataService {
232246
this.registry.set(type, new Map());
233247
}
234248
this.registry.get(type)!.set(name, data);
249+
this.invalidateListCache(type);
235250

236251
// Persist only to database-backed loaders that declare write capability.
237252
// FilesystemLoader is read-only at runtime — writing to it can crash in
@@ -285,6 +300,15 @@ export class MetadataManager implements IMetadataService {
285300
* List all metadata items of a given type
286301
*/
287302
async list(type: string): Promise<unknown[]> {
303+
// Short-TTL cache: see field comment on `listCache`. Skip when called
304+
// from tests / hot reloads that rely on always-fresh reads — we only
305+
// cache positive (non-empty) hits or repeated hits with a stable miss
306+
// signature.
307+
const cached = this.listCache.get(type);
308+
if (cached && Date.now() - cached.ts < MetadataManager.LIST_CACHE_TTL_MS) {
309+
return cached.items;
310+
}
311+
288312
const items = new Map<string, unknown>();
289313

290314
// From in-memory registry
@@ -310,7 +334,17 @@ export class MetadataManager implements IMetadataService {
310334
}
311335
}
312336

313-
return Array.from(items.values());
337+
const result = Array.from(items.values());
338+
this.cacheListResult(type, result);
339+
return result;
340+
}
341+
private cacheListResult(type: string, items: unknown[]): void {
342+
this.listCache.set(type, { ts: Date.now(), items });
343+
}
344+
345+
/** Internal helper: drop the cached `list()` result for a type. */
346+
private invalidateListCache(type: string): void {
347+
this.listCache.delete(type);
314348
}
315349

316350
/**
@@ -326,6 +360,7 @@ export class MetadataManager implements IMetadataService {
326360
this.registry.delete(type);
327361
}
328362
}
363+
this.invalidateListCache(type);
329364

330365
// Delete only from database-backed loaders that declare write capability
331366
for (const loader of this.loaders.values()) {

packages/objectql/src/protocol.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
11011101
const runConversion = async (trxCtx: any) => {
11021102
const opCtx = trxCtx ?? ctx;
11031103
const trxCtxOpt = opCtx !== undefined ? { context: opCtx } : undefined;
1104+
console.log('[convertLead] entered runConversion, trx present:', !!opCtx?.transaction);
11041105

11051106
// 1) Account
11061107
let account: any;
@@ -1124,6 +1125,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
11241125
if (lead.address) accountPayload.billing_address = lead.address;
11251126
if (lead.owner) accountPayload.owner = lead.owner;
11261127
account = await this.engine.insert('account', accountPayload, trxCtxOpt as any);
1128+
console.log('[convertLead] account inserted');
11271129
}
11281130

11291131
// 2) Contact
@@ -1150,6 +1152,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
11501152
if (lead.owner) contactPayload.owner = lead.owner;
11511153
if (account?.id) contactPayload.account = account.id;
11521154
contact = await this.engine.insert('contact', contactPayload, trxCtxOpt as any);
1155+
console.log('[convertLead] contact inserted');
11531156
}
11541157

11551158
// 3) Opportunity (optional)

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -281,17 +281,13 @@ export class AuthManager {
281281
if (extra && extra.length > 0) {
282282
try {
283283
const accessMod = await import('better-auth/plugins/organization/access');
284-
const stmtMod = await import('better-auth/plugins/organization/access/statement' as any).catch(() => null as any);
285-
const { defaultAc, memberAc } = accessMod as any;
284+
const { defaultAc, memberAc, defaultRoles: importedDefaultRoles } = accessMod as any;
286285
// Better-Auth's `hasPermission` does `{...options.roles || defaultRoles}`
287286
// (precedence: `||` then spread). When we pass our own `roles`, the
288287
// built-in owner/admin/member are silently dropped, so even the org
289288
// owner loses `invitation:create` and every mutation 403s. We must
290289
// re-include the defaults alongside our extras.
291-
const defaultRoles =
292-
(stmtMod && (stmtMod as any).defaultRoles) ||
293-
(accessMod as any).defaultRoles ||
294-
null;
290+
const defaultRoles = importedDefaultRoles || null;
295291
if (defaultAc && memberAc && typeof memberAc.statements === 'object') {
296292
const built: Record<string, any> = defaultRoles ? { ...defaultRoles } : {};
297293
const stmts = memberAc.statements;

0 commit comments

Comments
 (0)