@burdenoff/be-sdk 2026.527.2 → 2026.527.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,265 @@
1
+ import { logger } from './chunk-CBZNP6RF.js';
2
+ import { __name } from './chunk-XE4OXN2W.js';
3
+ import { GraphQLError } from 'graphql';
4
+
5
+ var RESOURCE_CAP_DEFAULTS = {
6
+ workspace: {
7
+ free: 3,
8
+ paidDefault: 50,
9
+ scope: "user"
10
+ },
11
+ workspaceMember: {
12
+ free: 5,
13
+ paidDefault: 500,
14
+ scope: "workspace"
15
+ },
16
+ sandbox: {
17
+ free: 2,
18
+ paidDefault: 50,
19
+ scope: "user"
20
+ },
21
+ vibe: {
22
+ free: 10,
23
+ paidDefault: 500,
24
+ scope: "user"
25
+ },
26
+ agent: {
27
+ free: 2,
28
+ paidDefault: 20,
29
+ scope: "workspace"
30
+ },
31
+ conversation: {
32
+ free: 20,
33
+ paidDefault: 2e3,
34
+ scope: "user/day"
35
+ },
36
+ message: {
37
+ free: 200,
38
+ paidDefault: 2e4,
39
+ scope: "user/day"
40
+ },
41
+ tag: {
42
+ free: 50,
43
+ paidDefault: 5e3,
44
+ scope: "workspace"
45
+ },
46
+ file: {
47
+ free: 100,
48
+ paidDefault: 1e4,
49
+ scope: "user"
50
+ },
51
+ group: {
52
+ free: 5,
53
+ paidDefault: 500,
54
+ scope: "workspace"
55
+ },
56
+ calendar: {
57
+ free: 3,
58
+ paidDefault: 100,
59
+ scope: "user"
60
+ },
61
+ scheduledTask: {
62
+ free: 5,
63
+ paidDefault: 500,
64
+ scope: "user"
65
+ },
66
+ form: {
67
+ free: 5,
68
+ paidDefault: 500,
69
+ scope: "user"
70
+ },
71
+ apiKey: {
72
+ free: 2,
73
+ paidDefault: 50,
74
+ scope: "user"
75
+ },
76
+ webhook: {
77
+ free: 2,
78
+ paidDefault: 100,
79
+ scope: "workspace"
80
+ }
81
+ };
82
+ var ResourceCapExceededError = class extends GraphQLError {
83
+ static {
84
+ __name(this, "ResourceCapExceededError");
85
+ }
86
+ constructor(detail) {
87
+ super(`Resource cap exceeded for ${detail.resource}: ${detail.currentCount}/${detail.limit} on plan '${detail.planName}'.`, {
88
+ extensions: {
89
+ code: "RESOURCE_CAP_EXCEEDED",
90
+ resource: detail.resource,
91
+ currentCount: detail.currentCount,
92
+ limit: detail.limit,
93
+ planName: detail.planName
94
+ }
95
+ });
96
+ this.name = "ResourceCapExceededError";
97
+ }
98
+ };
99
+ var PLAN_QUERY = (
100
+ /* GraphQL */
101
+ `
102
+ query GetUserPlanForCaps($userId: ID!) {
103
+ userBillingPlan(userId: $userId) {
104
+ planName
105
+ tier
106
+ resourceCaps
107
+ }
108
+ }
109
+ `
110
+ );
111
+ var ResourceCapsModule = class {
112
+ static {
113
+ __name(this, "ResourceCapsModule");
114
+ }
115
+ client;
116
+ cacheTtlMs;
117
+ defaultTier;
118
+ planCache = /* @__PURE__ */ new Map();
119
+ constructor(config = {}) {
120
+ this.client = config.client;
121
+ this.cacheTtlMs = config.cacheTtlMs ?? 6e4;
122
+ this.defaultTier = config.defaultTier ?? "free";
123
+ }
124
+ /**
125
+ * Resolve the cap for a given resource for the supplied user.
126
+ * Order: plan-override → plan-tier-default → static free default.
127
+ */
128
+ async getResourceCap(userId, resource) {
129
+ if (!userId) {
130
+ throw new Error("getResourceCap: userId is required");
131
+ }
132
+ const defaults = RESOURCE_CAP_DEFAULTS[resource];
133
+ if (!defaults) {
134
+ throw new Error(`getResourceCap: unknown resource '${resource}'`);
135
+ }
136
+ const plan = await this.resolvePlan(userId);
137
+ const override = plan.overrides[resource];
138
+ if (typeof override === "number" && override >= 0) {
139
+ return {
140
+ resource,
141
+ limit: override,
142
+ planName: plan.planName,
143
+ planTier: plan.planTier,
144
+ isFallback: false
145
+ };
146
+ }
147
+ const limit = plan.planTier === "paid" ? defaults.paidDefault : defaults.free;
148
+ return {
149
+ resource,
150
+ limit,
151
+ planName: plan.planName,
152
+ planTier: plan.planTier,
153
+ isFallback: plan.planName === "fallback-free" || plan.planName === "fallback-paid"
154
+ };
155
+ }
156
+ /**
157
+ * Throws ResourceCapExceededError when currentCount >= limit.
158
+ * Use BEFORE the create transaction to avoid orphan rows.
159
+ */
160
+ async enforceResourceCap(userId, resource, currentCount) {
161
+ const cap = await this.getResourceCap(userId, resource);
162
+ if (currentCount >= cap.limit) {
163
+ throw new ResourceCapExceededError({
164
+ resource,
165
+ currentCount,
166
+ limit: cap.limit,
167
+ planName: cap.planName
168
+ });
169
+ }
170
+ return cap;
171
+ }
172
+ /** Invalidate cache for a user (e.g. after plan upgrade) */
173
+ invalidate(userId) {
174
+ this.planCache.delete(userId);
175
+ }
176
+ /** Clear entire plan cache */
177
+ invalidateAll() {
178
+ this.planCache.clear();
179
+ }
180
+ // -------------------------------------------------------------------------
181
+ // Internals
182
+ // -------------------------------------------------------------------------
183
+ async resolvePlan(userId) {
184
+ const cached = this.planCache.get(userId);
185
+ if (cached && Date.now() - cached.fetchedAt < this.cacheTtlMs) {
186
+ return cached;
187
+ }
188
+ const plan = await this.fetchPlan(userId);
189
+ this.planCache.set(userId, plan);
190
+ return plan;
191
+ }
192
+ async fetchPlan(userId) {
193
+ if (!this.client) {
194
+ return this.fallbackPlan();
195
+ }
196
+ try {
197
+ const res = await this.client.query(PLAN_QUERY, {
198
+ userId
199
+ });
200
+ const planNode = res?.userBillingPlan;
201
+ if (!planNode) {
202
+ return this.fallbackPlan();
203
+ }
204
+ const tier = normaliseTier(planNode.tier, this.defaultTier);
205
+ const overrides = sanitizeOverrides(planNode.resourceCaps);
206
+ return {
207
+ planName: planNode.planName ?? (tier === "paid" ? "paid" : "free"),
208
+ planTier: tier,
209
+ overrides,
210
+ fetchedAt: Date.now()
211
+ };
212
+ } catch (err) {
213
+ logger.warn("resource-caps: plan lookup failed; falling back to default tier", {
214
+ err,
215
+ userId
216
+ });
217
+ return this.fallbackPlan();
218
+ }
219
+ }
220
+ fallbackPlan() {
221
+ return {
222
+ planName: this.defaultTier === "paid" ? "fallback-paid" : "fallback-free",
223
+ planTier: this.defaultTier,
224
+ overrides: {},
225
+ fetchedAt: Date.now()
226
+ };
227
+ }
228
+ };
229
+ function normaliseTier(raw, fallback) {
230
+ if (!raw) return fallback;
231
+ const lc = raw.toLowerCase();
232
+ if (lc === "free" || lc === "paid") return lc;
233
+ if (lc === "enterprise" || lc === "business" || lc === "pro" || lc === "team") {
234
+ return "paid";
235
+ }
236
+ return fallback;
237
+ }
238
+ __name(normaliseTier, "normaliseTier");
239
+ function sanitizeOverrides(raw) {
240
+ if (!raw || typeof raw !== "object") return {};
241
+ const out = {};
242
+ for (const [key, value] of Object.entries(raw)) {
243
+ if (key in RESOURCE_CAP_DEFAULTS && typeof value === "number" && Number.isFinite(value)) {
244
+ out[key] = Math.max(0, Math.floor(value));
245
+ }
246
+ }
247
+ return out;
248
+ }
249
+ __name(sanitizeOverrides, "sanitizeOverrides");
250
+ var defaultInstance = null;
251
+ function getResourceCaps(config) {
252
+ if (!defaultInstance) {
253
+ defaultInstance = new ResourceCapsModule(config);
254
+ }
255
+ return defaultInstance;
256
+ }
257
+ __name(getResourceCaps, "getResourceCaps");
258
+ function __resetResourceCapsForTests() {
259
+ defaultInstance = null;
260
+ }
261
+ __name(__resetResourceCapsForTests, "__resetResourceCapsForTests");
262
+
263
+ export { RESOURCE_CAP_DEFAULTS, ResourceCapExceededError, ResourceCapsModule, __resetResourceCapsForTests, getResourceCaps };
264
+ //# sourceMappingURL=chunk-7BMKOD7N.js.map
265
+ //# sourceMappingURL=chunk-7BMKOD7N.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/resource-caps/index.ts"],"names":["RESOURCE_CAP_DEFAULTS","workspace","free","paidDefault","scope","workspaceMember","sandbox","vibe","agent","conversation","message","tag","file","group","calendar","scheduledTask","form","apiKey","webhook","ResourceCapExceededError","GraphQLError","detail","resource","currentCount","limit","planName","extensions","code","name","PLAN_QUERY","ResourceCapsModule","client","cacheTtlMs","defaultTier","planCache","Map","config","getResourceCap","userId","Error","defaults","plan","resolvePlan","override","overrides","planTier","isFallback","enforceResourceCap","cap","invalidate","delete","invalidateAll","clear","cached","get","Date","now","fetchedAt","fetchPlan","set","fallbackPlan","res","query","planNode","userBillingPlan","tier","normaliseTier","sanitizeOverrides","resourceCaps","err","logger","warn","raw","fallback","lc","toLowerCase","out","key","value","Object","entries","Number","isFinite","Math","max","floor","defaultInstance","getResourceCaps","__resetResourceCapsForTests"],"mappings":"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","file":"chunk-7BMKOD7N.js","sourcesContent":["/**\n * Resource Caps Module\n *\n * Enforces per-user / per-workspace resource ceilings to prevent unbounded\n * resource use on a multi-tenant SaaS. Caps come from the user's billing plan\n * (resolved via global-billing-svc) and fall back to static defaults defined\n * here when the plan lookup fails or the resource isn't overridden.\n *\n * Two plan tiers exist by default:\n * - free: tight per-user / per-workspace ceilings\n * - paid (default): ~100x looser; individual paid plans can override via the\n * `Plan.resourceCaps` JSON column in global-billing-svc.\n *\n * @example\n * ```ts\n * import { resourceCaps } from '@burdenoff/be-sdk/resource-caps';\n *\n * const { limit } = await resourceCaps.getResourceCap(userId, 'tag');\n * await resourceCaps.enforceResourceCap(userId, 'tag', currentCount);\n * ```\n */\n\nimport { GraphQLError } from 'graphql';\n\nimport type { BurdenoffGraphQLClient } from '../client/graphql.js';\nimport { logger } from '../logger/index.js';\n\n// ---------------------------------------------------------------------------\n// Resource registry\n// ---------------------------------------------------------------------------\n\n/**\n * Canonical resource names. Adding a new capped resource? Append it here,\n * extend RESOURCE_CAP_DEFAULTS, and reference it from the relevant svc.\n */\nexport type ResourceName =\n | 'workspace' // per user\n | 'workspaceMember' // per workspace\n | 'sandbox' // per user\n | 'vibe' // per user\n | 'agent' // per workspace\n | 'conversation' // per user/day\n | 'message' // per user/day\n | 'tag' // per workspace\n | 'file' // per user (count only)\n | 'group' // per workspace\n | 'calendar' // per user\n | 'scheduledTask' // per user\n | 'form' // per user\n | 'apiKey' // per user\n | 'webhook'; // per workspace\n\nexport type PlanTier = 'free' | 'paid';\n\nexport interface ResourceCapDefaults {\n free: number;\n paidDefault: number;\n /** Optional scope hint used by callers when wiring guards */\n scope: 'user' | 'workspace' | 'user/day';\n}\n\n/**\n * Static fallback caps. Real values may be overridden per plan in\n * global-billing-svc Plan.resourceCaps JSON.\n */\nexport const RESOURCE_CAP_DEFAULTS: Record<ResourceName, ResourceCapDefaults> = {\n workspace: { free: 3, paidDefault: 50, scope: 'user' },\n workspaceMember: { free: 5, paidDefault: 500, scope: 'workspace' },\n sandbox: { free: 2, paidDefault: 50, scope: 'user' },\n vibe: { free: 10, paidDefault: 500, scope: 'user' },\n agent: { free: 2, paidDefault: 20, scope: 'workspace' },\n conversation: { free: 20, paidDefault: 2000, scope: 'user/day' },\n message: { free: 200, paidDefault: 20000, scope: 'user/day' },\n tag: { free: 50, paidDefault: 5000, scope: 'workspace' },\n file: { free: 100, paidDefault: 10000, scope: 'user' },\n group: { free: 5, paidDefault: 500, scope: 'workspace' },\n calendar: { free: 3, paidDefault: 100, scope: 'user' },\n scheduledTask: { free: 5, paidDefault: 500, scope: 'user' },\n form: { free: 5, paidDefault: 500, scope: 'user' },\n apiKey: { free: 2, paidDefault: 50, scope: 'user' },\n webhook: { free: 2, paidDefault: 100, scope: 'workspace' },\n};\n\n// ---------------------------------------------------------------------------\n// Types & errors\n// ---------------------------------------------------------------------------\n\nexport interface ResourceCap {\n resource: ResourceName;\n limit: number;\n planName: string;\n planTier: PlanTier;\n /** True when the cap came from defaults, not from the billing service */\n isFallback: boolean;\n}\n\nexport interface ResourceCapsConfig {\n client?: BurdenoffGraphQLClient;\n /** Optional in-memory TTL for plan lookups; defaults to 60s */\n cacheTtlMs?: number;\n /** Default plan tier when billing lookup fails */\n defaultTier?: PlanTier;\n}\n\ninterface CachedPlan {\n planName: string;\n planTier: PlanTier;\n overrides: Partial<Record<ResourceName, number>>;\n fetchedAt: number;\n}\n\n/**\n * Thrown when an entity create operation would push the user past the cap.\n * The matching GraphQL error has extensions.code = RESOURCE_CAP_EXCEEDED\n * so clients can detect and route to an upgrade flow.\n */\nexport class ResourceCapExceededError extends GraphQLError {\n constructor(detail: { resource: ResourceName; currentCount: number; limit: number; planName: string }) {\n super(\n `Resource cap exceeded for ${detail.resource}: ${detail.currentCount}/${detail.limit} on plan '${detail.planName}'.`,\n {\n extensions: {\n code: 'RESOURCE_CAP_EXCEEDED',\n resource: detail.resource,\n currentCount: detail.currentCount,\n limit: detail.limit,\n planName: detail.planName,\n },\n }\n );\n this.name = 'ResourceCapExceededError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Module\n// ---------------------------------------------------------------------------\n\nconst PLAN_QUERY = /* GraphQL */ `\n query GetUserPlanForCaps($userId: ID!) {\n userBillingPlan(userId: $userId) {\n planName\n tier\n resourceCaps\n }\n }\n`;\n\ninterface PlanQueryResponse {\n userBillingPlan?: {\n planName?: string | null;\n tier?: string | null;\n resourceCaps?: Record<string, number> | null;\n } | null;\n}\n\nexport class ResourceCapsModule {\n private readonly client?: BurdenoffGraphQLClient;\n private readonly cacheTtlMs: number;\n private readonly defaultTier: PlanTier;\n private readonly planCache = new Map<string, CachedPlan>();\n\n constructor(config: ResourceCapsConfig = {}) {\n this.client = config.client;\n this.cacheTtlMs = config.cacheTtlMs ?? 60_000;\n this.defaultTier = config.defaultTier ?? 'free';\n }\n\n /**\n * Resolve the cap for a given resource for the supplied user.\n * Order: plan-override → plan-tier-default → static free default.\n */\n async getResourceCap(userId: string, resource: ResourceName): Promise<ResourceCap> {\n if (!userId) {\n throw new Error('getResourceCap: userId is required');\n }\n const defaults = RESOURCE_CAP_DEFAULTS[resource];\n if (!defaults) {\n throw new Error(`getResourceCap: unknown resource '${resource}'`);\n }\n\n const plan = await this.resolvePlan(userId);\n const override = plan.overrides[resource];\n if (typeof override === 'number' && override >= 0) {\n return {\n resource,\n limit: override,\n planName: plan.planName,\n planTier: plan.planTier,\n isFallback: false,\n };\n }\n\n const limit = plan.planTier === 'paid' ? defaults.paidDefault : defaults.free;\n return {\n resource,\n limit,\n planName: plan.planName,\n planTier: plan.planTier,\n isFallback: plan.planName === 'fallback-free' || plan.planName === 'fallback-paid',\n };\n }\n\n /**\n * Throws ResourceCapExceededError when currentCount >= limit.\n * Use BEFORE the create transaction to avoid orphan rows.\n */\n async enforceResourceCap(userId: string, resource: ResourceName, currentCount: number): Promise<ResourceCap> {\n const cap = await this.getResourceCap(userId, resource);\n if (currentCount >= cap.limit) {\n throw new ResourceCapExceededError({\n resource,\n currentCount,\n limit: cap.limit,\n planName: cap.planName,\n });\n }\n return cap;\n }\n\n /** Invalidate cache for a user (e.g. after plan upgrade) */\n invalidate(userId: string): void {\n this.planCache.delete(userId);\n }\n\n /** Clear entire plan cache */\n invalidateAll(): void {\n this.planCache.clear();\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private async resolvePlan(userId: string): Promise<CachedPlan> {\n const cached = this.planCache.get(userId);\n if (cached && Date.now() - cached.fetchedAt < this.cacheTtlMs) {\n return cached;\n }\n\n const plan = await this.fetchPlan(userId);\n this.planCache.set(userId, plan);\n return plan;\n }\n\n private async fetchPlan(userId: string): Promise<CachedPlan> {\n if (!this.client) {\n return this.fallbackPlan();\n }\n try {\n const res = await this.client.query<PlanQueryResponse>(PLAN_QUERY, { userId });\n const planNode = res?.userBillingPlan;\n if (!planNode) {\n return this.fallbackPlan();\n }\n const tier = normaliseTier(planNode.tier, this.defaultTier);\n const overrides = sanitizeOverrides(planNode.resourceCaps);\n return {\n planName: planNode.planName ?? (tier === 'paid' ? 'paid' : 'free'),\n planTier: tier,\n overrides,\n fetchedAt: Date.now(),\n };\n } catch (err) {\n logger.warn('resource-caps: plan lookup failed; falling back to default tier', {\n err,\n userId,\n });\n return this.fallbackPlan();\n }\n }\n\n private fallbackPlan(): CachedPlan {\n return {\n planName: this.defaultTier === 'paid' ? 'fallback-paid' : 'fallback-free',\n planTier: this.defaultTier,\n overrides: {},\n fetchedAt: Date.now(),\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction normaliseTier(raw: string | null | undefined, fallback: PlanTier): PlanTier {\n if (!raw) return fallback;\n const lc = raw.toLowerCase();\n if (lc === 'free' || lc === 'paid') return lc;\n // Treat anything non-free as paid (enterprise, business, pro, etc.)\n if (lc === 'enterprise' || lc === 'business' || lc === 'pro' || lc === 'team') {\n return 'paid';\n }\n return fallback;\n}\n\nfunction sanitizeOverrides(raw: Record<string, number> | null | undefined): Partial<Record<ResourceName, number>> {\n if (!raw || typeof raw !== 'object') return {};\n const out: Partial<Record<ResourceName, number>> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (key in RESOURCE_CAP_DEFAULTS && typeof value === 'number' && Number.isFinite(value)) {\n out[key as ResourceName] = Math.max(0, Math.floor(value));\n }\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Convenience singleton\n// ---------------------------------------------------------------------------\n\nlet defaultInstance: ResourceCapsModule | null = null;\n\n/**\n * Obtain a process-wide singleton. The first call binds the GraphQL client\n * (typically the SDK's internal client). Later calls return the same instance.\n */\nexport function getResourceCaps(config?: ResourceCapsConfig): ResourceCapsModule {\n if (!defaultInstance) {\n defaultInstance = new ResourceCapsModule(config);\n }\n return defaultInstance;\n}\n\n/** Reset the singleton (for tests) */\nexport function __resetResourceCapsForTests(): void {\n defaultInstance = null;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ import { B as BurdenoffGraphQLClient } from './graphql-DH3L5Hj7.js';
17
17
  import { TemporalModule } from './temporal/index.js';
