@agent-team-foundation/first-tree-hub 0.11.3 → 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.
@@ -1,10 +1,10 @@
1
1
  import { a as __toCommonJS, o as __toESM, t as __commonJSMin } from "./chunk-BSw8zbkd.mjs";
2
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-D4rdqM2F.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
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 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-BAqGZkco.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
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-BRtalKpQ.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
8
  import { n as init_esm, r as trace, t as esm_exports } from "./esm-Ci8E1Gtj.mjs";
9
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";
10
10
  import { createRequire } from "node:module";
@@ -1292,6 +1292,57 @@ z.object({
1292
1292
  displayName: z.string().optional(),
1293
1293
  next: z.string().max(256).optional()
1294
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);
1295
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;
1296
1347
  z.object({
1297
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"),
@@ -1684,21 +1735,6 @@ defineConfig({
1684
1735
  refreshTokenExpiry: field(z.string().default("30d"), { env: "FIRST_TREE_HUB_AUTH_REFRESH_TOKEN_EXPIRY" }),
1685
1736
  connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
1686
1737
  },
1687
- contextTree: optional({
1688
- repo: field(z.string().optional(), {
1689
- env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
1690
- prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
1691
- }),
1692
- localPath: field(z.string().optional(), { env: "FIRST_TREE_HUB_CONTEXT_TREE_PATH" }),
1693
- branch: field(z.string().default("main"))
1694
- }),
1695
- github: {
1696
- webhookSecret: field(z.string().optional(), {
1697
- env: "FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET",
1698
- secret: true
1699
- }),
1700
- allowedOrg: field(z.string().optional(), { env: "FIRST_TREE_HUB_GITHUB_ALLOWED_ORG" })
1701
- },
1702
1738
  oauth: optional({ github: optional({
1703
1739
  clientId: field(z.string(), { env: "FIRST_TREE_HUB_GITHUB_OAUTH_CLIENT_ID" }),
1704
1740
  clientSecret: field(z.string(), {
@@ -7820,6 +7856,12 @@ function printResults(results) {
7820
7856
  }
7821
7857
  //#endregion
7822
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
+ }
7823
7865
  /**
7824
7866
  * Resolve the drizzle migrations directory.
7825
7867
  * 1. npm install: embedded at dist/drizzle/ (relative to the built CLI)
@@ -7853,14 +7895,21 @@ function validateJournalOrder(migrationsFolder) {
7853
7895
  async function runMigrations(databaseUrl) {
7854
7896
  const migrationsFolder = resolveMigrationsFolder();
7855
7897
  validateJournalOrder(migrationsFolder);
7856
- const client = postgres(databaseUrl, { max: 1 });
7898
+ const ssl = sslOptions$1(databaseUrl);
7899
+ const client = postgres(databaseUrl, {
7900
+ max: 1,
7901
+ ...ssl
7902
+ });
7857
7903
  const db = drizzle(client);
7858
7904
  try {
7859
7905
  await migrate(db, { migrationsFolder });
7860
7906
  } finally {
7861
7907
  await client.end();
7862
7908
  }
7863
- const countClient = postgres(databaseUrl, { max: 1 });
7909
+ const countClient = postgres(databaseUrl, {
7910
+ max: 1,
7911
+ ...ssl
7912
+ });
7864
7913
  try {
7865
7914
  return (await countClient`
7866
7915
  SELECT count(*)::int AS count
@@ -8254,7 +8303,7 @@ async function onboardCreate(args) {
8254
8303
  }
8255
8304
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
8256
8305
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
8257
- const { bindFeishuBot } = await import("./feishu-Th_-ivJ7.mjs").then((n) => n.r);
8306
+ const { bindFeishuBot } = await import("./feishu-AI3pwmqN.mjs").then((n) => n.r);
8258
8307
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
8259
8308
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
8260
8309
  else {
@@ -9467,7 +9516,7 @@ function createFeedbackHandler(config) {
9467
9516
  return { handle };
9468
9517
  }
9469
9518
  //#endregion
9470
- //#region ../server/dist/app-EvpSNDM6.mjs
9519
+ //#region ../server/dist/app-B8Ncyl76.mjs
9471
9520
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
9472
9521
  init_esm();
9473
9522
  var __defProp = Object.defineProperty;
@@ -15218,10 +15267,18 @@ async function authRoutes(app) {
15218
15267
  return reply.send(result);
15219
15268
  });
15220
15269
  }
15221
- async function bootstrapConfigRoutes(app) {
15222
- /** Public endpoint — returns bootstrap prerequisites for CLI auto-discovery. */
15223
- app.get("/config", async () => {
15224
- 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 };
15225
15282
  });
15226
15283
  }
15227
15284
  /** Extract a plain-text summary from a message's JSONB content field.
@@ -15990,12 +16047,212 @@ async function clientRoutes(app) {
15990
16047
  });
15991
16048
  });
15992
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
+ }
15993
16238
  async function contextTreeInfoRoutes(app) {
15994
- /** Public endpoint — returns Context Tree repo metadata for CLI auto-discovery. */
15995
- 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);
15996
16253
  return {
15997
- repo: app.config.contextTree?.repo ?? null,
15998
- branch: app.config.contextTree?.branch ?? null
16254
+ repo: tree.repo ?? null,
16255
+ branch: tree.branch ?? null
15999
16256
  };
16000
16257
  });
16001
16258
  }
@@ -16021,10 +16278,10 @@ const WINDOW_DAYS = {
16021
16278
  "30d": 30
16022
16279
  };
16023
16280
  const snapshotCache = /* @__PURE__ */ new Map();
16024
- async function getContextTreeSnapshot(config, window = CONTEXT_TREE_SNAPSHOT_WINDOWS.SEVEN_DAYS) {
16025
- const repo = config.contextTree?.repo ?? null;
16026
- const branch = config.contextTree?.branch ?? null;
16027
- 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);
16028
16285
  if (!resolved.root) return unavailableSnapshot(repo, branch, resolved.reason);
16029
16286
  const now = (/* @__PURE__ */ new Date()).toISOString();
16030
16287
  try {
@@ -16696,7 +16953,9 @@ async function contextTreeSnapshotRoutes(app) {
16696
16953
  keyGenerator: (request) => request.user?.userId ?? request.ip
16697
16954
  } } }, async (request) => {
16698
16955
  const query = querySchema.parse(request.query);
16699
- 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");
16700
16959
  return contextTreeSnapshotSchema.parse(snapshot);
16701
16960
  });
16702
16961
  }
