@agent-team-foundation/first-tree-hub 0.11.2 → 0.11.4

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.
Files changed (41) hide show
  1. package/dist/{bootstrap-B-FRMuvL.mjs → bootstrap-D-Yf8yOc.mjs} +2 -16
  2. package/dist/cli/index.mjs +44 -41
  3. package/dist/cli-fetch--tiwKm5S.mjs +167 -0
  4. package/dist/client-By1K4VVT-DuI6EnSh.mjs +4 -0
  5. package/dist/{client-CLdRbuml-B416INrm.mjs → client-CLdRbuml-svTO0Eat.mjs} +2 -2
  6. package/dist/{dist-BLY7Bu-l.mjs → dist-BQtAQNRD.mjs} +1 -1
  7. package/dist/{dist-FuUBFTEB.mjs → dist-ClFs4WMj.mjs} +55 -1
  8. package/dist/drizzle/0032_organization_settings.sql +36 -0
  9. package/dist/drizzle/meta/_journal.json +8 -1
  10. package/dist/{feishu-GvFABWW5.mjs → feishu-AI3pwmqN.mjs} +4 -3
  11. package/dist/{getMachineId-bsd-DjLgZlll.mjs → getMachineId-bsd-DyySs8xz.mjs} +2 -2
  12. package/dist/{getMachineId-bsd-DR4-Dysy.mjs → getMachineId-bsd-c2VImogj.mjs} +2 -2
  13. package/dist/{getMachineId-darwin-CaD2juTg.mjs → getMachineId-darwin-Cl7TSzgO.mjs} +2 -2
  14. package/dist/{getMachineId-darwin-B6WCAhc4.mjs → getMachineId-darwin-DKgI8b1d.mjs} +2 -2
  15. package/dist/{getMachineId-linux-Dk3gWdQK.mjs → getMachineId-linux-1OIMWfdh.mjs} +1 -1
  16. package/dist/{getMachineId-linux-BeWHG1gK.mjs → getMachineId-linux-cT7EbP10.mjs} +1 -1
  17. package/dist/{getMachineId-unsupported-BMJQItvF.mjs → getMachineId-unsupported-CkX-YOG1.mjs} +1 -1
  18. package/dist/{getMachineId-unsupported-Bgz_Je1J.mjs → getMachineId-unsupported-CmVlhzIo.mjs} +1 -1
  19. package/dist/{getMachineId-win-vJ6VfDRI.mjs → getMachineId-win-C2cM60YT.mjs} +2 -2
  20. package/dist/{getMachineId-win-CdgcrzCW.mjs → getMachineId-win-Chl03TYe.mjs} +2 -2
  21. package/dist/index.mjs +10 -9
  22. package/dist/invitation-DWlyNb8x-BvXubk24.mjs +4 -0
  23. package/dist/{invitation-Dnn5gGGX-Ce7zbZpn.mjs → invitation-Dnn5gGGX-DXryyvRG.mjs} +1 -1
  24. package/dist/{multipart-parser-BIksYTkk.mjs → multipart-parser-QRu3OKK4.mjs} +1 -1
  25. package/dist/{observability-C3nY6Jcz-Bk7FX689.mjs → observability-BAScT_5S-gw1ODB_o.mjs} +140 -17
  26. package/dist/observability-CYsdAcoF.mjs +5 -0
  27. package/dist/{saas-connect-Df2CVAGp.mjs → saas-connect-CVoRK0Ex.mjs} +462 -214
  28. package/dist/{src-CzQ5KF6D.mjs → src-DFlbpJfU.mjs} +2 -2
  29. package/dist/web/assets/{index-CD7rTdqm.js → index-Bm6hgcvt.js} +1 -1
  30. package/dist/web/assets/{index-43trJLR8.js → index-k2bWRKc-.js} +87 -87
  31. package/dist/web/index.html +1 -1
  32. package/package.json +1 -1
  33. package/dist/client-By1K4VVT-nVOhsXBy.mjs +0 -4
  34. package/dist/invitation-DWlyNb8x-BEgoZ9k1.mjs +0 -4
  35. package/dist/observability-DttujCqj.mjs +0 -5
  36. /package/dist/{errors-BmyRwN0Y-CIZZ_sDc.mjs → errors-BmyRwN0Y-Dad3eV8F.mjs} +0 -0
  37. /package/dist/{esm-iadMkGbV.mjs → esm-Ci8E1Gtj.mjs} +0 -0
  38. /package/dist/{execAsync-pImxPKN5.mjs → execAsync-DUfRkc4a.mjs} +0 -0
  39. /package/dist/{execAsync-CCyouKZM.mjs → execAsync-YbEZSOYd.mjs} +0 -0
  40. /package/dist/{from-CaD373S1.mjs → from-DQ7eNRwu.mjs} +0 -0
  41. /package/dist/{src-DNBS5Yjj.mjs → src-aJMV60mR.mjs} +0 -0
@@ -1,11 +1,12 @@
1
1
  import { a as __toCommonJS, o as __toESM, t as __commonJSMin } from "./chunk-BSw8zbkd.mjs";
2
- import { C as withSpan, D as require_pino, E as redactUrl, S as startWsConnectionSpan, T as FIRST_TREE_HUB_ATTR, a as createLogger$1, b as stampOrgScope, d as observabilityPlugin, g as setWsConnectionAttrs, i as bodyCaptureOnSendHook, l as messageAttrs, m as rootLogger$1, n as applyLoggerConfig, o as currentTraceId, p as reportErrorToRoot, r as attachRequestContext, s as endWsConnectionSpan, t as adapterAttrs, v as stampAgentResource, w as withWsMessageSpan, y as stampChatResource } from "./observability-C3nY6Jcz-Bk7FX689.mjs";
3
- import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, b as migrateLegacyHome, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR$1, g as collectMissingPrompts, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR$1, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, y as loadAgents } from "./bootstrap-B-FRMuvL.mjs";
4
- import { $ as notificationQuerySchema, A as createChatSchema, B as githubDevCallbackQuerySchema, Ct as updateTaskStatusSchema, D as createAdapterConfigSchema, E as contextTreeSnapshotSchema, F as defaultRuntimeConfigPayload, G as inboxPollQuerySchema, H as imageInlineContentSchema, I as delegateFeishuUserSchema, J as joinByInvitationSchema, K as isRedactedEnvValue, L as dryRunAgentRuntimeConfigSchema, M as createMemberSchema, N as createOrgFromMeSchema, O as createAdapterMappingSchema, P as createTaskSchema, Q as messageSourceSchema$1, R as extractMentions, S as agentTypeSchema$1, St as updateOrganizationSchema, T as connectTokenExchangeSchema, U as inboxAckFrameSchema, V as githubStartQuerySchema, W as inboxDeliverFrameSchema$1, X as listMeChatsQuerySchema, Y as linkTaskChatSchema, Z as loginSchema, _ as adminCreateTaskSchema, _t as updateAgentRuntimeConfigSchema, a as AGENT_STATUSES, at as scanMentionTokens, b as agentPinnedMessageSchema$1, bt as updateClientCapabilitiesSchema, ct as sendToAgentSchema, d as TASK_HEALTH_SIGNALS, dt as sessionEventSchema$1, et as paginationQuerySchema, f as TASK_STATUSES, ft as sessionReconcileRequestSchema, g as addParticipantSchema, gt as updateAdapterConfigSchema, h as addMeChatParticipantsSchema, ht as taskListQuerySchema, i as AGENT_SOURCES, it as safeRedirectPath, j as createMeChatSchema, k as createAgentSchema, l as MENTION_REGEX, lt as sessionCompletionMessageSchema, m as WS_AUTH_FRAME_TIMEOUT_MS, mt as stripCode, n as AGENT_NAME_REGEX$1, nt as refreshTokenSchema, o as AGENT_TYPES, ot as selfServiceFeishuBotSchema, p as TASK_TERMINAL_STATUSES, pt as sessionStateMessageSchema, q as isReservedAgentName$1, r as AGENT_SELECTOR_HEADER$1, rt as runtimeStateMessageSchema, s as AGENT_VISIBILITY, st as sendMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as rebindAgentSchema, u as TASK_CREATOR_TYPES, ut as sessionEventMessageSchema, v as adminUpdateTaskSchema, vt as updateAgentSchema, w as clientRegisterSchema, wt as wsAuthFrameSchema, x as agentRuntimeConfigPayloadSchema$1, xt as updateMemberSchema, y as agentBindRequestSchema, yt as updateChatSchema, z as githubCallbackQuerySchema } from "./dist-FuUBFTEB.mjs";
5
- import { a as ConflictError, c as UnauthorizedError, i as ClientUserMismatchError$1, l as organizations, n as BadRequestError, o as ForbiddenError, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as users } from "./errors-BmyRwN0Y-CIZZ_sDc.mjs";
6
- import { C as retireClient, D as touchAgent, E as setRuntimeState, O as unbindAgent, S as registerClient, T as setOffline, _ as listClients, a as claimClient, b as markStaleAgents, c as clients, d as getClient, f as getOnlineCount, g as listActiveAgentsPinnedToClient, h as heartbeatInstance, i as bindAgent, k as updateClientCapabilities, l as deriveAuthState, m as heartbeatClient, n as agents, o as cleanupStaleClients, p as getPresence, r as assertClientOwner, s as cleanupStalePresence, t as agentPresence, u as disconnectClient, v as listClientsForOrgAdmin, w as serverInstances, x as members } from "./client-CLdRbuml-B416INrm.mjs";
7
- import { n as init_esm, r as trace, t as esm_exports } from "./esm-iadMkGbV.mjs";
8
- import { a as invitationRedemptions, c as recordRedemption, i as getActiveInvitation, l as rotateInvitation, n as ensureActiveInvitation, o as invitations, r as findActiveByToken, t as buildInviteUrl, u as uuidv7 } from "./invitation-Dnn5gGGX-Ce7zbZpn.mjs";
2
+ import { A as FIRST_TREE_HUB_ATTR, C as stampOrgScope, D as untrustedAttrs, E as startWsConnectionSpan, M as require_pino, O as withSpan, S as stampChatResource, _ as rootLogger$1, a as buildRateLimitError, c as currentTraceId, f as messageAttrs, g as reportErrorToRoot, i as bodyCaptureOnSendHook, j as redactUrl, k as withWsMessageSpan, l as decodeJwtForTrace, m as observabilityPlugin, n as applyLoggerConfig, o as classifyJoseError, r as attachRequestContext, s as createLogger$1, t as adapterAttrs, u as endWsConnectionSpan, x as stampAgentResource, y as setWsConnectionAttrs } from "./observability-BAScT_5S-gw1ODB_o.mjs";
3
+ import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, b as migrateLegacyHome, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR$1, g as collectMissingPrompts, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR$1, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, y as loadAgents } from "./bootstrap-D-Yf8yOc.mjs";
4
+ import { a as print, i as blank, n as CLI_USER_AGENT, r as COMMAND_VERSION, s as status, t as cliFetch } from "./cli-fetch--tiwKm5S.mjs";
5
+ import { $ as loginSchema, A as createAgentSchema, B as githubCallbackQuerySchema, C as agentTypeSchema$1, Ct as updateMemberSchema, D as contextTreeSnapshotSchema, E as connectTokenExchangeSchema, Et as wsAuthFrameSchema, F as createTaskSchema, G as inboxDeliverFrameSchema$1, H as githubStartQuerySchema, I as defaultRuntimeConfigPayload, J as isRedactedEnvValue, K as inboxPollQuerySchema, L as delegateFeishuUserSchema, M as createMeChatSchema, N as createMemberSchema, O as createAdapterConfigSchema, P as createOrgFromMeSchema, Q as listMeChatsQuerySchema, R as dryRunAgentRuntimeConfigSchema, S as agentRuntimeConfigPayloadSchema$1, St as updateClientCapabilitiesSchema, T as clientRegisterSchema, Tt as updateTaskStatusSchema, U as imageInlineContentSchema, V as githubDevCallbackQuerySchema, W as inboxAckFrameSchema, X as joinByInvitationSchema, Y as isReservedAgentName$1, Z as linkTaskChatSchema, _ as addParticipantSchema, _t as taskListQuerySchema, a as AGENT_STATUSES, at as runtimeStateMessageSchema, b as agentBindRequestSchema, bt as updateAgentSchema, ct as selfServiceFeishuBotSchema, d as TASK_CREATOR_TYPES, dt as sessionCompletionMessageSchema, et as messageSourceSchema$1, f as TASK_HEALTH_SIGNALS, ft as sessionEventMessageSchema, g as addMeChatParticipantsSchema, gt as stripCode, h as WS_AUTH_FRAME_TIMEOUT_MS, ht as sessionStateMessageSchema, i as AGENT_SOURCES, it as refreshTokenSchema, j as createChatSchema, k as createAdapterMappingSchema, l as MENTION_REGEX, lt as sendMessageSchema, m as TASK_TERMINAL_STATUSES, mt as sessionReconcileRequestSchema, n as AGENT_NAME_REGEX$1, nt as paginationQuerySchema, o as AGENT_TYPES, ot as safeRedirectPath, p as TASK_STATUSES, pt as sessionEventSchema$1, q as isOrgSettingNamespace, r as AGENT_SELECTOR_HEADER$1, rt as rebindAgentSchema, s as AGENT_VISIBILITY, st as scanMentionTokens, t as AGENT_BIND_REJECT_REASONS, tt as notificationQuerySchema, u as ORG_SETTINGS_NAMESPACES$1, ut as sendToAgentSchema, v as adminCreateTaskSchema, vt as updateAdapterConfigSchema, wt as updateOrganizationSchema, x as agentPinnedMessageSchema$1, xt as updateChatSchema, y as adminUpdateTaskSchema, yt as updateAgentRuntimeConfigSchema, z as extractMentions } from "./dist-ClFs4WMj.mjs";
6
+ import { a as ConflictError, c as UnauthorizedError, i as ClientUserMismatchError$1, l as organizations, n as BadRequestError, o as ForbiddenError, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as users } from "./errors-BmyRwN0Y-Dad3eV8F.mjs";
7
+ import { C as retireClient, D as touchAgent, E as setRuntimeState, O as unbindAgent, S as registerClient, T as setOffline, _ as listClients, a as claimClient, b as markStaleAgents, c as clients, d as getClient, f as getOnlineCount, g as listActiveAgentsPinnedToClient, h as heartbeatInstance, i as bindAgent, k as updateClientCapabilities, l as deriveAuthState, m as heartbeatClient, n as agents, o as cleanupStaleClients, p as getPresence, r as assertClientOwner, s as cleanupStalePresence, t as agentPresence, u as disconnectClient, v as listClientsForOrgAdmin, w as serverInstances, x as members } from "./client-CLdRbuml-svTO0Eat.mjs";
8
+ import { n as init_esm, r as trace, t as esm_exports } from "./esm-Ci8E1Gtj.mjs";
9
+ import { a as invitationRedemptions, c as recordRedemption, i as getActiveInvitation, l as rotateInvitation, n as ensureActiveInvitation, o as invitations, r as findActiveByToken, t as buildInviteUrl, u as uuidv7 } from "./invitation-Dnn5gGGX-DXryyvRG.mjs";
9
10
  import { createRequire } from "node:module";
