@hachej/boring-core 0.1.20 → 0.1.22

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.
@@ -7,6 +7,7 @@ interface CoreFrontAuthPagesOverride {
7
7
  forgotPassword?: React.FC;
8
8
  resetPassword?: React.FC;
9
9
  verifyEmail?: React.FC;
10
+ authError?: React.FC;
10
11
  userSettings?: React.FC;
11
12
  }
12
13
  interface CoreFrontProps {
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { C as CoreFrontAuthPagesOverride } from '../../CoreFront-CDeLdfb0.js';
3
+ import { C as CoreFrontAuthPagesOverride } from '../../CoreFront-CgAkiEts.js';
4
4
  import { WorkspaceAgentSession, WorkspaceAgentFrontProps } from '@hachej/boring-workspace/app/front';
5
5
 
6
6
  interface CoreWorkspaceAgentFrontProps<TSession extends WorkspaceAgentSession = WorkspaceAgentSession> extends Omit<WorkspaceAgentFrontProps<TSession>, 'workspaceId' | 'frontPluginHotReload' | 'hotReloadEnabled'> {
@@ -3,7 +3,7 @@ import {
3
3
  UserMenu,
4
4
  WorkspaceSwitcher,
5
5
  useCurrentWorkspace
6
- } from "../../chunk-A5TMALZR.js";
6
+ } from "../../chunk-JMCBLJ6W.js";
7
7
  import "../../chunk-HYNKZSTF.js";
8
8
  import "../../chunk-H5KU6R6Y.js";
9
9
  import "../../chunk-MLKGABMK.js";
@@ -2,9 +2,10 @@ import { RuntimeProvisioningContribution, RegisterAgentRoutesOptions } from '@ha
2
2
  import { DirPluginEntry, CreateWorkspaceAgentServerOptions } from '@hachej/boring-workspace/app/server';
3
3
  import { WorkspaceServerPlugin } from '@hachej/boring-workspace/server';
4
4
  import { FastifyInstance } from 'fastify';
5
- import { C as CoreConfig } from '../../index-COZa03RP.js';
6
- import { B as BetterAuthInstance, L as LoadConfigOptions } from '../../authHook-C5ShLjAS.js';
7
- import { D as Database, U as UserStore, W as WorkspaceStore } from '../../connection-jW1Xwcne.js';
5
+ import { C as CoreConfig } from '../../types-CbMOXLBf.js';
6
+ import { TelemetrySink } from '../../shared/index.js';
7
+ import { B as BetterAuthInstance, L as LoadConfigOptions } from '../../authHook-DUqyxueY.js';
8
+ import { D as Database, U as UserStore, W as WorkspaceStore } from '../../connection-AL8KSENV.js';
8
9
  import { IncomingMessage, ServerResponse } from 'node:http';
9
10
  import 'better-auth';
10
11
  import 'drizzle-orm/postgres-js';
@@ -38,6 +39,8 @@ interface CreateCoreWorkspaceAgentServerOptions extends Omit<RegisterAgentRoutes
38
39
  extraTools?: RegisterAgentRoutesOptions['extraTools'];
39
40
  systemPromptAppend?: string;
40
41
  serveFrontend?: boolean;
42
+ /** Optional best-effort telemetry sink. Defaults to core's DB-backed env helper. */
43
+ telemetry?: TelemetrySink;
41
44
  }
42
45
  declare function createCoreWorkspaceAgentServer(options?: CreateCoreWorkspaceAgentServerOptions): Promise<CoreWorkspaceAgentServer>;
43
46
 
@@ -9,13 +9,20 @@ import {
9
9
  registerRoutes,
10
10
  registerSettingsRoutes,
11
11
  registerWorkspaceRoutes
12
- } from "../../chunk-V5CXMFHP.js";
12
+ } from "../../chunk-6D7LEQSL.js";
13
13
  import {
14
14
  PostgresUserStore,
15
15
  PostgresWorkspaceStore,
16
- createDatabase
17
- } from "../../chunk-HSRBZLKT.js";
18
- import "../../chunk-H5KU6R6Y.js";
16
+ createDatabase,
17
+ telemetryEvents
18
+ } from "../../chunk-C3YMOITB.js";
19
+ import {
20
+ noopTelemetry,
21
+ safeCapture
22
+ } from "../../chunk-AQBXNPMD.js";
23
+ import {
24
+ ERROR_CODES
25
+ } from "../../chunk-H5KU6R6Y.js";
19
26
  import "../../chunk-MLKGABMK.js";
