@hachej/boring-core 0.1.81 → 0.1.83
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/{PostgresModelBudgetStore-BbqIF5TR.d.ts → PostgresModelBudgetStore-Ulw6ijJG.d.ts} +33 -2
- package/dist/app/front/index.js +2 -2
- package/dist/app/server/index.d.ts +9 -5
- package/dist/app/server/index.js +135 -38
- package/dist/{chunk-3B7LU76T.js → chunk-DGBJFPUK.js} +1 -1
- package/dist/{chunk-QZGYKLXB.js → chunk-H7EYII35.js} +2 -0
- package/dist/{chunk-H5BQOHWR.js → chunk-RVVE5WOB.js} +30 -4
- package/dist/{chunk-VB2N2DG5.js → chunk-YFCR6SAS.js} +316 -33
- package/dist/front/index.d.ts +1 -1
- package/dist/front/index.js +2 -2
- package/dist/{loadConfig-C3iHuZNw.d.ts → loadConfig-CuRGCFmt.d.ts} +3 -2
- package/dist/server/db/index.d.ts +19 -10
- package/dist/server/db/index.js +2 -2
- package/dist/server/index.d.ts +34 -7
- package/dist/server/index.js +3 -3
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js +1 -1
- package/dist/{types-Dm11Zm56.d.ts → types-CHdS89JJ.d.ts} +19 -5
- package/dist/{types-DDnayJjI.d.ts → types-Di-eoc58.d.ts} +8 -1
- package/package.json +5 -4
package/dist/{PostgresModelBudgetStore-BbqIF5TR.d.ts → PostgresModelBudgetStore-Ulw6ijJG.d.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import postgres from 'postgres';
|
|
2
|
-
import { C as CoreConfig } from './types-
|
|
2
|
+
import { C as CoreConfig } from './types-Di-eoc58.js';
|
|
3
3
|
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
4
4
|
|
|
5
5
|
type CoreMigrationSql = postgres.Sql;
|
|
@@ -275,6 +275,30 @@ interface ReserveBudgetResult {
|
|
|
275
275
|
created: boolean;
|
|
276
276
|
period: string;
|
|
277
277
|
}
|
|
278
|
+
type BudgetSpendQuery = {
|
|
279
|
+
scope: 'user';
|
|
280
|
+
userId: string;
|
|
281
|
+
now?: Date;
|
|
282
|
+
} | {
|
|
283
|
+
scope: 'model';
|
|
284
|
+
userId: string;
|
|
285
|
+
provider: string;
|
|
286
|
+
model: string;
|
|
287
|
+
now?: Date;
|
|
288
|
+
};
|
|
289
|
+
interface BudgetSpendSnapshot {
|
|
290
|
+
scope: BudgetReservationScope;
|
|
291
|
+
/** Settled usage (ledger + settled fallback reservations) in micros for the current period. */
|
|
292
|
+
usedMicros: number;
|
|
293
|
+
/** In-flight active holds in micros for the current period. */
|
|
294
|
+
heldMicros: number;
|
|
295
|
+
/** Period key (e.g. "2026-07"). */
|
|
296
|
+
period: string;
|
|
297
|
+
/** Inclusive UTC start of the current budget period. */
|
|
298
|
+
periodStart: Date;
|
|
299
|
+
/** Exclusive UTC end of the current budget period == the reset boundary. */
|
|
300
|
+
periodEnd: Date;
|
|
301
|
+
}
|
|
278
302
|
interface BudgetReservationAdmissionInput {
|
|
279
303
|
user?: Extract<ReserveBudgetInput, {
|
|
280
304
|
scope: 'user';
|
|
@@ -305,6 +329,13 @@ declare class PostgresBudgetReservationStore {
|
|
|
305
329
|
});
|
|
306
330
|
static monthPeriodUtc(now?: Date): string;
|
|
307
331
|
sweepExpired(now?: Date): Promise<number>;
|
|
332
|
+
/**
|
|
333
|
+
* Read-only snapshot of current-period used/held spend for a budget target,
|
|
334
|
+
* reusing the exact same accounting the admission path applies during a
|
|
335
|
+
* reservation (usage ledger + settled/active reservations). No rows are
|
|
336
|
+
* written. The reset boundary is derived from the same monthly period logic.
|
|
337
|
+
*/
|
|
338
|
+
getSpendSnapshot(query: BudgetSpendQuery): Promise<BudgetSpendSnapshot>;
|
|
308
339
|
reserve(input: ReserveBudgetInput): Promise<ReserveBudgetResult>;
|
|
309
340
|
reserveAdmission(input: BudgetReservationAdmissionInput): Promise<BudgetReservationAdmission>;
|
|
310
341
|
reserveMany(inputs: readonly ReserveBudgetInput[]): Promise<ReserveBudgetResult[]>;
|
|
@@ -354,4 +385,4 @@ declare class PostgresModelBudgetStore {
|
|
|
354
385
|
release(input: FinishModelBudgetReservationInput): Promise<void>;
|
|
355
386
|
}
|
|
356
387
|
|
|
357
|
-
export { type BudgetReservationAdmission as B, type CreditLedgerEntry as C, type FinishReservationInput as F, type GrantOnceInput as G, InsufficientCreditError as I, type MeteringBalance as M, PostgresBudgetReservationStore as P, type RecordUsageInput as R, UserBudgetExceededError as U, type BudgetReservationAdmissionInput as a, type BudgetReservationScope as b, type FinishReservationInput$1 as
|
|
388
|
+
export { type BudgetReservationAdmission as B, type CreditLedgerEntry as C, type FinishReservationInput as F, type GrantOnceInput as G, InsufficientCreditError as I, type MeteringBalance as M, PostgresBudgetReservationStore as P, type RecordUsageInput as R, UserBudgetExceededError as U, type BudgetReservationAdmissionInput as a, type BudgetReservationScope as b, type BudgetSpendQuery as c, type BudgetSpendSnapshot as d, type FinishReservationInput$1 as e, ModelBudgetExceededError as f, PostgresMeteringStore as g, PostgresModelBudgetStore as h, type RecordUsageResult as i, type ReservationFinalStatus as j, type ReserveBudgetInput as k, type ReserveBudgetResult as l, type ReserveInput as m, type ReserveResult as n, type RunMigrationsOptions as o, runMigrations as r };
|
package/dist/app/front/index.js
CHANGED
|
@@ -9,12 +9,12 @@ import {
|
|
|
9
9
|
useSignIn,
|
|
10
10
|
useSignUp,
|
|
11
11
|
useWorkspaceRouteStatus
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-DGBJFPUK.js";
|
|
13
13
|
import "../../chunk-HYNKZSTF.js";
|
|
14
14
|
import {
|
|
15
15
|
isRuntimeEmailVerificationEnabled
|
|
16
16
|
} from "../../chunk-I4PGL4ZD.js";
|
|
17
|
-
import "../../chunk-
|
|
17
|
+
import "../../chunk-H7EYII35.js";
|
|
18
18
|
import "../../chunk-MLKGABMK.js";
|
|
19
19
|
|
|
20
20
|
// src/app/front/CoreWorkspaceAgentFront.tsx
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { RuntimeProvisioningContribution, RegisterAgentRoutesOptions } from '@hachej/boring-agent/server';
|
|
2
2
|
import { DirPluginEntry, CreateWorkspaceAgentServerOptions, WorkspaceAgentServerPluginContext } from '@hachej/boring-workspace/app/server';
|
|
3
3
|
import { WorkspaceServerPlugin, WorkspaceBridgeOperationDefinition, WorkspaceBridgeHandler, WorkspaceBridgeRuntimeEnvOptions, WorkspaceBridgeCallRequest, WorkspaceBridgeCallResponse } from '@hachej/boring-workspace/server';
|
|
4
|
-
import { FastifyInstance } from 'fastify';
|
|
5
|
-
import { C as CoreConfig } from '../../types-
|
|
4
|
+
import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify';
|
|
5
|
+
import { C as CoreConfig } from '../../types-Di-eoc58.js';
|
|
6
6
|
import { T as TelemetrySink } from '../../telemetry-DR18MeI0.js';
|
|
7
|
-
import { B as BetterAuthInstance, L as LoadConfigOptions } from '../../loadConfig-
|
|
8
|
-
import { D as Database, U as UserStore, W as WorkspaceStore } from '../../types-
|
|
7
|
+
import { B as BetterAuthInstance, L as LoadConfigOptions } from '../../loadConfig-CuRGCFmt.js';
|
|
8
|
+
import { D as Database, U as UserStore, W as WorkspaceStore, C as CoreRequestScopeResolver } from '../../types-CHdS89JJ.js';
|
|
9
|
+
export { a as CoreRequestScope } from '../../types-CHdS89JJ.js';
|
|
9
10
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
10
11
|
import 'better-auth';
|
|
11
12
|
import 'drizzle-orm/postgres-js';
|
|
@@ -72,7 +73,10 @@ interface CreateCoreWorkspaceAgentServerOptions extends Omit<RegisterAgentRoutes
|
|
|
72
73
|
telemetry?: TelemetrySink;
|
|
73
74
|
/** Verified actor resolver exposed only to boot-time internal plugins. */
|
|
74
75
|
trustedPluginActorResolver?: NonNullable<WorkspaceAgentServerPluginContext['trusted']>['actorResolver'];
|
|
76
|
+
requestScopeResolver?: CoreRequestScopeResolver;
|
|
77
|
+
frontendRootHandler?: CoreFrontendRootHandler;
|
|
75
78
|
}
|
|
79
|
+
type CoreFrontendRootHandler = (request: FastifyRequest, reply: FastifyReply) => boolean | Promise<boolean>;
|
|
76
80
|
type AgentPiOptions = RegisterAgentRoutesOptions['pi'];
|
|
77
81
|
declare function createCoreWorkspaceAgentServer(options?: CreateCoreWorkspaceAgentServerOptions): Promise<CoreWorkspaceAgentServer>;
|
|
78
82
|
|
|
@@ -126,4 +130,4 @@ interface RunCoreWorkspaceAgentServerOptions {
|
|
|
126
130
|
}
|
|
127
131
|
declare function runCoreWorkspaceAgentServer(importMetaUrl: string, opts?: RunCoreWorkspaceAgentServerOptions): Promise<void>;
|
|
128
132
|
|
|
129
|
-
export { type CoreWorkspaceAgentDevServerHandle, type CoreWorkspaceAgentServer, type CoreWorkspaceAgentServerPlugin, type CreateCoreWorkspaceAgentServerOptions, type CreateVercelFastifyHandlerOptions, type RunCoreWorkspaceAgentServerOptions, type StartCoreWorkspaceAgentDevServerFromMetaOptions, type StartCoreWorkspaceAgentDevServerOptions, type VercelFastifyHandler, type VercelFastifyLikeServer, appRootFromImportMeta, createCoreWorkspaceAgentServer, createVercelFastifyHandler, runCoreWorkspaceAgentServer, startCoreWorkspaceAgentDevServer, startCoreWorkspaceAgentDevServerFromMeta };
|
|
133
|
+
export { type CoreFrontendRootHandler, CoreRequestScopeResolver, type CoreWorkspaceAgentDevServerHandle, type CoreWorkspaceAgentServer, type CoreWorkspaceAgentServerPlugin, type CreateCoreWorkspaceAgentServerOptions, type CreateVercelFastifyHandlerOptions, type RunCoreWorkspaceAgentServerOptions, type StartCoreWorkspaceAgentDevServerFromMetaOptions, type StartCoreWorkspaceAgentDevServerOptions, type VercelFastifyHandler, type VercelFastifyLikeServer, appRootFromImportMeta, createCoreWorkspaceAgentServer, createVercelFastifyHandler, runCoreWorkspaceAgentServer, startCoreWorkspaceAgentDevServer, startCoreWorkspaceAgentDevServerFromMeta };
|
package/dist/app/server/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
REQUEST_SCOPE_WORKSPACE_HEADER,
|
|
2
3
|
WorkspaceRuntimeSandboxHandleStore,
|
|
3
4
|
authHook,
|
|
4
5
|
createAuth,
|
|
@@ -9,21 +10,22 @@ import {
|
|
|
9
10
|
registerRoutes,
|
|
10
11
|
registerSettingsRoutes,
|
|
11
12
|
registerWorkspaceRoutes
|
|
12
|
-
} from "../../chunk-
|
|
13
|
+
} from "../../chunk-YFCR6SAS.js";
|
|
13
14
|
import {
|
|
14
15
|
PostgresUserStore,
|
|
15
16
|
PostgresWorkspaceStore,
|
|
16
17
|
createDatabase,
|
|
17
18
|
telemetryEvents
|
|
18
|
-
} from "../../chunk-
|
|
19
|
+
} from "../../chunk-RVVE5WOB.js";
|
|
19
20
|
import {
|
|
20
21
|
noopTelemetry,
|
|
21
22
|
safeCapture
|
|
22
23
|
} from "../../chunk-AQBXNPMD.js";
|
|
23
24
|
import "../../chunk-I4PGL4ZD.js";
|
|
24
25
|
import {
|
|
25
|
-
ERROR_CODES
|
|
26
|
-
|
|
26
|
+
ERROR_CODES,
|
|
27
|
+
HttpError
|
|
28
|
+
} from "../../chunk-H7EYII35.js";
|
|
27
29
|
import "../../chunk-MLKGABMK.js";
|
|
28
30
|
|
|
29
31
|
// src/app/server/createCoreWorkspaceAgentServer.ts
|
|
@@ -66,6 +68,8 @@ import {
|
|
|
66
68
|
var MAX_SESSION_OWNER_CACHE = 5e3;
|
|
67
69
|
function createCoreWorkspaceBridge(options) {
|
|
68
70
|
const { resolveWorkspaceId, workspaceStore, validateWorkspaceId, agentSessionId } = options;
|
|
71
|
+
const admittedBrowserWorkspaces = /* @__PURE__ */ new WeakMap();
|
|
72
|
+
const admittedRuntimeWorkspaces = /* @__PURE__ */ new WeakMap();
|
|
69
73
|
const bridges = /* @__PURE__ */ new Map();
|
|
70
74
|
const getBridge = (workspaceId) => {
|
|
71
75
|
const safeWorkspaceId = validateWorkspaceId(workspaceId);
|
|
@@ -97,13 +101,15 @@ function createCoreWorkspaceBridge(options) {
|
|
|
97
101
|
return runtime;
|
|
98
102
|
};
|
|
99
103
|
const resolveBridgeWorkspaceId = async (request) => {
|
|
104
|
+
const admittedRuntimeWorkspace = admittedRuntimeWorkspaces.get(request);
|
|
105
|
+
if (admittedRuntimeWorkspace) return admittedRuntimeWorkspace;
|
|
100
106
|
const authHeader = request.headers.authorization;
|
|
101
107
|
if (options.workspaceBridge?.runtimeTokenSecret && typeof authHeader === "string" && authHeader.startsWith("Bearer ")) {
|
|
102
108
|
return verifyWorkspaceBridgeRuntimeToken(authHeader.slice("Bearer ".length), {
|
|
103
109
|
secret: options.workspaceBridge.runtimeTokenSecret
|
|
104
110
|
}).authContext.workspaceId;
|
|
105
111
|
}
|
|
106
|
-
return await resolveWorkspaceId(request);
|
|
112
|
+
return admittedBrowserWorkspaces.get(request) ?? await resolveWorkspaceId(request);
|
|
107
113
|
};
|
|
108
114
|
const rememberSessionOwner = async (request) => {
|
|
109
115
|
const user = request.user;
|
|
@@ -173,25 +179,39 @@ function createCoreWorkspaceBridge(options) {
|
|
|
173
179
|
}
|
|
174
180
|
} : void 0;
|
|
175
181
|
const registerHttpRoutes = async (app) => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
182
|
+
const assertRuntimeWorkspaceScope = options.assertRuntimeWorkspaceScope;
|
|
183
|
+
await app.register(async (bridgeApp) => {
|
|
184
|
+
bridgeApp.addHook("preHandler", async (request) => {
|
|
185
|
+
const pathname = request.url.split("?")[0];
|
|
186
|
+
const authHeader = request.headers.authorization;
|
|
187
|
+
if (request.requestScope && pathname === "/api/v1/workspace-bridge/call" && !(typeof authHeader === "string" && authHeader.startsWith("Bearer "))) {
|
|
188
|
+
admittedBrowserWorkspaces.set(request, await resolveWorkspaceId(request));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
await bridgeApp.register(workspaceBridgeHttpRoutes, {
|
|
192
|
+
getRegistry: async (request) => getRuntime(await resolveBridgeWorkspaceId(request)).registry,
|
|
193
|
+
getIdempotencyStore: async (request) => getRuntime(await resolveBridgeWorkspaceId(request)).idempotencyStore,
|
|
194
|
+
runtimeTokenSecret: options.workspaceBridge?.runtimeTokenSecret,
|
|
195
|
+
runtimeRefreshTokenSecret: options.workspaceBridge?.runtimeRefreshTokenSecret,
|
|
196
|
+
assertRuntimeWorkspaceScope: assertRuntimeWorkspaceScope ? async (request, claims) => {
|
|
197
|
+
await assertRuntimeWorkspaceScope(request, claims.workspaceId);
|
|
198
|
+
admittedRuntimeWorkspaces.set(request, claims.workspaceId);
|
|
199
|
+
} : void 0,
|
|
200
|
+
getRuntimeRefreshTokenStore: (_request, claims) => getRuntime(claims.workspaceId).refreshTokenStore,
|
|
201
|
+
getOwnerWorkspaceId: async (request) => await resolveBridgeWorkspaceId(request),
|
|
202
|
+
browserAuthPolicy: createBrowserBridgeAuthPolicy({
|
|
203
|
+
getPrincipal: (input) => {
|
|
204
|
+
const user = input.request?.user;
|
|
205
|
+
return user?.id ? { userId: user.id, email: user.email ?? void 0 } : null;
|
|
206
|
+
},
|
|
207
|
+
authorizeWorkspace: async ({ principal, workspaceId, definition }) => ({
|
|
208
|
+
allowed: await workspaceStore.isMember(workspaceId, principal.userId),
|
|
209
|
+
capabilities: definition.requiredCapabilities
|
|
210
|
+
}),
|
|
211
|
+
allowedOrigins: options.corsOrigins,
|
|
212
|
+
requireCsrfHeader: true
|
|
213
|
+
})
|
|
214
|
+
});
|
|
195
215
|
});
|
|
196
216
|
};
|
|
197
217
|
return { getBridge, callAsRuntime, rememberSessionOwner, runtimeEnvContribution, registerHttpRoutes };
|
|
@@ -542,8 +562,54 @@ function resolveWorkspaceIdFromRequest(request) {
|
|
|
542
562
|
const query = request.query;
|
|
543
563
|
return validateWorkspaceIdSegment(firstString(headerValue) ?? firstString(query?.workspaceId) ?? "");
|
|
544
564
|
}
|
|
545
|
-
|
|
546
|
-
|
|
565
|
+
function d1HostScopeViolation(request) {
|
|
566
|
+
throw new HttpError({
|
|
567
|
+
status: 421,
|
|
568
|
+
code: ERROR_CODES.D1_HOST_SCOPE_VIOLATION,
|
|
569
|
+
message: ERROR_CODES.D1_HOST_SCOPE_VIOLATION,
|
|
570
|
+
requestId: request.id
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
function resolveRequestScopedWorkspaceId(request, presentedWorkspaceId) {
|
|
574
|
+
const scope = request.requestScope;
|
|
575
|
+
if (!scope) return resolveWorkspaceIdFromRequest(request);
|
|
576
|
+
const presented = [];
|
|
577
|
+
const rawHeaders = request.raw?.rawHeaders;
|
|
578
|
+
if (rawHeaders) {
|
|
579
|
+
for (let index = 0; index < rawHeaders.length; index += 2) {
|
|
580
|
+
if (rawHeaders[index]?.toLowerCase() === "x-boring-workspace-id") presented.push(...rawHeaders[index + 1]?.split(",") ?? [void 0]);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (!presented.length) {
|
|
584
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
585
|
+
if (key.toLowerCase() !== "x-boring-workspace-id") continue;
|
|
586
|
+
if (Array.isArray(value) && value.length === 0) d1HostScopeViolation(request);
|
|
587
|
+
presented.push(...Array.isArray(value) ? value : [value]);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const query = request.query;
|
|
591
|
+
if (query && Object.prototype.hasOwnProperty.call(query, "workspaceId")) {
|
|
592
|
+
const value = query.workspaceId;
|
|
593
|
+
const values = Array.isArray(value) ? value : [value];
|
|
594
|
+
if (values.length === 0) d1HostScopeViolation(request);
|
|
595
|
+
presented.push(...values);
|
|
596
|
+
}
|
|
597
|
+
if (presentedWorkspaceId !== void 0) presented.push(presentedWorkspaceId);
|
|
598
|
+
for (const value of presented) {
|
|
599
|
+
if (typeof value !== "string") d1HostScopeViolation(request);
|
|
600
|
+
let normalized;
|
|
601
|
+
try {
|
|
602
|
+
normalized = validateWorkspaceIdSegment(value);
|
|
603
|
+
} catch {
|
|
604
|
+
d1HostScopeViolation(request);
|
|
605
|
+
}
|
|
606
|
+
if (normalized !== scope.workspaceId) d1HostScopeViolation(request);
|
|
607
|
+
}
|
|
608
|
+
request.headers["x-boring-workspace-id"] = scope.workspaceId;
|
|
609
|
+
return scope.workspaceId;
|
|
610
|
+
}
|
|
611
|
+
async function resolveAuthorizedWorkspaceId(request, workspaceStore, presentedWorkspaceId) {
|
|
612
|
+
const normalizedWorkspaceId = resolveRequestScopedWorkspaceId(request, presentedWorkspaceId);
|
|
547
613
|
const user = request.user;
|
|
548
614
|
if (!user?.id) throw httpError("authentication required", 401);
|
|
549
615
|
let member = false;
|
|
@@ -602,10 +668,15 @@ async function registerAuthProxy(app, options) {
|
|
|
602
668
|
}
|
|
603
669
|
const body = encodeAuthRequestBody(request);
|
|
604
670
|
const targetUrl = new URL(request.url, app.config.auth.url).toString();
|
|
671
|
+
const authHeaders = toHeaders(request.headers);
|
|
672
|
+
authHeaders.delete(REQUEST_SCOPE_WORKSPACE_HEADER);
|
|
673
|
+
if (request.requestScope) {
|
|
674
|
+
authHeaders.set(REQUEST_SCOPE_WORKSPACE_HEADER, encodeURIComponent(request.requestScope.workspaceId));
|
|
675
|
+
}
|
|
605
676
|
const response = await app.auth.handler(
|
|
606
677
|
new Request(targetUrl, {
|
|
607
678
|
method: request.method,
|
|
608
|
-
headers:
|
|
679
|
+
headers: authHeaders,
|
|
609
680
|
body
|
|
610
681
|
})
|
|
611
682
|
);
|
|
@@ -658,10 +729,17 @@ async function registerFrontendAuthPages(app, appRoot, telemetry) {
|
|
|
658
729
|
app.get(pagePath, async (request, reply) => serveFrontendShell(request, reply, indexPath, telemetry));
|
|
659
730
|
}
|
|
660
731
|
}
|
|
661
|
-
async function registerFrontendFallback(app, appRoot, telemetry) {
|
|
732
|
+
async function registerFrontendFallback(app, appRoot, telemetry, rootHandler) {
|
|
662
733
|
const frontDistDir = path.resolve(appRoot, "dist/front");
|
|
663
734
|
const indexPath = path.resolve(frontDistDir, "index.html");
|
|
664
|
-
|
|
735
|
+
if (rootHandler) {
|
|
736
|
+
app.get("/", async (request, reply) => {
|
|
737
|
+
if (await rootHandler(request, reply)) return reply;
|
|
738
|
+
return serveFrontendShell(request, reply, indexPath, telemetry);
|
|
739
|
+
});
|
|
740
|
+
} else {
|
|
741
|
+
app.get("/", async (request, reply) => serveFrontendShell(request, reply, indexPath, telemetry));
|
|
742
|
+
}
|
|
665
743
|
app.get("/*", async (request, reply) => {
|
|
666
744
|
const pathname = request.url.split("?")[0] ?? "/";
|
|
667
745
|
if (!shouldServeFrontend(pathname)) return reply.callNotFound();
|
|
@@ -686,7 +764,7 @@ async function registerFrontendFallback(app, appRoot, telemetry) {
|
|
|
686
764
|
return serveFrontendShell(request, reply, indexPath, telemetry);
|
|
687
765
|
});
|
|
688
766
|
}
|
|
689
|
-
async function createCoreRuntime(config, customTelemetry) {
|
|
767
|
+
async function createCoreRuntime(config, customTelemetry, requestScopeResolver) {
|
|
690
768
|
if (config.stores !== "postgres") {
|
|
691
769
|
throw new Error("createCoreWorkspaceAgentServer currently supports only CORE_STORES=postgres");
|
|
692
770
|
}
|
|
@@ -697,11 +775,16 @@ async function createCoreRuntime(config, customTelemetry) {
|
|
|
697
775
|
storeDb,
|
|
698
776
|
config.encryption.workspaceSettingsKey
|
|
699
777
|
);
|
|
700
|
-
const app = await createCoreApp(config);
|
|
778
|
+
const app = await createCoreApp(config, { requestScopeResolver });
|
|
701
779
|
const telemetry = customTelemetry ?? createDatabaseTelemetryFromEnv(db, { appId: config.appId }, process.env);
|
|
702
780
|
const telemetrySource = customTelemetry ? "custom" : process.env.BORING_TELEMETRY_ENABLED === "true" ? "db-env" : "noop-env";
|
|
703
781
|
app.log.debug({ telemetry: { source: telemetrySource } }, "resolved telemetry sink");
|
|
704
|
-
const auth = createAuth(config, db, {
|
|
782
|
+
const auth = createAuth(config, db, {
|
|
783
|
+
workspaceStore,
|
|
784
|
+
logger: app.log,
|
|
785
|
+
telemetry,
|
|
786
|
+
disableDefaultWorkspaceCreation: requestScopeResolver !== void 0
|
|
787
|
+
});
|
|
705
788
|
app.decorate("db", db);
|
|
706
789
|
app.decorate("auth", auth);
|
|
707
790
|
app.decorate("userStore", userStore);
|
|
@@ -740,7 +823,7 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
740
823
|
}
|
|
741
824
|
assertCoreStaticPluginEntries(options.plugins);
|
|
742
825
|
const config = options.config ?? await loadConfig(resolveCoreLoadConfigOptions(options));
|
|
743
|
-
const { app, sql, db, userStore, workspaceStore, telemetry } = await createCoreRuntime(config, options.telemetry);
|
|
826
|
+
const { app, sql, db, userStore, workspaceStore, telemetry } = await createCoreRuntime(config, options.telemetry, options.requestScopeResolver);
|
|
744
827
|
const appRoot = options.appRoot;
|
|
745
828
|
const serveFrontend = options.serveFrontend ?? (process.env.NODE_ENV !== "development" && Boolean(appRoot));
|
|
746
829
|
const pluginWorkspaceRoot = process.cwd();
|
|
@@ -785,12 +868,20 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
785
868
|
workspaceRoot: pluginWorkspaceRoot,
|
|
786
869
|
bridge: createUnavailableCorePluginBridge()
|
|
787
870
|
};
|
|
788
|
-
const
|
|
871
|
+
const defaultPluginActorResolver = async (request) => {
|
|
789
872
|
const workspaceId = await resolveAuthorizedWorkspaceId(request, workspaceStore);
|
|
790
873
|
const userId = request.user?.id;
|
|
791
874
|
if (!userId) throw httpError("authentication required", 401);
|
|
792
875
|
return { workspaceId, userId };
|
|
793
|
-
}
|
|
876
|
+
};
|
|
877
|
+
const trustedPluginActorResolver = async (request) => {
|
|
878
|
+
if (!options.trustedPluginActorResolver) return await defaultPluginActorResolver(request);
|
|
879
|
+
if (!request.requestScope) return await options.trustedPluginActorResolver(request);
|
|
880
|
+
const workspaceId = await resolveAuthorizedWorkspaceId(request, workspaceStore);
|
|
881
|
+
const actor = await options.trustedPluginActorResolver(request);
|
|
882
|
+
if (actor.workspaceId !== workspaceId) d1HostScopeViolation(request);
|
|
883
|
+
return actor;
|
|
884
|
+
};
|
|
794
885
|
const trustedPluginResolveContext = {
|
|
795
886
|
...basePluginResolveContext,
|
|
796
887
|
trusted: {
|
|
@@ -823,7 +914,7 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
823
914
|
excludeDefaults: options.excludeDefaults,
|
|
824
915
|
installPluginAuthoring
|
|
825
916
|
});
|
|
826
|
-
const resolveWorkspaceId = async (request) => options.getWorkspaceId ? await options.getWorkspaceId(request) : await resolveAuthorizedWorkspaceId(request, workspaceStore);
|
|
917
|
+
const resolveWorkspaceId = async (request, presentedWorkspaceId) => request.requestScope ? await resolveAuthorizedWorkspaceId(request, workspaceStore, presentedWorkspaceId) : options.getWorkspaceId ? await options.getWorkspaceId(request) : await resolveAuthorizedWorkspaceId(request, workspaceStore, presentedWorkspaceId);
|
|
827
918
|
const resolveRoot = async (workspaceId, request) => {
|
|
828
919
|
const root = options.getWorkspaceRoot ? await options.getWorkspaceRoot(workspaceId, request) : await resolveWorkspaceRoot(workspaceRoot, workspaceId);
|
|
829
920
|
return root;
|
|
@@ -840,7 +931,10 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
840
931
|
workspaceStore,
|
|
841
932
|
corsOrigins: app.config.cors.origins,
|
|
842
933
|
validateWorkspaceId: validateWorkspaceIdSegment,
|
|
843
|
-
agentSessionId: agentSessionIdFromRequest
|
|
934
|
+
agentSessionId: agentSessionIdFromRequest,
|
|
935
|
+
assertRuntimeWorkspaceScope: options.requestScopeResolver ? (request, workspaceId) => {
|
|
936
|
+
if (request.requestScope?.workspaceId !== workspaceId) d1HostScopeViolation(request);
|
|
937
|
+
} : void 0
|
|
844
938
|
});
|
|
845
939
|
app.addHook("preHandler", async (request) => {
|
|
846
940
|
await coreBridge.rememberSessionOwner(request);
|
|
@@ -890,6 +984,9 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
890
984
|
projectName: workspace?.name ?? "Workspace"
|
|
891
985
|
};
|
|
892
986
|
} catch (error) {
|
|
987
|
+
if (error?.status === 421 && error?.code === ERROR_CODES.D1_HOST_SCOPE_VIOLATION) {
|
|
988
|
+
throw error;
|
|
989
|
+
}
|
|
893
990
|
const statusCode = typeof error?.statusCode === "number" ? error.statusCode : 500;
|
|
894
991
|
const message = error instanceof Error ? error.message : "workspace meta failed";
|
|
895
992
|
return reply.code(statusCode).send({ error: message });
|
|
@@ -976,7 +1073,7 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
976
1073
|
await app.register(routes);
|
|
977
1074
|
}
|
|
978
1075
|
if (serveFrontend && appRoot) {
|
|
979
|
-
await registerFrontendFallback(app, appRoot, telemetry);
|
|
1076
|
+
await registerFrontendFallback(app, appRoot, telemetry, options.frontendRootHandler);
|
|
980
1077
|
}
|
|
981
1078
|
return app;
|
|
982
1079
|
}
|
|
@@ -29,6 +29,8 @@ var ERROR_CODES = {
|
|
|
29
29
|
MAIL_DISABLED: "mail_disabled",
|
|
30
30
|
DB_UNAVAILABLE: "db_unavailable",
|
|
31
31
|
INTERNAL_ERROR: "internal_error",
|
|
32
|
+
D1_HOST_SCOPE_VIOLATION: "D1_HOST_SCOPE_VIOLATION",
|
|
33
|
+
D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN: "D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN",
|
|
32
34
|
// Credits + purchases
|
|
33
35
|
PAYMENT_REQUIRED: "payment_required",
|
|
34
36
|
INVALID_PACK: "invalid_pack",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ERROR_CODES,
|
|
3
3
|
HttpError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-H7EYII35.js";
|
|
5
5
|
import {
|
|
6
6
|
__export
|
|
7
7
|
} from "./chunk-MLKGABMK.js";
|
|
@@ -296,10 +296,15 @@ var LocalWorkspaceStore = class {
|
|
|
296
296
|
this.members.set(key, member);
|
|
297
297
|
return member;
|
|
298
298
|
}
|
|
299
|
-
async
|
|
299
|
+
async createMemberIfAbsent(workspaceId, userId, role) {
|
|
300
|
+
if (this.members.has(`${workspaceId}:${userId}`)) return null;
|
|
301
|
+
return this.upsertMember(workspaceId, userId, role);
|
|
302
|
+
}
|
|
303
|
+
async updateMemberRole(workspaceId, userId, role, opts) {
|
|
300
304
|
const key = `${workspaceId}:${userId}`;
|
|
301
305
|
const membership = this.members.get(key);
|
|
302
306
|
if (!membership) return { code: ERROR_CODES.NOT_MEMBER };
|
|
307
|
+
if (opts?.forbidExistingOwnerMutation && membership.role === "owner" && role !== "owner") return { code: ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN };
|
|
303
308
|
if (membership.role === "owner" && role !== "owner") {
|
|
304
309
|
let otherOwnerExists = false;
|
|
305
310
|
for (const m of this.members.values()) {
|
|
@@ -318,6 +323,7 @@ var LocalWorkspaceStore = class {
|
|
|
318
323
|
const key = `${workspaceId}:${userId}`;
|
|
319
324
|
const membership = this.members.get(key);
|
|
320
325
|
if (!membership) return { removed: false, code: ERROR_CODES.NOT_MEMBER };
|
|
326
|
+
if (opts?.forbidExistingOwnerMutation && membership.role === "owner") return { removed: false, code: ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN };
|
|
321
327
|
if (!opts?.allowLastOwner && membership.role === "owner") {
|
|
322
328
|
let otherOwnerExists = false;
|
|
323
329
|
for (const m of this.members.values()) {
|
|
@@ -1293,7 +1299,11 @@ var PostgresWorkspaceStore = class {
|
|
|
1293
1299
|
}).returning();
|
|
1294
1300
|
return toWorkspaceMember(row);
|
|
1295
1301
|
}
|
|
1296
|
-
async
|
|
1302
|
+
async createMemberIfAbsent(workspaceId, userId, role) {
|
|
1303
|
+
const [row] = await this.db.insert(workspaceMembers).values({ workspaceId, userId, role }).onConflictDoNothing().returning();
|
|
1304
|
+
return row ? toWorkspaceMember(row) : null;
|
|
1305
|
+
}
|
|
1306
|
+
async updateMemberRole(workspaceId, userId, role, opts) {
|
|
1297
1307
|
return this.db.transaction(async (tx) => {
|
|
1298
1308
|
await tx.execute(sql4`
|
|
1299
1309
|
SELECT user_id
|
|
@@ -1309,6 +1319,7 @@ var PostgresWorkspaceStore = class {
|
|
|
1309
1319
|
).limit(1);
|
|
1310
1320
|
const currentRole = memberRows[0]?.role;
|
|
1311
1321
|
if (!currentRole) return { code: ERROR_CODES.NOT_MEMBER };
|
|
1322
|
+
if (opts?.forbidExistingOwnerMutation && currentRole === "owner" && role !== "owner") return { code: ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN };
|
|
1312
1323
|
if (currentRole === "owner" && role !== "owner") {
|
|
1313
1324
|
const [{ count }] = await tx.select({ count: sql4`count(*)::int` }).from(workspaceMembers).where(
|
|
1314
1325
|
and(
|
|
@@ -1335,7 +1346,7 @@ var PostgresWorkspaceStore = class {
|
|
|
1335
1346
|
SELECT user_id
|
|
1336
1347
|
FROM workspace_members
|
|
1337
1348
|
WHERE workspace_id = ${workspaceId}
|
|
1338
|
-
AND role = 'owner'
|
|
1349
|
+
${opts?.forbidExistingOwnerMutation ? sql4`` : sql4`AND role = 'owner'`}
|
|
1339
1350
|
FOR UPDATE
|
|
1340
1351
|
`);
|
|
1341
1352
|
const memberRows = await tx.select({ role: workspaceMembers.role }).from(workspaceMembers).where(
|
|
@@ -1346,6 +1357,7 @@ var PostgresWorkspaceStore = class {
|
|
|
1346
1357
|
).limit(1);
|
|
1347
1358
|
const role = memberRows[0]?.role;
|
|
1348
1359
|
if (!role) return { removed: false, code: ERROR_CODES.NOT_MEMBER };
|
|
1360
|
+
if (opts?.forbidExistingOwnerMutation && role === "owner") return { removed: false, code: ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN };
|
|
1349
1361
|
if (!opts?.allowLastOwner && role === "owner") {
|
|
1350
1362
|
const [{ count }] = await tx.select({ count: sql4`count(*)::int` }).from(workspaceMembers).where(
|
|
1351
1363
|
and(
|
|
@@ -2772,6 +2784,20 @@ var PostgresBudgetReservationStore = class {
|
|
|
2772
2784
|
return count;
|
|
2773
2785
|
});
|
|
2774
2786
|
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Read-only snapshot of current-period used/held spend for a budget target,
|
|
2789
|
+
* reusing the exact same accounting the admission path applies during a
|
|
2790
|
+
* reservation (usage ledger + settled/active reservations). No rows are
|
|
2791
|
+
* written. The reset boundary is derived from the same monthly period logic.
|
|
2792
|
+
*/
|
|
2793
|
+
async getSpendSnapshot(query) {
|
|
2794
|
+
if (!query.userId) throw new Error("getSpendSnapshot requires userId");
|
|
2795
|
+
const now = query.now ?? /* @__PURE__ */ new Date();
|
|
2796
|
+
const period = monthPeriodUtc(now);
|
|
2797
|
+
const input = query.scope === "model" ? { scope: "model", userId: query.userId, provider: query.provider, model: query.model, runId: "", budgetMicros: 1, holdMicros: 1, ttlSeconds: 1 } : { scope: "user", userId: query.userId, runId: "", budgetMicros: 1, holdMicros: 1, ttlSeconds: 1 };
|
|
2798
|
+
const { usedMicros, heldMicros } = await this.db.transaction((tx) => this.spend(tx, input, period.period, period.start, period.end));
|
|
2799
|
+
return { scope: query.scope, usedMicros, heldMicros, period: period.period, periodStart: period.start, periodEnd: period.end };
|
|
2800
|
+
}
|
|
2775
2801
|
async reserve(input) {
|
|
2776
2802
|
assertReserveInput(input);
|
|
2777
2803
|
const now = input.now ?? /* @__PURE__ */ new Date();
|