@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.
- package/dist/{bootstrap-D4rdqM2F.mjs → bootstrap-D-Yf8yOc.mjs} +0 -15
- package/dist/cli/index.mjs +7 -7
- package/dist/client-By1K4VVT-DuI6EnSh.mjs +4 -0
- package/dist/{client-CLdRbuml-BRtalKpQ.mjs → client-CLdRbuml-svTO0Eat.mjs} +1 -1
- package/dist/{dist-BAqGZkco.mjs → dist-ClFs4WMj.mjs} +55 -1
- package/dist/drizzle/0032_organization_settings.sql +36 -0
- package/dist/drizzle/meta/_journal.json +8 -1
- package/dist/{feishu-Th_-ivJ7.mjs → feishu-AI3pwmqN.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/{invitation-DWlyNb8x-D3zjZSwI.mjs → invitation-DWlyNb8x-BvXubk24.mjs} +1 -1
- package/dist/{saas-connect-gcT6Q10z.mjs → saas-connect-CVoRK0Ex.mjs} +391 -63
- package/dist/web/assets/{index-CD7rTdqm.js → index-Bm6hgcvt.js} +1 -1
- package/dist/web/assets/{index-43trJLR8.js → index-k2bWRKc-.js} +87 -87
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/client-By1K4VVT-C5K7WZo6.mjs +0 -4
|
@@ -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-
|
|
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
|
|
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-
|
|
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
|
|
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, {
|
|
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-
|
|
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-
|
|
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(
|
|
15222
|
-
/**
|
|
15223
|
-
|
|
15224
|
-
|
|
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
|
-
/**
|
|
15995
|
-
|
|
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:
|
|
15998
|
-
branch:
|
|
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(
|
|
16025
|
-
const repo =
|
|
16026
|
-
const branch =
|
|
16027
|
-
const resolved = resolveContextTreeRoot(repo,
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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, {
|
|
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) => {
|