diff --git a/README.md b/README.md index 4dd7334..800911d 100644 --- a/README.md +++ b/README.md @@ -552,11 +552,13 @@ listings, the installed connections (`int_…`) with their secrets, token mappings and validation test, the vendor function catalog, and the functions added on a connection (`func_…`). Every tag is of type `CodeMarketplace`. -| Area | Hooks | Cache tag (`CodeMarketplace/…`) | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | -| Catalog | `useGetMarketplaceListingsQuery`, `useGetMarketplaceListingQuery`, `useGetMarketplaceListingFunctionTokensQuery` | provides `LISTINGS` | +| Area | Hooks | Cache tag (`CodeMarketplace/…`) | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Catalog | `useGetMarketplaceListingsQuery`, `useGetMarketplaceListingQuery`, `useGetMarketplaceListingFunctionTokensQuery` | provides `LISTINGS` | | Connections | `useGetMarketplaceIntegrationsQuery`, `useGetMarketplaceIntegrationQuery`, `useSaveMarketplaceIntegrationMutation`, `useDeleteMarketplaceIntegrationMutation`, `useEnableMarketplaceIntegrationMutation`, `useDisableMarketplaceIntegrationMutation`, `useReplaceMarketplaceIntegrationSecretsMutation`, `useRevealMarketplaceIntegrationSecretsMutation`, `useSetMarketplaceIntegrationTokenMappingsMutation`, `useTestMarketplaceIntegrationMutation`, `useGetMarketplaceFunctionCatalogQuery`, `useGetMarketplaceIntegrationToolsQuery` (the stored MCP tool catalog of an MCP connection, core SDK 4.23.0+) | `INTEGRATIONS`, ``, `CATALOG:`, `TOOLS:` (test and delete invalidate it); reveal touches nothing | -| Functions | `useGetMarketplaceFunctionsQuery`, `useGetMarketplaceFunctionQuery`, `useSaveMarketplaceFunctionMutation`, `useDeleteMarketplaceFunctionMutation`, `useEnableMarketplaceFunctionMutation`, `useDisableMarketplaceFunctionMutation`, `useGetMarketplaceFunctionTokensQuery`, `useInvokeMarketplaceFunctionMutation`, `useGetMarketplaceFunctionRunsQuery` | `FUNCTIONS:`, ``, `RUNS`; invoke touches nothing | +| Access | `useGetMarketplaceIntegrationAccessQuery` (the access matrix of a connection: roles, packages, tools, functions), `useSetMarketplaceIntegrationRoleAccessMutation` (a role's package + Manage), `useSaveMarketplaceIntegrationCustomAccessMutation` (a custom policy for one role) — core SDK 4.24.0+ | `ACCESS:`; the two writes invalidate only that connection's matrix, and so do test, delete, function save and function delete | +| Sign-in | `useStartMarketplaceIntegrationSignInMutation` (OAuth sign-in of an MCP connection: open `authorizeUrl`; core SDK 4.24.0+) | invalidates nothing — read the connection again (`signedIn`) after the person returns | +| Functions | `useGetMarketplaceFunctionsQuery`, `useGetMarketplaceFunctionQuery`, `useSaveMarketplaceFunctionMutation`, `useDeleteMarketplaceFunctionMutation`, `useEnableMarketplaceFunctionMutation`, `useDisableMarketplaceFunctionMutation`, `useGetMarketplaceFunctionTokensQuery`, `useInvokeMarketplaceFunctionMutation`, `useGetMarketplaceFunctionRunsQuery` | `FUNCTIONS:`, ``, `RUNS`; invoke touches nothing | Revealing secrets is a mutation on purpose: a query would keep the clear values in the store's cache. diff --git a/package-lock.json b/package-lock.json index 1df7334..9a210d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@commitlint/cli": "^21.2.2", "@commitlint/config-conventional": "^21.2.2", "@eslint/js": "^10.0.1", - "@norbix.ai/ts": "^4.23.0", + "@norbix.ai/ts": "^4.24.0", "@reduxjs/toolkit": "^2.12.0", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/github": "^12.0.9", @@ -1490,9 +1490,9 @@ } }, "node_modules/@norbix.ai/ts": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@norbix.ai/ts/-/ts-4.23.0.tgz", - "integrity": "sha512-NWac3JPPoDJ4Y/2wmjgvkfoS2sqghrdr5rv9JwTca5va9/cLX7XThqk6G9a1bxmaeExwMHPh1Qj5VwmSI3v2Gw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@norbix.ai/ts/-/ts-4.24.0.tgz", + "integrity": "sha512-YJrbsb/qvtVkjkzT+DZKVE0MhqxTbVD7K+tkNyB/Eh82dMEnJNu4kvM6YP/t9tbYms5N4rilcl9zISr/VTTDUQ==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 7df230d..c81db94 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@commitlint/cli": "^21.2.2", "@commitlint/config-conventional": "^21.2.2", "@eslint/js": "^10.0.1", - "@norbix.ai/ts": "^4.23.0", + "@norbix.ai/ts": "^4.24.0", "@reduxjs/toolkit": "^2.12.0", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/github": "^12.0.9", diff --git a/src/hooks/hub/marketplace.ts b/src/hooks/hub/marketplace.ts index 2af232e..9a90cf0 100644 --- a/src/hooks/hub/marketplace.ts +++ b/src/hooks/hub/marketplace.ts @@ -18,10 +18,18 @@ import type { Arg, Result } from '../types.js'; * - `CATALOG:` — the vendor functions a connection offers * - `TOOLS:` — the stored MCP tool catalog of an MCP connection; * validation (test) rewrites it (core SDK 4.23.0) + * - `ACCESS:` — the access matrix of a connection (which role holds + * which package); a role package or a custom policy + * write, validation, delete and function writes + * rewrite it (core SDK 4.24.0) * - `FUNCTIONS:`— the functions added on a connection * - `` — one function and its tokens * - `RUNS` — the last runs of a trigger or task (core SDK 4.21.0) * + * Starting the OAuth sign-in (route 28) invalidates nothing: the connection + * changes only when the vendor sends the browser back to the gateway, so read + * the connection again after the person returns (`signedIn`). + * * Revealing secrets is a mutation on purpose: a query would keep the clear * values in the store's cache. */ @@ -44,6 +52,10 @@ export const hubCodeMarketplace = (b: Builder) => { type TestIntegration = Fn<'testMarketplaceIntegration'>; type GetCatalog = Fn<'getMarketplaceFunctionCatalog'>; type GetTools = Fn<'getMarketplaceIntegrationTools'>; + type GetAccess = Fn<'getMarketplaceIntegrationAccess'>; + type SetRoleAccess = Fn<'setMarketplaceIntegrationRoleAccess'>; + type SaveCustomAccess = Fn<'saveMarketplaceIntegrationCustomAccess'>; + type StartSignIn = Fn<'startMarketplaceIntegrationSignIn'>; type GetFunctions = Fn<'getMarketplaceFunctions'>; type GetFunction = Fn<'getMarketplaceFunction'>; type SaveFunction = Fn<'saveMarketplaceFunction'>; @@ -70,6 +82,7 @@ export const hubCodeMarketplace = (b: Builder) => { { type: T, id: 'INTEGRATIONS' }, { type: T, id: integrationId(arg) }, ]; + const access = (arg: unknown) => ({ type: T, id: `ACCESS:${integrationId(arg)}` }); const functions = (arg: unknown) => [ { type: T, id: `FUNCTIONS:${integrationId(arg)}` }, { type: T, id: functionId(arg) }, @@ -112,6 +125,7 @@ export const hubCodeMarketplace = (b: Builder) => { ...integration(arg), { type: T, id: `FUNCTIONS:${integrationId(arg)}` }, { type: T, id: `TOOLS:${integrationId(arg)}` }, + access(arg), ], }), enableMarketplaceIntegration: b.mutation, Arg>({ @@ -141,6 +155,7 @@ export const hubCodeMarketplace = (b: Builder) => { invalidatesTags: (_res, _err, arg) => [ { type: T, id: integrationId(arg) }, { type: T, id: `TOOLS:${integrationId(arg)}` }, + access(arg), ], }), getMarketplaceFunctionCatalog: b.query, Arg>({ @@ -152,6 +167,28 @@ export const hubCodeMarketplace = (b: Builder) => { providesTags: (_res, _err, arg) => [{ type: T, id: `TOOLS:${integrationId(arg)}` }], }), + // ---- Access (core SDK 4.24.0) ---- + getMarketplaceIntegrationAccess: b.query, Arg>({ + query: (args) => (norbix) => callHC(norbix).getMarketplaceIntegrationAccess(args), + providesTags: (_res, _err, arg) => [access(arg)], + }), + setMarketplaceIntegrationRoleAccess: b.mutation, Arg>({ + query: (args) => (norbix) => callHC(norbix).setMarketplaceIntegrationRoleAccess(args), + invalidatesTags: (_res, _err, arg) => [access(arg)], + }), + saveMarketplaceIntegrationCustomAccess: b.mutation< + Result, + Arg + >({ + query: (args) => (norbix) => callHC(norbix).saveMarketplaceIntegrationCustomAccess(args), + invalidatesTags: (_res, _err, arg) => [access(arg)], + }), + + // ---- OAuth sign-in (core SDK 4.24.0) ---- + startMarketplaceIntegrationSignIn: b.mutation, Arg>({ + query: (args) => (norbix) => callHC(norbix).startMarketplaceIntegrationSignIn(args), + }), + // ---- Functions on a connection ---- getMarketplaceFunctions: b.query, Arg>({ query: (args) => (norbix) => callHC(norbix).getMarketplaceFunctions(args), @@ -163,11 +200,11 @@ export const hubCodeMarketplace = (b: Builder) => { }), saveMarketplaceFunction: b.mutation, Arg>({ query: (args) => (norbix) => callHC(norbix).saveMarketplaceFunction(args), - invalidatesTags: (_res, _err, arg) => functions(arg), + invalidatesTags: (_res, _err, arg) => [...functions(arg), access(arg)], }), deleteMarketplaceFunction: b.mutation, Arg>({ query: (args) => (norbix) => callHC(norbix).deleteMarketplaceFunction(args), - invalidatesTags: (_res, _err, arg) => functions(arg), + invalidatesTags: (_res, _err, arg) => [...functions(arg), access(arg)], }), enableMarketplaceFunction: b.mutation, Arg>({ query: (args) => (norbix) => callHC(norbix).enableMarketplaceFunction(args), diff --git a/tests/marketplace.test.ts b/tests/marketplace.test.ts index dc4a7eb..053a32d 100644 --- a/tests/marketplace.test.ts +++ b/tests/marketplace.test.ts @@ -34,6 +34,7 @@ type Def = { const INTEGRATION = 'int_6Qp1dW2kRm8NsT4vYc0Hb'; const FUNCTION = 'func_4hT2b9QkLm3XcT1yRfGh7a'; const LISTING = 'ml_slack_v1'; +const ROLE = 'acc_6Qp1dW2kRm8NsT4vYc0Hb_nr_3kLm9QwErTyU'; const IDS = { integrationViewId: INTEGRATION }; const FN_IDS = { integrationViewId: INTEGRATION, functionViewId: FUNCTION }; const I = `/code/marketplace/integrations/${INTEGRATION}`; @@ -99,6 +100,39 @@ const CASES: Array< ['testMarketplaceIntegration', 'mutation', 'POST', `${I}/test`, IDS, {}], ['getMarketplaceFunctionCatalog', 'query', 'GET', `${I}/catalog`, IDS, {}], ['getMarketplaceIntegrationTools', 'query', 'GET', `${I}/tools`, IDS, {}], + ['getMarketplaceIntegrationAccess', 'query', 'GET', `${I}/access`, IDS, {}], + [ + 'setMarketplaceIntegrationRoleAccess', + 'mutation', + 'PUT', + `${I}/access/roles`, + { ...IDS, roleId: ROLE, package: 'ReadOnly', manage: false }, + { roleId: ROLE, package: 'ReadOnly', manage: false }, + ], + [ + 'saveMarketplaceIntegrationCustomAccess', + 'mutation', + 'POST', + `${I}/access/roles/custom`, + { + ...IDS, + roleId: ROLE, + basePackage: 'ReadOnly', + tools: ['create_issue'], + functions: [FUNCTION], + manage: false, + newPolicyForThisRoleOnly: true, + }, + { + roleId: ROLE, + basePackage: 'ReadOnly', + tools: ['create_issue'], + functions: [FUNCTION], + manage: false, + newPolicyForThisRoleOnly: true, + }, + ], + ['startMarketplaceIntegrationSignIn', 'mutation', 'POST', `${I}/oauth/start`, IDS, {}], ['getMarketplaceFunctions', 'query', 'GET', `${I}/functions`, IDS, {}], ['getMarketplaceFunction', 'query', 'GET', `${I}/functions/${FUNCTION}`, FN_IDS, {}], [ @@ -153,7 +187,7 @@ function tagsFor(tags: Tags | undefined, arg: unknown): unknown { const endpoints = hubCodeMarketplace(fakeBuilder()) as unknown as Record; describe('hubCodeMarketplace', () => { - it('wraps the 24 marketplace methods of the published core SDK', () => { + it('wraps the 28 marketplace methods of the published core SDK', () => { expect(Object.keys(endpoints).sort()).toEqual([...NAMES].sort()); }); @@ -204,6 +238,7 @@ describe('hubCodeMarketplace', () => { { type: 'CodeMarketplace', id: INTEGRATION }, { type: 'CodeMarketplace', id: `FUNCTIONS:${INTEGRATION}` }, { type: 'CodeMarketplace', id: `TOOLS:${INTEGRATION}` }, + { type: 'CodeMarketplace', id: `ACCESS:${INTEGRATION}` }, ]); expect(tagsFor(endpoints['saveMarketplaceIntegration']!.invalidatesTags, {})).toEqual([ { type: 'CodeMarketplace', id: 'INTEGRATIONS' }, @@ -216,10 +251,12 @@ describe('hubCodeMarketplace', () => { { type: 'CodeMarketplace', id: INTEGRATION }, ]); } - // Validation lists the MCP server's tools again and stores the catalog (gateway item mcp-step1). + // Validation lists the MCP server's tools again and stores the catalog (gateway item mcp-step1), + // and re-syncs the connection's access packages (gateway item access). expect(tagsFor(endpoints['testMarketplaceIntegration']!.invalidatesTags, IDS)).toEqual([ { type: 'CodeMarketplace', id: INTEGRATION }, { type: 'CodeMarketplace', id: `TOOLS:${INTEGRATION}` }, + { type: 'CodeMarketplace', id: `ACCESS:${INTEGRATION}` }, ]); }); @@ -229,6 +266,32 @@ describe('hubCodeMarketplace', () => { ]); }); + it('the access matrix provides ACCESS:; a role package and a custom policy invalidate it (core SDK 4.24.0)', () => { + expect(tagsFor(endpoints['getMarketplaceIntegrationAccess']!.providesTags, IDS)).toEqual([ + { type: 'CodeMarketplace', id: `ACCESS:${INTEGRATION}` }, + ]); + for (const key of [ + 'setMarketplaceIntegrationRoleAccess', + 'saveMarketplaceIntegrationCustomAccess', + ]) { + expect(endpoints[key]!.__kind).toBe('mutation'); + expect(tagsFor(endpoints[key]!.invalidatesTags, IDS)).toEqual([ + { type: 'CodeMarketplace', id: `ACCESS:${INTEGRATION}` }, + ]); + } + // Only the connection named in the call: another connection's matrix stays cached. + expect( + tagsFor(endpoints['setMarketplaceIntegrationRoleAccess']!.invalidatesTags, { + integrationViewId: 'int_other', + }), + ).toEqual([{ type: 'CodeMarketplace', id: 'ACCESS:int_other' }]); + }); + + it('starting the OAuth sign-in is a mutation that invalidates nothing (core SDK 4.24.0)', () => { + expect(endpoints['startMarketplaceIntegrationSignIn']!.__kind).toBe('mutation'); + expect(endpoints['startMarketplaceIntegrationSignIn']!.invalidatesTags).toBeUndefined(); + }); + it('the last runs provide CodeMarketplace/RUNS', () => { expect(tagsFor(endpoints['getMarketplaceFunctionRuns']!.providesTags, {})).toEqual([ { type: 'CodeMarketplace', id: 'RUNS' }, @@ -250,15 +313,18 @@ describe('hubCodeMarketplace', () => { { type: 'CodeMarketplace', id: FUNCTION }, ]); } - for (const key of [ - 'saveMarketplaceFunction', - 'deleteMarketplaceFunction', - 'enableMarketplaceFunction', - 'disableMarketplaceFunction', - ]) { + for (const key of ['enableMarketplaceFunction', 'disableMarketplaceFunction']) { + expect(tagsFor(endpoints[key]!.invalidatesTags, FN_IDS)).toEqual([ + { type: 'CodeMarketplace', id: `FUNCTIONS:${INTEGRATION}` }, + { type: 'CodeMarketplace', id: FUNCTION }, + ]); + } + // Adding or deleting a function changes the matrix's function list and packages (gateway item access). + for (const key of ['saveMarketplaceFunction', 'deleteMarketplaceFunction']) { expect(tagsFor(endpoints[key]!.invalidatesTags, FN_IDS)).toEqual([ { type: 'CodeMarketplace', id: `FUNCTIONS:${INTEGRATION}` }, { type: 'CodeMarketplace', id: FUNCTION }, + { type: 'CodeMarketplace', id: `ACCESS:${INTEGRATION}` }, ]); } });