mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
core: enable enterprise-managed session sharing with configurable enterprise URLs
When an enterprise URL is configured, session shares now route through the enterprise API instead of the public API, allowing organizations to manage session sharing infrastructure on their own servers.
This commit is contained in:
parent
fd4a93c621
commit
bf316018d7
10 changed files with 100 additions and 11 deletions
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["opencode-openai-codex-auth"],
|
||||
"enterprise": {
|
||||
"url": "http://localhost:3000",
|
||||
},
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"options": {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
"prepare": "husky",
|
||||
"random": "echo 'Random script'"
|
||||
"random": "echo 'Random script'",
|
||||
"hello": "echo 'Hello World!'"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export namespace Share {
|
|||
const exists = await get(info.id)
|
||||
if (exists) throw new Errors.AlreadyExists(info.id)
|
||||
await Storage.write(["share", info.id], info)
|
||||
console.log("created share", info.id)
|
||||
return info
|
||||
})
|
||||
|
||||
|
|
@ -50,6 +51,17 @@ export namespace Share {
|
|||
return Storage.read<Info>(["share", sessionID])
|
||||
}
|
||||
|
||||
export const remove = fn(Info.pick({ id: true, secret: true }), async (body) => {
|
||||
const share = await get(body.id)
|
||||
if (!share) throw new Errors.NotFound(body.id)
|
||||
if (share.secret !== body.secret) throw new Errors.InvalidSecret(body.id)
|
||||
await Storage.remove(["share", body.id])
|
||||
const list = await Storage.list(["share_data", body.id])
|
||||
for (const item of list) {
|
||||
await Storage.remove(item)
|
||||
}
|
||||
})
|
||||
|
||||
export async function data(sessionID: string) {
|
||||
const list = await Storage.list(["share_data", sessionID])
|
||||
const promises = []
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export namespace Storage {
|
|||
return {
|
||||
async read(path: string): Promise<string | undefined> {
|
||||
try {
|
||||
console.log("reading", bucket, path)
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: path,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Hono } from "hono"
|
||||
import { describeResponse, describeRoute, openAPIRouteHandler, resolver } from "hono-openapi"
|
||||
import { describeRoute, openAPIRouteHandler, resolver } from "hono-openapi"
|
||||
import { validator } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import { cors } from "hono/cors"
|
||||
|
|
@ -51,10 +51,11 @@ app
|
|||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
const share = await Share.create({ id: body.sessionID })
|
||||
console.log(share)
|
||||
const protocol = c.req.header("x-forwarded-proto") ?? c.req.header("x-forwarded-protocol") ?? "https"
|
||||
const host = c.req.header("x-forwarded-host") ?? c.req.header("host")
|
||||
return c.json({
|
||||
secret: share.secret,
|
||||
url: "/s/" + share.id,
|
||||
url: `${protocol}://${host}/share/${share.id}`,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
@ -108,6 +109,31 @@ app
|
|||
return c.json(await Share.data(sessionID))
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/share/:sessionID",
|
||||
describeRoute({
|
||||
description: "Remove a share",
|
||||
operationId: "share.remove",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Success",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.object({})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ sessionID: z.string() })),
|
||||
validator("json", z.object({ secret: z.string() })),
|
||||
async (c) => {
|
||||
const { sessionID } = c.req.valid("param")
|
||||
const body = c.req.valid("json")
|
||||
await Share.remove({ id: sessionID, secret: body.secret })
|
||||
return c.json({})
|
||||
},
|
||||
)
|
||||
|
||||
export function GET(event: APIEvent) {
|
||||
return app.fetch(event.request)
|
||||
|
|
|
|||
|
|
@ -609,6 +609,11 @@ export namespace Config {
|
|||
})
|
||||
.optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
enterprise: z
|
||||
.object({
|
||||
url: z.string().optional().describe("Enterprise URL"),
|
||||
})
|
||||
.optional(),
|
||||
experimental: z
|
||||
.object({
|
||||
hook: z
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { Snapshot } from "@/snapshot"
|
|||
import { SessionSummary } from "@/session/summary"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { SessionStatus } from "@/session/status"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
|
||||
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { SessionPrompt } from "./prompt"
|
|||
import { fn } from "@/util/fn"
|
||||
import { Command } from "../command"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
|
|
@ -221,6 +222,15 @@ export namespace Session {
|
|||
throw new Error("Sharing is disabled in configuration")
|
||||
}
|
||||
|
||||
if (cfg.enterprise?.url) {
|
||||
const share = await ShareNext.create(id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = {
|
||||
url: share.url,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const session = await get(id)
|
||||
if (session.share) return session.share
|
||||
const share = await Share.create(id)
|
||||
|
|
@ -241,6 +251,13 @@ export namespace Session {
|
|||
})
|
||||
|
||||
export const unshare = fn(Identifier.schema("session"), async (id) => {
|
||||
const cfg = await Config.get()
|
||||
if (cfg.enterprise?.url) {
|
||||
await ShareNext.remove(id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = undefined
|
||||
})
|
||||
}
|
||||
const share = await getShare(id)
|
||||
if (!share) return
|
||||
await Storage.remove(["share", id])
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import { Bus } from "@/bus"
|
||||
import { Config } from "@/config/config"
|
||||
import { Session } from "@/session"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { Log } from "@/util/log"
|
||||
import type * as SDK from "@opencode-ai/sdk"
|
||||
|
||||
export namespace ShareNext {
|
||||
const URL = `http://localhost:3000/api`
|
||||
|
||||
export function init() {
|
||||
const log = Log.create({ service: "share-next" })
|
||||
export async function init() {
|
||||
const config = await Config.get()
|
||||
if (!config.enterprise) return
|
||||
Bus.subscribe(Session.Event.Updated, async (evt) => {
|
||||
await sync(evt.properties.info.id, [
|
||||
{
|
||||
|
|
@ -43,7 +46,9 @@ export namespace ShareNext {
|
|||
}
|
||||
|
||||
export async function create(sessionID: string) {
|
||||
const result = await fetch(`${URL}/share`, {
|
||||
log.info("creating share", { sessionID })
|
||||
const url = await Config.get().then((x) => x.enterprise!.url)
|
||||
const result = await fetch(`${url}/api/share`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -57,6 +62,7 @@ export namespace ShareNext {
|
|||
...result,
|
||||
})
|
||||
fullSync(sessionID)
|
||||
return result
|
||||
}
|
||||
|
||||
function get(sessionID: string) {
|
||||
|
|
@ -86,9 +92,10 @@ export namespace ShareNext {
|
|||
}
|
||||
|
||||
async function sync(sessionID: string, data: Data[]) {
|
||||
const url = await Config.get().then((x) => x.enterprise!.url)
|
||||
const share = await get(sessionID)
|
||||
if (!share) return
|
||||
await fetch(`${URL}/share/${share.id}/sync`, {
|
||||
await fetch(`${url}/api/share/${share.id}/sync`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -100,7 +107,25 @@ export namespace ShareNext {
|
|||
})
|
||||
}
|
||||
|
||||
export async function remove(sessionID: string) {
|
||||
log.info("removing share", { sessionID })
|
||||
const url = await Config.get().then((x) => x.enterprise!.url)
|
||||
const share = await get(sessionID)
|
||||
if (!share) return
|
||||
await fetch(`${url}/api/share/${share.id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secret: share.secret,
|
||||
}),
|
||||
})
|
||||
await Storage.remove(["session_share", share.id])
|
||||
}
|
||||
|
||||
async function fullSync(sessionID: string) {
|
||||
log.info("full sync", { sessionID })
|
||||
const session = await Session.get(sessionID)
|
||||
const diffs = await Session.diff(sessionID)
|
||||
const messages = await Array.fromAsync(MessageV2.stream(sessionID))
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { Installation } from "../installation"
|
|||
import { Session } from "../session"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
import { Log } from "../util/log"
|
||||
import { ShareNext } from "./share-next"
|
||||
|
||||
export namespace Share {
|
||||
const log = Log.create({ service: "share" })
|
||||
|
|
@ -71,7 +70,6 @@ export namespace Share {
|
|||
(Installation.isPreview() || Installation.isLocal() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai")
|
||||
|
||||
export async function create(sessionID: string) {
|
||||
ShareNext.create(sessionID)
|
||||
return fetch(`${URL}/share_create`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ sessionID: sessionID }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue