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
107 changes: 103 additions & 4 deletions src/endpoints/sdk/apps.tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const tools: MakeTool[] = [
{
name: 'sdk-apps_get',
title: 'Get SDK app',
description: 'Get a SDK app by name and version.',
description: 'Get an SDK app by name and version.',
category: 'sdk-apps',
scope: 'sdk-apps:read',
scopeId: undefined,
Expand Down Expand Up @@ -154,7 +154,7 @@ export const tools: MakeTool[] = [
{
name: 'sdk-apps_delete',
title: 'Delete SDK app',
description: 'Delete a SDK app by name and version.',
description: 'Delete an SDK app by name and version.',
category: 'sdk-apps',
scope: 'sdk-apps:write',
scopeId: undefined,
Expand All @@ -179,7 +179,7 @@ export const tools: MakeTool[] = [
{
name: 'sdk-apps_get-section',
title: 'Get SDK app section',
description: 'Get a specific section of a SDK app.',
description: 'Get a specific section of an SDK app.',
category: 'sdk-apps',
scope: 'sdk-apps:read',
scopeId: undefined,
Expand Down Expand Up @@ -211,7 +211,7 @@ export const tools: MakeTool[] = [
{
name: 'sdk-apps_set-section',
title: 'Set SDK app section',
description: 'Set/update a specific section of a SDK app.',
description: 'Set/update a specific section of an SDK app.',
category: 'sdk-apps',
scope: 'sdk-apps:write',
scopeId: undefined,
Expand Down Expand Up @@ -323,6 +323,105 @@ export const tools: MakeTool[] = [
return await make.sdk.apps.getCommon(args.name, args.version);
},
},
{
name: 'sdk-apps_set-icon',
title: 'Set SDK app icon',
description: 'Upload an icon for an SDK app.',
category: 'sdk-apps',
scope: 'sdk-apps:write',
Comment thread
patriksimek marked this conversation as resolved.
scopeId: undefined,
identifier: undefined,
annotations: {
idempotentHint: true,
destructiveHint: false,
},
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'The name of the app' },
version: { type: 'number', description: 'The version of the app' },
dataBase64: { type: 'string', description: 'Base64-encoded 512x512 PNG icon data to upload' },
},
required: ['name', 'version', 'dataBase64'],
},
examples: [{ name: 'my-app', version: 1, dataBase64: 'iVBORw0KGgo...' }],
execute: async (make: Make, args: { name: string; version: number; dataBase64: string }) => {
await make.sdk.apps.setIcon(args.name, args.version, Buffer.from(args.dataBase64, 'base64'));
return `Icon has been set.`;
},
},
{
name: 'sdk-apps_get-icon',
title: 'Get SDK app icon',
description: 'Download an SDK app icon and return it as base64-encoded PNG data.',
category: 'sdk-apps',
Comment thread
patriksimek marked this conversation as resolved.
scope: 'sdk-apps:read',
scopeId: undefined,
identifier: undefined,
annotations: {
readOnlyHint: true,
},
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'The name of the app' },
version: { type: 'number', description: 'The version of the app' },
size: { type: 'number', description: 'Icon size to download', default: 512 },
},
required: ['name', 'version'],
},
examples: [{ name: 'my-app', version: 1, size: 512 }],
execute: async (make: Make, args: { name: string; version: number; size?: number }) => {
const icon = Buffer.from(await make.sdk.apps.getIcon(args.name, args.version, args.size ?? 512));
return icon.toString('base64');
},
},
{
name: 'sdk-apps_set-public',
title: 'Set SDK app public',
description: 'Mark an SDK app version as public.',
category: 'sdk-apps',
Comment thread
patriksimek marked this conversation as resolved.
scope: 'sdk-apps:write',
scopeId: undefined,
identifier: undefined,
annotations: { idempotentHint: true, destructiveHint: false },
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'The name of the app' },
version: { type: 'number', description: 'The version of the app' },
},
required: ['name', 'version'],
},
examples: [{ name: 'my-app', version: 1 }],
execute: async (make: Make, args: { name: string; version: number }) => {
await make.sdk.apps.makePublic(args.name, args.version);
return `App has been made public.`;
},
},
{
name: 'sdk-apps_set-private',
title: 'Set SDK app private',
description: 'Mark an SDK app version as private.',
category: 'sdk-apps',
Comment thread
patriksimek marked this conversation as resolved.
scope: 'sdk-apps:write',
scopeId: undefined,
identifier: undefined,
annotations: { idempotentHint: true, destructiveHint: false },
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'The name of the app' },
version: { type: 'number', description: 'The version of the app' },
},
required: ['name', 'version'],
},
examples: [{ name: 'my-app', version: 1 }],
execute: async (make: Make, args: { name: string; version: number }) => {
await make.sdk.apps.makePrivate(args.name, args.version);
return `App has been made private.`;
},
},
{
name: 'sdk-apps_set-common',
title: 'Set SDK app common data',
Expand Down
78 changes: 78 additions & 0 deletions src/endpoints/sdk/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,44 @@ type UpdateSDKAppResponse = {
app: Pick<SDKApp, 'name' | 'label' | 'description' | 'version' | 'theme' | 'public' | 'approved'>;
};

type IconUploadResponse = {
changed: boolean;
};

/** PNG file signature (first 8 bytes). */
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];

