Skip to content
Merged
37 changes: 37 additions & 0 deletions src/endpoints/credential-requests.tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,4 +456,41 @@ export const tools = [
return await make.credentialRequests.extendConnection(args);
},
},
{
name: 'credential-requests_list-app-modules-with-credentials',
title: 'List app modules with credentials',
description:
'List all modules of a given Make app (and version) that require credentials, along with the required credential type and OAuth scopes. ' +
'Use this to discover which modules exist for an app before constructing a credential request — the returned `id` values are what you pass in `credentials[].appModules` for `credential-requests_create`. ' +
'For custom/SDK apps, prefix the app name with `app#` (e.g. `app#my-custom-app`).',
category: 'credential-requests',
scope: 'apps:read',
scopeId: 'appName',
identifier: 'appName',
annotations: {
readOnlyHint: true,
},
inputSchema: {
type: 'object',
properties: {
appName: {
type: 'string',
description:
'App name (e.g. `slack`). For custom/SDK apps, prefix with `app#` (e.g. `app#my-custom-app`).',
},
appVersion: {
oneOf: [{ type: 'number' }, { type: 'string', const: 'latest' }],
description: 'App major version number (e.g. `4`), or the literal string `"latest"`.',
},
},
required: ['appName', 'appVersion'],
},
examples: [
{ appName: 'slack', appVersion: 4 },
{ appName: 'slack', appVersion: 'latest' },
],
execute: async (make: Make, args: { appName: string; appVersion: number | 'latest' }) => {
return await make.credentialRequests.listAppModulesWithCredentials(args.appName, args.appVersion);
},
},
];
50 changes: 50 additions & 0 deletions src/endpoints/credential-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,36 @@ export type ExtendConnectionBody = {
scopes: string[];
};

/**
* Module of an app that requires credentials, as returned by
* {@link CredentialRequests.listAppModulesWithCredentials}.
*/
export type AppModuleWithCredentials = {
/**
* Unique identifier for the module credential configuration.
* For modules with a single credential this matches the module `name`;
* for modules with multiple credentials it is suffixed (e.g. `moduleName:paramName`).
*/
id: string;
/** Technical module name */
name: string;
/** Human-readable module label */
label: string;
/**
* Credential type required by the module.
* Typically `account:<app-name>` or `keychain:<app-name>`.
* Multiple types may be comma-separated (e.g. `account:slack2,slack3`).
*/
type: string;
/**
* OAuth scopes required by this module.
* Omitted by the API for non-OAuth credential types (e.g. keychain-based auth).
*/
scope?: string[];
/** Whether this module is a hook-based trigger */
hook: boolean;
};

/**
* Class providing methods for working with credential requests
*/
Expand Down Expand Up @@ -368,6 +398,26 @@ export class CredentialRequests {
},
);
}

