@hachej/boring-core 0.1.64 → 0.1.65

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.
@@ -12,7 +12,7 @@ import {
12
12
  workspaceRuntimes,
13
13
  workspaceSettings,
14
14
  workspaces
15
- } from "./chunk-6ZY63QYN.js";
15
+ } from "./chunk-EFM5IWTK.js";
16
16
  import {
17
17
  noopTelemetry,
18
18
  safeCapture
@@ -599,6 +599,30 @@ async function closeDbPoolIfPresent(app) {
599
599
  }
600
600
  function installShutdownHandlers(app) {
601
601
  let shuttingDown = false;
602
+ const activeRequests = /* @__PURE__ */ new Set();
603
+ const drainWaiters = /* @__PURE__ */ new Set();
604
+ const notifyRequestDrained = () => {
605
+ if (activeRequests.size > 0) return;
606
+ for (const resolve3 of drainWaiters) resolve3();
607
+ drainWaiters.clear();
608
+ };
609
+ app.addHook("onRequest", async (request) => {
610
+ activeRequests.add(request.id);
611
+ });
612
+ app.addHook("onResponse", async (request) => {
613
+ activeRequests.delete(request.id);
614
+ notifyRequestDrained();
615
+ });
616
+ app.addHook("onError", async (request) => {
617
+ activeRequests.delete(request.id);
618
+ notifyRequestDrained();
619
+ });
620
+ const waitForInflightRequests = async () => {
621
+ if (activeRequests.size === 0) return;
622
+ await new Promise((resolve3) => {
623
+ drainWaiters.add(resolve3);
624
+ });
625
+ };
602
626
  const onSignal = async (signal) => {
603
627
  if (shuttingDown) return;
604
628
  shuttingDown = true;
@@ -606,7 +630,7 @@ function installShutdownHandlers(app) {
606
630
  try {
607
631
  let timeoutHandle;
608
632
  const result = await Promise.race([
609
- app.close(),
633
+ Promise.all([app.close(), waitForInflightRequests()]).then(() => "closed"),
610
634
  new Promise((resolve3) => {
611
635
  timeoutHandle = setTimeout(() => {
612
636
  resolve3("timeout");
@@ -1757,14 +1781,14 @@ function createPostSignupHook(deps) {
1757
1781
  import { betterAuth, APIError } from "better-auth";
1758
1782
  import { drizzleAdapter } from "better-auth/adapters/drizzle";
1759
1783
  import { magicLink } from "better-auth/plugins/magic-link";
1760
- import { zxcvbn, zxcvbnOptions } from "@zxcvbn-ts/core";
1784
+ import { ZxcvbnFactory } from "@zxcvbn-ts/core";
1761
1785
  import * as zxcvbnCommon from "@zxcvbn-ts/language-common";
1762
1786
  import * as zxcvbnEn from "@zxcvbn-ts/language-en";
1763
1787
  var MIN_ZXCVBN_SCORE = 2;
1764
- var zxcvbnInitialized = false;
1765
- function ensureZxcvbn() {
1766
- if (zxcvbnInitialized) return;
1767
- zxcvbnOptions.setOptions({
1788
+ var zxcvbnInstance = null;
1789
+ function getZxcvbn() {
1790
+ if (zxcvbnInstance) return zxcvbnInstance;
1791
+ zxcvbnInstance = new ZxcvbnFactory({
1768
1792
  translations: zxcvbnEn.translations,
1769
1793
  graphs: zxcvbnCommon.adjacencyGraphs,
1770
1794
  dictionary: {
@@ -1772,11 +1796,10 @@ function ensureZxcvbn() {
1772
1796
  ...zxcvbnEn.dictionary
1773
1797
  }
1774
1798
  });
1775
- zxcvbnInitialized = true;
1799
+ return zxcvbnInstance;
1776
1800
  }
1777
1801
  function validatePasswordStrength(password) {
1778
- ensureZxcvbn();
1779
- const result = zxcvbn(password);
1802
+ const result = getZxcvbn().check(password);
1780
1803
  if (result.score < MIN_ZXCVBN_SCORE) {
1781
1804
  return {
1782
1805
  valid: false,
@@ -1790,6 +1813,29 @@ function buildMailTransport(config) {
1790
1813
  const env = process.env.NODE_ENV === "production" ? "production" : process.env.NODE_ENV === "test" ? "test" : "development";
1791
1814
  return createMailTransport(config.auth.mail.transportUrl, config.auth.mail.from, env);
1792
1815
  }
1816
+ async function createReplayableRequest(request) {
1817
+ if (request.bodyUsed || request.method === "GET" || request.method === "HEAD") return request;
1818
+ const body2 = await request.text();
1819
+ const init = {
1820
+ method: request.method,
1821
+ headers: request.headers,
1822
+ body: body2,
1823
+ redirect: request.redirect,
1824
+ referrer: request.referrer,
1825
+ referrerPolicy: request.referrerPolicy,
1826
+ integrity: request.integrity,
1827
+ keepalive: request.keepalive,
1828
+ credentials: request.credentials,
1829
+ cache: request.cache,
1830
+ mode: request.mode
1831
+ };
1832
+ const replayable = new Request(request.url, init);
1833
+ Object.defineProperty(replayable, "clone", {
1834
+ configurable: true,
1835
+ value: () => new Request(request.url, init)
1836
+ });
1837
+ return replayable;
1838
+ }
1793
1839
  function createAuth(config, db, opts) {
1794
1840
  const transport = buildMailTransport(config);
1795
1841
  const telemetry = opts?.telemetry ?? noopTelemetry;
@@ -1848,7 +1894,7 @@ function createAuth(config, db, opts) {
1848
1894
  }
1849
1895
  } : {}
1850
1896
  };
1851
- return betterAuth({
1897
+ const auth = betterAuth({
1852
1898
  database: drizzleAdapter(db, { provider: "pg", schema: schema_exports }),
1853
1899
  secret: config.auth.secret,
1854
1900
  baseURL: config.auth.url,
@@ -1953,6 +1999,9 @@ function createAuth(config, db, opts) {
1953
1999
  socialProviders,
1954
2000
  plugins
1955
2001
  });
2002
+ const handler = auth.handler.bind(auth);
2003
+ auth.handler = async (request) => handler(await createReplayableRequest(request));
2004
+ return auth;
1956
2005
  }
1957
2006
 
1958
2007
  // src/server/auth/authHook.ts
@@ -2082,6 +2131,7 @@ var updateWorkspaceBody = z3.object({
2082
2131
 
2083
2132
  // src/server/routes/workspaces.ts
2084
2133
  var DEFAULT_WORKSPACE_NAME = "Default workspace";
2134
+ var COMPANY_CONTEXT_WORKSPACE_MANAGED_BY = "company-context";
2085
2135
  var workspaceRoutesPlugin = async (app) => {
2086
2136
  const store = app.workspaceStore;
2087
2137
  const provisioner = app.provisioner;
@@ -2235,6 +2285,23 @@ var workspaceRoutesPlugin = async (app) => {
2235
2285
  async (request) => {
2236
2286
  const { id } = request.params;
2237
2287
  request.log.info({ workspaceId: id }, "workspace.delete.start");
2288
+ const workspace = await store.get(id);
2289
+ if (!workspace) {
2290
+ throw new HttpError({
2291
+ status: 404,
2292
+ code: ERROR_CODES.NOT_FOUND,
2293
+ message: "Workspace not found",
2294
+ requestId: request.id
2295
+ });
2296
+ }
2297
+ if (workspace.managedBy === COMPANY_CONTEXT_WORKSPACE_MANAGED_BY) {
2298
+ throw new HttpError({
2299
+ status: 403,
2300
+ code: ERROR_CODES.FORBIDDEN,
2301
+ message: "Managed workspace cannot be deleted",
2302
+ requestId: request.id
2303
+ });
2304
+ }
2238
2305
  if (provisioner) {
2239
2306
  try {
2240
2307
  await provisioner.destroy(id);
@@ -13,13 +13,13 @@ function createDatabase(config) {
13
13
  if (!config.databaseUrl) {
14
14
  throw new Error("databaseUrl is required to create a database connection");
15
15
  }
16
- const sql7 = postgres(config.databaseUrl, {
16
+ const sql8 = postgres(config.databaseUrl, {
17
17
  max: 10,
18
18
  idle_timeout: 20,
19
19
  connect_timeout: 10
20
20
  });
21
- const db = drizzle(sql7);
22
- return { db, sql: sql7 };
21
+ const db = drizzle(sql8);
22
+ return { db, sql: sql8 };
23
23
  }
24
24
 
25
25
  // src/server/db/migrate.ts
@@ -151,15 +151,19 @@ var LocalWorkspaceStore = class {
151
151
  wsSettings = /* @__PURE__ */ new Map();
152
152
  uiStates = /* @__PURE__ */ new Map();
153
153
  async create(userId, name, appId, opts) {
154
+ const id = opts?.id ?? randomUUID();
155
+ const existing = opts?.id ? this.workspaces.get(id) : void 0;
156
+ if (existing) return existing;
154
157
  const now = (/* @__PURE__ */ new Date()).toISOString();
155
158
  const ws = {
156
- id: randomUUID(),
159
+ id,
157
160
  appId,
158
161
  name,
159
162
  createdBy: userId,
160
163
  createdAt: now,
161
164
  deletedAt: null,
162
- isDefault: opts?.isDefault ?? false
165
+ isDefault: opts?.isDefault ?? false,
166
+ managedBy: opts?.managedBy ?? null
163
167
  };
164
168
  this.workspaces.set(ws.id, ws);
165
169
  const memberKey = `${ws.id}:${userId}`;
@@ -210,6 +214,16 @@ var LocalWorkspaceStore = class {
210
214
  if (!ws || ws.deletedAt) return null;
211
215
  return ws;
212
216
  }
217
+ async getIncludingDeleted(id) {
218
+ return this.workspaces.get(id) ?? null;
219
+ }
220
+ async restore(id) {
221
+ const ws = this.workspaces.get(id);
222
+ if (!ws) return null;
223
+ ws.deletedAt = null;
224
+ this.workspaces.set(id, ws);
225
+ return ws;
226
+ }
213
227
  async update(id, updates) {
214
228
  const ws = this.workspaces.get(id);
215
229
  if (!ws || ws.deletedAt) return null;
@@ -297,11 +311,11 @@ var LocalWorkspaceStore = class {
297
311
  this.members.set(key, updated);
298
312
  return { member: updated };
299
313
  }
300
- async removeMember(workspaceId, userId) {
314
+ async removeMember(workspaceId, userId, opts) {
301
315
  const key = `${workspaceId}:${userId}`;
302
316
  const membership = this.members.get(key);
303
317
  if (!membership) return { removed: false, code: ERROR_CODES.NOT_MEMBER };
304
- if (membership.role === "owner") {
318
+ if (!opts?.allowLastOwner && membership.role === "owner") {
305
319
  let otherOwnerExists = false;
306
320
  for (const m of this.members.values()) {
307
321
  if (m.workspaceId === workspaceId && m.userId !== userId && m.role === "owner") {
@@ -542,6 +556,7 @@ __export(schema_exports, {
542
556
  creditGrants: () => creditGrants,
543
557
  creditPurchases: () => creditPurchases,
544
558
  idempotencyKeys: () => idempotencyKeys,
559
+ modelBudgetReservations: () => modelBudgetReservations,
545
560
  sessions: () => sessions,
546
561
  sessionsRelations: () => sessionsRelations,
547
562
  telemetryEvents: () => telemetryEvents,
@@ -685,7 +700,8 @@ var workspaces = pgTable2(
685
700
  createdBy: uuid2("created_by").notNull().references(() => users.id),
686
701
  createdAt: timestamp2("created_at").defaultNow().notNull(),
687
702
  deletedAt: timestamp2("deleted_at"),
688
- isDefault: boolean2("is_default").notNull().default(false)
703
+ isDefault: boolean2("is_default").notNull().default(false),
704
+ managedBy: text2("managed_by")
689
705
  },
690
706
  (table) => [
691
707
  index2("workspaces_created_by_idx").on(table.createdBy),
@@ -952,6 +968,33 @@ var usageReservations = pgTable2(
952
968
  )
953
969
  ]
954
970
  );
971
+ var modelBudgetReservations = pgTable2(
972
+ "boring_model_budget_reservations",
973
+ {
974
+ id: uuid2("id").default(sql3`gen_random_uuid()`).primaryKey(),
975
+ userId: text2("user_id").notNull(),
976
+ workspaceId: text2("workspace_id"),
977
+ sessionId: text2("session_id"),
978
+ runId: text2("run_id").notNull(),
979
+ provider: text2("provider").notNull(),
980
+ model: text2("model").notNull(),
981
+ period: text2("period").notNull(),
982
+ amountMicros: bigint("amount_micros", { mode: "number" }).notNull(),
983
+ status: text2("status").notNull().default("active"),
984
+ createdAt: timestamp2("created_at").defaultNow().notNull(),
985
+ expiresAt: timestamp2("expires_at").notNull()
986
+ },
987
+ (table) => [
988
+ uniqueIndex("boring_model_budget_reservations_active_user_run_idx").on(table.userId, table.runId).where(sql3`${table.status} = 'active'`),
989
+ index2("boring_model_budget_reservations_budget_idx").on(table.userId, table.provider, table.model, table.period, table.status),
990
+ index2("boring_model_budget_reservations_stale_idx").on(table.status, table.expiresAt),
991
+ check("boring_model_budget_reservations_amount_check", sql3`${table.amountMicros} > 0`),
992
+ check(
993
+ "boring_model_budget_reservations_status_check",
994
+ sql3`${table.status} IN ('active', 'settled', 'released', 'expired')`
995
+ )
996
+ ]
997
+ );
955
998
  var usageLedger = pgTable2(
956
999
  "boring_usage_ledger",
957
1000
  {
@@ -1095,7 +1138,8 @@ function toWorkspace(row) {
1095
1138
  createdBy: row.createdBy,
1096
1139
  createdAt: row.createdAt.toISOString(),
1097
1140
  deletedAt: row.deletedAt?.toISOString() ?? null,
1098
- isDefault: row.isDefault
1141
+ isDefault: row.isDefault,
1142
+ managedBy: row.managedBy
1099
1143
  };
1100
1144
  }
1101
1145
  var PostgresWorkspaceStore = class {
@@ -1117,12 +1161,24 @@ var PostgresWorkspaceStore = class {
1117
1161
  // ---------------------------------------------------------------------------
1118
1162
  async create(userId, name, appId, opts) {
1119
1163
  return this.db.transaction(async (tx) => {
1120
- const [row] = await tx.insert(workspaces).values({ appId, name, createdBy: userId, isDefault: opts?.isDefault ?? false }).returning();
1121
- await tx.insert(workspaceMembers).values({
1122
- workspaceId: row.id,
1123
- userId,
1124
- role: "owner"
1164
+ const insert = tx.insert(workspaces).values({
1165
+ ...opts?.id ? { id: opts.id } : {},
1166
+ appId,
1167
+ name,
1168
+ createdBy: userId,
1169
+ isDefault: opts?.isDefault ?? false,
1170
+ managedBy: opts?.managedBy ?? null
1125
1171
  });
1172
+ const insertedRows = opts?.id ? await insert.onConflictDoNothing().returning() : await insert.returning();
1173
+ const row = insertedRows[0] ?? (opts?.id ? (await tx.select().from(workspaces).where(eq(workspaces.id, opts.id)).limit(1))[0] : void 0);
1174
+ if (!row) throw new Error(`Workspace ${opts?.id ?? name} was not created`);
1175
+ if (insertedRows.length > 0) {
1176
+ await tx.insert(workspaceMembers).values({
1177
+ workspaceId: row.id,
1178
+ userId,
1179
+ role: "owner"
1180
+ });
1181
+ }
1126
1182
  return toWorkspace(row);
1127
1183
  });
1128
1184
  }
@@ -1140,6 +1196,14 @@ var PostgresWorkspaceStore = class {
1140
1196
  const rows = await this.db.select().from(workspaces).where(and(eq(workspaces.id, id), isNull(workspaces.deletedAt))).limit(1);
1141
1197
  return rows.length > 0 ? toWorkspace(rows[0]) : null;
1142
1198
  }
1199
+ async getIncludingDeleted(id) {
1200
+ const rows = await this.db.select().from(workspaces).where(eq(workspaces.id, id)).limit(1);
1201
+ return rows.length > 0 ? toWorkspace(rows[0]) : null;
1202
+ }
1203
+ async restore(id) {
1204
+ const rows = await this.db.update(workspaces).set({ deletedAt: null }).where(eq(workspaces.id, id)).returning();
1205
+ return rows.length > 0 ? toWorkspace(rows[0]) : null;
1206
+ }
1143
1207
  async update(id, updates) {
1144
1208
  const rows = await this.db.update(workspaces).set(updates).where(and(eq(workspaces.id, id), isNull(workspaces.deletedAt))).returning();
1145
1209
  return rows.length > 0 ? toWorkspace(rows[0]) : null;
@@ -1253,7 +1317,7 @@ var PostgresWorkspaceStore = class {
1253
1317
  return { member: toWorkspaceMember(updated) };
1254
1318
  });
1255
1319
  }
1256
- async removeMember(workspaceId, userId) {
1320
+ async removeMember(workspaceId, userId, opts) {
1257
1321
  return this.db.transaction(async (tx) => {
1258
1322
  await tx.execute(sql4`
1259
1323
  SELECT user_id
@@ -1270,7 +1334,7 @@ var PostgresWorkspaceStore = class {
1270
1334
  ).limit(1);
1271
1335
  const role = memberRows[0]?.role;
1272
1336
  if (!role) return { removed: false, code: ERROR_CODES.NOT_MEMBER };
1273
- if (role === "owner") {
1337
+ if (!opts?.allowLastOwner && role === "owner") {
1274
1338
  const [{ count }] = await tx.select({ count: sql4`count(*)::int` }).from(workspaceMembers).where(
1275
1339
  and(
1276
1340
  eq(workspaceMembers.workspaceId, workspaceId),
@@ -2388,6 +2452,265 @@ function describeUsage(source) {
2388
2452
  return { kind: "usage", description: "Agent usage" };
2389
2453
  }
2390
2454
 
2455
+ // src/server/db/stores/PostgresModelBudgetStore.ts
2456
+ import { and as and3, eq as eq4, inArray as inArray2, lt as lt2, or as or2, sql as sql7 } from "drizzle-orm";
2457
+ var ModelBudgetExceededError = class extends Error {
2458
+ constructor(usedMicros, heldMicros, budgetMicros, requestedMicros) {
2459
+ super("Budget reached for this model.");
2460
+ this.usedMicros = usedMicros;
2461
+ this.heldMicros = heldMicros;
2462
+ this.budgetMicros = budgetMicros;
2463
+ this.requestedMicros = requestedMicros;
2464
+ this.name = "ModelBudgetExceededError";
2465
+ }
2466
+ usedMicros;
2467
+ heldMicros;
2468
+ budgetMicros;
2469
+ requestedMicros;
2470
+ statusCode = 402;
2471
+ code = "MODEL_BUDGET_EXCEEDED";
2472
+ };
2473
+ var UPDATE_ID_BATCH_SIZE = 1e3;
2474
+ function assertPositiveSafeInteger(name, value) {
2475
+ if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${name} must be a positive safe integer`);
2476
+ }
2477
+ function monthPeriodUtc(now) {
2478
+ const year = now.getUTCFullYear();
2479
+ const month = now.getUTCMonth();
2480
+ return {
2481
+ period: `${year}-${String(month + 1).padStart(2, "0")}`,
2482
+ start: new Date(Date.UTC(year, month, 1, 0, 0, 0, 0)),
2483
+ end: new Date(Date.UTC(year, month + 1, 1, 0, 0, 0, 0))
2484
+ };
2485
+ }
2486
+ function requireFinishKey(input) {
2487
+ if (input.reservationId) return eq4(modelBudgetReservations.id, input.reservationId);
2488
+ if (!input.runId || !input.userId) throw new Error("finish requires reservationId or runId+userId");
2489
+ return and3(eq4(modelBudgetReservations.runId, input.runId), eq4(modelBudgetReservations.userId, input.userId));
2490
+ }
2491
+ function chunkIds(ids) {
2492
+ const chunks = [];
2493
+ for (let index3 = 0; index3 < ids.length; index3 += UPDATE_ID_BATCH_SIZE) {
2494
+ chunks.push(ids.slice(index3, index3 + UPDATE_ID_BATCH_SIZE));
2495
+ }
2496
+ return chunks;
2497
+ }
2498
+ function modelBudgetAdvisoryLockKey(target) {
2499
+ return `model-budget:${target.userId}:${target.provider}:${target.model}:${target.period}`;
2500
+ }
2501
+ async function lockModelBudgetTargets(executor, targets) {
2502
+ const keys = Array.from(new Set(targets.map(modelBudgetAdvisoryLockKey))).sort();
2503
+ for (const key of keys) {
2504
+ await executor.execute(sql7`SELECT pg_advisory_xact_lock(hashtext(${key}))`);
2505
+ }
2506
+ }
2507
+ var PostgresModelBudgetStore = class {
2508
+ constructor(db) {
2509
+ this.db = db;
2510
+ }
2511
+ db;
2512
+ static monthPeriodUtc(now = /* @__PURE__ */ new Date()) {
2513
+ return monthPeriodUtc(now).period;
2514
+ }
2515
+ async sweepExpired(now = /* @__PURE__ */ new Date()) {
2516
+ return this.db.transaction(async (tx) => {
2517
+ const expired = await tx.select({
2518
+ id: modelBudgetReservations.id,
2519
+ userId: modelBudgetReservations.userId,
2520
+ provider: modelBudgetReservations.provider,
2521
+ model: modelBudgetReservations.model,
2522
+ period: modelBudgetReservations.period
2523
+ }).from(modelBudgetReservations).where(and3(eq4(modelBudgetReservations.status, "active"), lt2(modelBudgetReservations.expiresAt, now)));
2524
+ if (expired.length === 0) return 0;
2525
+ await lockModelBudgetTargets(tx, expired);
2526
+ let count = 0;
2527
+ for (const ids of chunkIds(expired.map((row) => row.id))) {
2528
+ const rows = await tx.update(modelBudgetReservations).set({ status: "expired" }).where(and3(
2529
+ inArray2(modelBudgetReservations.id, ids),
2530
+ eq4(modelBudgetReservations.status, "active"),
2531
+ lt2(modelBudgetReservations.expiresAt, now)
2532
+ )).returning({ id: modelBudgetReservations.id });
2533
+ count += rows.length;
2534
+ }
2535
+ return count;
2536
+ });
2537
+ }
2538
+ async reserve(input) {
2539
+ if (!input.userId) throw new Error("reserve requires userId");
2540
+ if (!input.provider || !input.model) throw new Error("reserve requires provider/model");
2541
+ assertPositiveSafeInteger("budgetMicros", input.budgetMicros);
2542
+ assertPositiveSafeInteger("holdMicros", input.holdMicros);
2543
+ if (!Number.isSafeInteger(input.ttlSeconds) || input.ttlSeconds <= 0) throw new Error("ttlSeconds must be a positive safe integer");
2544
+ const now = input.now ?? /* @__PURE__ */ new Date();
2545
+ const period = monthPeriodUtc(now);
2546
+ const expiresAt = new Date(now.getTime() + input.ttlSeconds * 1e3);
2547
+ return this.db.transaction(async (tx) => {
2548
+ const expired = await tx.select({
2549
+ id: modelBudgetReservations.id,
2550
+ userId: modelBudgetReservations.userId,
2551
+ provider: modelBudgetReservations.provider,
2552
+ model: modelBudgetReservations.model,
2553
+ period: modelBudgetReservations.period
2554
+ }).from(modelBudgetReservations).where(and3(
2555
+ eq4(modelBudgetReservations.status, "active"),
2556
+ lt2(modelBudgetReservations.expiresAt, now),
2557
+ or2(
2558
+ and3(
2559
+ eq4(modelBudgetReservations.userId, input.userId),
2560
+ eq4(modelBudgetReservations.provider, input.provider),
2561
+ eq4(modelBudgetReservations.model, input.model),
2562
+ eq4(modelBudgetReservations.period, period.period)
2563
+ ),
2564
+ and3(
2565
+ eq4(modelBudgetReservations.userId, input.userId),
2566
+ eq4(modelBudgetReservations.runId, input.runId)
2567
+ )
2568
+ )
2569
+ ));
2570
+ await lockModelBudgetTargets(tx, [
2571
+ { userId: input.userId, provider: input.provider, model: input.model, period: period.period },
2572
+ ...expired
2573
+ ]);
2574
+ for (const ids of chunkIds(expired.map((row) => row.id))) {
2575
+ await tx.update(modelBudgetReservations).set({ status: "expired" }).where(and3(
2576
+ inArray2(modelBudgetReservations.id, ids),
2577
+ eq4(modelBudgetReservations.status, "active"),
2578
+ lt2(modelBudgetReservations.expiresAt, now)
2579
+ ));
2580
+ }
2581
+ const existing = await tx.select({
2582
+ id: modelBudgetReservations.id,
2583
+ provider: modelBudgetReservations.provider,
2584
+ model: modelBudgetReservations.model,
2585
+ period: modelBudgetReservations.period
2586
+ }).from(modelBudgetReservations).where(and3(
2587
+ eq4(modelBudgetReservations.status, "active"),
2588
+ eq4(modelBudgetReservations.userId, input.userId),
2589
+ eq4(modelBudgetReservations.runId, input.runId)
2590
+ )).limit(1);
2591
+ const prior = existing[0];
2592
+ if (prior) {
2593
+ if (prior.provider !== input.provider || prior.model !== input.model || prior.period !== period.period) {
2594
+ throw new Error(`active model budget reservation for run ${input.runId} has conflicting tuple`);
2595
+ }
2596
+ return { reservationId: prior.id, created: false, period: period.period };
2597
+ }
2598
+ const periodStartIso = period.start.toISOString();
2599
+ const periodEndIso = period.end.toISOString();
2600
+ const usageRows = await tx.execute(sql7`
2601
+ SELECT COALESCE(SUM(${usageLedger.billedCostMicros}), 0)::text AS total
2602
+ FROM ${usageLedger}
2603
+ WHERE ${usageLedger.userId} = ${input.userId}
2604
+ AND ${usageLedger.provider} = ${input.provider}
2605
+ AND ${usageLedger.model} = ${input.model}
2606
+ AND (
2607
+ (
2608
+ EXISTS (
2609
+ SELECT 1 FROM ${modelBudgetReservations} r
2610
+ WHERE r.user_id = ${usageLedger.userId}
2611
+ AND r.provider = ${usageLedger.provider}
2612
+ AND r.model = ${usageLedger.model}
2613
+ AND r.run_id = ${usageLedger.runId}
2614
+ AND r.period = ${period.period}
2615
+ AND r.status NOT IN ('active', 'settled')
2616
+ AND (
2617
+ ${usageLedger.metadata}->>'modelBudgetReservationId' = r.id::text
2618
+ OR (
2619
+ ${usageLedger.metadata}->>'modelBudgetReservationId' IS NULL
2620
+ AND r.created_at <= ${usageLedger.createdAt}
2621
+ AND NOT EXISTS (
2622
+ SELECT 1 FROM ${modelBudgetReservations} newer
2623
+ WHERE newer.user_id = r.user_id
2624
+ AND newer.provider = r.provider
2625
+ AND newer.model = r.model
2626
+ AND newer.run_id = r.run_id
2627
+ AND newer.created_at <= ${usageLedger.createdAt}
2628
+ AND (
2629
+ newer.created_at > r.created_at
2630
+ OR (newer.created_at = r.created_at AND newer.id > r.id)
2631
+ )
2632
+ )
2633
+ )
2634
+ )
2635
+ )
2636
+ )
2637
+ OR (
2638
+ ${usageLedger.createdAt} >= ${periodStartIso}::timestamp
2639
+ AND ${usageLedger.createdAt} < ${periodEndIso}::timestamp
2640
+ AND ${usageLedger.metadata}->>'modelBudgetReservationId' IS NULL
2641
+ AND NOT EXISTS (
2642
+ SELECT 1 FROM ${modelBudgetReservations} r
2643
+ WHERE r.user_id = ${usageLedger.userId}
2644
+ AND r.provider = ${usageLedger.provider}
2645
+ AND r.model = ${usageLedger.model}
2646
+ AND r.run_id = ${usageLedger.runId}
2647
+ AND r.created_at <= ${usageLedger.createdAt}
2648
+ )
2649
+ )
2650
+ )
2651
+ `);
2652
+ const heldRows = await tx.execute(sql7`
2653
+ SELECT COALESCE(SUM(amount_micros), 0)::text AS total
2654
+ FROM ${modelBudgetReservations}
2655
+ WHERE status = 'active'
2656
+ AND user_id = ${input.userId}
2657
+ AND provider = ${input.provider}
2658
+ AND model = ${input.model}
2659
+ AND period = ${period.period}
2660
+ `);
2661
+ const settledFallbackRows = await tx.execute(sql7`
2662
+ SELECT COALESCE(SUM(amount_micros), 0)::text AS total
2663
+ FROM ${modelBudgetReservations}
2664
+ WHERE status = 'settled'
2665
+ AND user_id = ${input.userId}
2666
+ AND provider = ${input.provider}
2667
+ AND model = ${input.model}
2668
+ AND period = ${period.period}
2669
+ `);
2670
+ const usedMicros = Number(usageRows[0]?.total ?? 0) + Number(settledFallbackRows[0]?.total ?? 0);
2671
+ const heldMicros = Number(heldRows[0]?.total ?? 0);
2672
+ if (usedMicros + heldMicros + input.holdMicros > input.budgetMicros) {
2673
+ throw new ModelBudgetExceededError(usedMicros, heldMicros, input.budgetMicros, input.holdMicros);
2674
+ }
2675
+ const inserted = await tx.insert(modelBudgetReservations).values({
2676
+ userId: input.userId,
2677
+ workspaceId: input.workspaceId ?? null,
2678
+ sessionId: input.sessionId ?? null,
2679
+ runId: input.runId,
2680
+ provider: input.provider,
2681
+ model: input.model,
2682
+ period: period.period,
2683
+ amountMicros: input.holdMicros,
2684
+ expiresAt
2685
+ }).returning({ id: modelBudgetReservations.id });
2686
+ return { reservationId: inserted[0].id, created: true, period: period.period };
2687
+ });
2688
+ }
2689
+ async settle(input) {
2690
+ await this.finish(input, "settled");
2691
+ }
2692
+ async release(input) {
2693
+ await this.finish(input, "released");
2694
+ }
2695
+ async finish(input, status) {
2696
+ await this.db.transaction(async (tx) => {
2697
+ const active = await tx.select({
2698
+ id: modelBudgetReservations.id,
2699
+ userId: modelBudgetReservations.userId,
2700
+ provider: modelBudgetReservations.provider,
2701
+ model: modelBudgetReservations.model,
2702
+ period: modelBudgetReservations.period
2703
+ }).from(modelBudgetReservations).where(and3(requireFinishKey(input), eq4(modelBudgetReservations.status, "active")));
2704
+ if (active.length === 0) return;
2705
+ await lockModelBudgetTargets(tx, active);
2706
+ await tx.update(modelBudgetReservations).set({ status }).where(and3(
2707
+ inArray2(modelBudgetReservations.id, active.map((row) => row.id)),
2708
+ eq4(modelBudgetReservations.status, "active")
2709
+ ));
2710
+ });
2711
+ }
2712
+ };
2713
+
2391
2714
  export {
2392
2715
  users,
2393
2716
  verification_tokens,
@@ -2410,5 +2733,7 @@ export {
2410
2733
  PostgresWorkspaceStore,
2411
2734
  PostgresUserStore,
2412
2735
  InsufficientCreditError,
2413
- PostgresMeteringStore
2736
+ PostgresMeteringStore,
2737
+ ModelBudgetExceededError,
2738
+ PostgresModelBudgetStore
2414
2739
  };
@@ -1,4 +1,4 @@
1
- import { C as CoreConfig, c as Workspace, E as ERROR_CODES, M as MemberRole, d as WorkspaceMember, U as User, e as WorkspaceInvite, f as WorkspaceRuntime, W as WorkspaceRuntimeResourceSelector, a as WorkspaceRuntimeResource, b as WorkspaceRuntimeResourceInput, g as CapabilitiesResponse } from './types-Bb7I-83I.js';
1
+ import { C as CoreConfig, c as Workspace, E as ERROR_CODES, M as MemberRole, d as WorkspaceMember, U as User, e as WorkspaceInvite, f as WorkspaceRuntime, W as WorkspaceRuntimeResourceSelector, a as WorkspaceRuntimeResource, b as WorkspaceRuntimeResourceInput, g as CapabilitiesResponse } from './types-DVgeIw1d.js';
2
2
  import { drizzle } from 'drizzle-orm/postgres-js';
3
3
  import postgres from 'postgres';
4
4
 
@@ -54,9 +54,13 @@ interface UserStore {
54
54
  interface WorkspaceStore {
55
55
  create(userId: string, name: string, appId: string, opts?: {
56
56
  isDefault?: boolean;
57
+ id?: string;
58
+ managedBy?: string;
57
59
  }): Promise<Workspace>;
58
60
  list(userId: string, appId: string): Promise<Workspace[]>;
59
61
  get(id: string): Promise<Workspace | null>;
62
+ getIncludingDeleted(id: string): Promise<Workspace | null>;
63
+ restore(id: string): Promise<Workspace | null>;
60
64
  update(id: string, updates: Partial<Pick<Workspace, 'name'>>): Promise<Workspace | null>;
61
65
  delete(id: string): Promise<{
62
66
  removed: boolean;
@@ -73,7 +77,9 @@ interface WorkspaceStore {
73
77
  member?: WorkspaceMember;
74
78
  code?: typeof ERROR_CODES.LAST_OWNER | typeof ERROR_CODES.NOT_MEMBER;
75
79
  }>;
76
- removeMember(workspaceId: string, userId: string): Promise<{
80
+ removeMember(workspaceId: string, userId: string, opts?: {
81
+ allowLastOwner?: boolean;
82
+ }): Promise<{
77
83
  removed: boolean;
78
84
  code?: typeof ERROR_CODES.LAST_OWNER | typeof ERROR_CODES.NOT_MEMBER;
79
85
  }>;