/** IHDR chunk type marker ("IHDR"), located at bytes 12-15 of a PNG. */
const PNG_IHDR = [0x49, 0x48, 0x44, 0x52];

/** Required app icon dimensions (square, in pixels). */
const APP_ICON_SIZE = 512;

/**
* Validate that an icon payload is a 512x512 PNG before upload, mirroring the
* server constraint so callers fail fast with a clear error. Platform-neutral
* (works with both `Uint8Array` and `ArrayBuffer`, no Node `Buffer` required).
*/
function assertAppIcon(iconData: Uint8Array | ArrayBuffer): void {
const bytes = iconData instanceof Uint8Array ? iconData : new Uint8Array(iconData);
if (bytes.length < 24 || PNG_SIGNATURE.some((byte, index) => bytes[index] !== byte)) {
throw new Error('App icon must be a PNG image.');
}

if (PNG_IHDR.some((byte, index) => bytes[12 + index] !== byte)) {
throw new Error('App icon is not a valid PNG image: missing IHDR chunk.');
}

const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const width = view.getUint32(16);
const height = view.getUint32(20);
if (width !== APP_ICON_SIZE || height !== APP_ICON_SIZE) {
throw new Error(
`App icon must be ${APP_ICON_SIZE}x${APP_ICON_SIZE} PNG. Got ${width}x${height}. Resize it first.`,
);
}
}

/**
* Class providing methods for working with Apps
*/
Expand Down Expand Up @@ -265,4 +303,44 @@ export class SDKApps {
});
return response.changed;
}

/**
* Upload an app icon. The icon must be a 512x512 PNG; otherwise an error is thrown before upload.
*/
async setIcon(name: string, version: number, iconData: Uint8Array | ArrayBuffer): Promise<boolean> {
assertAppIcon(iconData);
const response = await this.#fetch<IconUploadResponse>(`/sdk/apps/${name}/${version}/icon`, {
method: 'PUT',
headers: {
'Content-Type': 'image/png',
},
body: iconData,
});
return response.changed;
}

/**
* Download an app icon at a given rendered size.
*/
async getIcon(name: string, version: number, size = 512): Promise<ArrayBuffer> {
return await this.#fetch<ArrayBuffer>(`/sdk/apps/${name}/${version}/icon/${size}`);
}

/**
* Make app private.
*/
async makePrivate(name: string, version: number): Promise<void> {
await this.#fetch(`/sdk/apps/${name}/${version}/private`, {
method: 'POST',
});
}

/**
* Make app public.
*/
async makePublic(name: string, version: number): Promise<void> {
await this.#fetch(`/sdk/apps/${name}/${version}/public`, {
method: 'POST',
});
}
}
48 changes: 48 additions & 0 deletions src/endpoints/sdk/modules.tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,54 @@ export const tools: MakeTool[] = [
return await make.sdk.modules.getSection(args.appName, args.appVersion, args.moduleName, args.section);
},
},
{
name: 'sdk-modules_set-public',
title: 'Set SDK module public',
description: 'Mark an SDK app module as public.',
category: 'sdk-modules',
Comment thread
patriksimek marked this conversation as resolved.
scope: 'sdk-apps:write',
scopeId: undefined,
identifier: undefined,
annotations: { idempotentHint: true, destructiveHint: false },
inputSchema: {
type: 'object',
properties: {
appName: { type: 'string', description: 'The name of the app' },
appVersion: { type: 'number', description: 'The version of the app' },
moduleName: { type: 'string', description: 'The name of the module' },
},
required: ['appName', 'appVersion', 'moduleName'],
},
examples: [{ appName: 'my-app', appVersion: 1, moduleName: 'listItems' }],
execute: async (make: Make, args: { appName: string; appVersion: number; moduleName: string }) => {
await make.sdk.modules.makePublic(args.appName, args.appVersion, args.moduleName);
return `Module has been made public.`;
},
},
{
name: 'sdk-modules_set-private',
title: 'Set SDK module private',
description: 'Mark an SDK app module as private.',
category: 'sdk-modules',
Comment thread
patriksimek marked this conversation as resolved.
scope: 'sdk-apps:write',
scopeId: undefined,
identifier: undefined,
annotations: { idempotentHint: true, destructiveHint: false },
inputSchema: {
type: 'object',
properties: {
appName: { type: 'string', description: 'The name of the app' },
appVersion: { type: 'number', description: 'The version of the app' },
moduleName: { type: 'string', description: 'The name of the module' },
},
required: ['appName', 'appVersion', 'moduleName'],
},
examples: [{ appName: 'my-app', appVersion: 1, moduleName: 'listItems' }],
execute: async (make: Make, args: { appName: string; appVersion: number; moduleName: string }) => {
await make.sdk.modules.makePrivate(args.appName, args.appVersion, args.moduleName);
return `Module has been made private.`;
},
},
{
name: 'sdk-modules_set-section',
title: 'Set SDK module section',
Expand Down
14 changes: 14 additions & 0 deletions src/endpoints/sdk/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,18 @@ export class SDKModules {
body: JSONStringifyIfNotString(body),
});
}

