Download superlog/apps/api/src/settings.ts from smlflg/Experimente: direct link, hf CLI and curl.
- Browser
- Download file 18.6 kB
-
https://huggingface.co/smlflg/Experimente/resolve/main/superlog/apps/api/src/settings.ts
- Command line
-
hf download hf://smlflg/Experimente/superlog/apps/api/src/settings.ts
-
curl -L -o settings.ts https://huggingface.co/smlflg/Experimente/resolve/main/superlog/apps/api/src/settings.ts
18.6 kB
| import { db, encryptIntegrationSecret, schema } from "@superlog/db"; | |
| import { and, asc, eq, inArray, isNull } from "drizzle-orm"; | |
| import type { Hono } from "hono"; | |
| import type { Context } from "hono"; | |
| import { INTEGRATION_MANIFESTS } from "./integrations-manifest.js"; | |
| import { resolveActiveOrgContext } from "./org-context.js"; | |
| type Vars = { userId: string; orgId: string | null }; | |
| const ORG_INSTRUCTIONS_MAX_LEN = 8000; | |
| const PROJECT_CONTEXT_MAX_LEN = 8000; | |
| const PROJECT_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/; | |
| const PROJECT_NAME_MAX_LEN = 80; | |
| function slugifyProjectName(name: string): string { | |
| const base = name | |
| .toLowerCase() | |
| .replace(/[^a-z0-9]+/g, "-") | |
| .replace(/^-+|-+$/g, "") | |
| .slice(0, 40); | |
| return base || "project"; | |
| } | |
| // biome-ignore lint/suspicious/noExplicitAny: Hono Variables invariance. | |
| export function mountSettingsAuthed(app: Hono<any>): void { | |
| app.get("/api/org/agent-settings", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| if (!ctx) return c.json({ customInstructions: "" }); | |
| const row = await db.query.orgAgentSettings.findFirst({ | |
| where: eq(schema.orgAgentSettings.orgId, ctx.orgId), | |
| }); | |
| return c.json({ customInstructions: row?.customInstructions ?? "" }); | |
| }); | |
| app.put("/api/org/agent-settings", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| if (!ctx) return c.json({ error: "no org for user" }, 404); | |
| const body = (await c.req.json().catch(() => ({}))) as { | |
| customInstructions?: unknown; | |
| }; | |
| if (typeof body.customInstructions !== "string") { | |
| return c.json({ error: "customInstructions must be a string" }, 400); | |
| } | |
| const customInstructions = body.customInstructions.slice(0, ORG_INSTRUCTIONS_MAX_LEN); | |
| await db | |
| .insert(schema.orgAgentSettings) | |
| .values({ orgId: ctx.orgId, customInstructions }) | |
| .onConflictDoUpdate({ | |
| target: schema.orgAgentSettings.orgId, | |
| set: { customInstructions, updatedAt: new Date() }, | |
| }); | |
| return c.json({ customInstructions }); | |
| }); | |
| app.get("/api/org/digest", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| if (!ctx) { | |
| return c.json({ | |
| enabled: false, | |
| channelId: null, | |
| channelName: null, | |
| installationId: null, | |
| lastRunAt: null, | |
| }); | |
| } | |
| const row = await db.query.orgAgentSettings.findFirst({ | |
| where: eq(schema.orgAgentSettings.orgId, ctx.orgId), | |
| }); | |
| return c.json({ | |
| enabled: row?.digestEnabled ?? false, | |
| channelId: row?.digestSlackChannelId ?? null, | |
| channelName: row?.digestSlackChannelName ?? null, | |
| installationId: row?.digestSlackInstallationId ?? null, | |
| lastRunAt: row?.digestLastRunAt?.toISOString() ?? null, | |
| }); | |
| }); | |
| app.put("/api/org/digest", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| if (!ctx) return c.json({ error: "no org for user" }, 404); | |
| const body = (await c.req.json().catch(() => ({}))) as { | |
| enabled?: unknown; | |
| channelId?: unknown; | |
| channelName?: unknown; | |
| }; | |
| const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined; | |
| const channelIdRaw = | |
| body.channelId === null | |
| ? null | |
| : typeof body.channelId === "string" | |
| ? body.channelId | |
| : undefined; | |
| const channelNameRaw = | |
| body.channelName === null | |
| ? null | |
| : typeof body.channelName === "string" | |
| ? body.channelName | |
| : undefined; | |
| // Digest is org-scoped today; pick any active slack install in the org | |
| // by joining through projects. (One install per project — for orgs with | |
| // multiple projects + multiple installs this returns an arbitrary one, | |
| // same ambiguity as the previous org-scoped lookup.) | |
| const installs = await db | |
| .select({ | |
| id: schema.slackInstallations.id, | |
| }) | |
| .from(schema.slackInstallations) | |
| .innerJoin(schema.projects, eq(schema.projects.id, schema.slackInstallations.projectId)) | |
| .where(and(eq(schema.projects.orgId, ctx.orgId), isNull(schema.slackInstallations.revokedAt))) | |
| .limit(1); | |
| const install = installs[0] ?? null; | |
| if (enabled === true && !channelIdRaw && !install) { | |
| return c.json({ error: "Slack must be installed to enable the digest" }, 400); | |
| } | |
| const update: Partial<typeof schema.orgAgentSettings.$inferInsert> = { updatedAt: new Date() }; | |
| if (enabled !== undefined) update.digestEnabled = enabled; | |
| if (channelIdRaw !== undefined) { | |
| update.digestSlackChannelId = channelIdRaw; | |
| update.digestSlackInstallationId = channelIdRaw ? (install?.id ?? null) : null; | |
| } | |
| if (channelNameRaw !== undefined) update.digestSlackChannelName = channelNameRaw; | |
| await db | |
| .insert(schema.orgAgentSettings) | |
| .values({ | |
| orgId: ctx.orgId, | |
| digestEnabled: enabled ?? false, | |
| digestSlackChannelId: channelIdRaw ?? null, | |
| digestSlackChannelName: channelNameRaw ?? null, | |
| digestSlackInstallationId: | |
| channelIdRaw && install ? install.id : channelIdRaw === null ? null : null, | |
| }) | |
| .onConflictDoUpdate({ | |
| target: schema.orgAgentSettings.orgId, | |
| set: update, | |
| }); | |
| const row = await db.query.orgAgentSettings.findFirst({ | |
| where: eq(schema.orgAgentSettings.orgId, ctx.orgId), | |
| }); | |
| return c.json({ | |
| enabled: row?.digestEnabled ?? false, | |
| channelId: row?.digestSlackChannelId ?? null, | |
| channelName: row?.digestSlackChannelName ?? null, | |
| installationId: row?.digestSlackInstallationId ?? null, | |
| lastRunAt: row?.digestLastRunAt?.toISOString() ?? null, | |
| }); | |
| }); | |
| app.post("/api/org/digest/run-now", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| if (!ctx) return c.json({ error: "no org for user" }, 404); | |
| const row = await db.query.orgAgentSettings.findFirst({ | |
| where: eq(schema.orgAgentSettings.orgId, ctx.orgId), | |
| }); | |
| if (!row?.digestSlackChannelId || !row?.digestSlackInstallationId) { | |
| return c.json({ error: "configure a Slack channel for the digest first" }, 400); | |
| } | |
| // Worker fires next tick when last_run_at is null. We also bump enabled to | |
| // true so manual runs work even if the user hasn't flipped the toggle yet. | |
| await db | |
| .update(schema.orgAgentSettings) | |
| .set({ digestEnabled: true, digestLastRunAt: null, updatedAt: new Date() }) | |
| .where(eq(schema.orgAgentSettings.orgId, ctx.orgId)); | |
| return c.json({ ok: true }); | |
| }); | |
| app.get("/api/org/projects", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| if (!ctx) return c.json({ projects: [] }); | |
| const rows = await db.query.projects.findMany({ | |
| where: eq(schema.projects.orgId, ctx.orgId), | |
| orderBy: [asc(schema.projects.createdAt)], | |
| }); | |
| return c.json({ | |
| projects: rows.map((p) => ({ | |
| id: p.id, | |
| name: p.name, | |
| slug: p.slug, | |
| projectContext: p.projectContext, | |
| })), | |
| }); | |
| }); | |
| app.post("/api/org/projects", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| if (!ctx) return c.json({ error: "no org for user" }, 404); | |
| const body = (await c.req.json().catch(() => ({}))) as { | |
| name?: unknown; | |
| slug?: unknown; | |
| projectContext?: unknown; | |
| }; | |
| const name = typeof body.name === "string" ? body.name.trim() : ""; | |
| if (!name) return c.json({ error: "name is required" }, 400); | |
| if (name.length > PROJECT_NAME_MAX_LEN) { | |
| return c.json({ error: `name must be ≤ ${PROJECT_NAME_MAX_LEN} chars` }, 400); | |
| } | |
| const slugInput = | |
| typeof body.slug === "string" && body.slug.trim() | |
| ? body.slug.trim().toLowerCase() | |
| : slugifyProjectName(name); | |
| if (!PROJECT_SLUG_RE.test(slugInput) || slugInput.length > 40) { | |
| return c.json({ error: "slug must be lowercase alphanumeric + dashes, max 40 chars" }, 400); | |
| } | |
| const existing = await db.query.projects.findFirst({ | |
| where: and(eq(schema.projects.orgId, ctx.orgId), eq(schema.projects.slug, slugInput)), | |
| }); | |
| if (existing) return c.json({ error: "slug already in use in this org" }, 409); | |
| const [project] = await db | |
| .insert(schema.projects) | |
| .values({ | |
| orgId: ctx.orgId, | |
| name, | |
| slug: slugInput, | |
| projectContext: | |
| typeof body.projectContext === "string" | |
| ? body.projectContext.slice(0, PROJECT_CONTEXT_MAX_LEN) | |
| : "", | |
| }) | |
| .returning(); | |
| if (!project) return c.json({ error: "failed to create project" }, 500); | |
| await db | |
| .insert(schema.projectAutomationSettings) | |
| .values({ projectId: project.id }) | |
| .onConflictDoNothing({ target: schema.projectAutomationSettings.projectId }); | |
| return c.json({ | |
| project: { | |
| id: project.id, | |
| name: project.name, | |
| slug: project.slug, | |
| projectContext: project.projectContext, | |
| }, | |
| }); | |
| }); | |
| app.patch("/api/org/projects/:projectId", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| if (!ctx) return c.json({ error: "no org for user" }, 404); | |
| const projectId = c.req.param("projectId"); | |
| const target = await db.query.projects.findFirst({ | |
| where: and(eq(schema.projects.id, projectId), eq(schema.projects.orgId, ctx.orgId)), | |
| }); | |
| if (!target) return c.json({ error: "project not found" }, 404); | |
| const body = (await c.req.json().catch(() => ({}))) as { | |
| name?: unknown; | |
| slug?: unknown; | |
| projectContext?: unknown; | |
| }; | |
| const patch: Partial<typeof schema.projects.$inferInsert> = {}; | |
| if (typeof body.name === "string") { | |
| const name = body.name.trim(); | |
| if (!name) return c.json({ error: "name cannot be empty" }, 400); | |
| if (name.length > PROJECT_NAME_MAX_LEN) { | |
| return c.json({ error: `name must be ≤ ${PROJECT_NAME_MAX_LEN} chars` }, 400); | |
| } | |
| patch.name = name; | |
| } | |
| if (typeof body.slug === "string") { | |
| const slug = body.slug.trim().toLowerCase(); | |
| if (!PROJECT_SLUG_RE.test(slug) || slug.length > 40) { | |
| return c.json({ error: "slug must be lowercase alphanumeric + dashes, max 40 chars" }, 400); | |
| } | |
| if (slug !== target.slug) { | |
| const clash = await db.query.projects.findFirst({ | |
| where: and(eq(schema.projects.orgId, ctx.orgId), eq(schema.projects.slug, slug)), | |
| }); | |
| if (clash) return c.json({ error: "slug already in use in this org" }, 409); | |
| patch.slug = slug; | |
| } | |
| } | |
| if (typeof body.projectContext === "string") { | |
| patch.projectContext = body.projectContext.slice(0, PROJECT_CONTEXT_MAX_LEN); | |
| } | |
| if (Object.keys(patch).length === 0) { | |
| return c.json({ | |
| project: { | |
| id: target.id, | |
| name: target.name, | |
| slug: target.slug, | |
| projectContext: target.projectContext, | |
| }, | |
| }); | |
| } | |
| const [updated] = await db | |
| .update(schema.projects) | |
| .set(patch) | |
| .where(eq(schema.projects.id, target.id)) | |
| .returning(); | |
| // A concurrent DELETE between the findFirst above and this UPDATE leaves | |
| // `updated` undefined — return a clean 404 rather than a TypeError 500. | |
| if (!updated) return c.json({ error: "project not found" }, 404); | |
| return c.json({ | |
| project: { | |
| id: updated.id, | |
| name: updated.name, | |
| slug: updated.slug, | |
| projectContext: updated.projectContext, | |
| }, | |
| }); | |
| }); | |
| app.delete("/api/org/projects/:projectId", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| if (!ctx) return c.json({ error: "no org for user" }, 404); | |
| const projectId = c.req.param("projectId"); | |
| // The count-check and the delete must be atomic, or two concurrent | |
| // deletes in a 2-project org can both pass the `> 1` guard and leave | |
| // the org with zero projects. Wrap in a tx and take SELECT FOR UPDATE | |
| // locks on every project row in this org — any concurrent delete of a | |
| // sibling will block until our tx commits, at which point its count | |
| // sees the post-delete state. | |
| const result = await db.transaction(async (tx) => { | |
| const target = await tx.query.projects.findFirst({ | |
| where: and(eq(schema.projects.id, projectId), eq(schema.projects.orgId, ctx.orgId)), | |
| }); | |
| if (!target) return { status: 404 as const, body: { error: "project not found" } }; | |
| const locked = await tx | |
| .select({ id: schema.projects.id }) | |
| .from(schema.projects) | |
| .where(eq(schema.projects.orgId, ctx.orgId)) | |
| .for("update"); | |
| if (locked.length <= 1) { | |
| return { | |
| status: 409 as const, | |
| body: { error: "cannot delete the last project in an org" }, | |
| }; | |
| } | |
| await tx.delete(schema.projects).where(eq(schema.projects.id, target.id)); | |
| return { status: 200 as const, body: { ok: true } }; | |
| }); | |
| return c.json(result.body, result.status); | |
| }); | |
| app.get("/api/org/integrations", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| const installed = ctx | |
| ? await db.query.orgIntegrations.findMany({ | |
| where: eq(schema.orgIntegrations.orgId, ctx.orgId), | |
| }) | |
| : []; | |
| const installedBySlug = new Map(installed.map((row) => [row.slug, row])); | |
| const presentSecrets = ctx | |
| ? await db.query.orgIntegrationSecrets.findMany({ | |
| where: | |
| installed.length > 0 | |
| ? inArray( | |
| schema.orgIntegrationSecrets.orgIntegrationId, | |
| installed.map((row) => row.id), | |
| ) | |
| : undefined, | |
| }) | |
| : []; | |
| const presentByIntegrationId = new Map<string, Set<string>>(); | |
| for (const s of presentSecrets) { | |
| const set = presentByIntegrationId.get(s.orgIntegrationId) ?? new Set<string>(); | |
| set.add(s.secretName); | |
| presentByIntegrationId.set(s.orgIntegrationId, set); | |
| } | |
| const items = Object.values(INTEGRATION_MANIFESTS).map((manifest) => { | |
| const row = installedBySlug.get(manifest.slug); | |
| const present = row | |
| ? (presentByIntegrationId.get(row.id) ?? new Set<string>()) | |
| : new Set<string>(); | |
| return { | |
| slug: manifest.slug, | |
| name: manifest.name, | |
| description: manifest.description, | |
| installed: !!row, | |
| enabled: row?.enabled ?? false, | |
| required_secrets: manifest.required_secrets.map((spec) => ({ | |
| name: spec.name, | |
| description: spec.description, | |
| present: present.has(spec.name), | |
| })), | |
| }; | |
| }); | |
| return c.json({ integrations: items }); | |
| }); | |
| app.put("/api/org/integrations/:slug", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| if (!ctx) return c.json({ error: "no org for user" }, 404); | |
| const slug = c.req.param("slug"); | |
| const manifest = INTEGRATION_MANIFESTS[slug]; | |
| if (!manifest) return c.json({ error: "unknown integration" }, 404); | |
| const body = (await c.req.json().catch(() => ({}))) as { | |
| enabled?: unknown; | |
| secrets?: unknown; | |
| }; | |
| const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined; | |
| const secrets = | |
| body.secrets && typeof body.secrets === "object" && !Array.isArray(body.secrets) | |
| ? (body.secrets as Record<string, unknown>) | |
| : null; | |
| if (secrets) { | |
| for (const key of Object.keys(secrets)) { | |
| if (!manifest.required_secrets.some((s) => s.name === key)) { | |
| return c.json({ error: `unknown secret: ${key}` }, 400); | |
| } | |
| const value = secrets[key]; | |
| if (value !== null && typeof value !== "string") { | |
| return c.json({ error: `secret ${key} must be a string or null` }, 400); | |
| } | |
| } | |
| } | |
| const existing = await db.query.orgIntegrations.findFirst({ | |
| where: and( | |
| eq(schema.orgIntegrations.orgId, ctx.orgId), | |
| eq(schema.orgIntegrations.slug, slug), | |
| ), | |
| }); | |
| const integrationRow = existing | |
| ? ( | |
| await db | |
| .update(schema.orgIntegrations) | |
| .set({ | |
| enabled: enabled ?? existing.enabled, | |
| updatedAt: new Date(), | |
| }) | |
| .where(eq(schema.orgIntegrations.id, existing.id)) | |
| .returning() | |
| )[0]! | |
| : ( | |
| await db | |
| .insert(schema.orgIntegrations) | |
| .values({ | |
| orgId: ctx.orgId, | |
| slug, | |
| enabled: enabled ?? true, | |
| }) | |
| .returning() | |
| )[0]!; | |
| if (secrets) { | |
| for (const [name, value] of Object.entries(secrets)) { | |
| if (value === null) { | |
| await db | |
| .delete(schema.orgIntegrationSecrets) | |
| .where( | |
| and( | |
| eq(schema.orgIntegrationSecrets.orgIntegrationId, integrationRow.id), | |
| eq(schema.orgIntegrationSecrets.secretName, name), | |
| ), | |
| ); | |
| continue; | |
| } | |
| if (typeof value !== "string" || value.length === 0) continue; | |
| const cipher = encryptIntegrationSecret(value); | |
| await db | |
| .insert(schema.orgIntegrationSecrets) | |
| .values({ | |
| orgIntegrationId: integrationRow.id, | |
| secretName: name, | |
| ciphertext: cipher.ciphertext, | |
| nonce: cipher.nonce, | |
| keyVersion: cipher.keyVersion, | |
| }) | |
| .onConflictDoUpdate({ | |
| target: [ | |
| schema.orgIntegrationSecrets.orgIntegrationId, | |
| schema.orgIntegrationSecrets.secretName, | |
| ], | |
| set: { | |
| ciphertext: cipher.ciphertext, | |
| nonce: cipher.nonce, | |
| keyVersion: cipher.keyVersion, | |
| updatedAt: new Date(), | |
| }, | |
| }); | |
| } | |
| } | |
| return c.json({ ok: true }); | |
| }); | |
| app.delete("/api/org/integrations/:slug", async (c) => { | |
| const ctx = await resolveUserOrg(c); | |
| if (!ctx) return c.json({ error: "no org for user" }, 404); | |
| const slug = c.req.param("slug"); | |
| if (!INTEGRATION_MANIFESTS[slug]) return c.json({ error: "unknown integration" }, 404); | |
| await db | |
| .delete(schema.orgIntegrations) | |
| .where( | |
| and(eq(schema.orgIntegrations.orgId, ctx.orgId), eq(schema.orgIntegrations.slug, slug)), | |
| ); | |
| return c.json({ ok: true }); | |
| }); | |
| } | |
| async function resolveUserOrg( | |
| c: Context<{ Variables: Vars }>, | |
| ): Promise<{ userId: string; orgId: string } | null> { | |
| const userId = c.var.userId; | |
| if (!userId) return null; | |
| const ctx = await resolveActiveOrgContext({ | |
| userId, | |
| preferredOrgId: c.var.orgId, | |
| }).catch(() => null); | |
| if (!ctx) return null; | |
| return { userId: ctx.user.id, orgId: ctx.org.id }; | |
| } | |