/**
* Craft.io API client
* Handles authentication and API requests to craft.io
*/
export interface CraftClientConfig {
apiKey: string;
workspaceId: string; // The actual workspace ID for /workspace/{id}/items
accountId?: string; // The account ID for /workspaces/{id} (listing workspaces)
portalId?: string;
baseUrl?: string;
}
export class CraftClient {
private apiKey: string;
private workspaceId: string;
private accountId?: string;
private portalId?: string;
private baseUrl: string;
constructor(config: CraftClientConfig) {
this.apiKey = config.apiKey;
this.workspaceId = config.workspaceId;
// If accountId not provided, use workspaceId for backward compatibility
this.accountId = config.accountId ?? config.workspaceId;
this.portalId = config.portalId;
this.baseUrl = config.baseUrl ?? "https://api.craft.io";
}
/**
* Make an authenticated request to the craft.io API
*/
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"x-api-key": this.apiKey,
...options.headers,
},
});
if (!response.ok) {
const body = await response.text();
throw new Error(
`Craft API error: ${response.status} ${response.statusText} - ${body}`
);
}
return response.json() as Promise<T>;
}
/**
* Get workspace details (lists workspaces for the account)
* Uses /workspaces/{accountId} endpoint
*/
async getWorkspace(): Promise<unknown> {
return this.request(`/workspaces/${this.accountId}`);
}
/**
* Get items from workspace
*/
async getItems(params?: { fields?: string; limit?: number }): Promise<unknown> {
const searchParams = new URLSearchParams();
if (params?.fields) searchParams.set("fields", params.fields);
if (params?.limit) searchParams.set("limit", params.limit.toString());
const query = searchParams.toString();
// Note: API uses singular /workspace/ not /workspaces/
const endpoint = `/workspace/${this.workspaceId}/items${query ? `?${query}` : ""}`;
return this.request(endpoint);
}
/**
* Get a specific item by ID (shortId like UPS-1234 or numeric ID)
*/
async getItem(itemId: string): Promise<unknown> {
// Check if it's a numeric ID (can use /item/{id} directly)
if (/^\d+$/.test(itemId)) {
return this.request(`/item/${itemId}`);
}
// It's a shortId - need to search through paginated items
const perPage = 100;
let page = 1;
let totalPages = 1;
do {
const response = await this.request<{
items: Array<{ id: string; shortId: string; [key: string]: unknown }>;
metadata: { totalPages: number };
}>(`/workspace/${this.workspaceId}/items?fields=all&limit=${perPage}&page=${page}`);
totalPages = response.metadata.totalPages;
// Find item with matching shortId
const item = response.items.find((i) => i.shortId === itemId);
if (item) {
// Found it - return the full item (already have all fields)
return item;
}
page++;
} while (page <= totalPages);
throw new Error(`Item with shortId "${itemId}" not found`);
}
/**
* Test API connectivity by fetching workspace
*/
async ping(): Promise<boolean> {
try {
await this.getWorkspace();
return true;
} catch {
return false;
}
}
}