@hachej/boring-core 0.1.43 → 0.1.45

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.
@@ -118,12 +118,16 @@ interface CreditLedgerEntry {
118
118
  createdAt: string;
119
119
  description: string;
120
120
  }
121
- /** Format SIGNED credit micros as a euro string with an explicit +/− sign. */
122
- declare function formatSignedCreditMicros(micros: number, locale?: string): string;
121
+ /** Format SIGNED credit micros as a currency string with an explicit +/− sign.
122
+ * `currency` is the configured display currency (1 credit-unit = 1 major unit); defaults
123
+ * to EUR for callers without a configured purchase currency. */
124
+ declare function formatSignedCreditMicros(micros: number, currency?: string, locale?: string): string;
123
125
  /** Format a minor-unit price (e.g. cents) in its currency for pack labels/buttons. */
124
126
  declare function formatMinorPrice(priceMinor: number, currency: string, locale?: string): string;
125
- /** Format credit micros as a euro string. 1 credit = €0.000001 µ/1e6 euros. */
126
- declare function formatCreditMicros(micros: number, locale?: string): string;
127
+ /** Format credit micros as a currency string. 1 credit-unit = 1 major unit of the
128
+ * configured display `currency` (µ/1e6). Defaults to EUR for callers without a
129
+ * configured purchase currency (e.g. a consumption-only deployment). */
130
+ declare function formatCreditMicros(micros: number, currency?: string, locale?: string): string;
127
131
  /** True when the remaining balance is at or below the low-balance threshold. */
128
132
  declare function isLowBalance(micros: number, thresholdMicros?: number): boolean;
129
133
  /** Stable server error code for an out-of-credits rejection (mirrors the agent's
@@ -2,6 +2,7 @@ import {
2
2
  CoreFront,
3
3
  UserMenu,
4
4
  WorkspaceSwitcher,
5
+ routes,
5
6
  useCurrentWorkspace,
6
7
  useSession,
7
8
  useSignIn,
@@ -144,6 +145,7 @@ function AuthCard({
144
145
  mode === "signup" ? /* @__PURE__ */ jsx2("input", { className: "w-full rounded-xl border border-border bg-background px-3 py-2.5 text-sm outline-none focus:border-ring", placeholder: "Name", value: name, onChange: (event) => setName(event.currentTarget.value) }) : null,
145
146
  /* @__PURE__ */ jsx2("input", { className: "w-full rounded-xl border border-border bg-background px-3 py-2.5 text-sm outline-none focus:border-ring", type: "email", autoComplete: "email", placeholder: "Email", value: email, onChange: (event) => setEmail(event.currentTarget.value), required: true }),
146
147
  /* @__PURE__ */ jsx2("input", { className: "w-full rounded-xl border border-border bg-background px-3 py-2.5 text-sm outline-none focus:border-ring", type: "password", autoComplete: mode === "signin" ? "current-password" : "new-password", placeholder: "Password", value: password, onChange: (event) => setPassword(event.currentTarget.value), required: true }),
148
+ mode === "signin" ? /* @__PURE__ */ jsx2("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx2("a", { href: routes.forgotPassword, className: "text-xs text-muted-foreground hover:underline", children: "Forgot password?" }) }) : null,
147
149
  /* @__PURE__ */ jsx2("button", { type: "submit", className: "w-full rounded-xl bg-primary px-3 py-2.5 text-sm font-medium text-primary-foreground disabled:opacity-50", disabled: submitting, children: submitting ? "Please wait\u2026" : mode === "signin" ? "Continue with email" : "Create account" })
148
150
  ] })
149
151
  ] });
