Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions packages/opencode/src/altimate/workspace/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export interface DatamateRef {
* toggle in the workspace app, so callers that write memory must respect it.
* Undefined when the backend omitted the field. */
memoryEnabled?: boolean
/** The user who owns the workspace. Linking needs only visibility, but
* attaching a skill is a write against the workspace and needs ownership —
* a caller that can see a colleague's shared workspace may link to it and
* still not publish into it. Undefined when the backend omitted the field. */
ownerId?: number
}

export interface Binding {
Expand Down Expand Up @@ -153,14 +158,19 @@ async function req<T>(
* instead of throwing. Only set for endpoints known to return 204 or a
* bare 200 with no payload. */
allowEmptyBody?: boolean
/** Override the shared 15s budget. That budget was sized for small JSON
* exchanges and covers the request body too, so a call that uploads
* megabytes (a skill bundle) needs its own. */
timeoutMs?: number
} = {},
): Promise<T> {
const timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS
const { url, instance, apiKey } = await creds()
const qs = opts.query ? "?" + new URLSearchParams(opts.query).toString() : ""
const basePath = opts.base ?? "/datamate-project-bindings"
const target = `${url}${basePath}${subpath}${qs}`
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
const timeout = setTimeout(() => controller.abort(), timeoutMs)
let res: Response
let text: string
try {
Expand Down Expand Up @@ -204,7 +214,7 @@ async function req<T>(
const name = (err as { name?: string } | undefined)?.name
if (name === "AbortError") {
throw new WorkspaceApiError(
`Request to ${target} timed out after ${Math.round(REQUEST_TIMEOUT_MS / 1000)}s`,
`Request to ${target} timed out after ${Math.round(timeoutMs / 1000)}s`,
)
}
const msg = err instanceof Error ? err.message : String(err)
Expand Down Expand Up @@ -426,7 +436,7 @@ export namespace WorkspaceApi {
// bare ``[...]``, and a generic ``{data: [...]}`` — so a backend
// contract change (or compat layer) doesn't silently empty the picker.
// (cubic-dev-ai round 3.)
type Row = { id: number | string; name: string; memory_enabled?: boolean }
type Row = { id: number | string; name: string; memory_enabled?: boolean; user_id?: number }
const body = await req<Row[] | { datamates?: Row[]; data?: Row[] }>("GET", "/", {
base: "/datamates",
})
Expand Down Expand Up @@ -454,7 +464,22 @@ export namespace WorkspaceApi {
// per-element rather than per-envelope malformed value.
return rows
.filter((d): d is Row => d !== null && typeof d === "object")
.map((d) => ({ id: Number(d.id), name: d.name, memoryEnabled: d.memory_enabled }))
.map((d) => ({
id: Number(d.id),
name: d.name,
memoryEnabled: d.memory_enabled,
ownerId: Number.isInteger(d.user_id) ? d.user_id : undefined,
}))
.filter((d) => Number.isInteger(d.id) && d.id > 0 && typeof d.name === "string")
}

/** The caller's own user id, from ``GET /users/me``. Needed wherever the
* client must compare ownership — skill attachment requires the caller to
* OWN the workspace, and the credentials carry no user id of their own. */
export async function whoami(): Promise<number> {
const me = await req<{ id?: unknown }>("GET", "/me", { base: "/users" })
const id = Number(me?.id)
if (!Number.isInteger(id) || id <= 0) throw new WorkspaceApiError("The server did not say who this account is.")
Comment on lines +481 to +482

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-numeric user IDs before coercion

H7 does not hold for malformed non-number IDs because Number coerces values before validation. For example, GET /users/me returning {"id":true} is accepted as user 1; if the workspace list omits its owner, publishing proceeds and writes the ledger under u1 rather than throwing WorkspaceApiError as claimed.

Useful? React with 👍 / 👎.

return id
}
}
Loading
Loading