@@ -16801,7 +17060,7 @@ async function healthzRoutes(app) {
16801
17060
  * `api/orgs/invitations.ts` (Class B, admin-gated).
16802
17061
  */
16803
17062
  async function publicInvitationRoutes(app) {
16804
- const { previewInvitation } = await import("./invitation-DWlyNb8x-D3zjZSwI.mjs");
17063
+ const { previewInvitation } = await import("./invitation-DWlyNb8x-BvXubk24.mjs");
16805
17064
  app.get("/:token/preview", async (request, reply) => {
16806
17065
  if (!request.params.token) throw new UnauthorizedError("Token required");
16807
17066
  const preview = await previewInvitation(app.db, request.params.token);
@@ -16898,7 +17157,7 @@ async function meRoutes(app) {
16898
17157
  */
16899
17158
  app.get("/me/pinned-agents", async (request) => {
16900
17159
  const { userId } = requireUser(request);
16901
- const { listMyPinnedAgents } = await import("./client-By1K4VVT-C5K7WZo6.mjs");
17160
+ const { listMyPinnedAgents } = await import("./client-By1K4VVT-DuI6EnSh.mjs");
16902
17161
  return listMyPinnedAgents(app.db, { userId });
16903
17162
  });
16904
17163
  /**
@@ -17621,6 +17880,64 @@ async function orgSessionRoutes(app) {
17621
17880
  });
17622
17881
  });
17623
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
+ }
17624
17941
  function dispatch$1(notifier, result) {
17625
17942
  if (!result) return;
17626
17943
  notifyRecipients(notifier, result.recipients, result.message.id);
@@ -17920,16 +18237,15 @@ function verifySignature(secret, rawBody, signatureHeader) {
17920
18237
  const receivedBuf = Buffer.from(signatureHeader, "utf8");
17921
18238
  if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) throw new UnauthorizedError("Invalid webhook signature");
17922
18239
  }
17923
- async function ensureGitHubAdapterAgent(db) {
17924
- const defaultOrgId = await resolveDefaultOrgId(db);
17925
- 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);
17926
18242
  if (existing) return existing.uuid;
17927
18243
  try {
17928
18244
  return (await createAgent(db, {
17929
18245
  name: GITHUB_ADAPTER_ID,
17930
18246
  type: "autonomous_agent",
17931
18247
  displayName: "GitHub Adapter",
17932
- organizationId: defaultOrgId,
18248
+ organizationId,
17933
18249
  metadata: {
17934
18250
  source: "github",
17935
18251
  managed: true
@@ -17937,19 +18253,19 @@ async function ensureGitHubAdapterAgent(db) {
17937
18253
  })).uuid;
17938
18254
  } catch (err) {
17939
18255
  if (err instanceof ConflictError) {
17940
- 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);
17941
18257
  if (created) return created.uuid;
17942
18258
  }
17943
18259
  throw err;
17944
18260
  }
17945
18261
  }
17946
- async function findTargetAgent(db, repoFullName) {
18262
+ async function findTargetAgent(db, organizationId, repoFullName) {
17947
18263
  const allAgents = await db.select({
17948
18264
  id: agents.uuid,
17949
18265
  name: agents.name,
17950
18266
  metadata: agents.metadata,
17951
18267
  type: agents.type
17952
- }).from(agents).where(eq(agents.status, "active"));
18268
+ }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.status, "active")));
17953
18269
  for (const agent of allAgents) {
17954
18270
  if (agent.name === GITHUB_ADAPTER_ID) continue;
17955
18271
  const meta = agent.metadata;
@@ -17983,14 +18299,14 @@ function extractMentions$1(text) {
17983
18299
  * For each mentioned user who has delegate_mention configured,
17984
18300
  * send a card message from the mentioned user to their delegate.
17985
18301
  */
17986
- async function routeMentionDelegations(app, mentionedNames, ctx) {
18302
+ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx) {
17987
18303
  if (mentionedNames.length === 0) return 0;
17988
18304
  const delegates = await app.db.select({
17989
18305
  id: agents.uuid,
17990
18306
  name: agents.name,
17991
18307
  delegateMention: agents.delegateMention,
17992
18308
  status: agents.status
17993
- }).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)));
17994
18310
  let routed = 0;
17995
18311
  for (const agent of delegates) {
17996
18312
  if (agent.status !== "active" || !agent.delegateMention) continue;
@@ -18078,13 +18394,14 @@ async function githubWebhookRoutes(app) {
18078
18394
  app.addContentTypeParser("application/json", { parseAs: "buffer" }, (_request, body, done) => {
18079
18395
  done(null, body);
18080
18396
  });
18081
- const webhookSecret = app.config.github.webhookSecret;
18082
18397
  const webhookMax = app.config.rateLimit?.webhookMax ?? 60;
18083
- app.post("/github", { config: { rateLimit: {
18398
+ app.post("/github/:orgId", { config: { rateLimit: {
18084
18399
  max: webhookMax,
18085
18400
  timeWindow: "1 minute"
18086
18401
  } } }, async (request, reply) => {
18087
- 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." });
18088
18405
  const rawBody = request.body;
18089
18406
  if (!Buffer.isBuffer(rawBody)) throw new BadRequestError("Expected raw body buffer");
18090
18407
  const signatureHeader = request.headers["x-hub-signature-256"];
@@ -18102,12 +18419,12 @@ async function githubWebhookRoutes(app) {
18102
18419
  ok: true,
18103
18420
  event: "ping"
18104
18421
  });
18105
- if (eventType === "issues") return handleIssuesEvent(app, eventType, payload, reply);
18106
- 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);
18107
18424
  let mentionsRouted = 0;
18108
18425
  const allowedActions = MENTION_ACTIONS[eventType];
18109
18426
  const action = isRecord(payload) && typeof payload.action === "string" ? payload.action : void 0;
18110
- 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);
18111
18428
  return reply.status(200).send({
18112
18429
  ok: true,
18113
18430
  event: eventType,
@@ -18261,10 +18578,10 @@ function extractEventContext(eventType, payload) {
18261
18578
  * Run mention delegation for a given event type and payload.
18262
18579
  * Only called after action gating confirms this is a "new content" event.
18263
18580
  */
18264
- async function handleMentionDelegation(app, eventType, payload) {
18581
+ async function handleMentionDelegation(app, organizationId, eventType, payload) {
18265
18582
  const mentions = extractMentions$1(extractEventText(eventType, payload));
18266
18583
  const mentionCtx = extractEventContext(eventType, payload);
18267
- if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, mentions, mentionCtx);
18584
+ if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, organizationId, mentions, mentionCtx);
18268
18585
  return 0;
18269
18586
  }
18270
18587
  /** Actions that represent new/changed content (worth scanning for @mentions). */
@@ -18278,9 +18595,9 @@ const MENTION_ACTIONS = {
18278
18595
  discussion_comment: ["created"],
18279
18596
  commit_comment: ["created"]
18280
18597
  };
18281
- async function handleIssuesEvent(app, eventType, payload, reply) {
18598
+ async function handleIssuesEvent(app, organizationId, eventType, payload, reply) {
18282
18599
  const data = parseIssuesPayload(payload);
18283
- 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);
18284
18601
  if (![
18285
18602
  "opened",
18286
18603
  "edited",
@@ -18291,7 +18608,7 @@ async function handleIssuesEvent(app, eventType, payload, reply) {
18291
18608
  action: data.action,
18292
18609
  handled: false
18293
18610
  });
18294
- 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)]);
18295
18612
  if (!targetAgentId) {
18296
18613
  log$1.warn({
18297
18614
  repo: data.repository.full_name,
@@ -18337,16 +18654,16 @@ async function handleIssuesEvent(app, eventType, payload, reply) {
18337
18654
  routed: true
18338
18655
  });
18339
18656
  }
18340
- async function handleIssueCommentEvent(app, eventType, payload, reply) {
18657
+ async function handleIssueCommentEvent(app, organizationId, eventType, payload, reply) {
18341
18658
  const data = parseIssueCommentPayload(payload);
18342
- 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);
18343
18660
  if (data.action !== "created") return reply.status(200).send({
18344
18661
  ok: true,
18345
18662
  event: "issue_comment",
18346
18663
  action: data.action,
18347
18664
  handled: false
18348
18665
  });
18349
- 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)]);
18350
18667
  if (!targetAgentId) {
18351
18668
  log$1.warn({
18352
18669
  repo: data.repository.full_name,
@@ -18416,6 +18733,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
18416
18733
  members: () => members,
18417
18734
  messages: () => messages,
18418
18735
  notifications: () => notifications,
18736
+ organizationSettings: () => organizationSettings,
18419
18737
  organizations: () => organizations,
18420
18738
  serverInstances: () => serverInstances,
18421
18739
  sessionEvents: () => sessionEvents,
@@ -18424,10 +18742,16 @@ var schema_exports = /* @__PURE__ */ __exportAll({
18424
18742
  users: () => users
18425
18743
  });
18426
18744
  function connectDatabase(url) {
18427
- const client = postgres(url);
18745
+ const client = postgres(url, sslOptions(url));
18428
18746
  const db = drizzle(client, { schema: schema_exports });
18429
18747
  return Object.assign(db, { end: () => client.end() });
18430
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
+ }
18431
18755
  /**
18432
18756
  * Agent-scoped HTTP authentication hook. Must run **after** userAuthHook
18433
18757
  * so `request.user` is populated.
@@ -19802,7 +20126,10 @@ async function buildApp(config) {
19802
20126
  const commandVersion = resolveCommandVersion(config.commandVersion);
19803
20127
  app.decorate("commandVersion", commandVersion);
19804
20128
  app.log.info({ commandVersion }, "Hub server advertising command version");
19805
- const listenClient = postgres(config.database.url, { max: 1 });
20129
+ const listenClient = postgres(config.database.url, {
20130
+ max: 1,
20131
+ ...sslOptions(config.database.url)
20132
+ });
19806
20133
  const notifier = createNotifier(listenClient);
19807
20134
  await app.register(websocket, { options: { maxPayload: config.ws?.maxPayload ?? 65536 } });
19808
20135
  const corsOrigin = config.cors?.origin;
@@ -19887,9 +20214,9 @@ async function buildApp(config) {
19887
20214
  await api.register(authRoutes, { prefix: "/auth" });
19888
20215
  await api.register(githubOauthRoutes, { prefix: "/auth/github" });
19889
20216
  await api.register(publicInvitationRoutes, { prefix: "/invitations" });
19890
- await api.register(contextTreeInfoRoutes, { prefix: "/context-tree" });
19891
20217
  await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
19892
20218
  await api.register(userScope("contextTreeScope", async (scope) => {
20219
+ await scope.register(contextTreeInfoRoutes);
19893
20220
  await scope.register(contextTreeSnapshotRoutes);
19894
20221
  }), { prefix: "/context-tree" });
19895
20222
  await api.register(userScope("meRoutesScope", async (scope) => {
@@ -19910,6 +20237,7 @@ async function buildApp(config) {
19910
20237
  await scope.register(orgClientRoutes, { prefix: "/clients" });
19911
20238
  await scope.register(orgInvitationRoutes, { prefix: "/invitations" });
19912
20239
  await scope.register(orgMemberRoutes, { prefix: "/members" });
20240
+ await scope.register(orgSettingsRoutes, { prefix: "/settings" });
19913
20241
  }), { prefix: "/orgs/:orgId" });
19914
20242
  await api.register(orgWsRoutes(notifier, config.secrets.jwtSecret), { prefix: "/orgs/:orgId/ws" });
19915
20243
  await api.register(userScope("resourcesScope", async (scope) => {