18
18
  export { TemporalClient, TemporalClientConfig, TemporalClientFactory, TemporalWorker, TemporalWorkerConfig, TemporalWorkerFactory, TemporalWorkerManager, WorkflowStartOptions } from './temporal/index.js';
19
19
  export { createInternalClient, createInternalClientForTenant, createInternalClientForWorkspaces } from './client/internal-client.js';
20
+ export { PlanTier, RESOURCE_CAP_DEFAULTS, ResourceCap, ResourceCapDefaults, ResourceCapExceededError, ResourceCapsConfig, ResourceCapsModule, ResourceName, getResourceCaps } from './resource-caps/index.js';
20
21
  export { i as Cache } from './index-cnFnfIjk.js';
21
22
  export { i as Middleware } from './index-DSOpj8DI.js';
22
23
  export { i as Observability } from './index-4gKQKZ2B.js';
@@ -45,6 +46,7 @@ import 'pg';
45
46
  import './prisma-extension-krXnYnrN.js';
46
47
  import '@temporalio/client';
47
48
  import '@temporalio/common';
49
+ import 'graphql';
48
50
  import 'elysia';
49
51
  import './types-Bo2wkjSP.js';
50
52
  import './graphql/services/index.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { prisma_exports as Prisma } from './chunk-BRYRHBAC.js';
