@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.
@@ -1,5 +1,5 @@
1
1
  import postgres from 'postgres';
2
- import { C as CoreConfig } from './types-DDnayJjI.js';
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 c, ModelBudgetExceededError as d, PostgresMeteringStore as e, PostgresModelBudgetStore as f, type RecordUsageResult as g, type ReservationFinalStatus as h, type ReserveBudgetInput as i, type ReserveBudgetResult as j, type ReserveInput as k, type ReserveResult as l, type RunMigrationsOptions as m, runMigrations as r };
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 };
@@ -9,12 +9,12 @@ import {
9
9
  useSignIn,
10
10
  useSignUp,
11
11
  useWorkspaceRouteStatus
12
- } from "../../chunk-3B7LU76T.js";
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-QZGYKLXB.js";
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-DDnayJjI.js';
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-C3iHuZNw.js';
8
- import { D as Database, U as UserStore, W as WorkspaceStore } from '../../types-Dm11Zm56.js';
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 };
@@ -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-VB2N2DG5.js";
13
+ } from "../../chunk-YFCR6SAS.js";
13
14
  import {
14
15
  PostgresUserStore,
15
16
  PostgresWorkspaceStore,
16
17
  createDatabase,
17
18
  telemetryEvents
18
- } from "../../chunk-H5BQOHWR.js";
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
- } from "../../chunk-QZGYKLXB.js";
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
- await app.register(workspaceBridgeHttpRoutes, {
177
- getRegistry: async (request) => getRuntime(await resolveBridgeWorkspaceId(request)).registry,
178
- getIdempotencyStore: async (request) => getRuntime(await resolveBridgeWorkspaceId(request)).idempotencyStore,
179
- runtimeTokenSecret: options.workspaceBridge?.runtimeTokenSecret,
180
- runtimeRefreshTokenSecret: options.workspaceBridge?.runtimeRefreshTokenSecret,
181
- getRuntimeRefreshTokenStore: (_request, claims) => getRuntime(claims.workspaceId).refreshTokenStore,
182
- getOwnerWorkspaceId: async (request) => await resolveBridgeWorkspaceId(request),
183
- browserAuthPolicy: createBrowserBridgeAuthPolicy({
184
- getPrincipal: (input) => {
185
- const user = input.request?.user;
186
- return user?.id ? { userId: user.id, email: user.email ?? void 0 } : null;
187
- },
188
- authorizeWorkspace: async ({ principal, workspaceId, definition }) => ({
189
- allowed: await workspaceStore.isMember(workspaceId, principal.userId),
190
- capabilities: definition.requiredCapabilities
191
- }),
192
- allowedOrigins: options.corsOrigins,
193
- requireCsrfHeader: true
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
- async function resolveAuthorizedWorkspaceId(request, workspaceStore) {
546
- const normalizedWorkspaceId = resolveWorkspaceIdFromRequest(request);
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: toHeaders(request.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
- app.get("/", async (request, reply) => serveFrontendShell(request, reply, indexPath, telemetry));
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, { workspaceStore, logger: app.log, telemetry });
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 trustedPluginActorResolver = options.trustedPluginActorResolver ?? (async (request) => {
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
  }
@@ -9,7 +9,7 @@ import {
9
9
  ConfigFetchError,
10
10
  ERROR_CODES,
11
11
  HttpError
12
- } from "./chunk-QZGYKLXB.js";
12
+ } from "./chunk-H7EYII35.js";
13
13
 
14
14
  // src/front/AppErrorBoundary.tsx
15
15
  import { Component } from "react";
@@ -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-QZGYKLXB.js";
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 updateMemberRole(workspaceId, userId, role) {
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 updateMemberRole(workspaceId, userId, role) {
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();