File size: 4,921 Bytes
3d700dd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | import { HttpError } from "@openhands/typescript-client";
import { getActiveBackend } from "../backend-registry/active-store";
import type { Backend } from "../backend-registry/types";
import { callCloudProxy } from "./proxy";
import type {
CloudApiKeyMetadata,
CloudOrganization,
CloudOrganizationMember,
CloudOrganizationsResponse,
} from "./types";
interface OrganizationsResult {
items: CloudOrganization[];
currentOrgId: string | null;
}
function normalizeResult(
data: CloudOrganizationsResponse | undefined | null,
): OrganizationsResult {
return {
items: data?.items ?? [],
currentOrgId: data?.current_org_id ?? null,
};
}
function resolveBackend(backend?: Backend): Backend {
if (backend) return backend;
const active = getActiveBackend().backend;
if (active.kind !== "cloud") {
throw new Error(
"Cloud organization calls require a cloud backend. Active backend is local.",
);
}
return active;
}
/**
* Fetch the org list for a cloud backend. With no argument, uses the active
* cloud backend; pass `backend` explicitly to fetch for an inactive cloud
* (used by the selector to flatten all cloud rows).
*
* Calls the cloud API directly with the backend's bearer token.
*/
export async function getCloudOrganizations(
backend?: Backend,
): Promise<OrganizationsResult> {
const target = resolveBackend(backend);
const data = await callCloudProxy<CloudOrganizationsResponse>({
backend: target,
method: "GET",
path: "/api/organizations",
});
return normalizeResult(data);
}
/**
* Fetch metadata for the API key used to authenticate this cloud backend.
* The returned `orgId` is the single org the key is authorized to act on
* (the cloud contract: one key → one org).
*
* Legacy keys minted before per-key org binding existed cause the upstream
* to return HTTP 400 — we surface that as `isLegacyKey: true` with a null
* `orgId` so the caller can fall back to the unfiltered behavior. Other
* statuses (401 revoked, 5xx outage) propagate so React Query can mark
* the query as failed and the selector can render the no-org-known row.
*/
export async function getCurrentCloudApiKey(
backend?: Backend,
): Promise<{ orgId: string | null; isLegacyKey: boolean }> {
const target = resolveBackend(backend);
try {
const data = await callCloudProxy<CloudApiKeyMetadata>({
backend: target,
method: "GET",
path: "/api/keys/current",
});
return { orgId: data?.org_id ?? null, isLegacyKey: false };
} catch (e) {
if (e instanceof HttpError && e.status === 400) {
return { orgId: null, isLegacyKey: true };
}
throw e;
}
}
/**
* Fetch `GET /api/organizations/{orgId}/me`. Identifies the calling user as
* a member of `orgId`. The GUI uses `me.org_id === me.user_id` to decide
* whether `orgId` is the user's personal workspace — that's the cloud
* contract (the auto-generated personal-workspace org has the same id as
* the user).
*
* `role` is the caller's role in the org (`owner` | `admin` | `member`, or
* `null` if the upstream omits it). `permissions` is the server-defined
* permission set for that role (e.g. `edit_org_settings`); it is `null` on
* older app-servers that don't return it, so callers fall back to the role.
* See `useCanManageOrgProfiles`.
*/
export async function getCloudOrganizationMe(
orgId: string,
backend?: Backend,
): Promise<{
orgId: string;
userId: string;
role: string | null;
permissions?: string[] | null;
}> {
const target = resolveBackend(backend);
const data = await callCloudProxy<{
org_id: string;
user_id: string;
role?: string;
permissions?: string[];
}>({
backend: target,
method: "GET",
path: `/api/organizations/${encodeURIComponent(orgId)}/me`,
});
return {
orgId: data?.org_id ?? orgId,
userId: data?.user_id ?? "",
role: data?.role ?? null,
// `null` when the field is absent (older app-server) so callers can fall
// back to a role check; a present array is the server's source of truth.
permissions: Array.isArray(data?.permissions) ? data.permissions : null,
};
}
/**
* Fetch `GET /api/organizations/{orgId}/members/{userId}`: one member of
* `orgId` by user id. Any org member may read it (same permission as the
* members list). The upstream responds 404 when `userId` is not (or no
* longer) a member of the org, and older app-servers that predate the route
* 404 as well, so callers should treat a failure as "identity unresolved".
*/
export async function getCloudOrganizationMember(
orgId: string,
userId: string,
backend?: Backend,
): Promise<CloudOrganizationMember> {
const target = resolveBackend(backend);
return callCloudProxy<CloudOrganizationMember>({
backend: target,
method: "GET",
path: `/api/organizations/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`,
});
}
|