2
+ export { RESOURCE_CAP_DEFAULTS, ResourceCapExceededError, ResourceCapsModule, getResourceCaps } from './chunk-7BMKOD7N.js';
2
3
  import './chunk-GTEMQXCA.js';
3
4
  export { TemporalClient, TemporalClientFactory, TemporalModule, TemporalWorker, TemporalWorkerFactory, TemporalWorkerManager } from './chunk-5SD7DU3L.js';
4
5
  import { ActivityModule, UsageModule, TrackerModule } from './chunk-PPIDJP2W.js';
@@ -32,8 +33,8 @@ export { layer_exports as Layer } from './chunk-GSEPF2B6.js';
32
33
  import { AuthModule } from './chunk-7NUQXGA7.js';
33
34
  export { AuthModule } from './chunk-7NUQXGA7.js';
34
35
  export { utils_exports as Utils } from './chunk-B7I4TABG.js';
35
- export { captcha_exports as Captcha, isCaptchaEnabled, verifyCaptcha, verifyCaptchaIfEnabled } from './chunk-YMZEUTD2.js';
36
36
  export { cache_exports as Cache } from './chunk-PDSV3TFO.js';
37
+ export { captcha_exports as Captcha, isCaptchaEnabled, verifyCaptcha, verifyCaptchaIfEnabled } from './chunk-YMZEUTD2.js';
37
38
  export { createInternalClient, createInternalClientForTenant, createInternalClientForWorkspaces } from './chunk-VI4VSNTI.js';
38
39
  export { crypto_exports as Crypto } from './chunk-SBS4S2PJ.js';
39
40
  import { DatabaseModule } from './chunk-UFQQSQG6.js';