@hachej/boring-core 0.1.45 → 0.1.46
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/{PostgresMeteringStore-CzNv6xil.d.ts → PostgresMeteringStore-DhOVVtau.d.ts} +4 -0
- package/dist/app/front/index.d.ts +15 -2
- package/dist/app/front/index.js +29 -5
- package/dist/app/server/index.d.ts +4 -1
- package/dist/app/server/index.js +35 -15
- package/dist/{authHook-CzBsMwwM.d.ts → authHook-DtzhSmqS.d.ts} +3 -0
- package/dist/{chunk-FZICNVJW.js → chunk-6GAQRQKO.js} +31 -4
- package/dist/{chunk-I56OTSPB.js → chunk-FZC3VL5D.js} +3 -3
- package/dist/server/db/index.d.ts +1 -1
- package/dist/server/db/index.js +1 -1
- package/dist/server/index.d.ts +12 -5
- package/dist/server/index.js +49 -11
- package/package.json +4 -4
|
@@ -59,6 +59,9 @@ interface ReserveInput {
|
|
|
59
59
|
}
|
|
60
60
|
interface ReserveResult {
|
|
61
61
|
reservationId: string;
|
|
62
|
+
/** false when an existing active reservation for this run was returned (idempotent
|
|
63
|
+
* retry), true when a new hold was placed. Lets callers avoid double-counting. */
|
|
64
|
+
created: boolean;
|
|
62
65
|
}
|
|
63
66
|
interface RecordUsageInput {
|
|
64
67
|
/** Stable idempotency key; a second insert with the same id is a no-op. */
|
|
@@ -120,6 +123,7 @@ declare class PostgresMeteringStore {
|
|
|
120
123
|
variantId?: string;
|
|
121
124
|
}): Promise<{
|
|
122
125
|
granted: boolean;
|
|
126
|
+
refundAppliedMicros?: number;
|
|
123
127
|
}>;
|
|
124
128
|
/**
|
|
125
129
|
* Revoke a refunded/disputed purchase. Under the same per-order advisory lock
|
|
@@ -9,12 +9,25 @@ interface ChatFirstPublicShellOptions {
|
|
|
9
9
|
emptyState?: {
|
|
10
10
|
eyebrow?: string;
|
|
11
11
|
title?: string;
|
|
12
|
-
description?:
|
|
12
|
+
description?: ReactNode;
|
|
13
13
|
footer?: ReactNode;
|
|
14
14
|
};
|
|
15
15
|
suggestions?: ChatSuggestion[];
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Predefined models for the composer's model picker. When provided, the
|
|
18
|
+
* composer settings row (model + thinking pickers) is shown in the no-auth
|
|
19
|
+
* hero, mirroring a regular chat session. The no-auth shell can't fetch the
|
|
20
|
+
* model list from the server (server resources are disabled), so it must be
|
|
21
|
+
* passed in here. `available` defaults to `true`.
|
|
22
|
+
*/
|
|
23
|
+
models?: Array<{
|
|
24
|
+
provider: string;
|
|
25
|
+
id: string;
|
|
26
|
+
label: string;
|
|
27
|
+
available?: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Hand-drawn "Ask your AI here" / "Review its work here" annotations point at the
|
|
18
31
|
* composer and the workspace surface to teach the empty public shell. Apps
|
|
19
32
|
* that open a center panel on load (e.g. a landing page) should disable them,
|
|
20
33
|
* otherwise the fixed-position arrows overlay the open panel. Defaults to on.
|
package/dist/app/front/index.js
CHANGED
|
@@ -54,7 +54,7 @@ function ChatFirstAuthenticatedShell({
|
|
|
54
54
|
...workspaceProps,
|
|
55
55
|
workspaceId,
|
|
56
56
|
appTitle,
|
|
57
|
-
topBarLeft: null,
|
|
57
|
+
topBarLeft: workspaceProps.topBarLeft ?? null,
|
|
58
58
|
sessions: [],
|
|
59
59
|
activeSessionId: null,
|
|
60
60
|
onSwitchSession: () => void 0,
|
|
@@ -264,6 +264,8 @@ function ChatFirstPublicShell({
|
|
|
264
264
|
const promptedDraft = new URLSearchParams(location.search).get("prompt")?.trim() ?? "";
|
|
265
265
|
const pendingReturnTo = promptedDraft ? "/" : returnTo;
|
|
266
266
|
const workspaceId = intendedWorkspaceId || "public";
|
|
267
|
+
const publicModels = (publicShell?.models ?? []).map((m) => ({ ...m, available: m.available ?? true }));
|
|
268
|
+
const showComposerSettings = publicModels.length > 0;
|
|
267
269
|
const openAuth = (draft = readComposerDraftFromDom()) => {
|
|
268
270
|
writePendingChatEntry({ draft, returnTo: pendingReturnTo, ...intendedWorkspaceId ? { intendedWorkspaceId } : {} });
|
|
269
271
|
setModalOpen(true);
|
|
@@ -356,7 +358,7 @@ function ChatFirstPublicShell({
|
|
|
356
358
|
return /* @__PURE__ */ jsxs3("div", { className: "public-chat-first-shell relative h-screen min-h-0 bg-background", children: [
|
|
357
359
|
publicShell?.showTeachingArrows !== false && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
358
360
|
/* @__PURE__ */ jsxs3("div", { className: "public-arrow public-arrow-computer", "aria-hidden": "true", children: [
|
|
359
|
-
/* @__PURE__ */ jsx4("span", { className: "public-arrow-label", children: "
|
|
361
|
+
/* @__PURE__ */ jsx4("span", { className: "public-arrow-label", children: "Review its work here" }),
|
|
360
362
|
/* @__PURE__ */ jsxs3("svg", { className: "public-arrow-svg", viewBox: "0 0 190 140", fill: "none", children: [
|
|
361
363
|
/* @__PURE__ */ jsx4(
|
|
362
364
|
"path",
|
|
@@ -381,7 +383,7 @@ function ChatFirstPublicShell({
|
|
|
381
383
|
/* @__PURE__ */ jsx4("path", { className: "paw-stroke", d: "M76 10 C 70 16 62 19 53 20" }),
|
|
382
384
|
/* @__PURE__ */ jsx4("path", { className: "paw-stroke", d: "M76 10 C 82 14 87 21 90 29" })
|
|
383
385
|
] }),
|
|
384
|
-
/* @__PURE__ */ jsx4("span", { className: "public-arrow-label", children: "
|
|
386
|
+
/* @__PURE__ */ jsx4("span", { className: "public-arrow-label", children: "Ask your AI here" })
|
|
385
387
|
] })
|
|
386
388
|
] }),
|
|
387
389
|
/* @__PURE__ */ jsx4("aside", { className: "pointer-events-none fixed bottom-6 left-6 z-20 w-[300px]", children: /* @__PURE__ */ jsx4("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx4(AuthCard, { returnTo }) }) }),
|
|
@@ -393,16 +395,38 @@ function ChatFirstPublicShell({
|
|
|
393
395
|
showComposerBlocker: false,
|
|
394
396
|
workspaceProps: {
|
|
395
397
|
...workspaceProps,
|
|
398
|
+
// No-auth landing should open with the workspace surface closed —
|
|
399
|
+
// a fresh load/hard refresh shows just the hero, not a re-opened
|
|
400
|
+
// panel. The /landing-page command still opens it on demand.
|
|
401
|
+
defaultSurfaceOpen: false,
|
|
396
402
|
topBarRight: /* @__PURE__ */ jsx4("button", { type: "button", className: "rounded-md border border-border px-3 py-1.5 text-sm font-medium hover:bg-muted", onClick: () => openAuth(), children: "Sign in" }),
|
|
403
|
+
// No-auth shell has no real session yet — show just the brand, hide the
|
|
404
|
+
// "· New session" placeholder that the default TopBar would render.
|
|
405
|
+
topBarLeft: /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
406
|
+
/* @__PURE__ */ jsx4(
|
|
407
|
+
"span",
|
|
408
|
+
{
|
|
409
|
+
"aria-hidden": "true",
|
|
410
|
+
className: "grid size-[22px] shrink-0 place-items-center rounded-sm bg-foreground text-[11px] font-semibold leading-none tracking-tight text-background",
|
|
411
|
+
children: (appTitle?.[0] ?? "B").toUpperCase()
|
|
412
|
+
}
|
|
413
|
+
),
|
|
414
|
+
/* @__PURE__ */ jsx4("span", { className: "truncate text-[13px] font-medium leading-none tracking-tight text-foreground", children: appTitle })
|
|
415
|
+
] }),
|
|
397
416
|
className: workspaceProps.className,
|
|
398
417
|
surfaceButtonBottomOffset: 456,
|
|
399
418
|
chatParams: {
|
|
400
419
|
...workspaceProps.chatParams,
|
|
401
420
|
emptyPlacement: "hero",
|
|
402
421
|
composerPlaceholder: publicShell?.composerPlaceholder ?? "Type /landing-page or /reach-out",
|
|
403
|
-
hideComposerSettings:
|
|
422
|
+
hideComposerSettings: !showComposerSettings,
|
|
404
423
|
suppressPreSubmitCancelledWarning: true,
|
|
405
|
-
thinkingControl:
|
|
424
|
+
thinkingControl: showComposerSettings,
|
|
425
|
+
...showComposerSettings ? {
|
|
426
|
+
availableModels: publicModels,
|
|
427
|
+
hideDefaultModelOption: true,
|
|
428
|
+
defaultModel: { provider: publicModels[0].provider, id: publicModels[0].id }
|
|
429
|
+
} : {},
|
|
406
430
|
initialDraft: promptedDraft || void 0,
|
|
407
431
|
emptyState: {
|
|
408
432
|
...defaultPublicEmptyState,
|
|
@@ -4,7 +4,7 @@ import { WorkspaceServerPlugin } from '@hachej/boring-workspace/server';
|
|
|
4
4
|
import { FastifyInstance } from 'fastify';
|
|
5
5
|
import { C as CoreConfig } from '../../types-CWtJ4kgd.js';
|
|
6
6
|
import { TelemetrySink } from '../../shared/index.js';
|
|
7
|
-
import { B as BetterAuthInstance, L as LoadConfigOptions } from '../../authHook-
|
|
7
|
+
import { B as BetterAuthInstance, L as LoadConfigOptions } from '../../authHook-DtzhSmqS.js';
|
|
8
8
|
import { D as Database, U as UserStore, W as WorkspaceStore } from '../../connection-C5SiqoNc.js';
|
|
9
9
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
10
10
|
import 'better-auth';
|
|
@@ -16,6 +16,9 @@ type CoreWorkspaceAgentServer = FastifyInstance & {
|
|
|
16
16
|
db: Database;
|
|
17
17
|
userStore: UserStore;
|
|
18
18
|
workspaceStore: WorkspaceStore;
|
|
19
|
+
/** Best-effort telemetry sink (DB-backed when BORING_TELEMETRY_ENABLED=true, else noop).
|
|
20
|
+
* Consumed by request hooks and the credit service to emit product events. */
|
|
21
|
+
telemetry: TelemetrySink;
|
|
19
22
|
};
|
|
20
23
|
type CoreWorkspaceAgentServerPlugin = WorkspaceServerPlugin & {
|
|
21
24
|
provisioning?: RuntimeProvisioningContribution;
|
package/dist/app/server/index.js
CHANGED
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
registerRoutes,
|
|
10
10
|
registerSettingsRoutes,
|
|
11
11
|
registerWorkspaceRoutes
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-6GAQRQKO.js";
|
|
13
13
|
import {
|
|
14
14
|
PostgresUserStore,
|
|
15
15
|
PostgresWorkspaceStore,
|
|
16
16
|
createDatabase,
|
|
17
17
|
telemetryEvents
|
|
18
|
-
} from "../../chunk-
|
|
18
|
+
} from "../../chunk-FZC3VL5D.js";
|
|
19
19
|
import {
|
|
20
20
|
noopTelemetry,
|
|
21
21
|
safeCapture
|
|
@@ -82,7 +82,13 @@ var ALLOWED_PROPERTY_KEYS = /* @__PURE__ */ new Set([
|
|
|
82
82
|
"skillCount",
|
|
83
83
|
"templateDirCount",
|
|
84
84
|
"packageName",
|
|
85
|
-
"packageVersion"
|
|
85
|
+
"packageVersion",
|
|
86
|
+
// Credits / billing (provider/pack/currency/reason are slugs; creditMicros is a non-negative int).
|
|
87
|
+
"provider",
|
|
88
|
+
"packId",
|
|
89
|
+
"currency",
|
|
90
|
+
"reason",
|
|
91
|
+
"creditMicros"
|
|
86
92
|
]);
|
|
87
93
|
function createDatabaseTelemetryFromEnv(db, options, env = process.env) {
|
|
88
94
|
if (env.BORING_TELEMETRY_ENABLED !== "true") return noopTelemetry;
|
|
@@ -135,6 +141,9 @@ function sanitizeTelemetryProperty(key, value) {
|
|
|
135
141
|
if (key.endsWith("Count")) {
|
|
136
142
|
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
137
143
|
}
|
|
144
|
+
if (key.endsWith("Micros")) {
|
|
145
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
146
|
+
}
|
|
138
147
|
if (key === "status" && typeof value === "number") {
|
|
139
148
|
return Number.isInteger(value) && value >= 100 && value <= 599 ? value : void 0;
|
|
140
149
|
}
|
|
@@ -171,6 +180,11 @@ function sanitizeTelemetryString(key, value) {
|
|
|
171
180
|
return SAFE_PACKAGE_NAME_PATTERN.test(value) ? value : void 0;
|
|
172
181
|
case "packageVersion":
|
|
173
182
|
return SAFE_PACKAGE_VERSION_PATTERN.test(value) ? value : void 0;
|
|
183
|
+
case "provider":
|
|
184
|
+
case "packId":
|
|
185
|
+
case "currency":
|
|
186
|
+
case "reason":
|
|
187
|
+
return SAFE_SLUG_PATTERN.test(value) ? value : void 0;
|
|
174
188
|
default:
|
|
175
189
|
return void 0;
|
|
176
190
|
}
|
|
@@ -443,12 +457,20 @@ function captureAppOpened(telemetry, requestId) {
|
|
|
443
457
|
}
|
|
444
458
|
function registerTelemetryHooks(app, telemetry) {
|
|
445
459
|
app.addHook("onResponse", async (request, reply) => {
|
|
446
|
-
|
|
460
|
+
const status = reply.statusCode;
|
|
461
|
+
if (status === 429) {
|
|
462
|
+
safeCapture(telemetry, {
|
|
463
|
+
name: "server.request.rate_limited",
|
|
464
|
+
properties: { requestId: request.id }
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (status < 500) return;
|
|
447
469
|
safeCapture(telemetry, {
|
|
448
470
|
name: "server.request.failed",
|
|
449
471
|
properties: {
|
|
450
472
|
requestId: request.id,
|
|
451
|
-
status
|
|
473
|
+
status,
|
|
452
474
|
errorCode: ERROR_CODES.INTERNAL_ERROR
|
|
453
475
|
}
|
|
454
476
|
});
|
|
@@ -481,7 +503,7 @@ async function registerFrontendFallback(app, appRoot, telemetry) {
|
|
|
481
503
|
return serveFrontendShell(request, reply, indexPath, telemetry);
|
|
482
504
|
});
|
|
483
505
|
}
|
|
484
|
-
async function createCoreRuntime(config) {
|
|
506
|
+
async function createCoreRuntime(config, customTelemetry) {
|
|
485
507
|
if (config.stores !== "postgres") {
|
|
486
508
|
throw new Error("createCoreWorkspaceAgentServer currently supports only CORE_STORES=postgres");
|
|
487
509
|
}
|
|
@@ -493,18 +515,19 @@ async function createCoreRuntime(config) {
|
|
|
493
515
|
config.encryption.workspaceSettingsKey
|
|
494
516
|
);
|
|
495
517
|
const app = await createCoreApp(config);
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
});
|
|
518
|
+
const telemetry = customTelemetry ?? createDatabaseTelemetryFromEnv(db, { appId: config.appId }, process.env);
|
|
519
|
+
const telemetrySource = customTelemetry ? "custom" : process.env.BORING_TELEMETRY_ENABLED === "true" ? "db-env" : "noop-env";
|
|
520
|
+
app.log.debug({ telemetry: { source: telemetrySource } }, "resolved telemetry sink");
|
|
521
|
+
const auth = createAuth(config, db, { workspaceStore, logger: app.log, telemetry });
|
|
500
522
|
app.decorate("db", db);
|
|
501
523
|
app.decorate("auth", auth);
|
|
502
524
|
app.decorate("userStore", userStore);
|
|
503
525
|
app.decorate("workspaceStore", workspaceStore);
|
|
526
|
+
app.decorate("telemetry", telemetry);
|
|
504
527
|
app.addHook("onClose", async () => {
|
|
505
528
|
await sql.end();
|
|
506
529
|
});
|
|
507
|
-
return { app, sql, db, userStore, workspaceStore };
|
|
530
|
+
return { app, sql, db, userStore, workspaceStore, telemetry };
|
|
508
531
|
}
|
|
509
532
|
async function registerCoreRoutes({
|
|
510
533
|
app,
|
|
@@ -534,14 +557,11 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
534
557
|
}
|
|
535
558
|
assertCoreStaticPluginEntries(options.plugins);
|
|
536
559
|
const config = options.config ?? await loadConfig(resolveCoreLoadConfigOptions(options));
|
|
537
|
-
const { app, sql, db, userStore, workspaceStore } = await createCoreRuntime(config);
|
|
560
|
+
const { app, sql, db, userStore, workspaceStore, telemetry } = await createCoreRuntime(config, options.telemetry);
|
|
538
561
|
const appRoot = options.appRoot;
|
|
539
562
|
const serveFrontend = options.serveFrontend ?? (process.env.NODE_ENV !== "development" && Boolean(appRoot));
|
|
540
563
|
const pluginWorkspaceRoot = process.cwd();
|
|
541
564
|
const workspaceRoot = options.workspaceRoot ?? process.env.BORING_AGENT_WORKSPACE_ROOT ?? process.cwd();
|
|
542
|
-
const telemetrySource = options.telemetry ? "custom" : process.env.BORING_TELEMETRY_ENABLED === "true" ? "db-env" : "noop-env";
|
|
543
|
-
const telemetry = options.telemetry ?? createDatabaseTelemetryFromEnv(db, { appId: config.appId }, process.env);
|
|
544
|
-
app.log.debug({ telemetry: { source: telemetrySource } }, "resolved telemetry sink");
|
|
545
565
|
registerTelemetryHooks(app, telemetry);
|
|
546
566
|
await registerCoreRoutes({ app, sql, db, userStore, workspaceStore });
|
|
547
567
|
if (serveFrontend && appRoot) {
|
|
@@ -2,6 +2,7 @@ import { C as CoreConfig, R as RuntimeConfig } from './types-CWtJ4kgd.js';
|
|
|
2
2
|
import { FastifyPluginAsync } from 'fastify';
|
|
3
3
|
import { Auth } from 'better-auth';
|
|
4
4
|
import { W as WorkspaceStore, D as Database } from './connection-C5SiqoNc.js';
|
|
5
|
+
import { TelemetrySink } from './shared/index.js';
|
|
5
6
|
|
|
6
7
|
interface LoadConfigOptions {
|
|
7
8
|
tomlPath?: string;
|
|
@@ -21,6 +22,8 @@ interface CreateAuthOptions {
|
|
|
21
22
|
logger?: {
|
|
22
23
|
warn: (obj: Record<string, unknown>, msg: string) => void;
|
|
23
24
|
};
|
|
25
|
+
/** Telemetry sink for auth.signed_up / auth.session_started (defaults to noop). */
|
|
26
|
+
telemetry?: TelemetrySink;
|
|
24
27
|
}
|
|
25
28
|
declare function createAuth(config: CoreConfig, db: Database, opts?: CreateAuthOptions): Auth<any>;
|
|
26
29
|
type BetterAuthInstance = Auth<any>;
|
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
workspaceRuntimes,
|
|
13
13
|
workspaceSettings,
|
|
14
14
|
workspaces
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-FZC3VL5D.js";
|
|
16
|
+
import {
|
|
17
|
+
noopTelemetry,
|
|
18
|
+
safeCapture
|
|
19
|
+
} from "./chunk-AQBXNPMD.js";
|
|
16
20
|
import {
|
|
17
21
|
ConfigValidationError,
|
|
18
22
|
ERROR_CODES,
|
|
@@ -1742,6 +1746,7 @@ function buildMailTransport(config) {
|
|
|
1742
1746
|
}
|
|
1743
1747
|
function createAuth(config, db, opts) {
|
|
1744
1748
|
const transport = buildMailTransport(config);
|
|
1749
|
+
const telemetry = opts?.telemetry ?? noopTelemetry;
|
|
1745
1750
|
const emailVerificationConfig = transport ? {
|
|
1746
1751
|
sendOnSignUp: true,
|
|
1747
1752
|
sendVerificationEmail: async (data) => {
|
|
@@ -1802,13 +1807,35 @@ function createAuth(config, db, opts) {
|
|
|
1802
1807
|
baseURL: config.auth.url,
|
|
1803
1808
|
basePath: "/auth",
|
|
1804
1809
|
trustedOrigins: config.cors.origins,
|
|
1805
|
-
databaseHooks:
|
|
1810
|
+
databaseHooks: {
|
|
1806
1811
|
user: {
|
|
1807
1812
|
create: {
|
|
1808
|
-
|
|
1813
|
+
// auth.signed_up is emitted here (not in postSignupHook) so it fires for ALL
|
|
1814
|
+
// signups, independent of whether workspace post-signup setup is wired.
|
|
1815
|
+
// distinctId = user id; no properties (no PII, nothing the DB sink would drop).
|
|
1816
|
+
after: async (user, ctx) => {
|
|
1817
|
+
safeCapture(telemetry, {
|
|
1818
|
+
name: "auth.signed_up",
|
|
1819
|
+
distinctId: typeof user?.id === "string" ? user.id : void 0
|
|
1820
|
+
});
|
|
1821
|
+
if (postSignupHook) await postSignupHook(user, ctx);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
},
|
|
1825
|
+
// Fires for every new session — sign-in AND the session minted on sign-up — so the
|
|
1826
|
+
// name reflects that (a true returning-sign-in count = session_started minus the
|
|
1827
|
+
// first session per user, derivable in SQL). distinctId = user id; no properties.
|
|
1828
|
+
session: {
|
|
1829
|
+
create: {
|
|
1830
|
+
after: async (session) => {
|
|
1831
|
+
safeCapture(telemetry, {
|
|
1832
|
+
name: "auth.session_started",
|
|
1833
|
+
distinctId: typeof session?.userId === "string" ? session.userId : void 0
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1809
1836
|
}
|
|
1810
1837
|
}
|
|
1811
|
-
}
|
|
1838
|
+
},
|
|
1812
1839
|
advanced: {
|
|
1813
1840
|
database: {
|
|
1814
1841
|
generateId: "uuid"
|
|
@@ -1890,7 +1890,7 @@ var PostgresMeteringStore = class {
|
|
|
1890
1890
|
metadata: { kind: "purchase_refund", orderId: input.orderId, refundedToMicros: revoke, appliedAtGrant: true }
|
|
1891
1891
|
});
|
|
1892
1892
|
}
|
|
1893
|
-
return { granted: true };
|
|
1893
|
+
return { granted: true, refundAppliedMicros: revoke > 0 ? revoke : void 0 };
|
|
1894
1894
|
}
|
|
1895
1895
|
await tx.insert(creditPurchases).values({
|
|
1896
1896
|
orderId: input.orderId,
|
|
@@ -2090,7 +2090,7 @@ var PostgresMeteringStore = class {
|
|
|
2090
2090
|
eq3(usageReservations.status, "active")
|
|
2091
2091
|
)).limit(1);
|
|
2092
2092
|
const existingId = existing[0]?.id;
|
|
2093
|
-
if (existingId) return { reservationId: existingId };
|
|
2093
|
+
if (existingId) return { reservationId: existingId, created: false };
|
|
2094
2094
|
const balance = await this.computeBalance(tx, input.userId, now);
|
|
2095
2095
|
if (balance.availableMicros < minAvailable) {
|
|
2096
2096
|
throw new InsufficientCreditError(balance.availableMicros, minAvailable);
|
|
@@ -2107,7 +2107,7 @@ var PostgresMeteringStore = class {
|
|
|
2107
2107
|
}).returning({ id: usageReservations.id });
|
|
2108
2108
|
const reservationId = rows[0]?.id;
|
|
2109
2109
|
if (!reservationId) throw new Error("reservation insert returned no id");
|
|
2110
|
-
return { reservationId };
|
|
2110
|
+
return { reservationId, created: true };
|
|
2111
2111
|
});
|
|
2112
2112
|
}
|
|
2113
2113
|
/** Idempotent ledger insert; returns whether a new row was written. Serialized
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { U as UserStore, W as WorkspaceStore } from '../../connection-C5SiqoNc.js';
|
|
2
2
|
export { D as Database, c as createDatabase } from '../../connection-C5SiqoNc.js';
|
|
3
|
-
export { F as FinishReservationInput, G as GrantOnceInput, I as InsufficientCreditError, M as MeteringBalance, P as PostgresMeteringStore, R as RecordUsageInput, a as RecordUsageResult, b as ReservationFinalStatus, c as ReserveInput, d as ReserveResult, e as RunMigrationsOptions, r as runMigrations } from '../../PostgresMeteringStore-
|
|
3
|
+
export { F as FinishReservationInput, G as GrantOnceInput, I as InsufficientCreditError, M as MeteringBalance, P as PostgresMeteringStore, R as RecordUsageInput, a as RecordUsageResult, b as ReservationFinalStatus, c as ReserveInput, d as ReserveResult, e as RunMigrationsOptions, r as runMigrations } from '../../PostgresMeteringStore-DhOVVtau.js';
|
|
4
4
|
import { U as User, W as Workspace, E as ERROR_CODES, M as MemberRole, a as WorkspaceMember, b as WorkspaceInvite, c as WorkspaceRuntime, d as WorkspaceRuntimeResourceSelector, e as WorkspaceRuntimeResource, f as WorkspaceRuntimeResourceInput } from '../../types-CWtJ4kgd.js';
|
|
5
5
|
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
6
6
|
import 'postgres';
|
package/dist/server/db/index.js
CHANGED
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { L as LoadConfigOptions } from '../authHook-
|
|
2
|
-
export { A as AuthHookOptions, B as BetterAuthInstance, C as CreateAuthOptions, a as authHook, b as buildRuntimeConfigPayload, c as createAuth, l as loadConfig, v as validateConfig, d as validatePasswordStrength } from '../authHook-
|
|
1
|
+
import { L as LoadConfigOptions } from '../authHook-DtzhSmqS.js';
|
|
2
|
+
export { A as AuthHookOptions, B as BetterAuthInstance, C as CreateAuthOptions, a as authHook, b as buildRuntimeConfigPayload, c as createAuth, l as loadConfig, v as validateConfig, d as validatePasswordStrength } from '../authHook-DtzhSmqS.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { C as CoreConfig, M as MemberRole, d as WorkspaceRuntimeResourceSelector, e as WorkspaceRuntimeResource, f as WorkspaceRuntimeResourceInput } from '../types-CWtJ4kgd.js';
|
|
5
5
|
import * as fastify from 'fastify';
|
|
@@ -9,8 +9,9 @@ import { IncomingMessage } from 'node:http';
|
|
|
9
9
|
import { C as CreateCoreAppOptions, D as Database, U as UserStore, W as WorkspaceStore, a as WorkspaceProvisioner } from '../connection-C5SiqoNc.js';
|
|
10
10
|
export { A as AuthProvider, b as CapabilitiesContributor, P as ProvisionContext, d as ProvisionResult, c as createDatabase } from '../connection-C5SiqoNc.js';
|
|
11
11
|
import postgres from 'postgres';
|
|
12
|
-
import { P as PostgresMeteringStore, C as CreditLedgerEntry } from '../PostgresMeteringStore-
|
|
13
|
-
export { F as FinishReservationInput, G as GrantOnceInput, I as InsufficientCreditError, M as MeteringBalance, R as RecordUsageInput, a as RecordUsageResult, b as ReservationFinalStatus, c as ReserveInput, d as ReserveResult, r as runMigrations } from '../PostgresMeteringStore-
|
|
12
|
+
import { P as PostgresMeteringStore, C as CreditLedgerEntry } from '../PostgresMeteringStore-DhOVVtau.js';
|
|
13
|
+
export { F as FinishReservationInput, G as GrantOnceInput, I as InsufficientCreditError, M as MeteringBalance, R as RecordUsageInput, a as RecordUsageResult, b as ReservationFinalStatus, c as ReserveInput, d as ReserveResult, r as runMigrations } from '../PostgresMeteringStore-DhOVVtau.js';
|
|
14
|
+
import { TelemetrySink } from '../shared/index.js';
|
|
14
15
|
import { AgentMeteringSink } from '@hachej/boring-agent/server';
|
|
15
16
|
import 'better-auth';
|
|
16
17
|
import 'drizzle-orm/postgres-js';
|
|
@@ -600,9 +601,10 @@ declare class CreditsService {
|
|
|
600
601
|
private readonly store;
|
|
601
602
|
readonly config: CreditsConfig;
|
|
602
603
|
private readonly log?;
|
|
604
|
+
private readonly telemetry;
|
|
603
605
|
/** Users whose signup grant was ensured this process; avoids an INSERT per balance poll. */
|
|
604
606
|
private readonly signupGrantedUsers;
|
|
605
|
-
constructor(store: CreditsMeteringStore, config?: CreditsConfig, log?: CreditsLogger | undefined);
|
|
607
|
+
constructor(store: CreditsMeteringStore, config?: CreditsConfig, log?: CreditsLogger | undefined, telemetry?: TelemetrySink);
|
|
606
608
|
/** Idempotently grant the free starter credits (call from the post-signup hook
|
|
607
609
|
* and lazily on first balance/reserve). The grant NEVER expires: an expiring
|
|
608
610
|
* grant would drop from grantedMicros on expiry while spent usage stayed, turning
|
|
@@ -999,6 +1001,11 @@ interface CreditsRoutesOptions {
|
|
|
999
1001
|
lemonSqueezy?: LemonSqueezyRouteOptions;
|
|
1000
1002
|
stripe?: StripeRouteOptions;
|
|
1001
1003
|
log?: (message: string, fields?: Record<string, unknown>) => void;
|
|
1004
|
+
/** Best-effort telemetry sink (default noop). Currently emits checkout.started /
|
|
1005
|
+
* purchase.webhook_rejected for the STRIPE routes only (the live provider); Lemon
|
|
1006
|
+
* Squeezy parity is a follow-up if it ships. purchase.completed/refunded + run.*
|
|
1007
|
+
* events come from CreditsService regardless of provider. */
|
|
1008
|
+
telemetry?: TelemetrySink;
|
|
1002
1009
|
}
|
|
1003
1010
|
/**
|
|
1004
1011
|
* Register the credit balance endpoint and (optionally) the Lemon Squeezy
|
package/dist/server/index.js
CHANGED
|
@@ -24,13 +24,17 @@ import {
|
|
|
24
24
|
requireWorkspaceMember,
|
|
25
25
|
validateConfig,
|
|
26
26
|
validatePasswordStrength
|
|
27
|
-
} from "../chunk-
|
|
27
|
+
} from "../chunk-6GAQRQKO.js";
|
|
28
28
|
import {
|
|
29
29
|
InsufficientCreditError,
|
|
30
30
|
PostgresMeteringStore,
|
|
31
31
|
createDatabase,
|
|
32
32
|
runMigrations
|
|
33
|
-
} from "../chunk-
|
|
33
|
+
} from "../chunk-FZC3VL5D.js";
|
|
34
|
+
import {
|
|
35
|
+
noopTelemetry,
|
|
36
|
+
safeCapture
|
|
37
|
+
} from "../chunk-AQBXNPMD.js";
|
|
34
38
|
import {
|
|
35
39
|
ERROR_CODES
|
|
36
40
|
} from "../chunk-LIBHVT7V.js";
|
|
@@ -238,10 +242,11 @@ function disabledBalance(userId) {
|
|
|
238
242
|
};
|
|
239
243
|
}
|
|
240
244
|
var CreditsService = class {
|
|
241
|
-
constructor(store, config = DEFAULT_CREDITS_CONFIG, log) {
|
|
245
|
+
constructor(store, config = DEFAULT_CREDITS_CONFIG, log, telemetry = noopTelemetry) {
|
|
242
246
|
this.store = store;
|
|
243
247
|
this.config = config;
|
|
244
248
|
this.log = log;
|
|
249
|
+
this.telemetry = telemetry;
|
|
245
250
|
if (!config.enabled) return;
|
|
246
251
|
validatePricingConfig(config.pricing);
|
|
247
252
|
const posInt = (n) => Number.isSafeInteger(n) && n > 0;
|
|
@@ -260,6 +265,7 @@ var CreditsService = class {
|
|
|
260
265
|
store;
|
|
261
266
|
config;
|
|
262
267
|
log;
|
|
268
|
+
telemetry;
|
|
263
269
|
/** Users whose signup grant was ensured this process; avoids an INSERT per balance poll. */
|
|
264
270
|
signupGrantedUsers = /* @__PURE__ */ new Set();
|
|
265
271
|
/** Idempotently grant the free starter credits (call from the post-signup hook
|
|
@@ -282,7 +288,21 @@ var CreditsService = class {
|
|
|
282
288
|
* optional provider identity is persisted for audit/refund reconciliation. */
|
|
283
289
|
async grantPurchase(userId, orderId, amountMicros, identity) {
|
|
284
290
|
if (!this.config.enabled) return { created: false };
|
|
285
|
-
const { granted } = await this.store.grantPurchaseOnce({ userId, orderId, amountMicros, ...identity });
|
|
291
|
+
const { granted, refundAppliedMicros } = await this.store.grantPurchaseOnce({ userId, orderId, amountMicros, ...identity });
|
|
292
|
+
if (granted) {
|
|
293
|
+
safeCapture(this.telemetry, {
|
|
294
|
+
name: "purchase.completed",
|
|
295
|
+
distinctId: userId,
|
|
296
|
+
properties: {
|
|
297
|
+
creditMicros: amountMicros,
|
|
298
|
+
currency: identity?.currency,
|
|
299
|
+
packId: identity?.variantId
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (refundAppliedMicros && refundAppliedMicros > 0) {
|
|
304
|
+
safeCapture(this.telemetry, { name: "purchase.refunded" });
|
|
305
|
+
}
|
|
286
306
|
return { created: granted };
|
|
287
307
|
}
|
|
288
308
|
/** Revoke a refunded/disputed purchase. `refundFraction` is the cumulative
|
|
@@ -293,13 +313,15 @@ var CreditsService = class {
|
|
|
293
313
|
* Idempotent per cumulative level. */
|
|
294
314
|
async revokePurchase(orderId, opts = {}) {
|
|
295
315
|
if (!this.config.enabled) return { revoked: false };
|
|
296
|
-
|
|
316
|
+
const result = await this.store.revokePurchase(orderId, {
|
|
297
317
|
refundFraction: opts.refundFraction,
|
|
298
318
|
allowTombstone: opts.allowTombstone,
|
|
299
319
|
expectedStoreId: opts.expectedStoreId,
|
|
300
320
|
expectedTestMode: opts.expectedTestMode,
|
|
301
321
|
expectedCurrency: opts.expectedCurrency
|
|
302
322
|
});
|
|
323
|
+
if (result.revoked) safeCapture(this.telemetry, { name: "purchase.refunded" });
|
|
324
|
+
return result;
|
|
303
325
|
}
|
|
304
326
|
async getBalance(userId) {
|
|
305
327
|
if (!this.config.enabled) return disabledBalance(userId);
|
|
@@ -332,7 +354,7 @@ var CreditsService = class {
|
|
|
332
354
|
if (!this.config.enabled) return void 0;
|
|
333
355
|
await this.grantSignupCredits(input.userId);
|
|
334
356
|
try {
|
|
335
|
-
const { reservationId } = await this.store.reserve({
|
|
357
|
+
const { reservationId, created } = await this.store.reserve({
|
|
336
358
|
userId: input.userId,
|
|
337
359
|
workspaceId: input.workspaceId,
|
|
338
360
|
sessionId: input.sessionId,
|
|
@@ -344,9 +366,11 @@ var CreditsService = class {
|
|
|
344
366
|
// is admitted only when available ≥ hold + floor (matches the config doc).
|
|
345
367
|
minAvailableMicros: this.config.runReservationMicros + this.config.minBalanceMicros
|
|
346
368
|
});
|
|
369
|
+
if (created) safeCapture(this.telemetry, { name: "run.started", distinctId: input.userId });
|
|
347
370
|
return reservationId;
|
|
348
371
|
} catch (error) {
|
|
349
372
|
if (error instanceof InsufficientCreditError) {
|
|
373
|
+
safeCapture(this.telemetry, { name: "credits.exhausted", distinctId: input.userId });
|
|
350
374
|
throw new CreditExhaustedError(await this.getBalance(input.userId));
|
|
351
375
|
}
|
|
352
376
|
throw error;
|
|
@@ -402,7 +426,8 @@ var CreditsService = class {
|
|
|
402
426
|
}
|
|
403
427
|
async settleRun(userId, runId, reservationId) {
|
|
404
428
|
if (!this.config.enabled) return;
|
|
405
|
-
await this.store.finishReservation(reservationId ? { reservationId } : { runId, userId }, "settled");
|
|
429
|
+
const { updated } = await this.store.finishReservation(reservationId ? { reservationId } : { runId, userId }, "settled");
|
|
430
|
+
if (updated) safeCapture(this.telemetry, { name: "run.completed", distinctId: userId });
|
|
406
431
|
}
|
|
407
432
|
async releaseRun(userId, runId, reservationId) {
|
|
408
433
|
if (!this.config.enabled) return;
|
|
@@ -435,10 +460,11 @@ var CreditsService = class {
|
|
|
435
460
|
metadata: { kind: metaKind, reservationId: input.reservationId ?? null, alreadyBilledMicros: alreadyBilled, currency: "credits" }
|
|
436
461
|
});
|
|
437
462
|
}
|
|
438
|
-
await this.store.finishReservation(
|
|
463
|
+
const { updated } = await this.store.finishReservation(
|
|
439
464
|
input.reservationId ? { reservationId: input.reservationId } : { runId: input.runId, userId: input.userId },
|
|
440
465
|
"settled"
|
|
441
466
|
);
|
|
467
|
+
if (updated) safeCapture(this.telemetry, { name: "run.completed", distinctId: input.userId });
|
|
442
468
|
this.log?.("credits: fallback hold charge \u2014 topped up to the hold and settled (reconcile against missing/non-billable usage)", {
|
|
443
469
|
kind: metaKind,
|
|
444
470
|
runId: input.runId,
|
|
@@ -1163,6 +1189,7 @@ function defaultGetUserId(request) {
|
|
|
1163
1189
|
}
|
|
1164
1190
|
function registerCreditsRoutes(app, options) {
|
|
1165
1191
|
const getUserId = options.getUserId ?? defaultGetUserId;
|
|
1192
|
+
const telemetry = options.telemetry ?? noopTelemetry;
|
|
1166
1193
|
const balancePath = options.balancePath ?? "/api/credits/balance";
|
|
1167
1194
|
if (options.lemonSqueezy && options.stripe) {
|
|
1168
1195
|
throw new Error("credits: configure at most one purchase provider (lemonSqueezy OR stripe), not both");
|
|
@@ -1197,7 +1224,7 @@ function registerCreditsRoutes(app, options) {
|
|
|
1197
1224
|
if (!options.service.config.enabled) {
|
|
1198
1225
|
throw new Error("credits: cannot register Stripe checkout/webhook with a disabled credits service (paid orders would be acknowledged without crediting)");
|
|
1199
1226
|
}
|
|
1200
|
-
registerStripeRoutes(app, options.service, getUserId, stripeOpts, options.log);
|
|
1227
|
+
registerStripeRoutes(app, options.service, getUserId, stripeOpts, options.log, telemetry);
|
|
1201
1228
|
return;
|
|
1202
1229
|
}
|
|
1203
1230
|
const ls = options.lemonSqueezy;
|
|
@@ -1392,7 +1419,7 @@ function registerCreditsRoutes(app, options) {
|
|
|
1392
1419
|
});
|
|
1393
1420
|
});
|
|
1394
1421
|
}
|
|
1395
|
-
function registerStripeRoutes(app, service, getUserId, stripe, log) {
|
|
1422
|
+
function registerStripeRoutes(app, service, getUserId, stripe, log, telemetry = noopTelemetry) {
|
|
1396
1423
|
const creditMicrosPerUnit = service.config.pricing.creditMicrosPerUnit;
|
|
1397
1424
|
const requireCurrency = (stripe.requireCurrency ?? "EUR").toUpperCase();
|
|
1398
1425
|
if (NON_TWO_DECIMAL_CURRENCIES.has(requireCurrency)) {
|
|
@@ -1487,6 +1514,7 @@ function registerStripeRoutes(app, service, getUserId, stripe, log) {
|
|
|
1487
1514
|
email: typeof email === "string" ? email : void 0,
|
|
1488
1515
|
redirectUrl: checkout.redirectUrl
|
|
1489
1516
|
});
|
|
1517
|
+
safeCapture(telemetry, { name: "checkout.started", distinctId: userId, properties: { provider: "stripe", packId } });
|
|
1490
1518
|
return reply.send({ url });
|
|
1491
1519
|
} catch (error) {
|
|
1492
1520
|
log?.("credits: stripe checkout creation failed", { error: String(error) });
|
|
@@ -1539,7 +1567,17 @@ function registerStripeRoutes(app, service, getUserId, stripe, log) {
|
|
|
1539
1567
|
}),
|
|
1540
1568
|
log
|
|
1541
1569
|
}
|
|
1542
|
-
)
|
|
1570
|
+
).catch((error) => {
|
|
1571
|
+
safeCapture(telemetry, { name: "purchase.webhook_rejected", properties: { provider: "stripe", reason: "handler_error" } });
|
|
1572
|
+
throw error;
|
|
1573
|
+
});
|
|
1574
|
+
if (result.status >= 400) {
|
|
1575
|
+
const reason = result.body?.reason;
|
|
1576
|
+
safeCapture(telemetry, {
|
|
1577
|
+
name: "purchase.webhook_rejected",
|
|
1578
|
+
properties: { provider: "stripe", reason: typeof reason === "string" ? reason : void 0 }
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1543
1581
|
return reply.code(result.status).send(result.body);
|
|
1544
1582
|
});
|
|
1545
1583
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
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-
|
|
83
|
-
"@hachej/boring-
|
|
84
|
-
"@hachej/boring-
|
|
82
|
+
"@hachej/boring-agent": "0.1.46",
|
|
83
|
+
"@hachej/boring-ui-kit": "0.1.46",
|
|
84
|
+
"@hachej/boring-workspace": "0.1.46"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@testing-library/jest-dom": "^6.9.1",
|