@hachej/boring-core 0.1.64 → 0.1.66
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.
- package/dist/CoreFront-DB1OfNJU.d.ts +65 -0
- package/dist/{PostgresMeteringStore-qjKGmVFr.d.ts → PostgresModelBudgetStore-CFbEEdQL.d.ts} +44 -2
- package/dist/app/front/index.d.ts +9 -8
- package/dist/app/front/index.js +4 -1
- package/dist/app/server/index.d.ts +3 -3
- package/dist/app/server/index.js +4 -2
- package/dist/{authHook-CcUgmE_N.d.ts → authHook-BUSGTx_o.d.ts} +2 -2
- package/dist/{chunk-NC46VGXS.js → chunk-3B7LU76T.js} +673 -502
- package/dist/{chunk-BU6GSLJL.js → chunk-CIRY3ZLU.js} +78 -11
- package/dist/{chunk-6ZY63QYN.js → chunk-EFM5IWTK.js} +342 -17
- package/dist/{connection-B5wXGP69.d.ts → connection-D9s7Td0I.d.ts} +8 -2
- package/dist/front/index.d.ts +29 -27
- package/dist/front/index.js +7 -1
- package/dist/front/top-bar-slot.d.ts +2 -2
- package/dist/server/db/index.d.ts +18 -6
- package/dist/server/db/index.js +5 -1
- package/dist/server/index.d.ts +8 -7
- package/dist/server/index.js +9 -3
- package/dist/shared/index.d.ts +2 -2
- package/dist/{types-Bb7I-83I.d.ts → types-DVgeIw1d.d.ts} +1 -0
- package/drizzle/0015_model_budget_reservations.sql +22 -0
- package/drizzle/0016_workspace_managed_by.sql +1 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +30 -30
- package/dist/CoreFront-N0QJSYaM.d.ts +0 -23
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
workspaceRuntimes,
|
|
13
13
|
workspaceSettings,
|
|
14
14
|
workspaces
|
|
15
|
-
} from "./chunk-
|
|
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 {
|
|
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
|
|
1765
|
-
function
|
|
1766
|
-
if (
|
|
1767
|
-
|
|
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
|
-
|
|
1799
|
+
return zxcvbnInstance;
|
|
1776
1800
|
}
|
|
1777
1801
|
function validatePasswordStrength(password) {
|
|
1778
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
22
|
-
return { db, sql:
|
|
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
|
|
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
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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-
|
|
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
|
|
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
|
}>;
|