Apr 11, 2026 2.8 KB typescript
1import { requestUrl } from "obsidian";
2
3const API_BASE = "https://api.sublimated.com";
4
5export interface Version {
6 id: string;
7 number: number;
8 authorName: string;
9 authorType: string;
10 message: string | null;
11 createdAt: string;
12}
13
14export class AuthRequiredError extends Error {
15 constructor(public status: number) {
16 super(`auth required (${status})`);
17 this.name = "AuthRequiredError";
18 }
19}
20
21// Raw wire format from api.sublimated.com
22interface RawActor {
23 username: string | null;
24 name: string | null;
25 role: string;
26 actor_type: string;
27}
28
29interface RawVersion {
30 id: string;
31 item_id: string;
32 version: number;
33 content_hash: string;
34 blob_sha1: string | null;
35 size_bytes: number;
36 message: string | null;
37 name: string | null;
38 created_at: string;
39 actors: RawActor[];
40}
41
42interface ApiEnvelope<T> {
43 ok: boolean;
44 data?: T;
45 error?: { type: string; message?: string };
46}
47
48export interface TestConnectionResult {
49 ok: boolean;
50 username?: string;
51 error?: string;
52}
53
54export class SublimatedApi {
55 constructor(private token: string) {}
56
57 private async request<T>(path: string): Promise<T> {
58 const headers: Record<string, string> = {
59 Accept: "application/json",
60 };
61 if (this.token) {
62 headers.Authorization = `Bearer ${this.token}`;
63 }
64 const response = await requestUrl({
65 url: `${API_BASE}${path}`,
66 headers,
67 throw: false,
68 });
69
70 if (response.status === 401 || response.status === 403) {
71 throw new AuthRequiredError(response.status);
72 }
73 if (response.status >= 400) {
74 throw new Error(
75 `API error ${response.status}: ${response.text}`
76 );
77 }
78
79 const body = response.json as ApiEnvelope<T>;
80 if (!body.ok || body.data === undefined) {
81 const msg = body.error?.message || body.error?.type || "unknown error";
82 throw new Error(`API error: ${msg}`);
83 }
84 return body.data;
85 }
86
87 async getVersions(
88 spaceSlug: string,
89 itemId: string
90 ): Promise<Version[]> {
91 const raw = await this.request<RawVersion[]>(
92 `/v1/spaces/${encodeURIComponent(spaceSlug)}/items/${encodeURIComponent(itemId)}/versions`
93 );
94 return raw.map(mapVersion).reverse();
95 }
96
97 async testConnection(): Promise<TestConnectionResult> {
98 try {
99 const data = await this.request<{ username: string }>(
100 "/v1/me"
101 );
102 return { ok: true, username: data.username };
103 } catch (e) {
104 return {
105 ok: false,
106 error: e instanceof Error ? e.message : "Unknown error",
107 };
108 }
109 }
110}
111
112function mapVersion(r: RawVersion): Version {
113 // pick the author actor; fall back to the first actor or a placeholder
114 const author =
115 r.actors.find((a) => a.role === "author") ?? r.actors[0] ?? null;
116 return {
117 id: r.id,
118 number: r.version,
119 authorName: author?.name ?? author?.username ?? "unknown",
120 authorType: author?.actor_type ?? "person",
121 message: r.message,
122 createdAt: r.created_at,
123 };
124}