diff --git a/docs/hub/code.md b/docs/hub/code.md index 17314a6..370997f 100644 --- a/docs/hub/code.md +++ b/docs/hub/code.md @@ -41,6 +41,7 @@ Accessed as `norbix.hub.code` on the [`Norbix`](../../README.md#authentication) | [`saveMarketplaceIntegrationCustomAccess`](#savemarketplaceintegrationcustomaccess) | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom` | `project` | | [`getMarketplaceIntegrationTools`](#getmarketplaceintegrationtools) | `GET` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/tools` | `project` | | [`startMarketplaceIntegrationSignIn`](#startmarketplaceintegrationsignin) | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start` | `project` | +| [`signOutMarketplaceIntegration`](#signoutmarketplaceintegration) | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/sign-out` | `project` | | [`replaceMarketplaceIntegrationSecrets`](#replacemarketplaceintegrationsecrets) | `PUT` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets` | `project` | | [`revealMarketplaceIntegrationSecrets`](#revealmarketplaceintegrationsecrets) | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets/reveal` | `project` | | [`setMarketplaceIntegrationTokenMappings`](#setmarketplaceintegrationtokenmappings) | `PUT` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/token-mappings` | `project` | @@ -802,6 +803,27 @@ const result = await norbix.hub.code.startMarketplaceIntegrationSignIn({ [↑ Top](#endpoints) +### signOutMarketplaceIntegration + +`POST` `/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/sign-out` + +**Request DTO**: `CodeMashHub2.SignOutMarketplaceIntegration` +**Response**: `CodeMashHub2.SignOutMarketplaceIntegrationResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.code.signOutMarketplaceIntegration({ + IntegrationViewId: 'IntegrationViewId-here', + // Other fields: see CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.SignOutMarketplaceIntegrationResponse +``` + +[↑ Top](#endpoints) + ### replaceMarketplaceIntegrationSecrets `PUT` `/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets` diff --git a/src/hub/code.ts b/src/hub/code.ts index 49e4864..8503dd2 100644 --- a/src/hub/code.ts +++ b/src/hub/code.ts @@ -8,7 +8,7 @@ import { LogsModule } from './logs.js'; * to refresh this file from the DTO definitions. * * Group: code - * Endpoints: 43 + * Endpoints: 44 */ export class CodeModule { constructor(private readonly transport: Transport) {} @@ -640,6 +640,25 @@ export class CodeModule { }); }; + /** + * POST /{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/sign-out + * Request DTO: SignOutMarketplaceIntegration + */ + signOutMarketplaceIntegration = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/sign-out', + method: 'POST', + request, + pathParams: ['IntegrationViewId'], + scope: 'project', + ...options, + }); + }; + /** * PUT /{version}/code/marketplace/integrations/{IntegrationViewId}/secrets * Request DTO: ReplaceMarketplaceIntegrationSecretsRequest diff --git a/src/types/hub2.contract.json b/src/types/hub2.contract.json index 8545506..3c51739 100644 --- a/src/types/hub2.contract.json +++ b/src/types/hub2.contract.json @@ -1967,6 +1967,10 @@ "requiresAuth": true, "returnsBytes": false }, + "SignOutMarketplaceIntegration": { + "requiresAuth": true, + "returnsBytes": false + }, "StartLicenseDomainVerificationRequest": { "requiresAuth": true, "returnsBytes": false diff --git a/src/types/hub2.dtos.ts b/src/types/hub2.dtos.ts index 5875a3f..a750259 100644 --- a/src/types/hub2.dtos.ts +++ b/src/types/hub2.dtos.ts @@ -1,6 +1,6 @@ // @ts-nocheck /* Options: -Date: 2026-10-13 13:23:35 +Date: 2026-10-13 16:21:07 Version: 10.20 Tip: To override a DTO option, remove "//" prefix before updating BaseUrl: http://localhost:5001 @@ -7678,6 +7678,7 @@ export module CodeMashHub2 { public name: string; public visibleKey?: string; public scopes: string[] = []; + public environment?: string; public createdDate: string; public expiryDate?: string; public cancelledDate?: string; @@ -9465,6 +9466,9 @@ export module CodeMashHub2 { // @DataMember public defaultNotApplied?: string; + // @DataMember + public canChange: boolean; + public constructor(init?: Partial) { (Object as any).assign(this, init); } @@ -9500,6 +9504,12 @@ export module CodeMashHub2 { // @DataMember public grantable: boolean; + // @DataMember + public curated: boolean; + + // @DataMember + public excludedFromFullAccess: boolean; + public constructor(init?: Partial) { (Object as any).assign(this, init); } @@ -9606,6 +9616,12 @@ export module CodeMashHub2 { // @DataMember public notGrantable: IReadOnlyList; + // @DataMember + public readOnlyHintOverridden: IReadOnlyList; + + // @DataMember + public excludedFromFullAccess: IReadOnlyList; + public constructor(init?: Partial) { (Object as any).assign(this, init); } @@ -9683,6 +9699,12 @@ export module CodeMashHub2 { // @DataMember public input?: string; + // @DataMember + public integrationId?: string; + + // @DataMember + public toolName?: string; + public constructor(init?: Partial) { (Object as any).assign(this, init); } @@ -9855,6 +9877,24 @@ export module CodeMashHub2 { // @DataMember public platformApiLevel?: number; + // @DataMember + public platform?: string; + + // @DataMember + public appVersion?: string; + + // @DataMember + public installationId?: string; + + // @DataMember + public locale?: string; + + // @DataMember + public timeZone?: string; + + // @DataMember + public updatedAtUtc?: string; + public constructor(init?: Partial) { (Object as any).assign(this, init); } @@ -9945,6 +9985,19 @@ export module CodeMashHub2 { } } + // @DataContract + export class NotificationReadMarkDto { + // @DataMember + public userId: string; + + // @DataMember + public readAtUtc: string; + + public constructor(init?: Partial) { + (Object as any).assign(this, init); + } + } + // @DataContract export class PushCampaignBatchNotificationDto extends CampaignBatchNotificationDto { // @DataMember @@ -9956,6 +10009,9 @@ export module CodeMashHub2 { // @DataMember public content?: PushMessageContentDto; + // @DataMember + public readBy?: NotificationReadMarkDto[]; + public constructor(init?: Partial) { super(init); (Object as any).assign(this, init); @@ -11911,6 +11967,8 @@ export module CodeMashHub2 { public id: number; public name: string; public key: string; + public environment?: string; + public responseStatus?: CodeMashResponseStatus; public constructor(init?: Partial) { (Object as any).assign(this, init); @@ -11919,6 +11977,7 @@ export module CodeMashHub2 { export class ListServiceUserApiKeysResponse { public keys: ServiceUserApiKeyDto[] = []; + public responseStatus?: CodeMashResponseStatus; public constructor(init?: Partial) { (Object as any).assign(this, init); @@ -12898,6 +12957,9 @@ export module CodeMashHub2 { // @DataMember public functions: IReadOnlyList; + // @DataMember + public canManage: boolean; + public constructor(init?: Partial) { super(init); (Object as any).assign(this, init); @@ -12962,6 +13024,16 @@ export module CodeMashHub2 { } } + export class SignOutMarketplaceIntegrationResponse extends ResponseBase { + public wasSignedIn: boolean; + public revoke?: string; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + } + export class EmptyMarketplaceSecretsResponse extends ResponseBase { public constructor(init?: Partial) { super(init); @@ -20137,6 +20209,8 @@ export module CodeMashHub2 { // @ApiMember(Description="Set true to get every reference value as { id, display } (display = the target's displayField per the schema; null when the target is gone). Needs read permission on every source the schema links to (users, roles, taxonomy, collection, files) — otherwise the read is refused with CM-ERRORS-DATABASE-056 naming the source. Default false returns the stored ids.") public expandReferences: boolean; + public allLanguages: boolean; + public constructor(init?: Partial) { super(init); (Object as any).assign(this, init); @@ -20169,6 +20243,8 @@ export module CodeMashHub2 { // @ApiMember(Description="Set true to get every reference value as { id, display } (display = the target's displayField per the schema; null when the target is gone). Needs read permission on every source the schema links to (users, roles, taxonomy, collection, files) — otherwise the read is refused with CM-ERRORS-DATABASE-056 naming the source. Default false returns the stored ids.") public expandReferences: boolean; + public allLanguages: boolean; + public constructor(init?: Partial) { super(init); (Object as any).assign(this, init); @@ -24028,6 +24104,30 @@ export module CodeMashHub2 { } } + // @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/sign-out", "POST") + export class SignOutMarketplaceIntegration + extends CodeMashRequestBase + implements IReturn + { + /** @description Integration view id, from get_marketplace_integrations. */ + // @ApiMember(Description="Integration view id, from get_marketplace_integrations.", IsRequired=true) + public integrationViewId: string; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + public getTypeName() { + return 'SignOutMarketplaceIntegration'; + } + public getMethod() { + return 'POST'; + } + public createResponse() { + return new SignOutMarketplaceIntegrationResponse(); + } + } + // @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets", "PUT") // @DataContract export class ReplaceMarketplaceIntegrationSecretsRequest @@ -24153,6 +24253,10 @@ export module CodeMashHub2 { // @ApiMember(Description="The scheduler task whose marketplace function runs to list (tsk_…). One of functionId / triggerId / taskId.") public taskId?: string; + /** @description The MCP connection whose tool runs to list — the calls an AI chat made to one of its tools without a saved function (int_…). One of functionId / triggerId / taskId / integrationId. */ + // @ApiMember(Description="The MCP connection whose tool runs to list — the calls an AI chat made to one of its tools without a saved function (int_…). One of functionId / triggerId / taskId / integrationId.") + public integrationId?: string; + /** @description How many runs, newest first: 1-50. Default 20. */ // @ApiMember(DataType="integer", Description="How many runs, newest first: 1-50. Default 20.") public limit?: number; diff --git a/tests/hub/code.test.ts b/tests/hub/code.test.ts index f7dfd18..98fbe54 100644 --- a/tests/hub/code.test.ts +++ b/tests/hub/code.test.ts @@ -7,7 +7,7 @@ import { createMockFetch, expectedUrl, makeClient, stubRequestForPath } from '.. * Auto-generated. Do not edit by hand — run `npm run generate-endpoints` * to refresh this file from the DTO definitions. * - * Tests for hub.code (43 endpoints). + * Tests for hub.code (44 endpoints). * * Each method is asserted against: * - presence on the module (smoke check) @@ -17,7 +17,7 @@ import { createMockFetch, expectedUrl, makeClient, stubRequestForPath } from '.. * - account-scope guard: throws NORBIX_ACCOUNT_SCOPE_REQUIRED without accountId */ describe('hub.code', () => { - it('module exposes 43 method(s)', () => { + it('module exposes 44 method(s)', () => { const mock = createMockFetch(); const mod = new CodeModule({} as never); void mod; // silence unused — we only need the type @@ -62,6 +62,7 @@ describe('hub.code', () => { expect(typeof ns['saveMarketplaceIntegrationCustomAccess']).toBe('function'); expect(typeof ns['getMarketplaceIntegrationTools']).toBe('function'); expect(typeof ns['startMarketplaceIntegrationSignIn']).toBe('function'); + expect(typeof ns['signOutMarketplaceIntegration']).toBe('function'); expect(typeof ns['replaceMarketplaceIntegrationSecrets']).toBe('function'); expect(typeof ns['revealMarketplaceIntegrationSecrets']).toBe('function'); expect(typeof ns['setMarketplaceIntegrationTokenMappings']).toBe('function'); @@ -867,6 +868,31 @@ describe('hub.code', () => { expect(mock.lastCall?.headers.get('norbix-project-id')).toBe('test-project'); }); + it('signOutMarketplaceIntegration: POST /{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/sign-out', async () => { + const stub = stubRequestForPath( + '/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/sign-out', + ); + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/sign-out', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['code']!['signOutMarketplaceIntegration']!; + await fn(stub); + expect(mock.lastCall).toBeDefined(); + expect(mock.lastCall?.method).toBe('POST'); + expect(mock.lastCall?.url.startsWith(expected)).toBe(true); + expect(mock.lastCall?.headers.get('Authorization')).toBe('Bearer test-token'); + expect(mock.lastCall?.headers.get('norbix-project-id')).toBe('test-project'); + }); + it('replaceMarketplaceIntegrationSecrets: PUT /{version}/code/marketplace/integrations/{IntegrationViewId}/secrets', async () => { const stub = stubRequestForPath( '/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets', diff --git a/tests/hub/marketplace-sign-out.test.ts b/tests/hub/marketplace-sign-out.test.ts new file mode 100644 index 0000000..08bcc39 --- /dev/null +++ b/tests/hub/marketplace-sign-out.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest'; + +import { createMockFetch, makeClient } from '../_helpers.js'; + +/** + * Route 30 of the marketplace contract (gateway item access-2b): sign an MCP + * connection out of OAuth. The answer says whether it was signed in and what + * happened to the vendor token (Revoked, NoRevocationEndpoint, Failed or null). + */ +const INTEGRATION = 'int_6Qp1dW2kRm8NsT4vYc0Hb'; + +describe('hub.code.signOutMarketplaceIntegration', () => { + it('POSTs with the connection id in the path only and returns wasSignedIn and revoke', async () => { + const mock = createMockFetch({ body: { wasSignedIn: true, revoke: 'Revoked' } }); + const { norbix } = makeClient({ fetch: mock.fetch }); + + const response = await norbix.hub.code.signOutMarketplaceIntegration({ + integrationViewId: INTEGRATION, + }); + + const call = mock.lastCall!; + const url = new URL(call.url); + expect(call.method).toBe('POST'); + expect(url.pathname).toBe(`/v2/code/marketplace/integrations/${INTEGRATION}/oauth/sign-out`); + expect(url.searchParams.has('integrationViewId')).toBe(false); + expect(JSON.parse((call.body as string | undefined) ?? '{}')).not.toHaveProperty( + 'integrationViewId', + ); + expect(response.wasSignedIn).toBe(true); + expect(response.revoke).toBe('Revoked'); + }); + + it('reads the access-2b fields of the matrix and the run rows', async () => { + const mock = createMockFetch({ + body: { + canManage: false, + roles: [{ roleId: 'acc_1_nr_2', name: 'Support', canChange: false }], + tools: [ + { + name: 'delete_repo', + readOnly: false, + grantable: true, + curated: true, + excludedFromFullAccess: true, + }, + ], + }, + }); + const { norbix } = makeClient({ fetch: mock.fetch }); + + const access = await norbix.hub.code.getMarketplaceIntegrationAccess({ + integrationViewId: INTEGRATION, + }); + + expect(access.canManage).toBe(false); + expect(access.roles?.[0]?.canChange).toBe(false); + expect(access.tools?.[0]?.excludedFromFullAccess).toBe(true); + expect(access.tools?.[0]?.curated).toBe(true); + }); + + it('sends integrationId on the runs query', async () => { + const mock = createMockFetch({ + body: { runs: [{ integrationId: INTEGRATION, toolName: 'create_issue' }] }, + }); + const { norbix } = makeClient({ fetch: mock.fetch }); + + const response = await norbix.hub.code.getMarketplaceFunctionRuns({ + integrationId: INTEGRATION, + limit: 5, + }); + + const url = new URL(mock.lastCall!.url); + expect(url.pathname).toBe('/v2/code/marketplace/functions/runs'); + expect(url.searchParams.get('integrationId')).toBe(INTEGRATION); + expect(response.runs?.[0]?.toolName).toBe('create_issue'); + }); +});