20
27
 
21
28
  // src/app/server/createCoreWorkspaceAgentServer.ts
@@ -24,13 +31,14 @@ import { createReadStream } from "fs";
24
31
  import path from "path";
25
32
  import {
26
33
  compactPiPackages,
34
+ provisionWorkspaceRuntime,
27
35
  registerAgentRoutes
28
36
  } from "@hachej/boring-agent/server";
29
37
  import {
30
38
  collectWorkspaceAgentServerPlugins,
31
39
  hasDirServerPlugin,
32
- provisionWorkspaceAgentServer,
33
40
  readWorkspacePluginPackagePiSnapshot,
41
+ readWorkspacePluginPackageRuntimePlugins,
34
42
  resolveDefaultWorkspacePluginPackagePaths,
35
43
  resolveOnePluginEntry
36
44
  } from "@hachej/boring-workspace/app/server";
@@ -39,6 +47,134 @@ import {
39
47
  createWorkspaceUiTools,
40
48
  uiRoutes
41
49
  } from "@hachej/boring-workspace/server";
50
+
51
+ // src/server/telemetry/db.ts
52
+ var EVENT_NAME_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*){0,8}$/;
53
+ var SAFE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
54
+ var SAFE_SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/;
55
+ var SAFE_STATUS_PATTERN = /^[a-z][a-z0-9_-]{0,31}$/;
56
+ var SAFE_ERROR_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_:-]{0,63}$/;
57
+ var SAFE_PACKAGE_NAME_PATTERN = /^(?:@[A-Za-z0-9_.-]+\/)?[A-Za-z0-9_.-]{1,96}$/;
58
+ var SAFE_PACKAGE_VERSION_PATTERN = /^v?\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/;
59
+ var SAFE_BOOLEAN_STRING_PATTERN = /^(?:true|false)$/;
60
+ var SUSPICIOUS_STRING_PATTERN = /(?:secret|token|bearer|password|api[_-]?key|private|\.env|sk[_-](?:live|test)|ghp_|github_pat_|glpat-|xox[baprs]-|AKIA|ASIA|ya29\.|eyJ|phc_|npm_)/i;
61
+ var ALLOWED_PROPERTY_KEYS = /* @__PURE__ */ new Set([
62
+ "workspaceId",
63
+ "sessionId",
64
+ "requestId",
65
+ "runtimeMode",
66
+ "modelProvider",
67
+ "toolName",
68
+ "panelId",
69
+ "commandId",
70
+ "status",
71
+ "durationMs",
72
+ "errorCode",
73
+ "phase",
74
+ "runtime",
75
+ "pluginId",
76
+ "packageId",
77
+ "changed",
78
+ "nodePackageCount",
79
+ "pythonPackageCount",
80
+ "skillCount",
81
+ "templateDirCount",
82
+ "packageName",
83
+ "packageVersion"
84
+ ]);
85
+ function createDatabaseTelemetryFromEnv(db, options, env = process.env) {
86
+ if (env.BORING_TELEMETRY_ENABLED !== "true") return noopTelemetry;
87
+ return createDatabaseTelemetry(db, { appId: options.appId, enabled: true });
88
+ }
89
+ function createDatabaseTelemetry(db, options) {
90
+ if (options.enabled === false) return noopTelemetry;
91
+ return {
92
+ capture(event) {
93
+ const eventName = sanitizeTelemetryEventName(event.name);
94
+ if (!eventName) return;
95
+ const row = {
96
+ appId: options.appId,
97
+ eventName,
98
+ distinctId: sanitizeTelemetryDistinctId(event.distinctId),
99
+ properties: sanitizeTelemetryProperties(event.properties)
100
+ };
101
+ try {
102
+ void Promise.resolve(db.insert(telemetryEvents).values(row)).catch(() => {
103
+ });
104
+ } catch {
105
+ }
106
+ }
107
+ };
108
+ }
109
+ function sanitizeTelemetryEventName(value) {
110
+ if (value.length > 128) return void 0;
111
+ if (SUSPICIOUS_STRING_PATTERN.test(value)) return void 0;
112
+ return EVENT_NAME_PATTERN.test(value) ? value : void 0;
113
+ }
114
+ function sanitizeTelemetryDistinctId(value) {
115
+ if (!value) return "anonymous";
116
+ return sanitizeTelemetryString("distinctId", value) ?? "anonymous";
117
+ }
118
+ function sanitizeTelemetryProperties(properties) {
119
+ const sanitized = {};
120
+ if (!properties) return sanitized;
121
+ for (const [key, value] of Object.entries(properties)) {
122
+ if (!ALLOWED_PROPERTY_KEYS.has(key)) continue;
123
+ const sanitizedValue = sanitizeTelemetryProperty(key, value);
124
+ if (sanitizedValue === void 0) continue;
125
+ sanitized[key] = sanitizedValue;
126
+ }
127
+ return sanitized;
128
+ }
129
+ function sanitizeTelemetryProperty(key, value) {
130
+ if (key === "durationMs") {
131
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
132
+ }
133
+ if (key.endsWith("Count")) {
134
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
135
+ }
136
+ if (key === "status" && typeof value === "number") {
137
+ return Number.isInteger(value) && value >= 100 && value <= 599 ? value : void 0;
138
+ }
139
+ if (typeof value !== "string") return void 0;
140
+ return sanitizeTelemetryString(key, value);
141
+ }
142
+ function sanitizeTelemetryString(key, value) {
143
+ if (value.length === 0) return void 0;
144
+ if (SUSPICIOUS_STRING_PATTERN.test(value)) return void 0;
145
+ switch (key) {
146
+ case "workspaceId":
147
+ case "sessionId":
148
+ case "requestId":
149
+ case "distinctId":
150
+ return SAFE_ID_PATTERN.test(value) ? value : void 0;
151
+ case "runtimeMode":
152
+ case "modelProvider":
153
+ case "toolName":
154
+ case "panelId":
155
+ case "commandId":
156
+ case "phase":
157
+ case "runtime":
158
+ return SAFE_SLUG_PATTERN.test(value) ? value : void 0;
159
+ case "pluginId":
160
+ case "packageId":
161
+ return SAFE_ID_PATTERN.test(value) ? value : void 0;
162
+ case "changed":
163
+ return SAFE_BOOLEAN_STRING_PATTERN.test(value) ? value : void 0;
164
+ case "status":
165
+ return SAFE_STATUS_PATTERN.test(value) ? value : void 0;
166
+ case "errorCode":
167
+ return SAFE_ERROR_CODE_PATTERN.test(value) ? value : void 0;
168
+ case "packageName":
169
+ return SAFE_PACKAGE_NAME_PATTERN.test(value) ? value : void 0;
170
+ case "packageVersion":
171
+ return SAFE_PACKAGE_VERSION_PATTERN.test(value) ? value : void 0;
172
+ default:
173
+ return void 0;
174
+ }
175
+ }
176
+
177
+ // src/app/server/createCoreWorkspaceAgentServer.ts
42
178
  var MIME_TYPES = {
43
179
  ".css": "text/css; charset=utf-8",
44
180
  ".html": "text/html; charset=utf-8",
@@ -62,11 +198,13 @@ var FRONTEND_AUTH_PAGES = /* @__PURE__ */ new Set([
62
198
  "/auth/signin",
63
199
  "/auth/signup",
64
200
  "/auth/forgot-password",
65
- "/auth/reset-password",
66
- "/auth/callback/github"
201
+ "/auth/reset-password"
67
202
  ]);
68
203
  var FRONTEND_AUTH_PAGES_SPA_ONLY = /* @__PURE__ */ new Set([
69
- "/auth/verify-email"
204
+ "/auth/verify-email",
205
+ "/auth/error",
206
+ "/auth/callback/github",
207
+ "/auth/callback/google"
70
208
  ]);
71
209
  function dedupeStrings(values) {
72
210
  return Array.from(new Set(values));
@@ -239,10 +377,29 @@ function shouldServeFrontend(pathname) {
239
377
  if (pathname.startsWith("/auth/")) return false;
240
378
  return true;
241
379
  }
242
- async function registerAuthProxy(app) {
380
+ async function serveFrontendShell(request, reply, indexPath, telemetry) {
381
+ if (!await pathExists(indexPath)) {
382
+ reply.status(503);
383
+ return {
384
+ error: "frontend_not_built",
385
+ message: "Build the frontend before starting in production mode."
386
+ };
387
+ }
388
+ const html = await readFile(indexPath, "utf-8");
389
+ captureAppOpened(telemetry, request.id);
390
+ reply.type("text/html; charset=utf-8");
391
+ return reply.send(injectCspNonceIntoHtml(html, request.cspNonce));
392
+ }
393
+ async function registerAuthProxy(app, options) {
243
394
  app.all("/auth/*", async (request, reply) => {
244
395
  const accept = String(request.headers?.accept ?? "");
245
- if (request.method === "GET" && accept.includes("text/html")) {
396
+ const pathname = request.url.split("?")[0] ?? "/";
397
+ const isSpaOnlyAuthPage = FRONTEND_AUTH_PAGES_SPA_ONLY.has(pathname);
398
+ const isExplicitShellAuthPage = FRONTEND_AUTH_PAGES.has(pathname);
399
+ if (request.method === "GET" && accept.includes("text/html") && (isExplicitShellAuthPage || isSpaOnlyAuthPage && pathname !== "/auth/callback/github" && pathname !== "/auth/callback/google")) {
400
+ if (options?.serveSpaShell) {
401
+ return options.serveSpaShell(request, reply);
402
+ }
246
403
  return reply.callNotFound();
247
404
  }
248
405
  const body = encodeAuthRequestBody(request);
@@ -269,39 +426,36 @@ async function registerAuthProxy(app) {
269
426
  return reply.send(responseBody);
270
427
  });
271
428
  }
272
- async function registerFrontendAuthPages(app, appRoot) {
429
+ function captureAppOpened(telemetry, requestId) {
430
+ safeCapture(telemetry, {
431
+ name: "app.opened",
432
+ properties: { requestId }
433
+ });
434
+ }
435
+ function registerTelemetryHooks(app, telemetry) {
436
+ app.addHook("onResponse", async (request, reply) => {
437
+ if (reply.statusCode < 500) return;
438
+ safeCapture(telemetry, {
439
+ name: "server.request.failed",
440
+ properties: {
441
+ requestId: request.id,
442
+ status: reply.statusCode,
443
+ errorCode: ERROR_CODES.INTERNAL_ERROR
444
+ }
445
+ });
446
+ });
447
+ }
448
+ async function registerFrontendAuthPages(app, appRoot, telemetry) {
273
449
  const frontDistDir = path.resolve(appRoot, "dist/front");
274
450
  const indexPath = path.resolve(frontDistDir, "index.html");
275
451
  for (const pagePath of FRONTEND_AUTH_PAGES) {
276
- app.get(pagePath, async (request, reply) => {
277
- if (!await pathExists(indexPath)) {
278
- reply.status(503);
279
- return {
280
- error: "frontend_not_built",
281
- message: "Build the frontend before starting in production mode."
282
- };
283
- }
284
- const html = await readFile(indexPath, "utf-8");
285
- reply.type("text/html; charset=utf-8");
286
- return reply.send(injectCspNonceIntoHtml(html, request.cspNonce));
287
- });
452
+ app.get(pagePath, async (request, reply) => serveFrontendShell(request, reply, indexPath, telemetry));
288
453
  }
289
454
  }
290
- async function registerFrontendFallback(app, appRoot) {
455
+ async function registerFrontendFallback(app, appRoot, telemetry) {
291
456
  const frontDistDir = path.resolve(appRoot, "dist/front");
292
457
  const indexPath = path.resolve(frontDistDir, "index.html");
293
- app.get("/", async (request, reply) => {
294
- if (!await pathExists(indexPath)) {
295
- reply.status(503);
296
- return {
297
- error: "frontend_not_built",
298
- message: "Build the frontend before starting in production mode."
299
- };
300
- }
301
- const html = await readFile(indexPath, "utf-8");
302
- reply.type("text/html; charset=utf-8");
303
- return reply.send(injectCspNonceIntoHtml(html, request.cspNonce));
304
- });
458
+ app.get("/", async (request, reply) => serveFrontendShell(request, reply, indexPath, telemetry));
305
459
  app.get("/*", async (request, reply) => {
306
460
  const pathname = request.url.split("?")[0] ?? "/";
307
461
  if (!shouldServeFrontend(pathname)) return reply.callNotFound();
@@ -315,16 +469,7 @@ async function registerFrontendFallback(app, appRoot) {
315
469
  reply.type(contentType(candidate));
316
470
  return reply.send(createReadStream(candidate));
317
471
  }
318
- if (!await pathExists(indexPath)) {
319
- reply.status(503);
320
- return {
321
- error: "frontend_not_built",
322
- message: "Build the frontend before starting in production mode."
323
- };
324
- }
325
- const html = await readFile(indexPath, "utf-8");
326
- reply.type("text/html; charset=utf-8");
327
- return reply.send(injectCspNonceIntoHtml(html, request.cspNonce));
472
+ return serveFrontendShell(request, reply, indexPath, telemetry);
328
473
  });
329
474
  }
330
475
  async function createCoreRuntime(config) {
@@ -387,17 +532,24 @@ async function createCoreWorkspaceAgentServer(options = {}) {
387
532
  const appRoot = options.appRoot;
388
533
  const serveFrontend = options.serveFrontend ?? (process.env.NODE_ENV !== "development" && Boolean(appRoot));
389
534
  const workspaceRoot = options.workspaceRoot ?? process.cwd();
535
+ const telemetrySource = options.telemetry ? "custom" : process.env.BORING_TELEMETRY_ENABLED === "true" ? "db-env" : "noop-env";
536
+ const telemetry = options.telemetry ?? createDatabaseTelemetryFromEnv(db, { appId: config.appId }, process.env);
537
+ app.log.debug({ telemetry: { source: telemetrySource } }, "resolved telemetry sink");
538
+ registerTelemetryHooks(app, telemetry);
390
539
  await registerCoreRoutes({ app, sql, db, userStore, workspaceStore });
391
540
  if (serveFrontend && appRoot) {
392
- await registerFrontendAuthPages(app, appRoot);
541
+ await registerFrontendAuthPages(app, appRoot, telemetry);
393
542
  }
394
- await registerAuthProxy(app);
543
+ await registerAuthProxy(app, serveFrontend && appRoot ? {
544
+ serveSpaShell: (request, reply) => serveFrontendShell(request, reply, path.resolve(appRoot, "dist/front/index.html"), telemetry)
545
+ } : void 0);
395
546
  const defaultPluginPackagePaths = resolveDefaultWorkspacePluginPackagePaths({
396
547
  workspaceRoot,
397
548
  appPackageJsonPath: options.appPackageJsonPath,
398
549
  defaultPluginPackages: options.defaultPluginPackages
399
550
  });
400
551
  const defaultPackagePiSnapshot = readWorkspacePluginPackagePiSnapshot(defaultPluginPackagePaths);
552
+ const defaultPackageRuntimePlugins = readWorkspacePluginPackageRuntimePlugins(defaultPluginPackagePaths);
401
553
  const { systemPromptAppend: defaultPackageSystemPromptAppend, ...defaultPackagePiOptions } = defaultPackagePiSnapshot;
402
554
  const staticSystemPromptAppend = [options.systemPromptAppend, defaultPackageSystemPromptAppend].filter(Boolean).join("\n\n") || void 0;
403
555
  const defaultPluginDirEntries = defaultPluginPackagePaths.map((dir) => ({ dir, hotReload: false })).filter((entry) => hasDirServerPlugin(entry));
@@ -432,28 +584,9 @@ async function createCoreWorkspaceAgentServer(options = {}) {
432
584
  plugins: resolvedPlugins,
433
585
  excludeDefaults: options.excludeDefaults
434
586
  });
435
- const provisionedWorkspaceRoots = /* @__PURE__ */ new Map();
436
- const ensureWorkspaceProvisioned = (root) => {
437
- if (pluginCollection.provisioningContributions.length === 0) return Promise.resolve();
438
- const resolvedRoot = path.resolve(root);
439
- const existing = provisionedWorkspaceRoots.get(resolvedRoot);
440
- if (existing) return existing;
441
- const pending = provisionWorkspaceAgentServer({
442
- workspaceRoot: resolvedRoot,
443
- provisioningContributions: pluginCollection.provisioningContributions,
444
- force: options.forceProvisioning
445
- }).catch((error) => {
446
- provisionedWorkspaceRoots.delete(resolvedRoot);
447
- throw error;
448
- });
449
- provisionedWorkspaceRoots.set(resolvedRoot, pending);
450
- return pending;
451
- };
452
- await ensureWorkspaceProvisioned(workspaceRoot);
453
587
  const resolveWorkspaceId = async (request) => options.getWorkspaceId ? await options.getWorkspaceId(request) : await resolveAuthorizedWorkspaceId(request, workspaceStore);
454
588
  const resolveRoot = async (workspaceId, request) => {
455
589
  const root = options.getWorkspaceRoot ? await options.getWorkspaceRoot(workspaceId, request) : await resolveWorkspaceRoot(workspaceRoot, workspaceId);
456
- await ensureWorkspaceProvisioned(root);
457
590
  return root;
458
591
  };
459
592
  const piOptionsByRoot = /* @__PURE__ */ new Map();
@@ -508,7 +641,26 @@ async function createCoreWorkspaceAgentServer(options = {}) {
508
641
  sandboxHandleStore: options.sandboxHandleStore ?? new WorkspaceRuntimeSandboxHandleStore(workspaceStore),
509
642
  getWorkspaceId: resolveWorkspaceId,
510
643
  getWorkspaceRoot: resolveRoot,
511
- registerHealthRoute: options.registerHealthRoute ?? false
644
+ provisionRuntime: async ({ provisioningAdapter, runtimeLayout, workspaceId, request, runtimeMode }) => {
645
+ if (!provisioningAdapter) return void 0;
646
+ return await provisionWorkspaceRuntime({
647
+ plugins: [
648
+ ...pluginCollection.runtimePlugins,
649
+ ...defaultPackageRuntimePlugins
650
+ ],
651
+ adapter: provisioningAdapter,
652
+ runtimeLayout,
653
+ telemetry,
654
+ telemetryContext: {
655
+ workspaceId,
656
+ requestId: request?.id,
657
+ runtimeMode
658
+ }
659
+ });
660
+ },
661
+ provisionWorkspace: options.provisionWorkspace,
662
+ registerHealthRoute: options.registerHealthRoute ?? false,
663
+ telemetry
512
664
  });
513
665
  await app.register(uiRoutes, {
514
666
  getBridge: async (request) => getUiBridge(await resolveWorkspaceId(request)),
@@ -518,7 +670,7 @@ async function createCoreWorkspaceAgentServer(options = {}) {
518
670
  await app.register(routes);
519
671
  }
520
672
  if (serveFrontend && appRoot) {
521
- await registerFrontendFallback(app, appRoot);
673
+ await registerFrontendFallback(app, appRoot, telemetry);
522
674
  }
523
675
  return app;
524
676
  }
@@ -1,7 +1,7 @@
1
- import { C as CoreConfig, R as RuntimeConfig } from './index-COZa03RP.js';
1
+ import { C as CoreConfig, R as RuntimeConfig } from './types-CbMOXLBf.js';
2
2
  import { FastifyPluginAsync } from 'fastify';
3
3
  import { Auth } from 'better-auth';
4
- import { W as WorkspaceStore, D as Database } from './connection-jW1Xwcne.js';
4
+ import { W as WorkspaceStore, D as Database } from './connection-AL8KSENV.js';
5
5
 
6
6
  interface LoadConfigOptions {
7
7
  tomlPath?: string;
@@ -8,7 +8,7 @@ import {
8
8
  workspaceRuntimes,
9
9
  workspaceSettings,
10
10
  workspaces
11
- } from "./chunk-HSRBZLKT.js";
11
+ } from "./chunk-C3YMOITB.js";
12
12
  import {
13
13
  ConfigValidationError,
14
14
  ERROR_CODES,
@@ -68,6 +68,10 @@ var coreConfigSchema = z.object({
68
68
  clientId: z.string().min(1),
69
69
  clientSecret: z.string().min(1)
70
70
  }).optional(),
71
+ google: z.object({
72
+ clientId: z.string().min(1),
73
+ clientSecret: z.string().min(1)
74
+ }).optional(),
71
75
  mail: z.object({
72
76
  from: z.string().min(1),
73
77
  transportUrl: mailTransportUrlSchema
@@ -77,6 +81,7 @@ var coreConfigSchema = z.object({
77
81
  }),
78
82
  features: z.object({
79
83
  githubOauth: z.boolean(),
84
+ googleOauth: z.boolean(),
80
85
  invitesEnabled: z.boolean(),
81
86
  sendWelcomeEmail: z.boolean(),
82
87
  inviteTtlDays: z.number().int().min(1).max(30).default(7)
@@ -162,6 +167,11 @@ async function loadConfig(options) {
162
167
  clientId: env.GITHUB_CLIENT_ID,
163
168
  clientSecret: env.GITHUB_CLIENT_SECRET
164
169
  } : void 0;
170
+ const googleOauth = toml.features?.google_oauth === true;
171
+ const google = env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET ? {
172
+ clientId: env.GOOGLE_CLIENT_ID,
173
+ clientSecret: env.GOOGLE_CLIENT_SECRET
174
+ } : void 0;
165
175
  const mailFrom = env.MAIL_FROM;
166
176
  const mailTransportUrl = env.MAIL_TRANSPORT_URL;
167
177
  const mail = mailFrom && mailTransportUrl ? { from: mailFrom, transportUrl: mailTransportUrl } : void 0;
@@ -194,6 +204,7 @@ async function loadConfig(options) {
194
204
  secret: authSecret,
195
205
  url: authUrl,
196
206
  github,
207
+ google,
197
208
  mail,
198
209
  sessionTtlSeconds: parseInt(
199
210
  env.SESSION_TTL_SECONDS ?? String(THIRTY_DAYS_SECONDS),
@@ -203,6 +214,7 @@ async function loadConfig(options) {
203
214
  },
204
215
  features: {
205
216
  githubOauth: githubOauth && github !== void 0,
217
+ googleOauth: googleOauth && google !== void 0,
206
218
  invitesEnabled: toml.features?.invites_enabled ?? true,
207
219
  sendWelcomeEmail: env.SEND_WELCOME_EMAIL !== "false",
208
220
  ...toml.features?.invite_ttl_days != null && { inviteTtlDays: toml.features.invite_ttl_days }
@@ -222,6 +234,9 @@ function validateConfig(raw) {
222
234
  }
223
235
  return result.data;
224
236
  }
237
+ function isGoogleOauthUsable(config) {
238
+ return config.features.googleOauth && config.auth.google !== void 0;
239
+ }
225
240
  function buildRuntimeConfigPayload(config) {
226
241
  return {
227
242
  appId: config.appId,
@@ -230,6 +245,7 @@ function buildRuntimeConfigPayload(config) {
230
245
  apiBase: config.auth.url,
231
246
  features: {
232
247
  githubOauth: config.features.githubOauth,
248
+ googleOauth: isGoogleOauthUsable(config),
233
249
  invitesEnabled: config.features.invitesEnabled,
234
250
  sendWelcomeEmail: config.features.sendWelcomeEmail
235
251
  }
@@ -362,16 +378,19 @@ function registerCapabilities(app) {
362
378
  );
363
379
  const hasMail = !!app.config.auth.mail;
364
380
  app.registerCapabilitiesContributor("core", () => {
381
+ const googleOauth = isGoogleOauthUsable(app.config);
365
382
  const core = {
366
383
  version: getCoreVersion(),
367
384
  features: {
368
385
  invitesEnabled: app.config.features.invitesEnabled,
369
386
  githubOauth: app.config.features.githubOauth,
387
+ googleOauth,
370
388
  emailFlows: hasMail
371
389
  },
372
390
  auth: {
373
391
  emailPassword: true,
374
392
  github: false,
393
+ google: googleOauth,
375
394
  emailVerification: hasMail,
376
395
  passwordReset: hasMail,
377
396
  magicLink: hasMail
@@ -1717,6 +1736,20 @@ function createAuth(config, db, opts) {
1717
1736
  transport,
1718
1737
  logger: opts.logger
1719
1738
  }) : void 0;
1739
+ const socialProviders = {
1740
+ ...config.auth.github ? {
1741
+ github: {
1742
+ clientId: config.auth.github.clientId,
1743
+ clientSecret: config.auth.github.clientSecret
1744
+ }
1745
+ } : {},
1746
+ ...config.features.googleOauth && config.auth.google ? {
1747
+ google: {
1748
+ clientId: config.auth.google.clientId,
1749
+ clientSecret: config.auth.google.clientSecret
1750
+ }
1751
+ } : {}
1752
+ };
1720
1753
  return betterAuth({
1721
1754
  database: drizzleAdapter(db, { provider: "pg", schema: schema_exports }),
1722
1755
  secret: config.auth.secret,
@@ -1797,7 +1830,7 @@ function createAuth(config, db, opts) {
1797
1830
  }
1798
1831
  },
1799
1832
  emailVerification: emailVerificationConfig,
1800
- socialProviders: config.auth.github ? { github: { clientId: config.auth.github.clientId, clientSecret: config.auth.github.clientSecret } } : {},
1833
+ socialProviders,
1801
1834
  plugins
1802
1835
  });
1803
1836
  }
@@ -0,0 +1,17 @@
1
+ // src/shared/telemetry.ts
2
+ var noopTelemetry = {
3
+ capture() {
4
+ }
5
+ };
6
+ function safeCapture(telemetry, event) {
7
+ try {
8
+ void Promise.resolve(telemetry.capture(event)).catch(() => {
9
+ });
10
+ } catch {
11
+ }
12
+ }
13
+
14
+ export {
15
+ noopTelemetry,
16
+ safeCapture
17
+ };
@@ -542,6 +542,7 @@ __export(schema_exports, {
542
542
  idempotencyKeys: () => idempotencyKeys,
543
543
  sessions: () => sessions,
544
544
  sessionsRelations: () => sessionsRelations,
545
+ telemetryEvents: () => telemetryEvents,
545
546
  userSettings: () => userSettings,
546
547
  userSettingsRelations: () => userSettingsRelations,
547
548
  users: () => users,
@@ -866,6 +867,21 @@ var idempotencyKeys = pgTable2(
866
867
  index2("idempotency_keys_created_at_idx").on(table.createdAt)
867
868
  ]
868
869
  );
870
+ var telemetryEvents = pgTable2(
871
+ "telemetry_events",
872
+ {
873
+ id: uuid2("id").default(sql3`gen_random_uuid()`).primaryKey(),
874
+ appId: text2("app_id").notNull(),
875
+ eventName: text2("event_name").notNull(),
876
+ distinctId: text2("distinct_id").notNull().default("anonymous"),
877
+ properties: jsonb("properties").notNull().default({}),
878
+ createdAt: timestamp2("created_at").defaultNow().notNull()
879
+ },
880
+ (table) => [
881
+ index2("telemetry_events_app_created_at_idx").on(table.appId, table.createdAt),
882
+ index2("telemetry_events_event_name_idx").on(table.eventName)
883
+ ]
884
+ );
869
885
 
870
886
  // src/server/db/stores/PostgresWorkspaceStore.ts
871
887
  var UI_STATE_KEY_PREFIX = "workspace_ui_state:";
@@ -1674,6 +1690,7 @@ export {
1674
1690
  workspaceRuntimes,
1675
1691
  workspaceInvites,
1676
1692
  idempotencyKeys,
1693
+ telemetryEvents,
1677
1694
  schema_exports,
1678
1695
  createDatabase,
1679
1696
  runMigrations,