585 lines
20 KiB
TypeScript
585 lines
20 KiB
TypeScript
// api.ts から分割(挙動不変): スペース(CRUD・メンバー・招待・ツールポリシー・Python パッケージ)。
|
|
import { BASE } from './client';
|
|
import type { SpaceTemplate } from '../../../src/spaces/space-templates';
|
|
|
|
// --- Spaces (個人/案件) ---------------------------------------------------
|
|
|
|
export interface Space {
|
|
id: string;
|
|
kind: 'personal' | 'case';
|
|
title: string;
|
|
description: string;
|
|
ownerId: string | null;
|
|
visibility: 'private' | 'org' | 'public';
|
|
visibilityScopeOrgId: string | null;
|
|
status: 'open' | 'archived';
|
|
brandColor: string | null;
|
|
workspaceDir: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
favorite?: boolean;
|
|
hidden?: boolean;
|
|
/** 閲覧者自身のメンバーロール。一覧(GET /spaces)・詳細(GET /spaces/:id)の両方で付与。
|
|
* 非メンバー(根オーナー含む。owner_id はメンバー行ではない)は null。 */
|
|
myRole?: SpaceMemberRole | null;
|
|
}
|
|
|
|
export async function fetchSpaces(): Promise<Space[]> {
|
|
const res = await fetch(`${BASE}/local/spaces`);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch spaces');
|
|
return data as Space[];
|
|
}
|
|
|
|
export async function updateSpaceDisplayPrefs(
|
|
id: string,
|
|
patch: { favorite?: boolean; hidden?: boolean },
|
|
): Promise<{ favorite: boolean; hidden: boolean }> {
|
|
const res = await fetch(`${BASE}/local/spaces/${id}/display`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to update workspace display settings');
|
|
return data as { favorite: boolean; hidden: boolean };
|
|
}
|
|
|
|
export type SpaceMemberRole = 'owner' | 'editor' | 'viewer';
|
|
|
|
export interface SpaceMember {
|
|
userId: string;
|
|
name: string | null;
|
|
email: string | null;
|
|
avatarUrl: string | null;
|
|
role: SpaceMemberRole;
|
|
isOwner: boolean;
|
|
}
|
|
|
|
export async function fetchSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/members`);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch members');
|
|
return data as SpaceMember[];
|
|
}
|
|
|
|
export async function addSpaceMember(
|
|
spaceId: string,
|
|
input: { userId: string; role: SpaceMemberRole },
|
|
): Promise<void> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/members`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
if (!res.ok) {
|
|
const d = await res.json().catch(() => ({}));
|
|
throw new Error(d?.error ?? 'Failed to add member');
|
|
}
|
|
}
|
|
|
|
export async function updateSpaceMemberRole(
|
|
spaceId: string,
|
|
userId: string,
|
|
role: SpaceMemberRole,
|
|
): Promise<void> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/members/${encodeURIComponent(userId)}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ role }),
|
|
});
|
|
if (!res.ok) {
|
|
const d = await res.json().catch(() => ({}));
|
|
throw new Error(d?.error ?? 'Failed to update member role');
|
|
}
|
|
}
|
|
|
|
export async function removeSpaceMember(spaceId: string, userId: string): Promise<void> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/members/${encodeURIComponent(userId)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
if (!res.ok) {
|
|
const d = await res.json().catch(() => ({}));
|
|
throw new Error(d?.error ?? 'Failed to remove member');
|
|
}
|
|
}
|
|
|
|
// ─── ワークスペース招待リンク(再利用トークン)──────────────────────
|
|
export type SpaceInviteRole = 'editor' | 'viewer';
|
|
|
|
export interface SpaceInviteInfo {
|
|
token: string;
|
|
/** 相対パス。origin を前置して共有する。 */
|
|
url: string;
|
|
role: SpaceInviteRole;
|
|
createdAt: string;
|
|
expiresAt: string | null;
|
|
revokedAt: string | null;
|
|
valid: boolean;
|
|
}
|
|
|
|
/** 現行の招待リンク(canManageSpace)。無ければ null。 */
|
|
export async function fetchSpaceInvite(spaceId: string): Promise<SpaceInviteInfo | null> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/invite`);
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch invite');
|
|
return (data?.invite ?? null) as SpaceInviteInfo | null;
|
|
}
|
|
|
|
/** 招待リンクを (再)生成する(canManageSpace)。 */
|
|
export async function createSpaceInvite(
|
|
spaceId: string,
|
|
input: { role: SpaceInviteRole; expiresInDays?: number | null },
|
|
): Promise<SpaceInviteInfo> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/invite`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to create invite');
|
|
return data.invite as SpaceInviteInfo;
|
|
}
|
|
|
|
/** 招待リンクを無効化する(canManageSpace)。 */
|
|
export async function revokeSpaceInvite(spaceId: string): Promise<void> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/invite`, { method: 'DELETE' });
|
|
if (!res.ok && res.status !== 204) {
|
|
const d = await res.json().catch(() => ({}));
|
|
throw new Error(d?.error ?? 'Failed to revoke invite');
|
|
}
|
|
}
|
|
|
|
export interface InvitePreview {
|
|
spaceId: string;
|
|
spaceTitle: string;
|
|
role: SpaceInviteRole;
|
|
}
|
|
|
|
/**
|
|
* 招待トークンのプレビュー。`status` で分岐を呼び出し側に渡す:
|
|
* 'ok' → preview / 'unauthorized'(要ログイン)/ 'invalid'(無効・期限切れ・不明)。
|
|
*/
|
|
export async function fetchInvitePreview(
|
|
token: string,
|
|
): Promise<{ status: 'ok'; preview: InvitePreview } | { status: 'unauthorized' | 'invalid' }> {
|
|
const res = await fetch(`${BASE}/local/spaces/invite/${encodeURIComponent(token)}`);
|
|
if (res.status === 401) return { status: 'unauthorized' };
|
|
if (!res.ok) return { status: 'invalid' };
|
|
const data = await res.json();
|
|
return { status: 'ok', preview: data as InvitePreview };
|
|
}
|
|
|
|
/** 招待を受諾して参加する。参加先 spaceId を返す。 */
|
|
export async function acceptSpaceInvite(token: string): Promise<{ spaceId: string; alreadyMember: boolean }> {
|
|
const res = await fetch(`${BASE}/local/spaces/invite/${encodeURIComponent(token)}/accept`, {
|
|
method: 'POST',
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to accept invite');
|
|
return data as { spaceId: string; alreadyMember: boolean };
|
|
}
|
|
|
|
|
|
// ─── ワークスペース ツールポリシー ────────────────────────────────────────
|
|
|
|
export interface ToolCategory {
|
|
name: string;
|
|
sensitive: boolean;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface SpaceToolPolicy {
|
|
disabledSafe: string[];
|
|
enabledSensitive: string[];
|
|
/** 3 値: undefined = 未設定(全体設定を継承)/ true = 許可 / false = 遮断。 */
|
|
allowNetwork?: boolean;
|
|
}
|
|
|
|
/** サンドボックスから外部ネットワークへ出られるかの、いまの状態(issue #1015)。 */
|
|
export interface SandboxNetworkState {
|
|
/** ワークスペースの設定。null = 未設定(全体設定を継承)。 */
|
|
workspace: boolean | null;
|
|
/** 全体設定(config.yaml の safety.bash_allow_network)。 */
|
|
global: boolean;
|
|
/** 実際に適用される値。 */
|
|
effective: boolean;
|
|
/** effective がどちらの設定から来たか。 */
|
|
source: 'workspace' | 'global';
|
|
/** サンドボックス機構の選択。'off' のときこの切り替えは効かない。 */
|
|
sandboxMode: 'auto' | 'always' | 'off';
|
|
/**
|
|
* 隔離の下地(bwrap)が実際に動いたか(issue #1215)。
|
|
* サーバが 1 回だけ走らせた点検の結果。省略されるのは古いサーバの応答だけ。
|
|
*/
|
|
bwrapAvailable?: boolean;
|
|
/** Bash の制限を外す設定。true のときこの切り替えは効かない。 */
|
|
bashUnrestricted: boolean;
|
|
}
|
|
|
|
export interface SpaceToolPolicyResponse {
|
|
policy: SpaceToolPolicy;
|
|
categories: ToolCategory[];
|
|
sensitiveTools: { name: string; enabled: boolean }[];
|
|
sandboxNetwork?: SandboxNetworkState;
|
|
}
|
|
|
|
export async function fetchSpaceToolPolicy(spaceId: string): Promise<SpaceToolPolicyResponse> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/tool-policy`);
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch tool policy');
|
|
return data as SpaceToolPolicyResponse;
|
|
}
|
|
|
|
export async function updateSpaceToolPolicy(
|
|
spaceId: string,
|
|
// allowNetwork: キーを送らなければ現状維持、null で未設定に戻す、boolean で設定。
|
|
patch: { disabledSafe?: string[]; enabledSensitive?: string[]; allowNetwork?: boolean | null },
|
|
): Promise<void> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/tool-policy`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch),
|
|
});
|
|
if (!res.ok) {
|
|
const d = await res.json().catch(() => ({}));
|
|
throw new Error(d?.error ?? 'Failed to update tool policy');
|
|
}
|
|
}
|
|
|
|
// ─── Workspace Auto Prompter rules ───────────────────────────────────────
|
|
|
|
export type AutoPrompterTiming = 'always' | 'tool.before' | 'completion.before_accept';
|
|
export type AutoPrompterToolCategory =
|
|
| 'command_execution'
|
|
| 'internet_search'
|
|
| 'file_read'
|
|
| 'file_write'
|
|
| 'external_communication'
|
|
| 'browser'
|
|
| 'ssh';
|
|
export type AutoPrompterAction =
|
|
| { type: 'allow' }
|
|
| { type: 'deny'; reason: string }
|
|
| { type: 'advise'; instruction: string }
|
|
| { type: 'approval'; message: string };
|
|
|
|
export interface AutoPrompterRule {
|
|
id: string;
|
|
name?: string;
|
|
condition?: string;
|
|
version: number;
|
|
enabled: boolean;
|
|
timing: AutoPrompterTiming;
|
|
target?: {
|
|
toolNames?: string[];
|
|
toolCategories?: AutoPrompterToolCategory[];
|
|
};
|
|
scope?: {
|
|
pieces?: string[];
|
|
movements?: string[];
|
|
};
|
|
fireLimits?: {
|
|
perTask?: number;
|
|
perMovement?: number;
|
|
};
|
|
action: AutoPrompterAction;
|
|
}
|
|
|
|
export interface SpaceAutoPrompterResponse {
|
|
enabled: boolean;
|
|
rules: AutoPrompterRule[];
|
|
}
|
|
|
|
const autoPrompterBase = (spaceId: string): string =>
|
|
`${BASE}/local/spaces/${encodeURIComponent(spaceId)}/auto-prompter`;
|
|
|
|
const autoPrompterResponse = async (res: Response): Promise<SpaceAutoPrompterResponse> => {
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to update Auto Prompter');
|
|
return data as SpaceAutoPrompterResponse;
|
|
};
|
|
|
|
export async function fetchSpaceAutoPrompter(spaceId: string): Promise<SpaceAutoPrompterResponse> {
|
|
return autoPrompterResponse(await fetch(autoPrompterBase(spaceId)));
|
|
}
|
|
|
|
export async function updateSpaceAutoPrompterEnabled(
|
|
spaceId: string,
|
|
enabled: boolean,
|
|
): Promise<SpaceAutoPrompterResponse> {
|
|
return autoPrompterResponse(await fetch(autoPrompterBase(spaceId), {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ enabled }),
|
|
}));
|
|
}
|
|
|
|
export async function createSpaceAutoPrompterRule(
|
|
spaceId: string,
|
|
rule: AutoPrompterRule,
|
|
): Promise<AutoPrompterRule> {
|
|
const res = await fetch(`${autoPrompterBase(spaceId)}/rules`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(rule),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to create Auto Prompter rule');
|
|
return data.rule as AutoPrompterRule;
|
|
}
|
|
|
|
export async function updateSpaceAutoPrompterRule(
|
|
spaceId: string,
|
|
rule: AutoPrompterRule,
|
|
): Promise<AutoPrompterRule> {
|
|
const res = await fetch(`${autoPrompterBase(spaceId)}/rules/${encodeURIComponent(rule.id)}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(rule),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to update Auto Prompter rule');
|
|
return data.rule as AutoPrompterRule;
|
|
}
|
|
|
|
export async function deleteSpaceAutoPrompterRule(
|
|
spaceId: string,
|
|
ruleId: string,
|
|
version: number,
|
|
): Promise<void> {
|
|
const res = await fetch(`${autoPrompterBase(spaceId)}/rules/${encodeURIComponent(ruleId)}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ version }),
|
|
});
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
throw new Error(data?.error ?? 'Failed to delete Auto Prompter rule');
|
|
}
|
|
}
|
|
|
|
// ─── Per-space Python packages ────────────────────────────────────────────
|
|
export interface SpacePythonPackage {
|
|
name: string;
|
|
spec: string;
|
|
addedAt: string;
|
|
}
|
|
export interface SpacePythonPackagesResponse {
|
|
enabled: boolean;
|
|
maxPackagesPerSpace: number;
|
|
preflight: { ok: boolean; reason?: string };
|
|
packages: SpacePythonPackage[];
|
|
}
|
|
|
|
export async function fetchSpacePythonPackages(spaceId: string): Promise<SpacePythonPackagesResponse> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/python-packages`);
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch python packages');
|
|
return data as SpacePythonPackagesResponse;
|
|
}
|
|
|
|
export async function addSpacePythonPackage(
|
|
spaceId: string,
|
|
spec: string,
|
|
): Promise<{ packages: SpacePythonPackage[] }> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/python-packages`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ spec }),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to add package');
|
|
return data as { packages: SpacePythonPackage[] };
|
|
}
|
|
|
|
export async function removeSpacePythonPackage(
|
|
spaceId: string,
|
|
name: string,
|
|
): Promise<{ packages: SpacePythonPackage[] }> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/python-packages/${encodeURIComponent(name)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to remove package');
|
|
return data as { packages: SpacePythonPackage[] };
|
|
}
|
|
|
|
// ─── Per-space webhook notifications (issue #797, PR1: Discord only) ─────
|
|
export type SpaceWebhookProvider = 'discord' | 'slack' | 'teams';
|
|
export type SpaceWebhookEvent = 'succeeded' | 'failed' | 'waiting_human';
|
|
|
|
export interface SpaceWebhook {
|
|
id: string;
|
|
provider: SpaceWebhookProvider;
|
|
label: string;
|
|
events: SpaceWebhookEvent[];
|
|
includeDetails: boolean;
|
|
enabled: boolean;
|
|
disabledReason: string | null;
|
|
lastSuccessAt: string | null;
|
|
lastFailureAt: string | null;
|
|
failureCount: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
// NOTE: no `url` / `urlEnc` field — the server never returns the webhook
|
|
// URL once saved (write-only secret).
|
|
}
|
|
|
|
export interface SpaceWebhookList {
|
|
webhooks: SpaceWebhook[];
|
|
/**
|
|
* Server-side master switch (`webhooks.enabled` in config.yaml). When
|
|
* false the delivery subsystem was never started, so a test send is
|
|
* refused before any connection is opened. The screen has to say so —
|
|
* without it the only feedback was a bare "送信失敗" (issue #1125).
|
|
*/
|
|
deliveryEnabled: boolean;
|
|
}
|
|
|
|
export async function fetchSpaceWebhooks(spaceId: string): Promise<SpaceWebhookList> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/webhooks`);
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch webhooks');
|
|
return {
|
|
webhooks: (data?.webhooks ?? []) as SpaceWebhook[],
|
|
// Absent field → treated as enabled. Blocking test sends on a server
|
|
// that is actually delivering would be the worse mistake of the two.
|
|
deliveryEnabled: data?.deliveryEnabled !== false,
|
|
};
|
|
}
|
|
|
|
export async function createSpaceWebhook(
|
|
spaceId: string,
|
|
input: { provider: SpaceWebhookProvider; label: string; url: string; events: SpaceWebhookEvent[]; includeDetails?: boolean },
|
|
): Promise<SpaceWebhook> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/webhooks`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to create webhook');
|
|
return data as SpaceWebhook;
|
|
}
|
|
|
|
export async function updateSpaceWebhook(
|
|
spaceId: string,
|
|
webhookId: string,
|
|
patch: { label?: string; url?: string; events?: SpaceWebhookEvent[]; includeDetails?: boolean },
|
|
): Promise<SpaceWebhook> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/webhooks/${webhookId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to update webhook');
|
|
return data as SpaceWebhook;
|
|
}
|
|
|
|
export async function setSpaceWebhookEnabled(
|
|
spaceId: string,
|
|
webhookId: string,
|
|
enabled: boolean,
|
|
): Promise<SpaceWebhook> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/webhooks/${webhookId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ enabled }),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to update webhook');
|
|
return data as SpaceWebhook;
|
|
}
|
|
|
|
export async function deleteSpaceWebhook(spaceId: string, webhookId: string): Promise<void> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/webhooks/${webhookId}`, { method: 'DELETE' });
|
|
if (!res.ok) {
|
|
const d = await res.json().catch(() => ({}));
|
|
throw new Error(d?.error ?? 'Failed to delete webhook');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Why a test send failed, so the screen can word it differently (issue
|
|
* #1125): delivery is switched off server-side, the destination answered and
|
|
* refused (`status` carries its HTTP status), or nothing answered at all.
|
|
*/
|
|
export type SpaceWebhookTestFailureCode =
|
|
| 'delivery_disabled'
|
|
| 'rejected'
|
|
| 'network'
|
|
| 'blocked'
|
|
| 'invalid_url'
|
|
| 'not_found'
|
|
| 'not_implemented'
|
|
| 'internal';
|
|
|
|
export interface SpaceWebhookTestResult {
|
|
ok: boolean;
|
|
error?: string;
|
|
code?: SpaceWebhookTestFailureCode;
|
|
status?: number;
|
|
}
|
|
|
|
export async function testSendSpaceWebhook(
|
|
spaceId: string,
|
|
webhookId: string,
|
|
): Promise<SpaceWebhookTestResult> {
|
|
const res = await fetch(`${BASE}/local/spaces/${spaceId}/webhooks/${webhookId}/test`, { method: 'POST' });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
return {
|
|
ok: false,
|
|
error: data?.error ?? 'Failed to send test notification',
|
|
code: data?.code,
|
|
status: data?.status,
|
|
};
|
|
}
|
|
return data as SpaceWebhookTestResult;
|
|
}
|
|
|
|
export async function createSpace(input: {
|
|
title: string;
|
|
description?: string;
|
|
brandColor?: string | null;
|
|
visibility?: Space['visibility'];
|
|
/**
|
|
* 用途テンプレート(issue #993 S2-3)。差し替わるのは AGENTS.md の雛形だけで、
|
|
* ワークスペースの属性としては残らない。未指定なら従来どおりの雛形。
|
|
*/
|
|
template?: SpaceTemplate;
|
|
}): Promise<Space> {
|
|
const res = await fetch(`${BASE}/local/spaces`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to create space');
|
|
return data as Space;
|
|
}
|
|
|
|
export async function updateSpace(
|
|
id: string,
|
|
patch: Partial<Pick<Space, 'title' | 'description' | 'brandColor' | 'visibility'>>,
|
|
): Promise<Space> {
|
|
const res = await fetch(`${BASE}/local/spaces/${id}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data?.error ?? 'Failed to update space');
|
|
return data as Space;
|
|
}
|
|
|
|
export async function archiveSpace(id: string): Promise<void> {
|
|
const res = await fetch(`${BASE}/local/spaces/${id}/archive`, { method: 'POST' });
|
|
if (!res.ok) {
|
|
const d = await res.json().catch(() => ({}));
|
|
throw new Error(d?.error ?? 'Failed to archive space');
|
|
}
|
|
}
|