/**
* List all modules of an app that require credentials, along with the required
* credential type and OAuth scopes. Useful for populating
* {@link CredentialSelection.appModules} when creating a credential request.
*
* For SDK/custom apps, prefix `appName` with `app#` — the SDK URL-encodes it.
*
* @param appName - The app name (e.g. `slack`, or `app#my-custom-app` for SDK apps).
* @param appVersion - App major version number, or `'latest'` for the most recent version.
*/
async listAppModulesWithCredentials(
appName: string,
appVersion: number | 'latest',
): Promise<AppModuleWithCredentials[]> {
const response = await this.#fetch<{ appModules: AppModuleWithCredentials[] }>(
`/credential-requests/apps/${encodeURIComponent(appName)}/${appVersion}/modules-with-credentials`,
);
return response.appModules;
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type {
GetCredentialRequestOptions,
Credential,
ListCredentialRequestsOptions,
AppModuleWithCredentials,
} from './endpoints/credential-requests.js';
export type {
DataStore,
Expand Down
24 changes: 20 additions & 4 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ import { tools as EnumsTools } from './endpoints/enums.tools.js';
* JSON Schema definition for input parameters.
*/
export type JSONSchema = {
/** The type of the schema (object, string, number, boolean, array, etc.) */
type: 'object' | 'string' | 'number' | 'boolean' | 'array' | 'null';
/**
* The type of the schema (object, string, number, boolean, array, etc.).
* Optional when the schema is expressed purely through composition
* (`oneOf`/`anyOf`/`allOf`) or a `const` value.
*/
type?: 'object' | 'string' | 'number' | 'boolean' | 'array' | 'null';
Comment thread
JanKulhavy marked this conversation as resolved.
/** Properties definition for object types */
properties?: Record<string, JSONSchema>;
/** Required property names for object types */
Expand All @@ -42,16 +46,28 @@ export type JSONSchema = {
description?: string;
/** Enum values for restricted choices */
enum?: JSONValue[];
/** Constant literal value the schema must equal */
const?: JSONValue;
/** Value must match exactly one of these schemas */
oneOf?: JSONSchema[];
/** Value must match at least one of these schemas */
anyOf?: JSONSchema[];
/** Value must match all of these schemas */
allOf?: JSONSchema[];
Comment thread
JanKulhavy marked this conversation as resolved.
/** Default value */
default?: JSONValue;
/** Minimum value for numbers */
minimum?: number;
/** Maximum value for numbers */
maximum?: number;
/** Minimum length for strings/arrays */
/** Minimum length for strings */
minLength?: number;
/** Maximum length for strings/arrays */
/** Maximum length for strings */
maxLength?: number;
/** Minimum number of items for arrays */
minItems?: number;
/** Maximum number of items for arrays */
maxItems?: number;
/** Pattern for string validation */
pattern?: string;
};
Expand Down
14 changes: 14 additions & 0 deletions test/credential-requests.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,18 @@ describe('Integration: CredentialRequests', () => {
const requests = await make.credentialRequests.list(MAKE_TEAM);
expect(requests.some(r => r.id === nameOverrideRequestId)).toBe(false);
});

it('Should list app modules with credentials for a public app', async () => {
const modules = await make.credentialRequests.listAppModulesWithCredentials('google-email', 'latest');
expect(Array.isArray(modules)).toBe(true);
expect(modules.length).toBeGreaterThan(0);

const module = modules[0]!;
expect(typeof module.id).toBe('string');
expect(typeof module.name).toBe('string');
expect(typeof module.label).toBe('string');
expect(typeof module.type).toBe('string');
expect(module.scope === undefined || Array.isArray(module.scope)).toBe(true);
expect(typeof module.hook).toBe('boolean');
});
});
34 changes: 34 additions & 0 deletions test/credential-requests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as deleteRemoteMock from './mocks/credential-requests/delete-remote.jso
import * as createActionMock from './mocks/credential-requests/create-action.json';
import * as createByCredentialsMock from './mocks/credential-requests/create-by-credentials.json';
import * as extendConnectionMock from './mocks/credential-requests/extend-connection.json';
import * as listAppModulesWithCredentialsMock from './mocks/credential-requests/list-app-modules-with-credentials.json';

const MAKE_API_KEY = 'api-key';
const MAKE_ZONE = 'make.local';
Expand Down Expand Up @@ -266,4 +267,37 @@ describe('Endpoints: CredentialRequests', () => {
publicUri: extendConnectionMock.publicUri,
});
});

it('Should list app modules with credentials by numeric version', async () => {
mockFetch(
'GET https://make.local/api/v2/credential-requests/apps/slack/4/modules-with-credentials',
listAppModulesWithCredentialsMock,
);

const result = await make.credentialRequests.listAppModulesWithCredentials('slack', 4);

expect(result).toStrictEqual(listAppModulesWithCredentialsMock.appModules);
});

it('Should list app modules with credentials by latest version', async () => {
mockFetch(
'GET https://make.local/api/v2/credential-requests/apps/slack/latest/modules-with-credentials',
listAppModulesWithCredentialsMock,
);

const result = await make.credentialRequests.listAppModulesWithCredentials('slack', 'latest');

expect(result).toStrictEqual(listAppModulesWithCredentialsMock.appModules);
});

it('Should list app modules with credentials for SDK apps (app# prefix URL-encoded)', async () => {
mockFetch(
'GET https://make.local/api/v2/credential-requests/apps/app%23my-custom-app/1/modules-with-credentials',
listAppModulesWithCredentialsMock,
);

const result = await make.credentialRequests.listAppModulesWithCredentials('app#my-custom-app', 1);

expect(result).toStrictEqual(listAppModulesWithCredentialsMock.appModules);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"appModules": [
{
"id": "CreateMessage",
"name": "CreateMessage",
"label": "Send a Message",
"type": "account:slack2,slack3",
"scope": [
"chat:write",
"chat:write.public",
"chat:write.customize",
"channels:read",
"groups:read",
"im:read",
"mpim:read",
"users:read"
],
"hook": false
},
{
"id": "WatchMessages",
"name": "WatchMessages",
"label": "Watch Public Channel Messages",
"type": "account:slack2",
"scope": ["channels:history", "channels:read"],
"hook": false
},
{
"id": "watchInteractivityEvents",
"name": "watchInteractivityEvents",
"label": "Watch Interactive Events",
"type": "account:slack2,slack3",
"scope": [],
"hook": true
},
{
"id": "MakeRequest:authenticationType:apiKey",
"name": "MakeRequest",
"label": "Make a request - API key",
"type": "keychain:apikeyauth",
"hook": false
}
]
}