/**
* Make a module private.
*/
async makePrivate(appName: string, appVersion: number, moduleName: string): Promise<void> {
await this.#fetch(`/sdk/apps/${appName}/${appVersion}/modules/${moduleName}/private`, { method: 'POST' });
}

/**
* Make a module public.
*/
async makePublic(appName: string, appVersion: number, moduleName: string): Promise<void> {
await this.#fetch(`/sdk/apps/${appName}/${appVersion}/modules/${moduleName}/public`, { method: 'POST' });
}
}
35 changes: 26 additions & 9 deletions src/make.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,20 +389,28 @@ export class Make {
/**
* Prepare the request body for API calls
*
* @param body The request body - can be an object, string, or undefined
* Objects and arrays are JSON-serialized (setting the JSON content-type),
* strings are passed through unchanged, and raw binary payloads
* (`Uint8Array`/`ArrayBuffer`) are returned as-is so the caller controls the
* content-type.
*
* @param body The request body - an object/array, string, raw binary payload, or undefined
* @param headers The headers object to potentially modify the content-type
* @returns The body serialized as a string
* @returns The JSON string for objects/arrays, or the string/binary/undefined body unchanged
* @protected
*/
protected prepareBody(
body: Record<string, JSONValue> | Array<JSONValue> | string | undefined,
body: Record<string, JSONValue> | Array<JSONValue> | string | Uint8Array | ArrayBuffer | undefined,
headers: Record<string, string>,
): string {
): string | Uint8Array | ArrayBuffer | undefined {
if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
return body;
}
if (body && typeof body !== 'string') {
headers['content-type'] = 'application/json';
return JSON.stringify(body);
}
return body as string;
return body as string | undefined;
}

/**
Expand Down Expand Up @@ -485,8 +493,9 @@ export class Make {
/**
* Handle successful API responses
*
* Parses the response based on content-type header.
* JSON responses are parsed as objects, other responses as text.
* Parses the response based on the content-type header: JSON responses are
* parsed as objects, binary responses (e.g. `image/*` or
* `application/octet-stream`) as an ArrayBuffer, and everything else as text.
*
* @template T The expected response type
* @param response The successful response from the API
Expand All @@ -498,8 +507,16 @@ export class Make {
const isJsonType: boolean = Boolean(
contentType === 'application/json' || contentType?.startsWith('application/json;'),
); //prevent application/jsonc to be parsed as json
const isBinaryType: boolean = Boolean(
contentType?.startsWith('image/') || contentType?.startsWith('application/octet-stream'),
);
Comment thread
patriksimek marked this conversation as resolved.

const result = isJsonType ? await response.json() : await response.text();
return result as T;
if (isJsonType) {
return (await response.json()) as T;
}
if (isBinaryType) {
return (await response.arrayBuffer()) as T;
}
return (await response.text()) as T;
}
}
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export type FetchOptions = {
headers?: Record<string, string>;
/** Query parameters to append to the URL */
query?: Record<string, QueryValue>;
/** Request body as an object or string */
body?: Record<string, JSONValue> | Array<JSONValue> | string;
/** Request body as an object, string, or raw binary payload */
body?: Record<string, JSONValue> | Array<JSONValue> | string | Uint8Array | ArrayBuffer;
/** HTTP method (GET, POST, PATCH, etc.) */
method?: string;
};
Expand Down
Loading