diff --git a/src/endpoints/credential-requests.tools.ts b/src/endpoints/credential-requests.tools.ts index f218abe..f52cd5c 100644 --- a/src/endpoints/credential-requests.tools.ts +++ b/src/endpoints/credential-requests.tools.ts @@ -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); + }, + }, ]; diff --git a/src/endpoints/credential-requests.ts b/src/endpoints/credential-requests.ts index 33c861a..be4d7ad 100644 --- a/src/endpoints/credential-requests.ts +++ b/src/endpoints/credential-requests.ts @@ -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:` or `keychain:`. + * 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 */ @@ -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 { + const response = await this.#fetch<{ appModules: AppModuleWithCredentials[] }>( + `/credential-requests/apps/${encodeURIComponent(appName)}/${appVersion}/modules-with-credentials`, + ); + return response.appModules; + } } /** diff --git a/src/index.ts b/src/index.ts index 31b88cd..dff4846 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ export type { GetCredentialRequestOptions, Credential, ListCredentialRequestsOptions, + AppModuleWithCredentials, } from './endpoints/credential-requests.js'; export type { DataStore, diff --git a/src/tools.ts b/src/tools.ts index e916673..39d6926 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -31,8 +31,12 @@ import { tools as PublicTemplatesTools } from './endpoints/public-templates.tool * 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'; /** Properties definition for object types */ properties?: Record; /** Required property names for object types */ @@ -43,16 +47,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[]; /** 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; }; diff --git a/test/credential-requests.integration.test.ts b/test/credential-requests.integration.test.ts index 56e4417..cf80892 100644 --- a/test/credential-requests.integration.test.ts +++ b/test/credential-requests.integration.test.ts @@ -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', 4); + 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'); + }); }); diff --git a/test/credential-requests.spec.ts b/test/credential-requests.spec.ts index afa508a..e998b8c 100644 --- a/test/credential-requests.spec.ts +++ b/test/credential-requests.spec.ts @@ -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'; @@ -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); + }); }); diff --git a/test/mocks/credential-requests/list-app-modules-with-credentials.json b/test/mocks/credential-requests/list-app-modules-with-credentials.json new file mode 100644 index 0000000..ca5ff8b --- /dev/null +++ b/test/mocks/credential-requests/list-app-modules-with-credentials.json @@ -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 + } + ] +}