@hachej/boring-core 0.1.44 → 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 +43 -19
- package/dist/{authHook-CzBsMwwM.d.ts → authHook-DtzhSmqS.d.ts} +3 -0
- package/dist/{chunk-UM5SHYIS.js → chunk-6GAQRQKO.js} +71 -6
- 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 +13 -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
|
}
|
|
@@ -208,6 +222,13 @@ var FRONTEND_AUTH_PAGES_SPA_ONLY = /* @__PURE__ */ new Set([
|
|
|
208
222
|
"/auth/callback/github",
|
|
209
223
|
"/auth/callback/google"
|
|
210
224
|
]);
|
|
225
|
+
function resolveCoreLoadConfigOptions(options = {}, nodeEnv = process.env.NODE_ENV) {
|
|
226
|
+
return {
|
|
227
|
+
allowMissingSecrets: nodeEnv !== "production",
|
|
228
|
+
...options.appRoot && !options.loadConfigOptions?.tomlPath ? { tomlPath: path.resolve(options.appRoot, "boring.app.toml") } : {},
|
|
229
|
+
...options.loadConfigOptions
|
|
230
|
+
};
|
|
231
|
+
}
|
|
211
232
|
function dedupeStrings(values) {
|
|
212
233
|
return Array.from(new Set(values));
|
|
213
234
|
}
|
|
@@ -436,12 +457,20 @@ function captureAppOpened(telemetry, requestId) {
|
|
|
436
457
|
}
|
|
437
458
|
function registerTelemetryHooks(app, telemetry) {
|
|
438
459
|
app.addHook("onResponse", async (request, reply) => {
|
|
439
|
-
|
|
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;
|
|
440
469
|
safeCapture(telemetry, {
|
|
441
470
|
name: "server.request.failed",
|
|
442
471
|
properties: {
|
|
443
472
|
requestId: request.id,
|
|
444
|
-
status
|
|
473
|
+
status,
|
|
445
474
|
errorCode: ERROR_CODES.INTERNAL_ERROR
|
|
446
475
|
}
|
|
447
476
|
});
|
|
@@ -474,7 +503,7 @@ async function registerFrontendFallback(app, appRoot, telemetry) {
|
|
|
474
503
|
return serveFrontendShell(request, reply, indexPath, telemetry);
|
|
475
504
|
});
|
|
476
505
|
}
|
|
477
|
-
async function createCoreRuntime(config) {
|
|
506
|
+
async function createCoreRuntime(config, customTelemetry) {
|
|
478
507
|
if (config.stores !== "postgres") {
|
|
479
508
|
throw new Error("createCoreWorkspaceAgentServer currently supports only CORE_STORES=postgres");
|
|
480
509
|
}
|
|
@@ -486,18 +515,19 @@ async function createCoreRuntime(config) {
|
|
|
486
515
|
config.encryption.workspaceSettingsKey
|
|
487
516
|
);
|
|
488
517
|
const app = await createCoreApp(config);
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
});
|
|
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 });
|
|
493
522
|
app.decorate("db", db);
|
|
494
523
|
app.decorate("auth", auth);
|
|
495
524
|
app.decorate("userStore", userStore);
|
|
496
525
|
app.decorate("workspaceStore", workspaceStore);
|
|
526
|
+
app.decorate("telemetry", telemetry);
|
|
497
527
|
app.addHook("onClose", async () => {
|
|
498
528
|
await sql.end();
|
|
499
529
|
});
|
|
500
|
-
return { app, sql, db, userStore, workspaceStore };
|
|
530
|
+
return { app, sql, db, userStore, workspaceStore, telemetry };
|
|
501
531
|
}
|
|
502
532
|
async function registerCoreRoutes({
|
|
503
533
|
app,
|
|
@@ -526,18 +556,12 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
526
556
|
);
|
|
527
557
|
}
|
|
528
558
|
assertCoreStaticPluginEntries(options.plugins);
|
|
529
|
-
const config = options.config ?? await loadConfig(
|
|
530
|
-
|
|
531
|
-
...options.loadConfigOptions
|
|
532
|
-
});
|
|
533
|
-
const { app, sql, db, userStore, workspaceStore } = await createCoreRuntime(config);
|
|
559
|
+
const config = options.config ?? await loadConfig(resolveCoreLoadConfigOptions(options));
|
|
560
|
+
const { app, sql, db, userStore, workspaceStore, telemetry } = await createCoreRuntime(config, options.telemetry);
|
|
534
561
|
const appRoot = options.appRoot;
|
|
535
562
|
const serveFrontend = options.serveFrontend ?? (process.env.NODE_ENV !== "development" && Boolean(appRoot));
|
|
536
563
|
const pluginWorkspaceRoot = process.cwd();
|
|
537
564
|
const workspaceRoot = options.workspaceRoot ?? process.env.BORING_AGENT_WORKSPACE_ROOT ?? process.cwd();
|
|
538
|
-
const telemetrySource = options.telemetry ? "custom" : process.env.BORING_TELEMETRY_ENABLED === "true" ? "db-env" : "noop-env";
|
|
539
|
-
const telemetry = options.telemetry ?? createDatabaseTelemetryFromEnv(db, { appId: config.appId }, process.env);
|
|
540
|
-
app.log.debug({ telemetry: { source: telemetrySource } }, "resolved telemetry sink");
|
|
541
565
|
registerTelemetryHooks(app, telemetry);
|
|
542
566
|
await registerCoreRoutes({ app, sql, db, userStore, workspaceStore });
|
|
543
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,
|
|
@@ -101,6 +105,30 @@ var SIXTEEN_MB = 16 * 1024 * 1024;
|
|
|
101
105
|
var INSECURE_PLACEHOLDER_SECRET = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
102
106
|
var INSECURE_DATABASE_URL = "postgres://placeholder:placeholder@localhost:5432/placeholder";
|
|
103
107
|
var INSECURE_ENCRYPTION_KEY = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
108
|
+
function formatDisplayName(name) {
|
|
109
|
+
const trimmed = name.trim().replace(/[<>]/g, "");
|
|
110
|
+
if (/^[A-Za-z0-9 ._-]+$/.test(trimmed)) return trimmed;
|
|
111
|
+
return `"${trimmed.replace(/["\\]/g, "\\$&")}"`;
|
|
112
|
+
}
|
|
113
|
+
function isDefaultBoringDisplayName(name) {
|
|
114
|
+
return name.toLowerCase().replace(/[\s._-]+/g, "") === "boringui";
|
|
115
|
+
}
|
|
116
|
+
function normalizeMailFrom(appName, rawFrom) {
|
|
117
|
+
const from = rawFrom.trim();
|
|
118
|
+
const addressWithDisplay = from.match(/^(.*?)\s*<([^>]+)>$/);
|
|
119
|
+
if (addressWithDisplay) {
|
|
120
|
+
const displayName = addressWithDisplay[1].trim().replace(/^"(.*)"$/, "$1");
|
|
121
|
+
const address = addressWithDisplay[2].trim();
|
|
122
|
+
if (!displayName || isDefaultBoringDisplayName(displayName)) {
|
|
123
|
+
return `${formatDisplayName(appName)} <${address}>`;
|
|
124
|
+
}
|
|
125
|
+
return from;
|
|
126
|
+
}
|
|
127
|
+
if (/^[^\s@<>]+@[^\s@<>]+$/.test(from)) {
|
|
128
|
+
return `${formatDisplayName(appName)} <${from}>`;
|
|
129
|
+
}
|
|
130
|
+
return from;
|
|
131
|
+
}
|
|
104
132
|
function parseRateLimitOverrides(raw) {
|
|
105
133
|
if (!raw) return void 0;
|
|
106
134
|
try {
|
|
@@ -224,7 +252,18 @@ async function loadConfig(options) {
|
|
|
224
252
|
...toml.features?.invite_ttl_days != null && { inviteTtlDays: toml.features.invite_ttl_days }
|
|
225
253
|
}
|
|
226
254
|
};
|
|
227
|
-
|
|
255
|
+
const config = validateConfig(raw);
|
|
256
|
+
if (!config.auth.mail) return config;
|
|
257
|
+
return {
|
|
258
|
+
...config,
|
|
259
|
+
auth: {
|
|
260
|
+
...config.auth,
|
|
261
|
+
mail: {
|
|
262
|
+
...config.auth.mail,
|
|
263
|
+
from: normalizeMailFrom(config.appName, config.auth.mail.from)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
228
267
|
}
|
|
229
268
|
function validateConfig(raw) {
|
|
230
269
|
const result = coreConfigSchema.safeParse(raw);
|
|
@@ -1423,6 +1462,7 @@ import { Button as Button4, Section as Section5, Text as Text5 } from "@react-em
|
|
|
1423
1462
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1424
1463
|
function WorkspaceInvite({
|
|
1425
1464
|
acceptUrl,
|
|
1465
|
+
appName,
|
|
1426
1466
|
inviterName,
|
|
1427
1467
|
workspaceName,
|
|
1428
1468
|
role,
|
|
@@ -1432,7 +1472,7 @@ function WorkspaceInvite({
|
|
|
1432
1472
|
Layout,
|
|
1433
1473
|
{
|
|
1434
1474
|
preview: `${inviterName} invited you to ${workspaceName}`,
|
|
1435
|
-
appName
|
|
1475
|
+
appName,
|
|
1436
1476
|
children: /* @__PURE__ */ jsxs5(Section5, { style: content4, children: [
|
|
1437
1477
|
/* @__PURE__ */ jsx5(Text5, { style: heading4, children: "You've been invited" }),
|
|
1438
1478
|
/* @__PURE__ */ jsxs5(Text5, { style: paragraph4, children: [
|
|
@@ -1568,6 +1608,7 @@ async function renderMagicLink(data) {
|
|
|
1568
1608
|
async function renderWorkspaceInvite(data) {
|
|
1569
1609
|
const element = WorkspaceInvite({
|
|
1570
1610
|
acceptUrl: data.acceptUrl,
|
|
1611
|
+
appName: data.appName,
|
|
1571
1612
|
inviterName: data.inviterName,
|
|
1572
1613
|
workspaceName: data.workspaceName,
|
|
1573
1614
|
role: data.role,
|
|
@@ -1705,6 +1746,7 @@ function buildMailTransport(config) {
|
|
|
1705
1746
|
}
|
|
1706
1747
|
function createAuth(config, db, opts) {
|
|
1707
1748
|
const transport = buildMailTransport(config);
|
|
1749
|
+
const telemetry = opts?.telemetry ?? noopTelemetry;
|
|
1708
1750
|
const emailVerificationConfig = transport ? {
|
|
1709
1751
|
sendOnSignUp: true,
|
|
1710
1752
|
sendVerificationEmail: async (data) => {
|
|
@@ -1765,13 +1807,35 @@ function createAuth(config, db, opts) {
|
|
|
1765
1807
|
baseURL: config.auth.url,
|
|
1766
1808
|
basePath: "/auth",
|
|
1767
1809
|
trustedOrigins: config.cors.origins,
|
|
1768
|
-
databaseHooks:
|
|
1810
|
+
databaseHooks: {
|
|
1769
1811
|
user: {
|
|
1770
1812
|
create: {
|
|
1771
|
-
|
|
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
|
+
}
|
|
1772
1836
|
}
|
|
1773
1837
|
}
|
|
1774
|
-
}
|
|
1838
|
+
},
|
|
1775
1839
|
advanced: {
|
|
1776
1840
|
database: {
|
|
1777
1841
|
generateId: "uuid"
|
|
@@ -2426,6 +2490,7 @@ var inviteRoutesPlugin = async (app, opts) => {
|
|
|
2426
2490
|
const email = await renderWorkspaceInvite({
|
|
2427
2491
|
to: parsed.data.email,
|
|
2428
2492
|
acceptUrl,
|
|
2493
|
+
appName: app.config.appName,
|
|
2429
2494
|
inviterName: request.user.name ?? request.user.email,
|
|
2430
2495
|
workspaceName: workspace?.name ?? "Workspace",
|
|
2431
2496
|
role: parsed.data.role,
|
|
@@ -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';
|
|
@@ -326,6 +327,7 @@ declare function renderMagicLink(data: MagicLinkData): Promise<RenderedEmail>;
|
|
|
326
327
|
interface WorkspaceInviteData {
|
|
327
328
|
to: string;
|
|
328
329
|
acceptUrl: string;
|
|
330
|
+
appName: string;
|
|
329
331
|
inviterName: string;
|
|
330
332
|
workspaceName: string;
|
|
331
333
|
role: string;
|
|
@@ -599,9 +601,10 @@ declare class CreditsService {
|
|
|
599
601
|
private readonly store;
|
|
600
602
|
readonly config: CreditsConfig;
|
|
601
603
|
private readonly log?;
|
|
604
|
+
private readonly telemetry;
|
|
602
605
|
/** Users whose signup grant was ensured this process; avoids an INSERT per balance poll. */
|
|
603
606
|
private readonly signupGrantedUsers;
|
|
604
|
-
constructor(store: CreditsMeteringStore, config?: CreditsConfig, log?: CreditsLogger | undefined);
|
|
607
|
+
constructor(store: CreditsMeteringStore, config?: CreditsConfig, log?: CreditsLogger | undefined, telemetry?: TelemetrySink);
|
|
605
608
|
/** Idempotently grant the free starter credits (call from the post-signup hook
|
|
606
609
|
* and lazily on first balance/reserve). The grant NEVER expires: an expiring
|
|
607
610
|
* grant would drop from grantedMicros on expiry while spent usage stayed, turning
|
|
@@ -998,6 +1001,11 @@ interface CreditsRoutesOptions {
|
|
|
998
1001
|
lemonSqueezy?: LemonSqueezyRouteOptions;
|
|
999
1002
|
stripe?: StripeRouteOptions;
|
|
1000
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;
|
|
1001
1009
|
}
|
|
1002
1010
|
/**
|
|
1003
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",
|