10
11
  import { ZodError, z } from "zod";
11
12
  import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
@@ -29,12 +30,12 @@ import { drizzle } from "drizzle-orm/postgres-js";
29
30
  import postgres from "postgres";
30
31
  import { migrate } from "drizzle-orm/postgres-js/migrator";
31
32
  import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique, uniqueIndex } from "drizzle-orm/pg-core";
33
+ import { SignJWT, jwtVerify } from "jose";
32
34
  import cors from "@fastify/cors";
33
35
  import rateLimit from "@fastify/rate-limit";
34
36
  import fastifyStatic from "@fastify/static";
35
37
  import websocket from "@fastify/websocket";
36
38
  import Fastify from "fastify";
37
- import { SignJWT, jwtVerify } from "jose";
38
39
  import { promisify } from "node:util";
39
40
  import matter from "gray-matter";
40
41
  import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
@@ -1291,6 +1292,57 @@ z.object({
1291
1292
  displayName: z.string().optional(),
1292
1293
  next: z.string().max(256).optional()
1293
1294
  });
1295
+ /**
1296
+ * Per-organization settings — schemas, namespaces, and the registry that
1297
+ * dispatches `(orgId, namespace)` lookups to the right validator.
1298
+ *
1299
+ * Each namespace has three schemas:
1300
+ * - `storage` — what is persisted in `organization_settings.value`. For
1301
+ * namespaces with secrets, the storage schema names the *cipher* field
1302
+ * (e.g. `webhookSecretCipher`); plaintext never touches the row.
1303
+ * - `input` — what the admin API accepts in PUT bodies. For namespaces
1304
+ * with secrets, `webhookSecret` is plaintext; the service layer
1305
+ * encrypts it before merging into storage.
1306
+ * - `output` — what GET returns. Secrets are replaced by a boolean
1307
+ * `…Configured` flag — plaintext is never echoed.
1308
+ *
1309
+ * Adding a new per-org config group:
1310
+ * 1. Define three schemas (storage / input / output).
1311
+ * 2. Add a key to `ORG_SETTINGS_NAMESPACES`.
1312
+ * 3. Done. No DB migration, no new API route.
1313
+ */
1314
+ const orgContextTreeStorageSchema = z.object({
1315
+ repo: z.string().url().optional(),
1316
+ branch: z.string().default("main")
1317
+ });
1318
+ const orgContextTreeInputSchema = z.object({
1319
+ repo: z.string().url().min(1).nullish(),
1320
+ branch: z.string().min(1).nullish()
1321
+ });
1322
+ const orgContextTreeOutputSchema = z.object({
1323
+ repo: z.string().optional(),
1324
+ branch: z.string().optional()
1325
+ });
1326
+ const orgGithubIntegrationStorageSchema = z.object({ webhookSecretCipher: z.string().optional() });
1327
+ const orgGithubIntegrationInputSchema = z.object({ webhookSecret: z.string().min(1).nullish() });
1328
+ const orgGithubIntegrationOutputSchema = z.object({
1329
+ webhookSecretConfigured: z.boolean(),
1330
+ webhookUrl: z.string()
1331
+ });
1332
+ const ORG_SETTINGS_NAMESPACES = {
1333
+ context_tree: {
1334
+ storage: orgContextTreeStorageSchema,
1335
+ input: orgContextTreeInputSchema,
1336
+ output: orgContextTreeOutputSchema
1337
+ },
1338
+ github_integration: {
1339
+ storage: orgGithubIntegrationStorageSchema,
1340
+ input: orgGithubIntegrationInputSchema,
1341
+ output: orgGithubIntegrationOutputSchema
1342
+ }
1343
+ };
1344
+ const ORG_SETTINGS_NAMESPACE_KEYS = Object.keys(ORG_SETTINGS_NAMESPACES);
1345
+ z.enum(ORG_SETTINGS_NAMESPACE_KEYS);
1294
1346
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1295
1347
  z.object({
1296
1348
  name: z.string().min(2).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Must start with a letter or digit and contain only lowercase alphanumeric and hyphens").refine((v) => !UUID_PATTERN.test(v), "Name must not be a UUID format"),
@@ -1683,21 +1735,6 @@ defineConfig({
1683
1735
  refreshTokenExpiry: field(z.string().default("30d"), { env: "FIRST_TREE_HUB_AUTH_REFRESH_TOKEN_EXPIRY" }),
1684
1736
  connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
1685
1737
  },
1686
- contextTree: optional({
1687
- repo: field(z.string().optional(), {
1688
- env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
1689
- prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
1690
- }),
1691
- localPath: field(z.string().optional(), { env: "FIRST_TREE_HUB_CONTEXT_TREE_PATH" }),
1692
- branch: field(z.string().default("main"))
1693
- }),
1694
- github: {
1695
- webhookSecret: field(z.string().optional(), {
1696
- env: "FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET",
1697
- secret: true
1698
- }),
1699
- allowedOrg: field(z.string().optional(), { env: "FIRST_TREE_HUB_GITHUB_ALLOWED_ORG" })
1700
- },
1701
1738
  oauth: optional({ github: optional({
1702
1739
  clientId: field(z.string(), { env: "FIRST_TREE_HUB_GITHUB_OAUTH_CLIENT_ID" }),
1703
1740
  clientSecret: field(z.string(), {
@@ -1808,10 +1845,12 @@ var FirstTreeHubSDK = class {
1808
1845
  _baseUrl;
1809
1846
  getAccessToken;
1810
1847
  _agentId;
1848
+ _userAgent;
1811
1849
  constructor(config) {
1812
1850
  this._baseUrl = config.serverUrl.replace(/\/+$/, "");
1813
1851
  this.getAccessToken = config.getAccessToken;
1814
1852
  this._agentId = config.agentId;
1853
+ this._userAgent = config.userAgent;
1815
1854
  }
1816
1855
  /** Server base URL (without trailing slash). */
1817
1856
  get serverUrl() {
@@ -1863,7 +1902,12 @@ var FirstTreeHubSDK = class {
1863
1902
  async isHubReachable(timeoutMs = 3e3) {
1864
1903
  try {
1865
1904
  const url = `${this._baseUrl}/api/v1/health`;
1866
- return (await fetch(url, { signal: AbortSignal.timeout(timeoutMs) })).ok;
1905
+ const headers = {};
1906
+ if (this._userAgent) headers["User-Agent"] = this._userAgent;
1907
+ return (await fetch(url, {
1908
+ signal: AbortSignal.timeout(timeoutMs),
1909
+ headers
1910
+ })).ok;
1867
1911
  } catch {
1868
1912
  return false;
1869
1913
  }
@@ -1922,6 +1966,7 @@ var FirstTreeHubSDK = class {
1922
1966
  const url = `${this._baseUrl}${path}`;
1923
1967
  const headers = { Authorization: `Bearer ${await this.getAccessToken()}` };
1924
1968
  if (this._agentId) headers[AGENT_SELECTOR_HEADER] = this._agentId;
1969
+ if (this._userAgent) headers["User-Agent"] = this._userAgent;
1925
1970
  if (init?.body) headers["Content-Type"] = "application/json";
1926
1971
  const timeout = AbortSignal.timeout(FETCH_TIMEOUT_MS);
1927
1972
  const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
@@ -2031,6 +2076,7 @@ var ClientConnection = class extends EventEmitter {
2031
2076
  clientId;
2032
2077
  serverUrl;
2033
2078
  sdkVersion;
2079
+ userAgent;
2034
2080
  getAccessToken;
2035
2081
  ws = null;
2036
2082
  wsConnectTimer = null;
@@ -2084,6 +2130,7 @@ var ClientConnection = class extends EventEmitter {
2084
2130
  this.clientId = config.clientId ?? process.env.FIRST_TREE_HUB_CLIENT_ID ?? `client_${randomUUID().slice(0, 8)}`;
2085
2131
  this.serverUrl = config.serverUrl.replace(/\/+$/, "");
2086
2132
  this.sdkVersion = config.sdkVersion;
2133
+ this.userAgent = config.userAgent;
2087
2134
  this.getAccessToken = config.getAccessToken;
2088
2135
  this.wsLogger = createLogger("ws").child({ clientId: this.clientId });
2089
2136
  this.authLogger = createLogger("auth").child({ clientId: this.clientId });
@@ -2225,7 +2272,7 @@ var ClientConnection = class extends EventEmitter {
2225
2272
  return new Promise((resolve, reject) => {
2226
2273
  const wsUrl = `${this.serverUrl.replace(/^http/, "ws")}/api/v1/agent/ws/client`;
2227
2274
  this.wsLogger.info({ url: wsUrl }, "connecting");
2228
- const ws = new WebSocket(wsUrl);
2275
+ const ws = new WebSocket(wsUrl, this.userAgent ? { headers: { "User-Agent": this.userAgent } } : void 0);
2229
2276
  let settled = false;
2230
2277
  const settle = (fn, value) => {
2231
2278
  if (settled) return;
@@ -2376,7 +2423,8 @@ var ClientConnection = class extends EventEmitter {
2376
2423
  const sdk = new FirstTreeHubSDK({
2377
2424
  serverUrl: this.serverUrl,
2378
2425
  getAccessToken: this.getAccessToken,
2379
- agentId
2426
+ agentId,
2427
+ userAgent: this.userAgent
2380
2428
  });
2381
2429
  const agent = {
2382
2430
  agentId,
@@ -6062,71 +6110,6 @@ function sleep(ms) {
6062
6110
  return new Promise((resolve) => setTimeout(resolve, ms));
6063
6111
  }
6064
6112
  //#endregion
6065
- //#region src/core/output.ts
6066
- /**
6067
- * Print layer — the only place CLI code should write to stdout/stderr.
6068
- *
6069
- * Contract:
6070
- * - `print.result(data)` / `print.fail(...)` emit machine-readable JSON on
6071
- * stdout / stderr respectively. Scripts pipe into `jq` and expect a clean
6072
- * envelope, so nothing else may touch stdout.
6073
- * - `print.status` / `print.check` / `print.blank` / `print.line` are
6074
- * human-friendly and go to stderr so they never pollute a redirected stdout.
6075
- * In `--json` mode they are silenced — scripted consumers only care about
6076
- * the envelope.
6077
- */
6078
- let jsonMode = false;
6079
- function setJsonMode(enabled) {
6080
- jsonMode = enabled;
6081
- }
6082
- function result(data) {
6083
- process.stdout.write(`${JSON.stringify({
6084
- ok: true,
6085
- data
6086
- })}\n`);
6087
- }
6088
- function fail$1(code, message, exitCode = 1) {
6089
- process.stderr.write(`${JSON.stringify({
6090
- ok: false,
6091
- error: {
6092
- code,
6093
- message
6094
- }
6095
- })}\n`);
6096
- process.exit(exitCode);
6097
- }
6098
- function status(label, message) {
6099
- if (jsonMode) return;
6100
- process.stderr.write(` ${label.padEnd(20)} ${message}\n`);
6101
- }
6102
- function check(pass, label, detail = "") {
6103
- if (jsonMode) return;
6104
- const icon = pass ? "✓" : "✗";
6105
- const tail = detail ? ` ${detail}` : "";
6106
- process.stderr.write(` ${icon} ${label.padEnd(22)}${tail}\n`);
6107
- }
6108
- function blank() {
6109
- if (jsonMode) return;
6110
- process.stderr.write("\n");
6111
- }
6112
- /**
6113
- * Generic stderr writer for pre-formatted human text (multi-line tables,
6114
- * interactive prompts). Prefer `status` / `check` when the text fits; this
6115
- * exists so the `--json` mode gate can silence arbitrary human chatter.
6116
- */
6117
- function line(text) {
6118
- if (jsonMode) return;
6119
- process.stderr.write(text);
6120
- }
6121
- const print = {
6122
- result,
6123
- fail: fail$1,
6124
- status,
6125
- check,
6126
- blank,
6127
- line
6128
- };
6129
- //#endregion
6130
6113
  //#region src/cli/output.ts
6131
6114
  /**
6132
6115
  * CLI output re-exports. The underlying implementation lives in
@@ -6491,6 +6474,7 @@ var ClientRuntime = class {
6491
6474
  serverUrl,
6492
6475
  clientId,
6493
6476
  sdkVersion: options.currentVersion,
6477
+ userAgent: CLI_USER_AGENT,
6494
6478
  getAccessToken: (opts) => ensureFreshAccessToken(opts)
6495
6479
  });
6496
6480
  registerBuiltinHandlers();
@@ -7645,7 +7629,7 @@ async function checkServerHealth() {
7645
7629
  const port = get(config, "server.port") ?? 8e3;
7646
7630
  const url = `http://${host}:${port}/healthz`;
7647
7631
  try {
7648
- const res = await fetch(url, { signal: AbortSignal.timeout(3e3) });
7632
+ const res = await cliFetch(url, { signal: AbortSignal.timeout(3e3) });
7649
7633
  if (res.ok) return {
7650
7634
  label: "Server Health",
7651
7635
  ok: true,
@@ -7696,7 +7680,7 @@ async function checkServerReachable() {
7696
7680
  detail: "not configured (FIRST_TREE_HUB_SERVER_URL or config file)"
7697
7681
  };
7698
7682
  try {
7699
- const res = await fetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(5e3) });
7683
+ const res = await cliFetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(5e3) });
7700
7684
  if (res.ok) return {
7701
7685
  label: "Server URL",
7702
7686
  ok: true,
@@ -7841,7 +7825,7 @@ async function checkWebSocket() {
7841
7825
  };
7842
7826
  const wsUrl = serverUrl.replace(/^http/, "ws");
7843
7827
  try {
7844
- if ((await fetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(3e3) })).ok) return {
7828
+ if ((await cliFetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(3e3) })).ok) return {
7845
7829
  label: "WebSocket",
7846
7830
  ok: true,
7847
7831
  detail: `${wsUrl} (server reachable)`
@@ -7872,6 +7856,12 @@ function printResults(results) {
7872
7856
  }
7873
7857
  //#endregion
7874
7858
  //#region src/core/migrate.ts
7859
+ function sslOptions$1(url) {
7860
+ try {
7861
+ if (new URL(url).hostname.endsWith(".rds.amazonaws.com")) return { ssl: { rejectUnauthorized: false } };
7862
+ } catch {}
7863
+ return {};
7864
+ }
7875
7865
  /**
7876
7866
  * Resolve the drizzle migrations directory.
7877
7867
  * 1. npm install: embedded at dist/drizzle/ (relative to the built CLI)
@@ -7905,14 +7895,21 @@ function validateJournalOrder(migrationsFolder) {
7905
7895
  async function runMigrations(databaseUrl) {
7906
7896
  const migrationsFolder = resolveMigrationsFolder();
7907
7897
  validateJournalOrder(migrationsFolder);
7908
- const client = postgres(databaseUrl, { max: 1 });
7898
+ const ssl = sslOptions$1(databaseUrl);
7899
+ const client = postgres(databaseUrl, {
7900
+ max: 1,
7901
+ ...ssl
7902
+ });
7909
7903
  const db = drizzle(client);
7910
7904
  try {
7911
7905
  await migrate(db, { migrationsFolder });
7912
7906
  } finally {
7913
7907
  await client.end();
7914
7908
  }
7915
- const countClient = postgres(databaseUrl, { max: 1 });
7909
+ const countClient = postgres(databaseUrl, {
7910
+ max: 1,
7911
+ ...ssl
7912
+ });
7916
7913
  try {
7917
7914
  return (await countClient`
7918
7915
  SELECT count(*)::int AS count
@@ -7931,7 +7928,7 @@ function createApiNameResolver(serverUrl, getAccessToken) {
7931
7928
  if (cache) return cache;
7932
7929
  const token = await getAccessToken();
7933
7930
  const map = /* @__PURE__ */ new Map();
7934
- const res = await fetch(`${serverUrl}/api/v1/me/managed-agents`, {
7931
+ const res = await cliFetch(`${serverUrl}/api/v1/me/managed-agents`, {
7935
7932
  method: "GET",
7936
7933
  headers: { Authorization: `Bearer ${token}` },
7937
7934
  signal: AbortSignal.timeout(1e4)
@@ -8163,7 +8160,7 @@ async function onboardCheck(args) {
8163
8160
  value: serverUrl
8164
8161
  });
8165
8162
  try {
8166
- const res = await fetch(`${serverUrl}/api/v1/health`);
8163
+ const res = await cliFetch(`${serverUrl}/api/v1/health`);
8167
8164
  items.push({
8168
8165
  key: "server_reachable",
8169
8166
  label: "Server reachable",
@@ -8235,7 +8232,7 @@ function formatCheckReport(items) {
8235
8232
  return lines.join("\n");
8236
8233
  }
8237
8234
  async function resolveDefaultOrgId$1(serverUrl, accessToken) {
8238
- const res = await fetch(`${serverUrl}/api/v1/me`, {
8235
+ const res = await cliFetch(`${serverUrl}/api/v1/me`, {
8239
8236
  headers: { Authorization: `Bearer ${accessToken}` },
8240
8237
  signal: AbortSignal.timeout(1e4)
8241
8238
  });
@@ -8247,7 +8244,7 @@ async function resolveDefaultOrgId$1(serverUrl, accessToken) {
8247
8244
  throw new Error("Multiple organizations — pass --org explicitly to onboard");
8248
8245
  }
8249
8246
  async function createAgentViaAdmin(serverUrl, accessToken, orgId, body) {
8250
- const res = await fetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/agents`, {
8247
+ const res = await cliFetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/agents`, {
8251
8248
  method: "POST",
8252
8249
  headers: {
8253
8250
  Authorization: `Bearer ${accessToken}`,
@@ -8306,7 +8303,7 @@ async function onboardCreate(args) {
8306
8303
  }
8307
8304
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
8308
8305
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
8309
- const { bindFeishuBot } = await import("./feishu-GvFABWW5.mjs").then((n) => n.r);
8306
+ const { bindFeishuBot } = await import("./feishu-AI3pwmqN.mjs").then((n) => n.r);
8310
8307
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
8311
8308
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
8312
8309
  else {
@@ -8406,7 +8403,7 @@ async function promptAddAgent(opts = {}) {
8406
8403
  validate: (v) => v.length > 0 ? true : "Agent UUID is required"
8407
8404
  });
8408
8405
  const token = await ensureFreshAccessToken();
8409
- const res = await fetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(agentId)}`, {
8406
+ const res = await cliFetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(agentId)}`, {
8410
8407
  headers: { Authorization: `Bearer ${token}` },
8411
8408
  signal: AbortSignal.timeout(1e4)
8412
8409
  });
@@ -8478,7 +8475,7 @@ function setNestedByDot(obj, dotPath, value) {
8478
8475
  * value (the in-band repair path catches any remaining drift on first bind).
8479
8476
  */
8480
8477
  async function reconcileLocalRuntimeProviders(opts) {
8481
- const res = await fetch(`${opts.serverUrl}/api/v1/me/pinned-agents`, { headers: { Authorization: `Bearer ${opts.accessToken}` } });
8478
+ const res = await cliFetch(`${opts.serverUrl}/api/v1/me/pinned-agents`, { headers: { Authorization: `Bearer ${opts.accessToken}` } });
8482
8479
  if (!res.ok) throw new Error(`hub returned ${res.status} on /clients/me/agents`);
8483
8480
  const items = await res.json();
8484
8481
  const byAgentId = new Map(items.map((it) => [it.agentId, it]));
@@ -8518,7 +8515,7 @@ async function reconcileLocalRuntimeProviders(opts) {
8518
8515
  * client startup since capabilities only matter for UI / admin checks.
8519
8516
  */
8520
8517
  async function uploadClientCapabilities(opts) {
8521
- const res = await fetch(`${opts.serverUrl}/api/v1/clients/${encodeURIComponent(opts.clientId)}/capabilities`, {
8518
+ const res = await cliFetch(`${opts.serverUrl}/api/v1/clients/${encodeURIComponent(opts.clientId)}/capabilities`, {
8522
8519
  method: "PATCH",
8523
8520
  headers: {
8524
8521
  Authorization: `Bearer ${opts.accessToken}`,
@@ -9519,7 +9516,7 @@ function createFeedbackHandler(config) {
9519
9516
  return { handle };
9520
9517
  }
9521
9518
  //#endregion
9522
- //#region ../server/dist/app-Cbgqlj0e.mjs
9519
+ //#region ../server/dist/app-B8Ncyl76.mjs
9523
9520
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
9524
9521
  init_esm();
9525
9522
  var __defProp = Object.defineProperty;
@@ -13653,8 +13650,12 @@ function clientWsRoutes(notifier, instanceId) {
13653
13650
  return async (app) => {
13654
13651
  const jwtSecretBytes = new TextEncoder().encode(app.config.secrets.jwtSecret);
13655
13652
  const inboxMaxInFlightPerAgent = app.config.inbox?.maxInFlightPerAgent ?? DEFAULT_INBOX_MAX_IN_FLIGHT_PER_AGENT;
13656
- app.get("/client", { websocket: true }, async (socket) => {
13657
- startWsConnectionSpan(socket);
13653
+ app.get("/client", { websocket: true }, async (socket, request) => {
13654
+ const ua = request.headers["user-agent"];
13655
+ startWsConnectionSpan(socket, {
13656
+ remoteIp: request.ip,
13657
+ userAgent: typeof ua === "string" ? ua.slice(0, 200) : void 0
13658
+ });
13658
13659
  let session = null;
13659
13660
  let jwtDefaultOrgId = null;
13660
13661
  let clientId = null;
@@ -14566,8 +14567,12 @@ async function refreshAccessToken(db, refreshToken, jwtSecretKey, expiries) {
14566
14567
  try {
14567
14568
  const { payload: p } = await jwtVerify(refreshToken, secret);
14568
14569
  payload = p;
14569
- } catch {
14570
- throw new UnauthorizedError("Invalid or expired refresh token", { "auth.refresh.reason": "jwt_verify_failed" });
14570
+ } catch (err) {
14571
+ const untrusted = decodeJwtForTrace(refreshToken);
14572
+ throw new UnauthorizedError("Invalid or expired refresh token", {
14573
+ "auth.refresh.reason": classifyJoseError(err),
14574
+ ...untrustedAttrs("auth.refresh", untrusted)
14575
+ });
14571
14576
  }
14572
14577
  if (payload.type !== "refresh" || !payload.sub) throw new UnauthorizedError("Invalid token type", {
14573
14578
  "auth.refresh.reason": "wrong_token_type",
@@ -14618,10 +14623,17 @@ async function exchangeConnectToken(db, connectToken, jwtSecretKey, expiries) {
14618
14623
  try {
14619
14624
  const { payload: p } = await jwtVerify(connectToken, secret);
14620
14625
  payload = p;
14621
- } catch {
14622
- throw new UnauthorizedError("Invalid or expired connect token");
14626
+ } catch (err) {
14627
+ const untrusted = decodeJwtForTrace(connectToken);
14628
+ throw new UnauthorizedError("Invalid or expired connect token", {
14629
+ "auth.connect.reason": classifyJoseError(err),
14630
+ ...untrustedAttrs("auth.connect", untrusted)
14631
+ });
14623
14632
  }
14624
- if (payload.type !== "connect" || !payload.sub) throw new UnauthorizedError("Invalid token type — expected connect token");
14633
+ if (payload.type !== "connect" || !payload.sub) throw new UnauthorizedError("Invalid token type — expected connect token", {
14634
+ "auth.connect.reason": "wrong_token_type",
14635
+ "auth.connect.actual_type": String(payload.type ?? "<missing>")
14636
+ });
14625
14637
  const jti = payload.jti;
14626
14638
  if (jti) {
14627
14639
  if (consumedConnectJtis.has(jti)) throw new UnauthorizedError("Connect token has already been used");
@@ -15255,10 +15267,18 @@ async function authRoutes(app) {
15255
15267
  return reply.send(result);
15256
15268
  });
15257
15269
  }
15258
- async function bootstrapConfigRoutes(app) {
15259
- /** Public endpoint — returns bootstrap prerequisites for CLI auto-discovery. */
15260
- app.get("/config", async () => {
15261
- return { allowedOrg: app.config.github.allowedOrg ?? null };
15270
+ async function bootstrapConfigRoutes(_app) {
15271
+ /**
15272
+ * Public endpoint returns bootstrap prerequisites for CLI auto-discovery.
15273
+ *
15274
+ * `allowedOrg` used to surface here from the global `github.allowedOrg`
15275
+ * config; it is now a per-org setting (see issue #255). A public bootstrap
15276
+ * endpoint can't resolve an org without a caller, so the field is
15277
+ * surfaced as `null` and consumers should fetch the per-org value via
15278
+ * `/api/v1/orgs/:orgId/settings/github_integration` after auth.
15279
+ */
15280
+ _app.get("/config", async () => {
15281
+ return { allowedOrg: null };
15262
15282
  });
15263
15283
  }
15264
15284
  /** Extract a plain-text summary from a message's JSONB content field.
@@ -16027,12 +16047,212 @@ async function clientRoutes(app) {
16027
16047
  });
16028
16048
  });
16029
16049
  }
16050
+ /**
16051
+ * Per-organization settings, keyed by `(organization_id, namespace)`.
16052
+ *
16053
+ * One row holds an entire group of related config as a JSONB blob — schema
16054
+ * for each namespace lives in `@agent-team-foundation/first-tree-hub-shared`
16055
+ * (`ORG_SETTINGS_NAMESPACES`) and is enforced by the service layer on every
16056
+ * read/write. Adding a new config group means registering a new namespace +
16057
+ * Zod schema in shared; the DB does not change.
16058
+ *
16059
+ * `version` is reserved for future optimistic locking (PUT with If-Match)
16060
+ * and is currently set unconditionally. We keep it on the table from day
16061
+ * one so tightening to compare-and-swap later is a code-only change.
16062
+ *
16063
+ * Sensitive fields inside `value` (e.g. `github_integration.webhookSecret`)
16064
+ * are AES-256-GCM-encrypted at the service layer using `crypto.ts`'s
16065
+ * `encryptValue` / `decryptValue` — same pattern as `adapter_configs`.
16066
+ */
16067
+ const organizationSettings = pgTable("organization_settings", {
16068
+ organizationId: text("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
16069
+ namespace: text("namespace").notNull(),
16070
+ value: jsonb("value").$type().notNull().default({}),
16071
+ version: integer("version").notNull().default(0),
16072
+ updatedBy: text("updated_by").references(() => users.id, { onDelete: "set null" }),
16073
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
16074
+ }, (table) => [primaryKey({ columns: [table.organizationId, table.namespace] }), index("idx_org_settings_namespace").on(table.namespace)]);
16075
+ /**
16076
+ * Per-organization settings, keyed by `(organizationId, namespace)`. The
16077
+ * registry of valid namespaces and their storage / input / output schemas
16078
+ * lives in `@agent-team-foundation/first-tree-hub-shared`.
16079
+ *
16080
+ * Read path: storage row → decrypt secrets → output (mask)
16081
+ * Write path: input → validate → encrypt secrets → merge with current storage → upsert (in tx)
16082
+ *
16083
+ * The generic getter returns the masked output. Callers needing plaintext
16084
+ * for a specific secret use a purpose-built helper (e.g.
16085
+ * `getDecryptedGithubWebhookSecret`) rather than the generic storage shape
16086
+ * — this avoids a `…Cipher` field name silently holding plaintext at
16087
+ * call-sites and limits secret exposure to one explicit code path per
16088
+ * secret. (#4)
16089
+ */
16090
+ function assertNamespace(ns) {
16091
+ if (!isOrgSettingNamespace(ns)) throw new BadRequestError(`Unknown organization-settings namespace: "${ns}"`);
16092
+ }
16093
+ async function fetchStorageRow(db, orgId, namespace) {
16094
+ const [row] = await db.select({ value: organizationSettings.value }).from(organizationSettings).where(and(eq(organizationSettings.organizationId, orgId), eq(organizationSettings.namespace, namespace))).limit(1);
16095
+ if (!row) return null;
16096
+ return ORG_SETTINGS_NAMESPACES$1[namespace].storage.parse(row.value);
16097
+ }
16098
+ function emptyStorage(namespace) {
16099
+ return ORG_SETTINGS_NAMESPACES$1[namespace].storage.parse({});
16100
+ }
16101
+ function ensureEncrypted(value, encryptionKey) {
16102
+ return isEncryptedValue(value) ? value : encryptValue(value, encryptionKey);
16103
+ }
16104
+ /**
16105
+ * Merge a validated input into the current storage row for a namespace.
16106
+ * Secret fields are encrypted here.
16107
+ *
16108
+ * Input semantics per nullish field:
16109
+ * `undefined` → unchanged
16110
+ * `null` → cleared
16111
+ * value → set / replace (already validated as non-empty by the input schema)
16112
+ */
16113
+ function applyInputDelta(namespace, current, input, encryptionKey) {
16114
+ if (namespace === "context_tree") {
16115
+ const cur = current;
16116
+ const inp = input;
16117
+ return {
16118
+ repo: inp.repo === void 0 ? cur.repo : inp.repo ?? void 0,
16119
+ branch: inp.branch === void 0 ? cur.branch : inp.branch ?? "main"
16120
+ };
16121
+ }
16122
+ if (namespace === "github_integration") {
16123
+ const cur = current;
16124
+ const inp = input;
16125
+ return { webhookSecretCipher: inp.webhookSecret === void 0 ? cur.webhookSecretCipher : inp.webhookSecret === null ? void 0 : ensureEncrypted(inp.webhookSecret, encryptionKey) };
16126
+ }
16127
+ return namespace;
16128
+ }
16129
+ /**
16130
+ * Project the storage row into the API output for a namespace, masking
16131
+ * any secret fields. `webhookUrl` for `github_integration` is left as an
16132
+ * empty string here — the route layer enriches it with the resolved
16133
+ * `server.publicUrl` (the service stays config-agnostic).
16134
+ */
16135
+ function toOutput(namespace, storage) {
16136
+ if (namespace === "context_tree") {
16137
+ const s = storage;
16138
+ return {
16139
+ repo: s.repo,
16140
+ branch: s.branch
16141
+ };
16142
+ }
16143
+ if (namespace === "github_integration") {
16144
+ const s = storage;
16145
+ return {
16146
+ webhookSecretConfigured: typeof s.webhookSecretCipher === "string" && s.webhookSecretCipher.length > 0,
16147
+ webhookUrl: ""
16148
+ };
16149
+ }
16150
+ return namespace;
16151
+ }
16152
+ /**
16153
+ * Read a setting masked for the API. Missing rows → namespace defaults
16154
+ * (parse `{}` against the storage schema).
16155
+ */
16156
+ async function getOrgSetting(db, orgId, namespace) {
16157
+ assertNamespace(namespace);
16158
+ return toOutput(namespace, await fetchStorageRow(db, orgId, namespace) ?? emptyStorage(namespace));
16159
+ }
16160
+ /**
16161
+ * Read the per-org Context Tree binding for server-internal consumers
16162
+ * (`/context-tree/info`, snapshot service). No secrets in this namespace,
16163
+ * so the storage shape is safe to expose directly. Missing row → defaults.
16164
+ */
16165
+ async function getOrgContextTree(db, orgId) {
16166
+ return await fetchStorageRow(db, orgId, "context_tree") ?? emptyStorage("context_tree");
16167
+ }
16168
+ /**
16169
+ * Decrypt and return the plaintext GitHub webhook secret for an org.
16170
+ * Returns `null` when the org has not configured one. The only intended
16171
+ * caller is the webhook route's signature verifier — the result must
16172
+ * never leak through HTTP responses or logs. (#4)
16173
+ */
16174
+ async function getDecryptedGithubWebhookSecret(db, orgId, encryptionKey) {
16175
+ const cipher = (await fetchStorageRow(db, orgId, "github_integration"))?.webhookSecretCipher;
16176
+ if (!cipher) return null;
16177
+ return isEncryptedValue(cipher) ? decryptValue(cipher, encryptionKey) : cipher;
16178
+ }
16179
+ /**
16180
+ * Upsert a setting. Returns the masked output of the resulting row.
16181
+ *
16182
+ * The fetch + merge + upsert sequence runs inside a single transaction so
16183
+ * two concurrent admin writes can't both base their delta on the same
16184
+ * pre-image and silently lose each other's fields. Optimistic locking
16185
+ * (the `version` column) remains reserved for a future If-Match flip.
16186
+ * (#6)
16187
+ */
16188
+ async function putOrgSetting(db, orgId, namespace, rawInput, options) {
16189
+ assertNamespace(namespace);
16190
+ const input = ORG_SETTINGS_NAMESPACES$1[namespace].input.parse(rawInput);
16191
+ return db.transaction(async (tx) => {
16192
+ const txDb = tx;
16193
+ const [org] = await txDb.select({ id: organizations.id }).from(organizations).where(eq(organizations.id, orgId)).limit(1);
16194
+ if (!org) throw new NotFoundError(`Organization "${orgId}" not found`);
16195
+ const merged = applyInputDelta(namespace, await fetchStorageRow(txDb, orgId, namespace) ?? emptyStorage(namespace), input, options.encryptionKey);
16196
+ const validated = ORG_SETTINGS_NAMESPACES$1[namespace].storage.parse(merged);
16197
+ await tx.insert(organizationSettings).values({
16198
+ organizationId: orgId,
16199
+ namespace,
16200
+ value: validated,
16201
+ version: 1,
16202
+ updatedBy: options.updatedBy,
16203
+ updatedAt: /* @__PURE__ */ new Date()
16204
+ }).onConflictDoUpdate({
16205
+ target: [organizationSettings.organizationId, organizationSettings.namespace],
16206
+ set: {
16207
+ value: validated,
16208
+ version: sql`${organizationSettings.version} + 1`,
16209
+ updatedBy: options.updatedBy,
16210
+ updatedAt: /* @__PURE__ */ new Date()
16211
+ }
16212
+ });
16213
+ return toOutput(namespace, validated);
16214
+ });
16215
+ }
16216
+ /**
16217
+ * Delete a namespace row; subsequent GETs return defaults.
16218
+ */
16219
+ async function deleteOrgSetting(db, orgId, namespace) {
16220
+ assertNamespace(namespace);
16221
+ await db.delete(organizationSettings).where(and(eq(organizationSettings.organizationId, orgId), eq(organizationSettings.namespace, namespace)));
16222
+ }
16223
+ /**
16224
+ * Resolve the caller's "primary org" — the earliest-joined active
16225
+ * membership for the given user. Used by user-scoped routes that
16226
+ * historically didn't take an `:orgId` (e.g. `/context-tree/info`) so
16227
+ * the SDK call shape doesn't have to change while the per-tenant lookup
16228
+ * still happens correctly.
16229
+ *
16230
+ * Returns `null` for users with no active membership. Tightening to
16231
+ * "explicit org selector" is a future change-once-multi-org-clients-arrive
16232
+ * concern. (#7)
16233
+ */
16234
+ async function resolveUserPrimaryOrgId(db, userId) {
16235
+ const [row] = await db.select({ organizationId: members.organizationId }).from(members).where(and(eq(members.userId, userId), eq(members.status, "active"))).orderBy(asc(members.createdAt)).limit(1);
16236
+ return row?.organizationId ?? null;
16237
+ }
16030
16238
  async function contextTreeInfoRoutes(app) {
16031
- /** Public endpoint — returns Context Tree repo metadata for CLI auto-discovery. */
16032
- app.get("/info", async () => {
16239
+ /**
16240
+ * Class A — `/api/v1/context-tree/info`. Returns the caller's
16241
+ * organization-scoped Context Tree binding for CLI auto-discovery.
16242
+ * Responds with `{ repo: null, branch: null }` when the user is not in
16243
+ * any org or the org hasn't configured a tree yet.
16244
+ */
16245
+ app.get("/info", async (request) => {
16246
+ const { userId } = requireUser(request);
16247
+ const orgId = await resolveUserPrimaryOrgId(app.db, userId);
16248
+ if (!orgId) return {
16249
+ repo: null,
16250
+ branch: null
16251
+ };
16252
+ const tree = await getOrgContextTree(app.db, orgId);
16033
16253
  return {
16034
- repo: app.config.contextTree?.repo ?? null,
16035
- branch: app.config.contextTree?.branch ?? null
16254
+ repo: tree.repo ?? null,
16255
+ branch: tree.branch ?? null
16036
16256
  };
16037
16257
  });
16038
16258
  }
@@ -16058,10 +16278,10 @@ const WINDOW_DAYS = {
16058
16278
  "30d": 30
16059
16279
  };
16060
16280
  const snapshotCache = /* @__PURE__ */ new Map();
16061
- async function getContextTreeSnapshot(config, window = CONTEXT_TREE_SNAPSHOT_WINDOWS.SEVEN_DAYS) {
16062
- const repo = config.contextTree?.repo ?? null;
16063
- const branch = config.contextTree?.branch ?? null;
16064
- const resolved = resolveContextTreeRoot(repo, config.contextTree?.localPath);
16281
+ async function getContextTreeSnapshot(binding, window = CONTEXT_TREE_SNAPSHOT_WINDOWS.SEVEN_DAYS) {
16282
+ const repo = binding.repo ?? null;
16283
+ const branch = binding.branch ?? null;
16284
+ const resolved = resolveContextTreeRoot(repo, binding.localPath);
16065
16285
  if (!resolved.root) return unavailableSnapshot(repo, branch, resolved.reason);
16066
16286
  const now = (/* @__PURE__ */ new Date()).toISOString();
16067
16287
  try {
@@ -16733,7 +16953,9 @@ async function contextTreeSnapshotRoutes(app) {
16733
16953
  keyGenerator: (request) => request.user?.userId ?? request.ip
16734
16954
  } } }, async (request) => {
16735
16955
  const query = querySchema.parse(request.query);
16736
- const snapshot = await getContextTreeSnapshot(app.config, query.window ?? "7d");
16956
+ const { userId } = requireUser(request);
16957
+ const orgId = await resolveUserPrimaryOrgId(app.db, userId);
16958
+ const snapshot = await getContextTreeSnapshot(orgId ? await getOrgContextTree(app.db, orgId) : {}, query.window ?? "7d");
16737
16959
  return contextTreeSnapshotSchema.parse(snapshot);
16738
16960
  });
16739
16961
  }
@@ -16838,7 +17060,7 @@ async function healthzRoutes(app) {
16838
17060
  * `api/orgs/invitations.ts` (Class B, admin-gated).
16839
17061
  */
16840
17062
  async function publicInvitationRoutes(app) {
16841
- const { previewInvitation } = await import("./invitation-DWlyNb8x-BEgoZ9k1.mjs");
17063
+ const { previewInvitation } = await import("./invitation-DWlyNb8x-BvXubk24.mjs");
16842
17064
  app.get("/:token/preview", async (request, reply) => {
16843
17065
  if (!request.params.token) throw new UnauthorizedError("Token required");
16844
17066
  const preview = await previewInvitation(app.db, request.params.token);
@@ -16935,7 +17157,7 @@ async function meRoutes(app) {
16935
17157
  */
16936
17158
  app.get("/me/pinned-agents", async (request) => {
16937
17159
  const { userId } = requireUser(request);
16938
- const { listMyPinnedAgents } = await import("./client-By1K4VVT-nVOhsXBy.mjs");
17160
+ const { listMyPinnedAgents } = await import("./client-By1K4VVT-DuI6EnSh.mjs");
16939
17161
  return listMyPinnedAgents(app.db, { userId });
16940
17162
  });
16941
17163
  /**
@@ -17658,6 +17880,64 @@ async function orgSessionRoutes(app) {
17658
17880
  });
17659
17881
  });
17660
17882
  }
17883
+ /**
17884
+ * Class B — `/api/v1/orgs/:orgId/settings/:namespace`.
17885
+ *
17886
+ * Generic per-org settings surface. The `:namespace` URL parameter is
17887
+ * dispatched against `ORG_SETTINGS_NAMESPACES` (in the shared package);
17888
+ * adding a new config group only requires registering it there — no new
17889
+ * route file.
17890
+ *
17891
+ * All three verbs are admin-only. Even GET, because the masked output
17892
+ * still leaks "configured / not-configured" booleans for secret fields,
17893
+ * which we don't want to expose to non-admin members.
17894
+ */
17895
+ async function orgSettingsRoutes(app) {
17896
+ app.get("/:namespace", async (request) => {
17897
+ const scope = await requireOrgAdmin(request, app.db);
17898
+ const namespace = parseNamespace(request.params.namespace);
17899
+ return enrichOutput(namespace, await getOrgSetting(app.db, scope.organizationId, namespace), scope.organizationId, app.config.server.publicUrl);
17900
+ });
17901
+ app.put("/:namespace", { config: { otelRecordBody: true } }, async (request) => {
17902
+ const scope = await requireOrgAdmin(request, app.db);
17903
+ const namespace = parseNamespace(request.params.namespace);
17904
+ return enrichOutput(namespace, await putOrgSetting(app.db, scope.organizationId, namespace, request.body, {
17905
+ updatedBy: scope.userId,
17906
+ encryptionKey: app.config.secrets.encryptionKey
17907
+ }), scope.organizationId, app.config.server.publicUrl);
17908
+ });
17909
+ app.delete("/:namespace", async (request, reply) => {
17910
+ const scope = await requireOrgAdmin(request, app.db);
17911
+ const namespace = parseNamespace(request.params.namespace);
17912
+ await deleteOrgSetting(app.db, scope.organizationId, namespace);
17913
+ reply.status(204).send();
17914
+ });
17915
+ }
17916
+ function parseNamespace(raw) {
17917
+ if (!isOrgSettingNamespace(raw)) throw new BadRequestError(`Unknown organization-settings namespace: "${raw}"`);
17918
+ return raw;
17919
+ }
17920
+ /**
17921
+ * Resolve namespace-specific server-config-derived fields. The service
17922
+ * layer stays config-agnostic — namespace knowledge that needs `app.config`
17923
+ * lives here. Currently only `github_integration.webhookUrl` qualifies.
17924
+ *
17925
+ * If `server.publicUrl` is unset on the Hub, `webhookUrl` is left as `""`
17926
+ * so the UI can render a "contact your site administrator" notice rather
17927
+ * than fall back to `window.location.origin` (which is wrong behind a
17928
+ * reverse proxy). (#12)
17929
+ */
17930
+ function enrichOutput(namespace, out, orgId, publicUrl) {
17931
+ if (namespace === "github_integration") {
17932
+ const o = out;
17933
+ const webhookUrl = publicUrl ? `${publicUrl.replace(/\/+$/, "")}/api/v1/webhooks/github/${orgId}` : "";
17934
+ return {
17935
+ ...o,
17936
+ webhookUrl
17937
+ };
17938
+ }
17939
+ return out;
17940
+ }
17661
17941
  function dispatch$1(notifier, result) {
17662
17942
  if (!result) return;
17663
17943
  notifyRecipients(notifier, result.recipients, result.message.id);
@@ -17760,7 +18040,11 @@ function orgWsRoutes(notifier, jwtSecret) {
17760
18040
  }
17761
18041
  return async (app) => {
17762
18042
  app.get("/", { websocket: true }, async (socket, request) => {
17763
- startWsConnectionSpan(socket, { remoteIp: request.ip });
18043
+ const ua = request.headers["user-agent"];
18044
+ startWsConnectionSpan(socket, {
18045
+ remoteIp: request.ip,
18046
+ userAgent: typeof ua === "string" ? ua.slice(0, 200) : void 0
18047
+ });
17764
18048
  const orgIdFromPath = request.params.orgId;
17765
18049
  const token = request.query.token;
17766
18050
  if (!token || !orgIdFromPath) {
@@ -17953,16 +18237,15 @@ function verifySignature(secret, rawBody, signatureHeader) {
17953
18237
  const receivedBuf = Buffer.from(signatureHeader, "utf8");
17954
18238
  if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) throw new UnauthorizedError("Invalid webhook signature");
17955
18239
  }
17956
- async function ensureGitHubAdapterAgent(db) {
17957
- const defaultOrgId = await resolveDefaultOrgId(db);
17958
- const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, defaultOrgId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
18240
+ async function ensureGitHubAdapterAgent(db, organizationId) {
18241
+ const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
17959
18242
  if (existing) return existing.uuid;
17960
18243
  try {
17961
18244
  return (await createAgent(db, {
17962
18245
  name: GITHUB_ADAPTER_ID,
17963
18246
  type: "autonomous_agent",
17964
18247
  displayName: "GitHub Adapter",
17965
- organizationId: defaultOrgId,
18248
+ organizationId,
17966
18249
  metadata: {
17967
18250
  source: "github",
17968
18251
  managed: true
@@ -17970,19 +18253,19 @@ async function ensureGitHubAdapterAgent(db) {
17970
18253
  })).uuid;
17971
18254
  } catch (err) {
17972
18255
  if (err instanceof ConflictError) {
17973
- const [created] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, defaultOrgId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
18256
+ const [created] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
17974
18257
  if (created) return created.uuid;
17975
18258
  }
17976
18259
  throw err;
17977
18260
  }
17978
18261
  }
17979
- async function findTargetAgent(db, repoFullName) {
18262
+ async function findTargetAgent(db, organizationId, repoFullName) {
17980
18263
  const allAgents = await db.select({
17981
18264
  id: agents.uuid,
17982
18265
  name: agents.name,
17983
18266
  metadata: agents.metadata,
17984
18267
  type: agents.type
17985
- }).from(agents).where(eq(agents.status, "active"));
18268
+ }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.status, "active")));
17986
18269
  for (const agent of allAgents) {
17987
18270
  if (agent.name === GITHUB_ADAPTER_ID) continue;
17988
18271
  const meta = agent.metadata;
@@ -18016,14 +18299,14 @@ function extractMentions$1(text) {
18016
18299
  * For each mentioned user who has delegate_mention configured,
18017
18300
  * send a card message from the mentioned user to their delegate.
18018
18301
  */
18019
- async function routeMentionDelegations(app, mentionedNames, ctx) {
18302
+ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx) {
18020
18303
  if (mentionedNames.length === 0) return 0;
18021
18304
  const delegates = await app.db.select({
18022
18305
  id: agents.uuid,
18023
18306
  name: agents.name,
18024
18307
  delegateMention: agents.delegateMention,
18025
18308
  status: agents.status
18026
- }).from(agents).where(and(inArray(agents.name, mentionedNames), isNotNull(agents.delegateMention)));
18309
+ }).from(agents).where(and(eq(agents.organizationId, organizationId), inArray(agents.name, mentionedNames), isNotNull(agents.delegateMention)));
18027
18310
  let routed = 0;
18028
18311
  for (const agent of delegates) {
18029
18312
  if (agent.status !== "active" || !agent.delegateMention) continue;
@@ -18111,13 +18394,14 @@ async function githubWebhookRoutes(app) {
18111
18394
  app.addContentTypeParser("application/json", { parseAs: "buffer" }, (_request, body, done) => {
18112
18395
  done(null, body);
18113
18396
  });
18114
- const webhookSecret = app.config.github.webhookSecret;
18115
18397
  const webhookMax = app.config.rateLimit?.webhookMax ?? 60;
18116
- app.post("/github", { config: { rateLimit: {
18398
+ app.post("/github/:orgId", { config: { rateLimit: {
18117
18399
  max: webhookMax,
18118
18400
  timeWindow: "1 minute"
18119
18401
  } } }, async (request, reply) => {
18120
- if (!webhookSecret) return reply.status(501).send({ error: "GitHub webhook is not configured. Set FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET to enable." });
18402
+ const { orgId } = request.params;
18403
+ const webhookSecret = await getDecryptedGithubWebhookSecret(app.db, orgId, app.config.secrets.encryptionKey);
18404
+ if (!webhookSecret) return reply.status(501).send({ error: "GitHub webhook is not configured for this organization. An admin must set the webhook secret in Team settings." });
18121
18405
  const rawBody = request.body;
18122
18406
  if (!Buffer.isBuffer(rawBody)) throw new BadRequestError("Expected raw body buffer");
18123
18407
  const signatureHeader = request.headers["x-hub-signature-256"];
@@ -18135,12 +18419,12 @@ async function githubWebhookRoutes(app) {
18135
18419
  ok: true,
18136
18420
  event: "ping"
18137
18421
  });
18138
- if (eventType === "issues") return handleIssuesEvent(app, eventType, payload, reply);
18139
- if (eventType === "issue_comment") return handleIssueCommentEvent(app, eventType, payload, reply);
18422
+ if (eventType === "issues") return handleIssuesEvent(app, orgId, eventType, payload, reply);
18423
+ if (eventType === "issue_comment") return handleIssueCommentEvent(app, orgId, eventType, payload, reply);
18140
18424
  let mentionsRouted = 0;
18141
18425
  const allowedActions = MENTION_ACTIONS[eventType];
18142
18426
  const action = isRecord(payload) && typeof payload.action === "string" ? payload.action : void 0;
18143
- if (allowedActions && action && allowedActions.includes(action)) mentionsRouted = await handleMentionDelegation(app, eventType, payload);
18427
+ if (allowedActions && action && allowedActions.includes(action)) mentionsRouted = await handleMentionDelegation(app, orgId, eventType, payload);
18144
18428
  return reply.status(200).send({
18145
18429
  ok: true,
18146
18430
  event: eventType,
@@ -18294,10 +18578,10 @@ function extractEventContext(eventType, payload) {
18294
18578
  * Run mention delegation for a given event type and payload.
18295
18579
  * Only called after action gating confirms this is a "new content" event.
18296
18580
  */
18297
- async function handleMentionDelegation(app, eventType, payload) {
18581
+ async function handleMentionDelegation(app, organizationId, eventType, payload) {
18298
18582
  const mentions = extractMentions$1(extractEventText(eventType, payload));
18299
18583
  const mentionCtx = extractEventContext(eventType, payload);
18300
- if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, mentions, mentionCtx);
18584
+ if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, organizationId, mentions, mentionCtx);
18301
18585
  return 0;
18302
18586
  }
18303
18587
  /** Actions that represent new/changed content (worth scanning for @mentions). */
@@ -18311,9 +18595,9 @@ const MENTION_ACTIONS = {
18311
18595
  discussion_comment: ["created"],
18312
18596
  commit_comment: ["created"]
18313
18597
  };
18314
- async function handleIssuesEvent(app, eventType, payload, reply) {
18598
+ async function handleIssuesEvent(app, organizationId, eventType, payload, reply) {
18315
18599
  const data = parseIssuesPayload(payload);
18316
- if (MENTION_ACTIONS.issues?.includes(data.action)) await handleMentionDelegation(app, eventType, payload);
18600
+ if (MENTION_ACTIONS.issues?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
18317
18601
  if (![
18318
18602
  "opened",
18319
18603
  "edited",
@@ -18324,7 +18608,7 @@ async function handleIssuesEvent(app, eventType, payload, reply) {
18324
18608
  action: data.action,
18325
18609
  handled: false
18326
18610
  });
18327
- const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db), findTargetAgent(app.db, data.repository.full_name)]);
18611
+ const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
18328
18612
  if (!targetAgentId) {
18329
18613
  log$1.warn({
18330
18614
  repo: data.repository.full_name,
@@ -18370,16 +18654,16 @@ async function handleIssuesEvent(app, eventType, payload, reply) {
18370
18654
  routed: true
18371
18655
  });
18372
18656
  }
18373
- async function handleIssueCommentEvent(app, eventType, payload, reply) {
18657
+ async function handleIssueCommentEvent(app, organizationId, eventType, payload, reply) {
18374
18658
  const data = parseIssueCommentPayload(payload);
18375
- if (MENTION_ACTIONS.issue_comment?.includes(data.action)) await handleMentionDelegation(app, eventType, payload);
18659
+ if (MENTION_ACTIONS.issue_comment?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
18376
18660
  if (data.action !== "created") return reply.status(200).send({
18377
18661
  ok: true,
18378
18662
  event: "issue_comment",
18379
18663
  action: data.action,
18380
18664
  handled: false
18381
18665
  });
18382
- const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db), findTargetAgent(app.db, data.repository.full_name)]);
18666
+ const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
18383
18667
  if (!targetAgentId) {
18384
18668
  log$1.warn({
18385
18669
  repo: data.repository.full_name,
@@ -18449,6 +18733,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
18449
18733
  members: () => members,
18450
18734
  messages: () => messages,
18451
18735
  notifications: () => notifications,
18736
+ organizationSettings: () => organizationSettings,
18452
18737
  organizations: () => organizations,
18453
18738
  serverInstances: () => serverInstances,
18454
18739
  sessionEvents: () => sessionEvents,
@@ -18457,10 +18742,16 @@ var schema_exports = /* @__PURE__ */ __exportAll({
18457
18742
  users: () => users
18458
18743
  });
18459
18744
  function connectDatabase(url) {
18460
- const client = postgres(url);
18745
+ const client = postgres(url, sslOptions(url));
18461
18746
  const db = drizzle(client, { schema: schema_exports });
18462
18747
  return Object.assign(db, { end: () => client.end() });
18463
18748
  }
18749
+ function sslOptions(url) {
18750
+ try {
18751
+ if (new URL(url).hostname.endsWith(".rds.amazonaws.com")) return { ssl: { rejectUnauthorized: false } };
18752
+ } catch {}
18753
+ return {};
18754
+ }
18464
18755
  /**
18465
18756
  * Agent-scoped HTTP authentication hook. Must run **after** userAuthHook
18466
18757
  * so `request.user` is populated.
@@ -18531,8 +18822,12 @@ function userAuthHook(db, jwtSecret) {
18531
18822
  try {
18532
18823
  const { payload: p } = await jwtVerify(token, secret);
18533
18824
  payload = p;
18534
- } catch {
18535
- throw new UnauthorizedError("Invalid or expired token", { "auth.failure_reason": "jwt_verify_failed" });
18825
+ } catch (err) {
18826
+ const untrusted = decodeJwtForTrace(token);
18827
+ throw new UnauthorizedError("Invalid or expired token", {
18828
+ "auth.failure_reason": classifyJoseError(err),
18829
+ ...untrustedAttrs("auth", untrusted)
18830
+ });
18536
18831
  }
18537
18832
  if (payload.type !== "access" || !payload.sub) throw new UnauthorizedError("Invalid token type", {
18538
18833
  "auth.failure_reason": "wrong_token_type",
@@ -19736,7 +20031,7 @@ function createPulseAggregator(options) {
19736
20031
  * Returning a string (rather than undefined) keeps the welcome frame well-
19737
20032
  * formed — the client treats the value advisorily.
19738
20033
  */
19739
- function resolveCommandVersion$1(injected) {
20034
+ function resolveCommandVersion(injected) {
19740
20035
  if (injected && injected.trim().length > 0) return injected;
19741
20036
  try {
19742
20037
  const pkg = createRequire(import.meta.url)("../package.json");
@@ -19828,10 +20123,13 @@ async function buildApp(config) {
19828
20123
  const db = connectDatabase(config.database.url);
19829
20124
  app.decorate("db", db);
19830
20125
  app.decorate("config", config);
19831
- const commandVersion = resolveCommandVersion$1(config.commandVersion);
20126
+ const commandVersion = resolveCommandVersion(config.commandVersion);
19832
20127
  app.decorate("commandVersion", commandVersion);
19833
20128
  app.log.info({ commandVersion }, "Hub server advertising command version");
19834
- const listenClient = postgres(config.database.url, { max: 1 });
20129
+ const listenClient = postgres(config.database.url, {
20130
+ max: 1,
20131
+ ...sslOptions(config.database.url)
20132
+ });
19835
20133
  const notifier = createNotifier(listenClient);
19836
20134
  await app.register(websocket, { options: { maxPayload: config.ws?.maxPayload ?? 65536 } });
19837
20135
  const corsOrigin = config.cors?.origin;
@@ -19843,7 +20141,8 @@ async function buildApp(config) {
19843
20141
  await app.register(rateLimit, {
19844
20142
  max: config.rateLimit?.max ?? 100,
19845
20143
  timeWindow: "1 minute",
19846
- hook: "preHandler"
20144
+ hook: "preHandler",
20145
+ errorResponseBuilder: buildRateLimitError
19847
20146
  });
19848
20147
  app.addHook("onSend", bodyCaptureOnSendHook);
19849
20148
  const userAuth = userAuthHook(db, config.secrets.jwtSecret);
@@ -19915,9 +20214,9 @@ async function buildApp(config) {
19915
20214
  await api.register(authRoutes, { prefix: "/auth" });
19916
20215
  await api.register(githubOauthRoutes, { prefix: "/auth/github" });
19917
20216
  await api.register(publicInvitationRoutes, { prefix: "/invitations" });
19918
- await api.register(contextTreeInfoRoutes, { prefix: "/context-tree" });
19919
20217
  await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
19920
20218
  await api.register(userScope("contextTreeScope", async (scope) => {
20219
+ await scope.register(contextTreeInfoRoutes);
19921
20220
  await scope.register(contextTreeSnapshotRoutes);
19922
20221
  }), { prefix: "/context-tree" });
19923
20222
  await api.register(userScope("meRoutesScope", async (scope) => {
@@ -19938,6 +20237,7 @@ async function buildApp(config) {
19938
20237
  await scope.register(orgClientRoutes, { prefix: "/clients" });
19939
20238
  await scope.register(orgInvitationRoutes, { prefix: "/invitations" });
19940
20239
  await scope.register(orgMemberRoutes, { prefix: "/members" });
20240
+ await scope.register(orgSettingsRoutes, { prefix: "/settings" });
19941
20241
  }), { prefix: "/orgs/:orgId" });
19942
20242
  await api.register(orgWsRoutes(notifier, config.secrets.jwtSecret), { prefix: "/orgs/:orgId/ws" });
19943
20243
  await api.register(userScope("resourcesScope", async (scope) => {
@@ -20042,58 +20342,6 @@ async function buildApp(config) {
20042
20342
  return app;
20043
20343
  }
20044
20344
  //#endregion
20045
- //#region src/core/version.ts
20046
- /**
20047
- * Version of the consumer-facing `@agent-team-foundation/first-tree-hub`
20048
- * package. Read once at module load so the CLI, client runtime, and server
20049
- * bootstrap all quote the same string.
20050
- *
20051
- * Path-based lookups (`require("../../package.json")`) do not survive the
20052
- * tsdown bundle: the source lives at `src/core/version.ts` but every
20053
- * emitted chunk lands in `dist/` — shifting the relative depth by one and
20054
- * pointing at `packages/package.json` instead of our own manifest (the
20055
- * v0.9.1 "Cannot find module ../../package.json" crash). Walk up from this
20056
- * module's URL and accept the first `package.json` whose `name` matches, so
20057
- * dev runs (`tsx src/cli/index.ts`) and the published bundle
20058
- * (`dist/cli/index.mjs`) both resolve the same file.
20059
- */
20060
- const PACKAGE_NAME$1 = "@agent-team-foundation/first-tree-hub";
20061
- /**
20062
- * Sentinel returned when the walker exhausts every parent directory without
20063
- * finding our manifest. Deliberately NOT valid SemVer so the client-side
20064
- * `UpdateManager` drops into its `semver.valid(current) === false` warn-and-
20065
- * skip branch instead of treating it as `< target` and triggering a spurious
20066
- * self-update loop (the scenario where the startup crash this module fixes
20067
- * would otherwise quietly reincarnate as repeated `npm install -g @latest`).
20068
- */
20069
- const UNRESOLVED_VERSION = "unknown";
20070
- /**
20071
- * Exported for tests. Walks up from `moduleUrl`'s directory looking for a
20072
- * `package.json` whose `name` field equals {@link PACKAGE_NAME}. Returns
20073
- * {@link UNRESOLVED_VERSION} as a last-resort fallback so the CLI never
20074
- * crashes on a missing manifest.
20075
- */
20076
- function resolveCommandVersion(moduleUrl = import.meta.url) {
20077
- let dir = dirname(fileURLToPath(moduleUrl));
20078
- for (let i = 0; i < 10; i++) {
20079
- try {
20080
- const pkg = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8"));
20081
- if (pkg.name === PACKAGE_NAME$1 && typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
20082
- } catch (err) {
20083
- const code = err.code;
20084
- if (code !== "ENOENT" && code !== "ENOTDIR") {
20085
- const message = err instanceof Error ? err.message : String(err);
20086
- print.line(`[first-tree-hub] warning: could not read ${dir}/package.json: ${message}\n`);
20087
- }
20088
- }
20089
- const parent = dirname(dir);
20090
- if (parent === dir) break;
20091
- dir = parent;
20092
- }
20093
- return UNRESOLVED_VERSION;
20094
- }
20095
- const COMMAND_VERSION = resolveCommandVersion();
20096
- //#endregion
20097
20345
  //#region src/core/server.ts
20098
20346
  /**
20099
20347
  * Full server start orchestration:
@@ -20148,7 +20396,7 @@ async function startServer(options) {
20148
20396
  instanceId: `srv_${randomUUID().slice(0, 8)}`,
20149
20397
  commandVersion: COMMAND_VERSION
20150
20398
  };
20151
- const { initTelemetry, shutdownTelemetry } = await import("./observability-DttujCqj.mjs");
20399
+ const { initTelemetry, shutdownTelemetry } = await import("./observability-CYsdAcoF.mjs");
20152
20400
  await initTelemetry(serverConfig.observability.tracing, config.instanceId);
20153
20401
  const app = await buildApp(config);
20154
20402
  const SHUTDOWN_FORCE_EXIT_MS = 8e3;
@@ -20509,7 +20757,7 @@ async function promptReplaceOrCancel(newMemberId) {
20509
20757
  }) === "replace" ? "proceed" : "cancel";
20510
20758
  }
20511
20759
  async function exchangeToken(url, token) {
20512
- const res = await fetch(`${url}/api/v1/auth/connect-token`, {
20760
+ const res = await cliFetch(`${url}/api/v1/auth/connect-token`, {
20513
20761
  method: "POST",
20514
20762
  headers: { "Content-Type": "application/json" },
20515
20763
  body: JSON.stringify({ token }),
@@ -20621,4 +20869,4 @@ function registerSaaSConnectCommand(program) {
20621
20869
  });
20622
20870
  }
20623
20871
  //#endregion
20624
- export { findStaleAliases as $, checkDatabase as A, installClientService as B, runHomeMigration as C, checkAgentConfigs as D, runMigrations as E, checkServerReachable as F, stopClientService as G, resolveCliInvocation as H, checkWebSocket as I, isDockerAvailable as J, uninstallClientService as K, printResults as L, checkNodeVersion as M, checkServerConfig as N, checkBackgroundService as O, checkServerHealth as P, rotateClientIdWithBackup as Q, reconcileAgentConfigs as R, saveOnboardState as S, migrateLocalAgentDirs as T, restartClientService as U, isServiceSupported as V, startClientService as W, ClientRuntime as X, stopPostgres as Y, handleClientOrgMismatch as Z, promptMissingFields as _, probeCapabilities as _t, declineUpdate as a, fail as at, onboardCheck as b, detectInstallMode as c, print as ct, startServer as d, ClientOrgMismatchError as dt, formatStaleReason as et, COMMAND_VERSION as f, ClientUserMismatchError as ft, promptAddAgent as g, cleanWorkspaces as gt, isInteractive as h, SessionRegistry as ht, createExecuteUpdate as i, resolveReplyToFromEnv as it, checkDocker as j, checkClientConfig as k, fetchLatestVersion as l, setJsonMode as lt, uploadClientCapabilities as m, SdkError as mt, deriveHubUrlFromToken as n, createOwner as nt, promptUpdate as o, success as ot, reconcileLocalRuntimeProviders as p, FirstTreeHubSDK as pt, ensurePostgres as q, registerSaaSConnectCommand as r, hasUser as rt, PACKAGE_NAME as s, blank as st, HubUrlDerivationError as t, removeLocalAgent as tt, installGlobalLatest as u, status as ut, formatCheckReport as v, applyClientLoggerConfig as vt, createApiNameResolver as w, onboardCreate as x, loadOnboardState as y, configureClientLoggerForService as yt, getClientServiceStatus as z };
20872
+ export { formatStaleReason as $, checkDocker as A, isServiceSupported as B, createApiNameResolver as C, checkBackgroundService as D, checkAgentConfigs as E, checkWebSocket as F, uninstallClientService as G, restartClientService as H, printResults as I, stopPostgres as J, ensurePostgres as K, reconcileAgentConfigs as L, checkServerConfig as M, checkServerHealth as N, checkClientConfig as O, checkServerReachable as P, findStaleAliases as Q, getClientServiceStatus as R, runHomeMigration as S, runMigrations as T, startClientService as U, resolveCliInvocation as V, stopClientService as W, handleClientOrgMismatch as X, ClientRuntime as Y, rotateClientIdWithBackup as Z, formatCheckReport as _, declineUpdate as a, success as at, onboardCreate as b, detectInstallMode as c, FirstTreeHubSDK as ct, startServer as d, cleanWorkspaces as dt, removeLocalAgent as et, reconcileLocalRuntimeProviders as f, probeCapabilities as ft, promptMissingFields as g, promptAddAgent as h, createExecuteUpdate as i, fail as it, checkNodeVersion as j, checkDatabase as k, fetchLatestVersion as l, SdkError as lt, isInteractive as m, configureClientLoggerForService as mt, deriveHubUrlFromToken as n, hasUser as nt, promptUpdate as o, ClientOrgMismatchError as ot, uploadClientCapabilities as p, applyClientLoggerConfig as pt, isDockerAvailable as q, registerSaaSConnectCommand as r, resolveReplyToFromEnv as rt, PACKAGE_NAME as s, ClientUserMismatchError as st, HubUrlDerivationError as t, createOwner as tt, installGlobalLatest as u, SessionRegistry as ut, loadOnboardState as v, migrateLocalAgentDirs as w, saveOnboardState as x, onboardCheck as y, installClientService as z };