@hachej/boring-core 0.1.81 → 0.1.82
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 +123 -37
- 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,7 @@ 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();
|
|
69
72
|
const bridges = /* @__PURE__ */ new Map();
|
|
70
73
|
const getBridge = (workspaceId) => {
|
|
71
74
|
const safeWorkspaceId = validateWorkspaceId(workspaceId);
|
|
@@ -103,7 +106,7 @@ function createCoreWorkspaceBridge(options) {
|
|
|
103
106
|
secret: options.workspaceBridge.runtimeTokenSecret
|
|
104
107
|
}).authContext.workspaceId;
|
|
105
108
|
}
|
|
106
|
-
return await resolveWorkspaceId(request);
|
|
109
|
+
return admittedBrowserWorkspaces.get(request) ?? await resolveWorkspaceId(request);
|
|
107
110
|
};
|
|
108
111
|
const rememberSessionOwner = async (request) => {
|
|
109
112
|
const user = request.user;
|
|
@@ -173,25 +176,34 @@ function createCoreWorkspaceBridge(options) {
|
|
|
173
176
|
}
|
|
174
177
|
} : void 0;
|
|
175
178
|
const registerHttpRoutes = async (app) => {
|
|
176
|
-
await app.register(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
179
|
+
await app.register(async (bridgeApp) => {
|
|
180
|
+
bridgeApp.addHook("preHandler", async (request) => {
|
|
181
|
+
const pathname = request.url.split("?")[0];
|
|
182
|
+
const authHeader = request.headers.authorization;
|
|
183
|
+
if (request.requestScope && pathname === "/api/v1/workspace-bridge/call" && !(typeof authHeader === "string" && authHeader.startsWith("Bearer "))) {
|
|
184
|
+
admittedBrowserWorkspaces.set(request, await resolveWorkspaceId(request));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
await bridgeApp.register(workspaceBridgeHttpRoutes, {
|
|
188
|
+
getRegistry: async (request) => getRuntime(await resolveBridgeWorkspaceId(request)).registry,
|
|
189
|
+
getIdempotencyStore: async (request) => getRuntime(await resolveBridgeWorkspaceId(request)).idempotencyStore,
|
|
190
|
+
runtimeTokenSecret: options.workspaceBridge?.runtimeTokenSecret,
|
|
191
|
+
runtimeRefreshTokenSecret: options.workspaceBridge?.runtimeRefreshTokenSecret,
|
|
192
|
+
getRuntimeRefreshTokenStore: (_request, claims) => getRuntime(claims.workspaceId).refreshTokenStore,
|
|
193
|
+
getOwnerWorkspaceId: async (request) => await resolveBridgeWorkspaceId(request),
|
|
194
|
+
browserAuthPolicy: createBrowserBridgeAuthPolicy({
|
|
195
|
+
getPrincipal: (input) => {
|
|
196
|
+
const user = input.request?.user;
|
|
197
|
+
return user?.id ? { userId: user.id, email: user.email ?? void 0 } : null;
|
|
198
|
+
},
|
|
199
|
+
authorizeWorkspace: async ({ principal, workspaceId, definition }) => ({
|
|
200
|
+
allowed: await workspaceStore.isMember(workspaceId, principal.userId),
|
|
201
|
+
capabilities: definition.requiredCapabilities
|
|
202
|
+
}),
|
|
203
|
+
allowedOrigins: options.corsOrigins,
|
|
204
|
+
requireCsrfHeader: true
|
|
205
|
+
})
|
|
206
|
+
});
|
|
195
207
|
});
|
|
196
208
|
};
|
|
197
209
|
return { getBridge, callAsRuntime, rememberSessionOwner, runtimeEnvContribution, registerHttpRoutes };
|
|
@@ -542,8 +554,54 @@ function resolveWorkspaceIdFromRequest(request) {
|
|
|
542
554
|
const query = request.query;
|
|
543
555
|
return validateWorkspaceIdSegment(firstString(headerValue) ?? firstString(query?.workspaceId) ?? "");
|
|
544
556
|
}
|
|
545
|
-
|
|
546
|
-
|
|
557
|
+
function d1HostScopeViolation(request) {
|
|
558
|
+
throw new HttpError({
|
|
559
|
+
status: 421,
|
|
560
|
+
code: ERROR_CODES.D1_HOST_SCOPE_VIOLATION,
|
|
561
|
+
message: ERROR_CODES.D1_HOST_SCOPE_VIOLATION,
|
|
562
|
+
requestId: request.id
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
function resolveRequestScopedWorkspaceId(request, presentedWorkspaceId) {
|
|
566
|
+
const scope = request.requestScope;
|
|
567
|
+
if (!scope) return resolveWorkspaceIdFromRequest(request);
|
|
568
|
+
const presented = [];
|
|
569
|
+
const rawHeaders = request.raw?.rawHeaders;
|
|
570
|
+
if (rawHeaders) {
|
|
571
|
+
for (let index = 0; index < rawHeaders.length; index += 2) {
|
|
572
|
+
if (rawHeaders[index]?.toLowerCase() === "x-boring-workspace-id") presented.push(...rawHeaders[index + 1]?.split(",") ?? [void 0]);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (!presented.length) {
|
|
576
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
577
|
+
if (key.toLowerCase() !== "x-boring-workspace-id") continue;
|
|
578
|
+
if (Array.isArray(value) && value.length === 0) d1HostScopeViolation(request);
|
|
579
|
+
presented.push(...Array.isArray(value) ? value : [value]);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const query = request.query;
|
|
583
|
+
if (query && Object.prototype.hasOwnProperty.call(query, "workspaceId")) {
|
|
584
|
+
const value = query.workspaceId;
|
|
585
|
+
const values = Array.isArray(value) ? value : [value];
|
|
586
|
+
if (values.length === 0) d1HostScopeViolation(request);
|
|
587
|
+
presented.push(...values);
|
|
588
|
+
}
|
|
589
|
+
if (presentedWorkspaceId !== void 0) presented.push(presentedWorkspaceId);
|
|
590
|
+
for (const value of presented) {
|
|
591
|
+
if (typeof value !== "string") d1HostScopeViolation(request);
|
|
592
|
+
let normalized;
|
|
593
|
+
try {
|
|
594
|
+
normalized = validateWorkspaceIdSegment(value);
|
|
595
|
+
} catch {
|
|
596
|
+
d1HostScopeViolation(request);
|
|
597
|
+
}
|
|
598
|
+
if (normalized !== scope.workspaceId) d1HostScopeViolation(request);
|
|
599
|
+
}
|
|
600
|
+
request.headers["x-boring-workspace-id"] = scope.workspaceId;
|
|
601
|
+
return scope.workspaceId;
|
|
602
|
+
}
|
|
603
|
+
async function resolveAuthorizedWorkspaceId(request, workspaceStore, presentedWorkspaceId) {
|
|
604
|
+
const normalizedWorkspaceId = resolveRequestScopedWorkspaceId(request, presentedWorkspaceId);
|
|
547
605
|
const user = request.user;
|
|
548
606
|
if (!user?.id) throw httpError("authentication required", 401);
|
|
549
607
|
let member = false;
|
|
@@ -602,10 +660,15 @@ async function registerAuthProxy(app, options) {
|
|
|
602
660
|
}
|
|
603
661
|
const body = encodeAuthRequestBody(request);
|
|
604
662
|
const targetUrl = new URL(request.url, app.config.auth.url).toString();
|
|
663
|
+
const authHeaders = toHeaders(request.headers);
|
|
664
|
+
authHeaders.delete(REQUEST_SCOPE_WORKSPACE_HEADER);
|
|
665
|
+
if (request.requestScope) {
|
|
666
|
+
authHeaders.set(REQUEST_SCOPE_WORKSPACE_HEADER, encodeURIComponent(request.requestScope.workspaceId));
|
|
667
|
+
}
|
|
605
668
|
const response = await app.auth.handler(
|
|
606
669
|
new Request(targetUrl, {
|
|
607
670
|
method: request.method,
|
|
608
|
-
headers:
|
|
671
|
+
headers: authHeaders,
|
|
609
672
|
body
|
|
610
673
|
})
|
|
611
674
|
);
|
|
@@ -658,10 +721,17 @@ async function registerFrontendAuthPages(app, appRoot, telemetry) {
|
|
|
658
721
|
app.get(pagePath, async (request, reply) => serveFrontendShell(request, reply, indexPath, telemetry));
|
|
659
722
|
}
|
|
660
723
|
}
|
|
661
|
-
async function registerFrontendFallback(app, appRoot, telemetry) {
|
|
724
|
+
async function registerFrontendFallback(app, appRoot, telemetry, rootHandler) {
|
|
662
725
|
const frontDistDir = path.resolve(appRoot, "dist/front");
|
|
663
726
|
const indexPath = path.resolve(frontDistDir, "index.html");
|
|
664
|
-
|
|
727
|
+
if (rootHandler) {
|
|
728
|
+
app.get("/", async (request, reply) => {
|
|
729
|
+
if (await rootHandler(request, reply)) return reply;
|
|
730
|
+
return serveFrontendShell(request, reply, indexPath, telemetry);
|
|
731
|
+
});
|
|
732
|
+
} else {
|
|
733
|
+
app.get("/", async (request, reply) => serveFrontendShell(request, reply, indexPath, telemetry));
|
|
734
|
+
}
|
|
665
735
|
app.get("/*", async (request, reply) => {
|
|
666
736
|
const pathname = request.url.split("?")[0] ?? "/";
|
|
667
737
|
if (!shouldServeFrontend(pathname)) return reply.callNotFound();
|
|
@@ -686,7 +756,7 @@ async function registerFrontendFallback(app, appRoot, telemetry) {
|
|
|
686
756
|
return serveFrontendShell(request, reply, indexPath, telemetry);
|
|
687
757
|
});
|
|
688
758
|
}
|
|
689
|
-
async function createCoreRuntime(config, customTelemetry) {
|
|
759
|
+
async function createCoreRuntime(config, customTelemetry, requestScopeResolver) {
|
|
690
760
|
if (config.stores !== "postgres") {
|
|
691
761
|
throw new Error("createCoreWorkspaceAgentServer currently supports only CORE_STORES=postgres");
|
|
692
762
|
}
|
|
@@ -697,11 +767,16 @@ async function createCoreRuntime(config, customTelemetry) {
|
|
|
697
767
|
storeDb,
|
|
698
768
|
config.encryption.workspaceSettingsKey
|
|
699
769
|
);
|
|
700
|
-
const app = await createCoreApp(config);
|
|
770
|
+
const app = await createCoreApp(config, { requestScopeResolver });
|
|
701
771
|
const telemetry = customTelemetry ?? createDatabaseTelemetryFromEnv(db, { appId: config.appId }, process.env);
|
|
702
772
|
const telemetrySource = customTelemetry ? "custom" : process.env.BORING_TELEMETRY_ENABLED === "true" ? "db-env" : "noop-env";
|
|
703
773
|
app.log.debug({ telemetry: { source: telemetrySource } }, "resolved telemetry sink");
|
|
704
|
-
const auth = createAuth(config, db, {
|
|
774
|
+
const auth = createAuth(config, db, {
|
|
775
|
+
workspaceStore,
|
|
776
|
+
logger: app.log,
|
|
777
|
+
telemetry,
|
|
778
|
+
disableDefaultWorkspaceCreation: requestScopeResolver !== void 0
|
|
779
|
+
});
|
|
705
780
|
app.decorate("db", db);
|
|
706
781
|
app.decorate("auth", auth);
|
|
707
782
|
app.decorate("userStore", userStore);
|
|
@@ -740,7 +815,7 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
740
815
|
}
|
|
741
816
|
assertCoreStaticPluginEntries(options.plugins);
|
|
742
817
|
const config = options.config ?? await loadConfig(resolveCoreLoadConfigOptions(options));
|
|
743
|
-
const { app, sql, db, userStore, workspaceStore, telemetry } = await createCoreRuntime(config, options.telemetry);
|
|
818
|
+
const { app, sql, db, userStore, workspaceStore, telemetry } = await createCoreRuntime(config, options.telemetry, options.requestScopeResolver);
|
|
744
819
|
const appRoot = options.appRoot;
|
|
745
820
|
const serveFrontend = options.serveFrontend ?? (process.env.NODE_ENV !== "development" && Boolean(appRoot));
|
|
746
821
|
const pluginWorkspaceRoot = process.cwd();
|
|
@@ -785,12 +860,20 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
785
860
|
workspaceRoot: pluginWorkspaceRoot,
|
|
786
861
|
bridge: createUnavailableCorePluginBridge()
|
|
787
862
|
};
|
|
788
|
-
const
|
|
863
|
+
const defaultPluginActorResolver = async (request) => {
|
|
789
864
|
const workspaceId = await resolveAuthorizedWorkspaceId(request, workspaceStore);
|
|
790
865
|
const userId = request.user?.id;
|
|
791
866
|
if (!userId) throw httpError("authentication required", 401);
|
|
792
867
|
return { workspaceId, userId };
|
|
793
|
-
}
|
|
868
|
+
};
|
|
869
|
+
const trustedPluginActorResolver = async (request) => {
|
|
870
|
+
if (!options.trustedPluginActorResolver) return await defaultPluginActorResolver(request);
|
|
871
|
+
if (!request.requestScope) return await options.trustedPluginActorResolver(request);
|
|
872
|
+
const workspaceId = await resolveAuthorizedWorkspaceId(request, workspaceStore);
|
|
873
|
+
const actor = await options.trustedPluginActorResolver(request);
|
|
874
|
+
if (actor.workspaceId !== workspaceId) d1HostScopeViolation(request);
|
|
875
|
+
return actor;
|
|
876
|
+
};
|
|
794
877
|
const trustedPluginResolveContext = {
|
|
795
878
|
...basePluginResolveContext,
|
|
796
879
|
trusted: {
|
|
@@ -823,7 +906,7 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
823
906
|
excludeDefaults: options.excludeDefaults,
|
|
824
907
|
installPluginAuthoring
|
|
825
908
|
});
|
|
826
|
-
const resolveWorkspaceId = async (request) => options.getWorkspaceId ? await options.getWorkspaceId(request) : await resolveAuthorizedWorkspaceId(request, workspaceStore);
|
|
909
|
+
const resolveWorkspaceId = async (request, presentedWorkspaceId) => request.requestScope ? await resolveAuthorizedWorkspaceId(request, workspaceStore, presentedWorkspaceId) : options.getWorkspaceId ? await options.getWorkspaceId(request) : await resolveAuthorizedWorkspaceId(request, workspaceStore, presentedWorkspaceId);
|
|
827
910
|
const resolveRoot = async (workspaceId, request) => {
|
|
828
911
|
const root = options.getWorkspaceRoot ? await options.getWorkspaceRoot(workspaceId, request) : await resolveWorkspaceRoot(workspaceRoot, workspaceId);
|
|
829
912
|
return root;
|
|
@@ -890,6 +973,9 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
890
973
|
projectName: workspace?.name ?? "Workspace"
|
|
891
974
|
};
|
|
892
975
|
} catch (error) {
|
|
976
|
+
if (error?.status === 421 && error?.code === ERROR_CODES.D1_HOST_SCOPE_VIOLATION) {
|
|
977
|
+
throw error;
|
|
978
|
+
}
|
|
893
979
|
const statusCode = typeof error?.statusCode === "number" ? error.statusCode : 500;
|
|
894
980
|
const message = error instanceof Error ? error.message : "workspace meta failed";
|
|
895
981
|
return reply.code(statusCode).send({ error: message });
|
|
@@ -976,7 +1062,7 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
976
1062
|
await app.register(routes);
|
|
977
1063
|
}
|
|
978
1064
|
if (serveFrontend && appRoot) {
|
|
979
|
-
await registerFrontendFallback(app, appRoot, telemetry);
|
|
1065
|
+
await registerFrontendFallback(app, appRoot, telemetry, options.frontendRootHandler);
|
|
980
1066
|
}
|
|
981
1067
|
return app;
|
|
982
1068
|
}
|
|
@@ -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();
|