@@ -617,6 +619,7 @@ function WorkspaceRoute({
617
619
  {
618
620
  ...workspaceProps,
619
621
  workspaceId,
622
+ workspaceLabel: workspaceProps.workspaceLabel ?? currentWorkspace.name,
620
623
  requestHeaders,
621
624
  authHeaders,
622
625
  chatParams,
@@ -735,19 +738,19 @@ function creditNetMicros(balance) {
735
738
  const debt = Number.isFinite(balance.debtMicros) ? balance.debtMicros : 0;
736
739
  return remaining - debt;
737
740
  }
738
- function formatSignedCreditMicros(micros, locale) {
739
- const euros = (Number.isFinite(micros) ? micros : 0) / 1e6;
740
- const sign = euros > 0 ? "+" : euros < 0 ? "\u2212" : "";
741
- const abs = new Intl.NumberFormat(locale, { style: "currency", currency: "EUR" }).format(Math.abs(euros));
741
+ function formatSignedCreditMicros(micros, currency = "EUR", locale) {
742
+ const major = (Number.isFinite(micros) ? micros : 0) / 1e6;
743
+ const sign = major > 0 ? "+" : major < 0 ? "\u2212" : "";
744
+ const abs = new Intl.NumberFormat(locale, { style: "currency", currency }).format(Math.abs(major));
742
745
  return `${sign}${abs}`;
743
746
  }
744
747
  function formatMinorPrice(priceMinor, currency, locale) {
745
748
  const major = (Number.isFinite(priceMinor) ? priceMinor : 0) / 100;
746
749
  return new Intl.NumberFormat(locale, { style: "currency", currency, maximumFractionDigits: major % 1 === 0 ? 0 : 2 }).format(major);
747
750
  }
748
- function formatCreditMicros(micros, locale) {
749
- const euros = (Number.isFinite(micros) ? Math.max(0, micros) : 0) / 1e6;
750
- return new Intl.NumberFormat(locale, { style: "currency", currency: "EUR" }).format(euros);
751
+ function formatCreditMicros(micros, currency = "EUR", locale) {
752
+ const major = (Number.isFinite(micros) ? Math.max(0, micros) : 0) / 1e6;
753
+ return new Intl.NumberFormat(locale, { style: "currency", currency }).format(major);
751
754
  }
752
755
  function isLowBalance(micros, thresholdMicros = 5e5) {
753
756
  return Number.isFinite(micros) && micros <= thresholdMicros;
@@ -924,6 +927,7 @@ function CreditBalanceBadge({
924
927
  const low = inDebt || isLowBalance(balance.remainingMicros);
925
928
  const showBuy = balance.checkoutEnabled ?? buyEnabled;
926
929
  const packs = (balance.packs ?? []).filter((p) => !p.custom);
930
+ const currency = balance.packs?.[0]?.currency ?? "EUR";
927
931
  const pick = async (packId) => {
928
932
  setOpen(false);
929
933
  await buy(packId);
@@ -934,7 +938,7 @@ function CreditBalanceBadge({
934
938
  {
935
939
  title: inDebt ? "Amount owed \u2014 top up to resume" : "Remaining credits",
936
940
  className: `text-[11px] tabular-nums ${low ? "text-destructive" : "text-muted-foreground"}`,
937
- children: inDebt ? `\u2212${formatCreditMicros(balance.debtMicros, locale)}` : formatCreditMicros(balance.remainingMicros, locale)
941
+ children: inDebt ? `\u2212${formatCreditMicros(balance.debtMicros, currency, locale)}` : formatCreditMicros(balance.remainingMicros, currency, locale)
938
942
  }
939
943
  ),
940
944
  showBuy ? /* @__PURE__ */ jsxs5(Popover, { open, onOpenChange: setOpen, children: [
@@ -961,7 +965,7 @@ function CreditBalanceBadge({
961
965
  className: "flex items-center justify-between gap-3 rounded-md px-2 py-1.5 text-[13px] hover:bg-muted disabled:opacity-50",
962
966
  children: [
963
967
  /* @__PURE__ */ jsx6("span", { className: "tabular-nums font-medium", children: formatMinorPrice(p.priceMinor, p.currency, locale) }),
964
- /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: formatCreditMicros(p.creditMicros, locale) })
968
+ /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: formatCreditMicros(p.creditMicros, p.currency, locale) })
965
969
  ]
966
970
  },
967
971
  p.id
@@ -1046,6 +1050,7 @@ function CreditsSettingsPanel({ apiBaseUrl = "", locale }) {
1046
1050
  const low = inDebt || isLowBalance(balance.remainingMicros);
1047
1051
  const showBuy = balance.checkoutEnabled ?? false;
1048
1052
  const packs = balance.packs ?? [];
1053
+ const currency = packs[0]?.currency ?? "EUR";
1049
1054
  const activePack = selectedPack ?? packs.find((p) => p.isDefault)?.id ?? packs[0]?.id ?? null;
1050
1055
  const activePackObj = packs.find((p) => p.id === activePack) ?? null;
1051
1056
  const doBuy = async (pack) => {
@@ -1067,10 +1072,10 @@ function CreditsSettingsPanel({ apiBaseUrl = "", locale }) {
1067
1072
  !inDebt && low && /* @__PURE__ */ jsx7(Notice, { role: "status", tone: "warning", description: "You're low on credits. Top up to avoid interruptions." }),
1068
1073
  /* @__PURE__ */ jsxs6(DetailList, { children: [
1069
1074
  /* @__PURE__ */ jsx7(DetailLine, { label: "Remaining balance", children: /* @__PURE__ */ jsxs6("p", { style: { fontVariantNumeric: "tabular-nums", fontWeight: 600 }, children: [
1070
- inDebt ? `\u2212${formatCreditMicros(balance.debtMicros, locale)}` : formatCreditMicros(balance.remainingMicros, locale),
1075
+ inDebt ? `\u2212${formatCreditMicros(balance.debtMicros, currency, locale)}` : formatCreditMicros(balance.remainingMicros, currency, locale),
1071
1076
  updating && /* @__PURE__ */ jsx7("span", { className: "ml-2 text-[11px] font-normal text-muted-foreground", "aria-live": "polite", children: "Updating\u2026" })
1072
1077
  ] }) }),
1073
- /* @__PURE__ */ jsx7(DetailLine, { label: "Used so far", children: /* @__PURE__ */ jsx7("p", { style: { fontVariantNumeric: "tabular-nums" }, children: formatCreditMicros(balance.usedMicros, locale) }) })
1078
+ /* @__PURE__ */ jsx7(DetailLine, { label: "Used so far", children: /* @__PURE__ */ jsx7("p", { style: { fontVariantNumeric: "tabular-nums" }, children: formatCreditMicros(balance.usedMicros, currency, locale) }) })
1074
1079
  ] }),
1075
1080
  showBuy && packs.length > 0 && /* @__PURE__ */ jsxs6("fieldset", { className: "space-y-2", children: [
1076
1081
  /* @__PURE__ */ jsx7("legend", { className: "text-[12px] font-medium text-foreground", children: "Buy credits" }),
@@ -1101,7 +1106,7 @@ function CreditsSettingsPanel({ apiBaseUrl = "", locale }) {
1101
1106
  /* @__PURE__ */ jsx7("span", { style: { fontVariantNumeric: "tabular-nums" }, children: formatMinorPrice(p.priceMinor, p.currency, locale) }),
1102
1107
  /* @__PURE__ */ jsxs6("span", { className: "text-muted-foreground", children: [
1103
1108
  "\xB7 ",
1104
- formatCreditMicros(p.creditMicros, locale)
1109
+ formatCreditMicros(p.creditMicros, p.currency, locale)
1105
1110
  ] })
1106
1111
  ] })
1107
1112
  ]
@@ -1139,7 +1144,7 @@ function CreditsSettingsPanel({ apiBaseUrl = "", locale }) {
1139
1144
  {
1140
1145
  style: { fontVariantNumeric: "tabular-nums" },
1141
1146
  className: e.amountMicros >= 0 ? "text-foreground" : "text-muted-foreground",
1142
- children: formatSignedCreditMicros(e.amountMicros, locale)
1147
+ children: formatSignedCreditMicros(e.amountMicros, currency, locale)
1143
1148
  }
1144
1149
  )
1145
1150
  ] }, e.id)) })
@@ -1315,7 +1320,17 @@ function BuyCreditsNoticeAction({ apiBaseUrl = "", label = "Buy credits" }) {
1315
1320
  if (message) setError(message);
1316
1321
  };
1317
1322
  return /* @__PURE__ */ jsxs8("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
1318
- /* @__PURE__ */ jsx9(Button3, { type: "button", size: "sm", onClick: () => void onBuy(), disabled: buying, children: buying ? "Opening\u2026" : label }),
1323
+ /* @__PURE__ */ jsx9(
1324
+ Button3,
1325
+ {
1326
+ type: "button",
1327
+ size: "sm",
1328
+ onClick: () => void onBuy(),
1329
+ disabled: buying,
1330
+ className: "bg-foreground text-background hover:bg-foreground/90",
1331
+ children: buying ? "Opening\u2026" : label
1332
+ }
1333
+ ),
1319
1334
  error ? /* @__PURE__ */ jsx9("span", { role: "alert", className: "text-xs text-destructive", children: error }) : null
1320
1335
  ] });
1321
1336
  }
@@ -9,7 +9,7 @@ import {
9
9
  registerRoutes,
10
10
  registerSettingsRoutes,
11
11
  registerWorkspaceRoutes
12
- } from "../../chunk-UM5SHYIS.js";
12
+ } from "../../chunk-FZICNVJW.js";
13
13
  import {
14
14
  PostgresUserStore,
15
15
  PostgresWorkspaceStore,
@@ -208,6 +208,13 @@ var FRONTEND_AUTH_PAGES_SPA_ONLY = /* @__PURE__ */ new Set([
208
208
  "/auth/callback/github",
209
209
  "/auth/callback/google"
210
210
  ]);
211
+ function resolveCoreLoadConfigOptions(options = {}, nodeEnv = process.env.NODE_ENV) {
212
+ return {
213
+ allowMissingSecrets: nodeEnv !== "production",
214
+ ...options.appRoot && !options.loadConfigOptions?.tomlPath ? { tomlPath: path.resolve(options.appRoot, "boring.app.toml") } : {},
215
+ ...options.loadConfigOptions
216
+ };
217
+ }
211
218
  function dedupeStrings(values) {
212
219
  return Array.from(new Set(values));
213
220
  }
@@ -526,10 +533,7 @@ async function createCoreWorkspaceAgentServer(options = {}) {
526
533
  );
527
534
  }
528
535
  assertCoreStaticPluginEntries(options.plugins);
529
- const config = options.config ?? await loadConfig({
530
- allowMissingSecrets: process.env.NODE_ENV !== "production",
531
- ...options.loadConfigOptions
532
- });
536
+ const config = options.config ?? await loadConfig(resolveCoreLoadConfigOptions(options));
533
537
  const { app, sql, db, userStore, workspaceStore } = await createCoreRuntime(config);
534
538
  const appRoot = options.appRoot;
535
539
  const serveFrontend = options.serveFrontend ?? (process.env.NODE_ENV !== "development" && Boolean(appRoot));
@@ -621,6 +625,25 @@ async function createCoreWorkspaceAgentServer(options = {}) {
621
625
  const callerOptions = options.getPi ? await options.getPi(ctx) : void 0;
622
626
  return mergePiOptions(pluginOptions, callerOptions);
623
627
  };
628
+ app.get("/api/v1/workspace/meta", async (request, reply) => {
629
+ try {
630
+ const workspaceId = await resolveWorkspaceId(request);
631
+ const [workspace, workspaceRootForRequest] = await Promise.all([
632
+ workspaceStore.get(workspaceId),
633
+ resolveRoot(workspaceId, request)
634
+ ]);
635
+ return {
636
+ workspaceId,
637
+ workspaceRoot: workspaceRootForRequest,
638
+ projectName: workspace?.name ?? "Workspace"
639
+ };
640
+ } catch (error) {
641
+ const statusCode = typeof error?.statusCode === "number" ? error.statusCode : 500;
642
+ const message = error instanceof Error ? error.message : "workspace meta failed";
643
+ return reply.code(statusCode).send({ error: message });
644
+ }
645
+ });
646
+ const resolveSessionNamespace = async (ctx) => options.getSessionNamespace ? await options.getSessionNamespace(ctx) : options.sessionNamespace ?? ctx.workspaceId;
624
647
  await app.register(registerAgentRoutes, {
625
648
  workspaceRoot,
626
649
  sessionId: options.sessionId,
@@ -637,8 +660,7 @@ async function createCoreWorkspaceAgentServer(options = {}) {
637
660
  systemPromptAppend: pluginCollection.agentOptions.systemPromptAppend,
638
661
  pi: pluginCollection.agentOptions.pi,
639
662
  getPi: resolvePiOptions,
640
- sessionNamespace: options.sessionNamespace,
641
- getSessionNamespace: options.getSessionNamespace,
663
+ getSessionNamespace: resolveSessionNamespace,
642
664
  getExtraTools: async (ctx) => {
643
665
  const callerTools = options.getExtraTools ? await options.getExtraTools(ctx) : [];
644
666
  return [
@@ -101,6 +101,30 @@ var SIXTEEN_MB = 16 * 1024 * 1024;
101
101
  var INSECURE_PLACEHOLDER_SECRET = "0000000000000000000000000000000000000000000000000000000000000000";
102
102
  var INSECURE_DATABASE_URL = "postgres://placeholder:placeholder@localhost:5432/placeholder";
103
103
  var INSECURE_ENCRYPTION_KEY = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
104
+ function formatDisplayName(name) {
105
+ const trimmed = name.trim().replace(/[<>]/g, "");
106
+ if (/^[A-Za-z0-9 ._-]+$/.test(trimmed)) return trimmed;
107
+ return `"${trimmed.replace(/["\\]/g, "\\$&")}"`;
108
+ }
109
+ function isDefaultBoringDisplayName(name) {
110
+ return name.toLowerCase().replace(/[\s._-]+/g, "") === "boringui";
111
+ }
112
+ function normalizeMailFrom(appName, rawFrom) {
113
+ const from = rawFrom.trim();
114
+ const addressWithDisplay = from.match(/^(.*?)\s*<([^>]+)>$/);
115
+ if (addressWithDisplay) {
116
+ const displayName = addressWithDisplay[1].trim().replace(/^"(.*)"$/, "$1");
117
+ const address = addressWithDisplay[2].trim();
118
+ if (!displayName || isDefaultBoringDisplayName(displayName)) {
119
+ return `${formatDisplayName(appName)} <${address}>`;
120
+ }
121
+ return from;
122
+ }
123
+ if (/^[^\s@<>]+@[^\s@<>]+$/.test(from)) {
124
+ return `${formatDisplayName(appName)} <${from}>`;
125
+ }
126
+ return from;
127
+ }
104
128
  function parseRateLimitOverrides(raw) {
105
129
  if (!raw) return void 0;
106
130
  try {
@@ -224,7 +248,18 @@ async function loadConfig(options) {
224
248
  ...toml.features?.invite_ttl_days != null && { inviteTtlDays: toml.features.invite_ttl_days }
225
249
  }
226
250
  };
227
- return validateConfig(raw);
251
+ const config = validateConfig(raw);
252
+ if (!config.auth.mail) return config;
253
+ return {
254
+ ...config,
255
+ auth: {
256
+ ...config.auth,
257
+ mail: {
258
+ ...config.auth.mail,
259
+ from: normalizeMailFrom(config.appName, config.auth.mail.from)
260
+ }
261
+ }
262
+ };
228
263
  }
229
264
  function validateConfig(raw) {
230
265
  const result = coreConfigSchema.safeParse(raw);
@@ -1423,6 +1458,7 @@ import { Button as Button4, Section as Section5, Text as Text5 } from "@react-em
1423
1458
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1424
1459
  function WorkspaceInvite({
1425
1460
  acceptUrl,
1461
+ appName,
1426
1462
  inviterName,
1427
1463
  workspaceName,
1428
1464
  role,
@@ -1432,7 +1468,7 @@ function WorkspaceInvite({
1432
1468
  Layout,
1433
1469
  {
1434
1470
  preview: `${inviterName} invited you to ${workspaceName}`,
1435
- appName: workspaceName,
1471
+ appName,
1436
1472
  children: /* @__PURE__ */ jsxs5(Section5, { style: content4, children: [
1437
1473
  /* @__PURE__ */ jsx5(Text5, { style: heading4, children: "You've been invited" }),
1438
1474
  /* @__PURE__ */ jsxs5(Text5, { style: paragraph4, children: [
@@ -1568,6 +1604,7 @@ async function renderMagicLink(data) {
1568
1604
  async function renderWorkspaceInvite(data) {
1569
1605
  const element = WorkspaceInvite({
1570
1606
  acceptUrl: data.acceptUrl,
1607
+ appName: data.appName,
1571
1608
  inviterName: data.inviterName,
1572
1609
  workspaceName: data.workspaceName,
1573
1610
  role: data.role,
@@ -2426,6 +2463,7 @@ var inviteRoutesPlugin = async (app, opts) => {
2426
2463
  const email = await renderWorkspaceInvite({
2427
2464
  to: parsed.data.email,
2428
2465
  acceptUrl,
2466
+ appName: app.config.appName,
2429
2467
  inviterName: request.user.name ?? request.user.email,
2430
2468
  workspaceName: workspace?.name ?? "Workspace",
2431
2469
  role: parsed.data.role,
@@ -326,6 +326,7 @@ declare function renderMagicLink(data: MagicLinkData): Promise<RenderedEmail>;
326
326
  interface WorkspaceInviteData {
327
327
  to: string;
328
328
  acceptUrl: string;
329
+ appName: string;
329
330
  inviterName: string;
330
331
  workspaceName: string;
331
332
  role: string;
@@ -24,7 +24,7 @@ import {
24
24
  requireWorkspaceMember,
25
25
  validateConfig,
26
26
  validatePasswordStrength
27
- } from "../chunk-UM5SHYIS.js";
27
+ } from "../chunk-FZICNVJW.js";
28
28
  import {
29
29
  InsufficientCreditError,
30
30
  PostgresMeteringStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-core",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Foundation package for boring-ui-v2 apps: DB, auth, config, HTTP app factory, and frontend app shell.",
@@ -79,9 +79,9 @@
79
79
  "react-router-dom": "^7.14.2",
80
80
  "smol-toml": "^1.6.1",
81
81
  "zod": "^3.25.76",
82
- "@hachej/boring-agent": "0.1.43",
83
- "@hachej/boring-workspace": "0.1.43",
84
- "@hachej/boring-ui-kit": "0.1.43"
82
+ "@hachej/boring-ui-kit": "0.1.45",
83
+ "@hachej/boring-workspace": "0.1.45",
84
+ "@hachej/boring-agent": "0.1.45"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@testing-library/jest-dom": "^6.9.1",