@hachej/boring-core 0.1.80 → 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.
@@ -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,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(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
- })
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
- async function resolveAuthorizedWorkspaceId(request, workspaceStore) {
546
- const normalizedWorkspaceId = resolveWorkspaceIdFromRequest(request);
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: toHeaders(request.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
- app.get("/", async (request, reply) => serveFrontendShell(request, reply, indexPath, telemetry));
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, { workspaceStore, logger: app.log, telemetry });
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,18 +860,30 @@ async function createCoreWorkspaceAgentServer(options = {}) {
785
860
  workspaceRoot: pluginWorkspaceRoot,
786
861
  bridge: createUnavailableCorePluginBridge()
787
862
  };
788
- const trustedPluginActorResolver = options.trustedPluginActorResolver ?? (async (request) => {
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: {
797
880
  workspaceAgentDispatcherResolver: trustedDispatcherProxy,
798
881
  actorResolver: trustedPluginActorResolver,
799
- sql
882
+ sql,
883
+ actorVerifier: async (actor) => Boolean(
884
+ await workspaceStore.get(actor.workspaceId) && await workspaceStore.isMember(actor.workspaceId, actor.userId) && await userStore.getById(actor.userId)
885
+ ),
886
+ hostedAutomationTriggerToken: process.env.BORING_AUTOMATION_TRIGGER_TOKEN
800
887
  }
801
888
  };
802
889
  const resolvedPlugins = await Promise.all(
@@ -819,7 +906,7 @@ async function createCoreWorkspaceAgentServer(options = {}) {
819
906
  excludeDefaults: options.excludeDefaults,
820
907
  installPluginAuthoring
821
908
  });
822
- 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);
823
910
  const resolveRoot = async (workspaceId, request) => {
824
911
  const root = options.getWorkspaceRoot ? await options.getWorkspaceRoot(workspaceId, request) : await resolveWorkspaceRoot(workspaceRoot, workspaceId);
825
912
  return root;
@@ -886,6 +973,9 @@ async function createCoreWorkspaceAgentServer(options = {}) {
886
973
  projectName: workspace?.name ?? "Workspace"
887
974
  };
888
975
  } catch (error) {
976
+ if (error?.status === 421 && error?.code === ERROR_CODES.D1_HOST_SCOPE_VIOLATION) {
977
+ throw error;
978
+ }
889
979
  const statusCode = typeof error?.statusCode === "number" ? error.statusCode : 500;
890
980
  const message = error instanceof Error ? error.message : "workspace meta failed";
891
981
  return reply.code(statusCode).send({ error: message });
@@ -972,7 +1062,7 @@ async function createCoreWorkspaceAgentServer(options = {}) {
972
1062
  await app.register(routes);
973
1063
  }
974
1064
  if (serveFrontend && appRoot) {
975
- await registerFrontendFallback(app, appRoot, telemetry);
1065
+ await registerFrontendFallback(app, appRoot, telemetry, options.frontendRootHandler);
976
1066
  }
977
1067
  return app;
978
1068
  }
@@ -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();