From 69d1381ba334037cc57eef483c1b23b58b0f314e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 24 Nov 2025 22:58:47 -0500 Subject: core: refactor share system to separate session IDs from share IDs - Generate shorter share IDs from session IDs for better URL structure - Update API routes to use shareID parameter instead of sessionID - Improve sync mechanism with better data queuing and deduplication - Maintain backward compatibility while improving security and organization --- packages/enterprise/src/core/share.ts | 23 +- packages/enterprise/src/core/storage.ts | 1 - packages/enterprise/src/routes/api/[...path].ts | 28 +- .../enterprise/src/routes/share/[sessionID].tsx | 307 -------------------- packages/enterprise/src/routes/share/[shareID].tsx | 309 +++++++++++++++++++++ 5 files changed, 333 insertions(+), 335 deletions(-) delete mode 100644 packages/enterprise/src/routes/share/[sessionID].tsx create mode 100644 packages/enterprise/src/routes/share/[shareID].tsx (limited to 'packages/enterprise/src') diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts index c24077ec3..dff7d0d74 100644 --- a/packages/enterprise/src/core/share.ts +++ b/packages/enterprise/src/core/share.ts @@ -8,6 +8,7 @@ export namespace Share { export const Info = z.object({ id: z.string(), secret: z.string(), + sessionID: z.string(), }) export type Info = z.infer @@ -28,10 +29,6 @@ export namespace Share { type: z.literal("session_diff"), data: z.custom(), }), - z.object({ - type: z.literal("session_status"), - data: z.custom(), - }), z.object({ type: z.literal("model"), data: z.custom(), @@ -39,9 +36,10 @@ export namespace Share { ]) export type Data = z.infer - export const create = fn(Info.pick({ id: true }), async (body) => { + export const create = fn(z.object({ sessionID: z.string() }), async (body) => { const info: Info = { - id: body.id, + id: body.sessionID.slice(-8), + sessionID: body.sessionID, secret: crypto.randomUUID(), } const exists = await get(info.id) @@ -51,8 +49,8 @@ export namespace Share { return info }) - async function get(sessionID: string) { - return Storage.read(["share", sessionID]) + export async function get(id: string) { + return Storage.read(["share", id]) } export const remove = fn(Info.pick({ id: true, secret: true }), async (body) => { @@ -66,8 +64,8 @@ export namespace Share { } }) - export async function data(sessionID: string) { - const list = await Storage.list(["share_data", sessionID]) + export async function data(id: string) { + const list = await Storage.list(["share_data", id]) const promises = [] for (const item of list) { promises.push( @@ -85,7 +83,7 @@ export namespace Share { export const sync = fn( z.object({ - share: Info, + share: Info.pick({ id: true, secret: true }), data: Data.array(), }), async (input) => { @@ -112,9 +110,6 @@ export namespace Share { case "session_diff": await Storage.write(["share_data", input.share.id, "session_diff"], item.data) break - case "session_status": - await Storage.write(["share_data", input.share.id, "session_status"], item.data) - break case "model": await Storage.write(["share_data", input.share.id, "model"], item.data) break diff --git a/packages/enterprise/src/core/storage.ts b/packages/enterprise/src/core/storage.ts index fb33ef389..db03e2160 100644 --- a/packages/enterprise/src/core/storage.ts +++ b/packages/enterprise/src/core/storage.ts @@ -19,7 +19,6 @@ export namespace Storage { return { async read(path: string): Promise { try { - console.log("reading", bucket, path) const command = new GetObjectCommand({ Bucket: bucket, Key: path, diff --git a/packages/enterprise/src/routes/api/[...path].ts b/packages/enterprise/src/routes/api/[...path].ts index bbca171bd..e77c00de9 100644 --- a/packages/enterprise/src/routes/api/[...path].ts +++ b/packages/enterprise/src/routes/api/[...path].ts @@ -37,6 +37,7 @@ app schema: resolver( z .object({ + id: z.string(), url: z.string(), secret: z.string(), }) @@ -50,17 +51,18 @@ app validator("json", z.object({ sessionID: z.string() })), async (c) => { const body = c.req.valid("json") - const share = await Share.create({ id: body.sessionID }) + const share = await Share.create({ sessionID: body.sessionID }) 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({ + id: share.id, secret: share.secret, url: `${protocol}://${host}/share/${share.id}`, }) }, ) .post( - "/share/:sessionID/sync", + "/share/:shareID/sync", describeRoute({ description: "Sync share data", operationId: "share.sync", @@ -75,20 +77,20 @@ app }, }, }), - validator("param", z.object({ sessionID: z.string() })), + validator("param", z.object({ shareID: z.string() })), validator("json", z.object({ secret: z.string(), data: Share.Data.array() })), async (c) => { - const { sessionID } = c.req.valid("param") + const { shareID } = c.req.valid("param") const body = c.req.valid("json") await Share.sync({ - share: { id: sessionID, secret: body.secret }, + share: { id: shareID, secret: body.secret }, data: body.data, }) return c.json({}) }, ) .get( - "/share/:sessionID/data", + "/share/:shareID/data", describeRoute({ description: "Get share data", operationId: "share.data", @@ -103,14 +105,14 @@ app }, }, }), - validator("param", z.object({ sessionID: z.string() })), + validator("param", z.object({ shareID: z.string() })), async (c) => { - const { sessionID } = c.req.valid("param") - return c.json(await Share.data(sessionID)) + const { shareID } = c.req.valid("param") + return c.json(await Share.data(shareID)) }, ) .delete( - "/share/:sessionID", + "/share/:shareID", describeRoute({ description: "Remove a share", operationId: "share.remove", @@ -125,12 +127,12 @@ app }, }, }), - validator("param", z.object({ sessionID: z.string() })), + validator("param", z.object({ shareID: z.string() })), validator("json", z.object({ secret: z.string() })), async (c) => { - const { sessionID } = c.req.valid("param") + const { shareID } = c.req.valid("param") const body = c.req.valid("json") - await Share.remove({ id: sessionID, secret: body.secret }) + await Share.remove({ id: shareID, secret: body.secret }) return c.json({}) }, ) diff --git a/packages/enterprise/src/routes/share/[sessionID].tsx b/packages/enterprise/src/routes/share/[sessionID].tsx deleted file mode 100644 index 6c29437bc..000000000 --- a/packages/enterprise/src/routes/share/[sessionID].tsx +++ /dev/null @@ -1,307 +0,0 @@ -import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk" -import { SessionTurn } from "@opencode-ai/ui/session-turn" -import { SessionReview } from "@opencode-ai/ui/session-review" -import { DataProvider } from "@opencode-ai/ui/context" -import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router" -import { createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js" -import { Share } from "~/core/share" -import { Logo, Mark } from "@opencode-ai/ui/logo" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { iife } from "@opencode-ai/util/iife" -import { Binary } from "@opencode-ai/util/binary" -import { NamedError } from "@opencode-ai/util/error" -import { DateTime } from "luxon" -import { MessageNav } from "@opencode-ai/ui/message-nav" -import { createStore } from "solid-js/store" -import z from "zod" -import NotFound from "../[...404]" -import { Tabs } from "@opencode-ai/ui/tabs" - -const SessionDataMissingError = NamedError.create( - "SessionDataMissingError", - z.object({ - sessionID: z.string(), - message: z.string().optional(), - }), -) - -const getData = query(async (sessionID) => { - const data = await Share.data(sessionID) - const result: { - session: Session[] - session_diff: { - [sessionID: string]: FileDiff[] - } - session_status: { - [sessionID: string]: SessionStatus - } - message: { - [sessionID: string]: Message[] - } - part: { - [messageID: string]: Part[] - } - model: { - [sessionID: string]: Model[] - } - } = { - session: [], - session_diff: { - [sessionID]: [], - }, - session_status: { - [sessionID]: { - type: "idle", - }, - }, - message: {}, - part: {}, - model: {}, - } - for (const item of data) { - switch (item.type) { - case "session": - result.session.push(item.data) - break - case "session_diff": - result.session_diff[sessionID] = item.data - break - case "session_status": - result.session_status[sessionID] = item.data - break - case "message": - result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? [] - result.message[item.data.sessionID].push(item.data) - break - case "part": - result.part[item.data.messageID] = result.part[item.data.messageID] ?? [] - result.part[item.data.messageID].push(item.data) - break - case "model": - result.model[sessionID] = item.data - break - } - } - const match = Binary.search(result.session, sessionID!, (s) => s.id) - if (!match.found) throw new SessionDataMissingError({ sessionID }) - return result -}, "getShareData") - -export const route = { - preload: ({ params }) => getData(params.sessionID), -} satisfies RouteDefinition - -export default function () { - const params = useParams() - const data = createAsync(async () => { - if (!params.sessionID) throw new Error("Missing sessionID") - return getData(params.sessionID) - }) - - return ( - { - return ( - - - - ) - }} - > - - {(data) => ( - - {iife(() => { - const [store, setStore] = createStore({ - messageId: undefined as string | undefined, - }) - const match = createMemo(() => Binary.search(data().session, params.sessionID!, (s) => s.id)) - if (!match().found) throw new Error(`Session ${params.sessionID} not found`) - const info = createMemo(() => data().session[match().index]) - const messages = createMemo(() => - params.sessionID - ? (data().message[params.sessionID]?.filter((m) => m.role === "user") ?? []).sort( - (a, b) => b.time.created - a.time.created, - ) - : [], - ) - const firstUserMessage = createMemo(() => messages().at(0)) - const activeMessage = createMemo( - () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(), - ) - function setActiveMessage(message: UserMessage | undefined) { - if (message) { - setStore("messageId", message.id) - } else { - setStore("messageId", undefined) - } - } - const provider = createMemo(() => activeMessage()?.model?.providerID) - const modelID = createMemo(() => activeMessage()?.model?.modelID) - const model = createMemo(() => data().model[params.sessionID!]?.find((m) => m.id === modelID())) - const diffs = createMemo(() => data().session_diff[params.sessionID!] ?? []) - - const title = () => ( -
-
-
- -
v{info().version}
-
-
- -
{model()?.name ?? modelID()}
-
-
- {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")} -
-
-
{info().title}
-
- ) - - const turns = () => ( -
- {title()} -
- - {(message) => ( - - )} - -
-
- -
-
- ) - - const wide = createMemo(() => diffs().length === 0) - - return ( -
-
-
- - - -
-
- - -
-
-
- - - 0}> - - - - Session - - - 5 Files Changed - - - - {turns()} - - -
- -
-
-
-
- -
{turns()}
-
-
-
-
- ) - })} -
- )} -
-
- ) -} diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx new file mode 100644 index 000000000..61f2d23ea --- /dev/null +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -0,0 +1,309 @@ +import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk" +import { SessionTurn } from "@opencode-ai/ui/session-turn" +import { SessionReview } from "@opencode-ai/ui/session-review" +import { DataProvider } from "@opencode-ai/ui/context" +import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router" +import { createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js" +import { Share } from "~/core/share" +import { Logo, Mark } from "@opencode-ai/ui/logo" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { iife } from "@opencode-ai/util/iife" +import { Binary } from "@opencode-ai/util/binary" +import { NamedError } from "@opencode-ai/util/error" +import { DateTime } from "luxon" +import { MessageNav } from "@opencode-ai/ui/message-nav" +import { createStore } from "solid-js/store" +import z from "zod" +import NotFound from "../[...404]" +import { Tabs } from "@opencode-ai/ui/tabs" + +const SessionDataMissingError = NamedError.create( + "SessionDataMissingError", + z.object({ + sessionID: z.string(), + message: z.string().optional(), + }), +) + +const getData = query(async (shareID) => { + const share = await Share.get(shareID) + if (!share) throw new SessionDataMissingError({ sessionID: shareID }) + const data = await Share.data(shareID) + const result: { + sessionID: string + session: Session[] + session_diff: { + [sessionID: string]: FileDiff[] + } + session_status: { + [sessionID: string]: SessionStatus + } + message: { + [sessionID: string]: Message[] + } + part: { + [messageID: string]: Part[] + } + model: { + [sessionID: string]: Model[] + } + } = { + sessionID: share.sessionID, + session: [], + session_diff: { + [share.sessionID]: [], + }, + session_status: { + [share.sessionID]: { + type: "idle", + }, + }, + message: {}, + part: {}, + model: {}, + } + for (const item of data) { + switch (item.type) { + case "session": + result.session.push(item.data) + break + case "session_diff": + result.session_diff[share.sessionID] = item.data + break + case "message": + result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? [] + result.message[item.data.sessionID].push(item.data) + break + case "part": + result.part[item.data.messageID] = result.part[item.data.messageID] ?? [] + result.part[item.data.messageID].push(item.data) + break + case "model": + result.model[share.sessionID] = item.data + break + } + } + const match = Binary.search(result.session, share.sessionID, (s) => s.id) + if (!match.found) throw new SessionDataMissingError({ sessionID: share.sessionID }) + console.log(result) + return result +}, "getShareData") + +export const route = { + preload: ({ params }) => getData(params.shareID), +} satisfies RouteDefinition + +export default function () { + const params = useParams() + const data = createAsync(async () => { + if (!params.shareID) throw new Error("Missing sessionID") + return getData(params.shareID) + }) + + return ( + { + return ( + + + + ) + }} + > + + {(data) => ( + + {iife(() => { + const [store, setStore] = createStore({ + messageId: undefined as string | undefined, + }) + const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id)) + if (!match().found) throw new Error(`Session ${data().sessionID} not found`) + const info = createMemo(() => data().session[match().index]) + const messages = createMemo(() => + data().sessionID + ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort( + (a, b) => b.time.created - a.time.created, + ) + : [], + ) + const firstUserMessage = createMemo(() => messages().at(0)) + const activeMessage = createMemo( + () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(), + ) + function setActiveMessage(message: UserMessage | undefined) { + if (message) { + setStore("messageId", message.id) + } else { + setStore("messageId", undefined) + } + } + const provider = createMemo(() => activeMessage()?.model?.providerID) + const modelID = createMemo(() => activeMessage()?.model?.modelID) + const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID())) + const diffs = createMemo(() => data().session_diff[data().sessionID] ?? []) + + const title = () => ( +
+
+
+ +
v{info().version}
+
+
+ +
{model()?.name ?? modelID()}
+
+
+ {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")} +
+
+
{info().title}
+
+ ) + + const turns = () => ( +
+ {title()} +
+ + {(message) => ( + + )} + +
+
+ +
+
+ ) + + const wide = createMemo(() => diffs().length === 0) + + return ( +
+
+
+ + + +
+
+ + +
+
+
+ + + 0}> + + + + Session + + + 5 Files Changed + + + + {turns()} + + +
+ +
+
+
+
+ +
{turns()}
+
+
+
+
+ ) + })} +
+ )} +
+
+ ) +} -- cgit v1.2.3