From ecd2e3c891c66e1e404cec8168ce3acab9f81c29 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Tue, 13 Oct 2026 15:56:41 +0300 Subject: [PATCH 1/4] =?UTF-8?q?chore(types):=20regenerate=20hub=20types=20?= =?UTF-8?q?from=20gateway=20refactoringV2=20509156d57=20(access,=20routes?= =?UTF-8?q?=2025=E2=80=9327)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the access matrix, role package and custom policy types (GetMarketplaceIntegrationAccess*, SetMarketplaceIntegrationRoleAccess, SaveMarketplaceIntegrationCustomAccess*, MarketplaceAccess*Dto, MarketplaceAccessLevel) and the additive fields TestMarketplaceIntegrationResponse.accessChanges, MarketplaceAccessRoleDto.defaultNotApplied and MarketplaceValidationStepResultDto.errorCode. Co-Authored-By: Claude Opus 5.5 --- src/types/hub2.contract.json | 12 ++ src/types/hub2.dtos.ts | 295 ++++++++++++++++++++++++++++++++++- 2 files changed, 306 insertions(+), 1 deletion(-) diff --git a/src/types/hub2.contract.json b/src/types/hub2.contract.json index c79b25c..668e0bf 100644 --- a/src/types/hub2.contract.json +++ b/src/types/hub2.contract.json @@ -1147,6 +1147,10 @@ "requiresAuth": true, "returnsBytes": false }, + "GetMarketplaceIntegrationAccess": { + "requiresAuth": true, + "returnsBytes": false + }, "GetMarketplaceIntegrationTools": { "requiresAuth": true, "returnsBytes": false @@ -1827,6 +1831,10 @@ "requiresAuth": true, "returnsBytes": false }, + "SaveMarketplaceIntegrationCustomAccess": { + "requiresAuth": true, + "returnsBytes": false + }, "SaveMcpIntegration": { "requiresAuth": true, "returnsBytes": false @@ -1935,6 +1943,10 @@ "requiresAuth": true, "returnsBytes": false }, + "SetMarketplaceIntegrationRoleAccess": { + "requiresAuth": true, + "returnsBytes": false + }, "SetMarketplaceIntegrationTokenMappingsRequest": { "requiresAuth": true, "returnsBytes": false diff --git a/src/types/hub2.dtos.ts b/src/types/hub2.dtos.ts index ffe9534..aeb9209 100644 --- a/src/types/hub2.dtos.ts +++ b/src/types/hub2.dtos.ts @@ -1,6 +1,6 @@ // @ts-nocheck /* Options: -Date: 2026-10-13 09:00:52 +Date: 2026-10-13 12:56:15 Version: 10.20 Tip: To override a DTO option, remove "//" prefix before updating BaseUrl: http://localhost:5001 @@ -3877,6 +3877,9 @@ export module CodeMashHub2 { // @DataMember public message?: string; + // @DataMember + public errorCode?: string; + public constructor(init?: Partial) { (Object as any).assign(this, init); } @@ -5708,6 +5711,15 @@ export module CodeMashHub2 { } } + export enum MarketplaceAccessLevel { + None = 'None', + Viewer = 'Viewer', + FunctionsOnly = 'FunctionsOnly', + ReadOnly = 'ReadOnly', + FullAccess = 'FullAccess', + Manage = 'Manage', + } + export class SavePushTemplate extends CodeMashRequestBase { /** @description The template's display name. */ // @ApiMember(Description="The template's display name.", IsRequired=true) @@ -9387,6 +9399,110 @@ export module CodeMashHub2 { } } + // @DataContract + export class MarketplaceAccessCustomPolicyDto { + // @DataMember + public policyId: string; + + // @DataMember + public name: string; + + // @DataMember + public usedByRoles: number; + + public constructor(init?: Partial) { + (Object as any).assign(this, init); + } + } + + // @DataContract + export class MarketplaceAccessRoleDto { + // @DataMember + public roleId: string; + + // @DataMember + public name: string; + + // @DataMember + public scope: string; + + // @DataMember + public isSystem: boolean; + + // @DataMember + public fixed: boolean; + + // @DataMember + public package: MarketplaceAccessLevel; + + // @DataMember + public isCustom: boolean; + + // @DataMember + public manage: boolean; + + // @DataMember + public custom: IReadOnlyList; + + // @DataMember + public defaultNotApplied?: string; + + public constructor(init?: Partial) { + (Object as any).assign(this, init); + } + } + + // @DataContract + export class MarketplaceAccessPackageDto { + // @DataMember + public kind: MarketplaceAccessLevel; + + // @DataMember + public policyId: string; + + // @DataMember + public name: string; + + // @DataMember + public exists: boolean; + + public constructor(init?: Partial) { + (Object as any).assign(this, init); + } + } + + // @DataContract + export class MarketplaceAccessToolDto { + // @DataMember + public name: string; + + // @DataMember + public readOnly: boolean; + + // @DataMember + public grantable: boolean; + + public constructor(init?: Partial) { + (Object as any).assign(this, init); + } + } + + // @DataContract + export class MarketplaceAccessFunctionDto { + // @DataMember + public functionId: string; + + // @DataMember + public name: string; + + // @DataMember + public functionKey: string; + + public constructor(init?: Partial) { + (Object as any).assign(this, init); + } + } + // @DataContract export class MarketplaceIntegrationListProjection implements IHasViewId { // @DataMember @@ -9455,6 +9571,22 @@ export module CodeMashHub2 { } } + // @DataContract + export class MarketplaceAccessChangesDto { + // @DataMember + public readOnlyAdded: IReadOnlyList; + + // @DataMember + public readOnlyRemoved: IReadOnlyList; + + // @DataMember + public notGrantable: IReadOnlyList; + + public constructor(init?: Partial) { + (Object as any).assign(this, init); + } + } + // @DataContract export class MarketplaceFunctionProjection implements IHasViewId { // @DataMember @@ -12728,6 +12860,43 @@ export module CodeMashHub2 { } } + // @DataContract + export class GetMarketplaceIntegrationAccessResponse extends ResponseBase { + // @DataMember + public roles: IReadOnlyList; + + // @DataMember + public packages: IReadOnlyList; + + // @DataMember + public tools: IReadOnlyList; + + // @DataMember + public functions: IReadOnlyList; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + } + + // @DataContract + export class SaveMarketplaceIntegrationCustomAccessResponse extends ResponseBase { + // @DataMember + public policyId?: string; + + // @DataMember + public created: boolean; + + // @DataMember + public usedByRoles: number; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + } + export class GetMarketplaceIntegrationResponse extends ResponseBase { public integration?: MarketplaceIntegrationDto; @@ -12782,6 +12951,9 @@ export module CodeMashHub2 { // @DataMember public steps?: IReadOnlyList; + // @DataMember + public accessChanges?: MarketplaceAccessChangesDto; + public constructor(init?: Partial) { super(init); (Object as any).assign(this, init); @@ -23652,6 +23824,127 @@ export module CodeMashHub2 { } } + // @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/access", "GET") + export class GetMarketplaceIntegrationAccess + 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 'GetMarketplaceIntegrationAccess'; + } + public getMethod() { + return 'GET'; + } + public createResponse() { + return new GetMarketplaceIntegrationAccessResponse(); + } + } + + // @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles", "PUT") + // @DataContract + export class SetMarketplaceIntegrationRoleAccess + extends CodeMashRequestBase + implements IReturn + { + /** @description Integration view id, from get_marketplace_integrations. */ + // @DataMember + // @ApiMember(Description="Integration view id, from get_marketplace_integrations.", IsRequired=true) + public integrationViewId: string; + + /** @description The role's composite id: acc_…_nr_… (account / team role) or pr_…_nr_… (project role). */ + // @DataMember + // @ApiMember(Description="The role's composite id: acc_…_nr_… (account / team role) or pr_…_nr_… (project role).", IsRequired=true) + public roleId: string; + + /** @description None, Viewer, FunctionsOnly, ReadOnly or FullAccess. */ + // @DataMember + // @ApiMember(Description="None, Viewer, FunctionsOnly, ReadOnly or FullAccess.") + public package: MarketplaceAccessLevel; + + /** @description True to also give the Manage package (edit the connection, its secrets, validation and functions). */ + // @DataMember + // @ApiMember(Description="True to also give the Manage package (edit the connection, its secrets, validation and functions).") + public manage: boolean; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + public getTypeName() { + return 'SetMarketplaceIntegrationRoleAccess'; + } + public getMethod() { + return 'PUT'; + } + public createResponse() { + return new IdResponse(); + } + } + + // @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom", "POST") + // @DataContract + export class SaveMarketplaceIntegrationCustomAccess + extends CodeMashRequestBase + implements IReturn + { + /** @description Integration view id, from get_marketplace_integrations. */ + // @DataMember + // @ApiMember(Description="Integration view id, from get_marketplace_integrations.", IsRequired=true) + public integrationViewId: string; + + /** @description The role's composite id: acc_…_nr_… or pr_…_nr_…. */ + // @DataMember + // @ApiMember(Description="The role's composite id: acc_…_nr_… or pr_…_nr_….", IsRequired=true) + public roleId: string; + + /** @description The package to start from: None, Viewer, FunctionsOnly, ReadOnly or FullAccess. */ + // @DataMember + // @ApiMember(Description="The package to start from: None, Viewer, FunctionsOnly, ReadOnly or FullAccess.") + public basePackage: MarketplaceAccessLevel; + + /** @description Tool names the role may run (from the connection's tools). */ + // @DataMember + // @ApiMember(Description="Tool names the role may run (from the connection's tools).") + public tools: string[] = []; + + /** @description Function ids (func_…) of this connection the role may run. */ + // @DataMember + // @ApiMember(Description="Function ids (func_…) of this connection the role may run.") + public functions: string[] = []; + + /** @description True to include the Manage rights (edit the connection, its secrets, validation and functions). */ + // @DataMember + // @ApiMember(Description="True to include the Manage rights (edit the connection, its secrets, validation and functions).") + public manage: boolean; + + /** @description True to write a new policy for this role only when its current custom policy is shared with other roles. */ + // @DataMember + // @ApiMember(Description="True to write a new policy for this role only when its current custom policy is shared with other roles.") + public newPolicyForThisRoleOnly: boolean; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + public getTypeName() { + return 'SaveMarketplaceIntegrationCustomAccess'; + } + public getMethod() { + return 'POST'; + } + public createResponse() { + return new SaveMarketplaceIntegrationCustomAccessResponse(); + } + } + // @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/tools", "GET") export class GetMarketplaceIntegrationTools extends CodeMashRequestBase From 6bd8de529a910b71fe9e8425a03b29f2ed0cb983 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Tue, 13 Oct 2026 15:59:52 +0300 Subject: [PATCH 2/4] =?UTF-8?q?feat(marketplace):=20connection=20access=20?= =?UTF-8?q?matrix,=20role=20package=20and=20custom=20policy=20(routes=2025?= =?UTF-8?q?=E2=80=9327)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeModule gains getMarketplaceIntegrationAccess (GET .../access), setMarketplaceIntegrationRoleAccess (PUT .../access/roles) and saveMarketplaceIntegrationCustomAccess (POST .../access/roles/custom), spliced from the generate-endpoints output (Code module only; the hand helper getMarketplaceFunctionRunDetails stays), with their generated tests and docs sections. marketplace-access.test.ts pins the id in the path, no body on the GET, the role / ticks in the PUT / POST body, and reads accessChanges and the step errorCode from the Test response. Co-Authored-By: Claude Opus 5.5 --- docs/hub/code.md | 70 +++++++++++++ src/hub/code.ts | 59 ++++++++++- tests/hub/code.test.ts | 82 ++++++++++++++- tests/hub/marketplace-access.test.ts | 144 +++++++++++++++++++++++++++ 4 files changed, 352 insertions(+), 3 deletions(-) create mode 100644 tests/hub/marketplace-access.test.ts diff --git a/docs/hub/code.md b/docs/hub/code.md index d10cfc5..fec1359 100644 --- a/docs/hub/code.md +++ b/docs/hub/code.md @@ -36,6 +36,9 @@ Accessed as `norbix.hub.code` on the [`Norbix`](../../README.md#authentication) | [`invokeMarketplaceFunction`](#invokemarketplacefunction) | `POST` | `/{version}/code/marketplace/functions/{FunctionViewId}/invoke` | `project` | | [`getMarketplaceListing`](#getmarketplacelisting) | `GET` | `/{version}/code/marketplace/listings/{ListingViewId}` | `project` | | [`testMarketplaceIntegration`](#testmarketplaceintegration) | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/test` | `project` | +| [`getMarketplaceIntegrationAccess`](#getmarketplaceintegrationaccess) | `GET` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/access` | `project` | +| [`setMarketplaceIntegrationRoleAccess`](#setmarketplaceintegrationroleaccess) | `PUT` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles` | `project` | +| [`saveMarketplaceIntegrationCustomAccess`](#savemarketplaceintegrationcustomaccess) | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom` | `project` | | [`getMarketplaceIntegrationTools`](#getmarketplaceintegrationtools) | `GET` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/tools` | `project` | | [`replaceMarketplaceIntegrationSecrets`](#replacemarketplaceintegrationsecrets) | `PUT` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets` | `project` | | [`revealMarketplaceIntegrationSecrets`](#revealmarketplaceintegrationsecrets) | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets/reveal` | `project` | @@ -687,6 +690,73 @@ const result = await norbix.hub.code.testMarketplaceIntegration({ [↑ Top](#endpoints) +### getMarketplaceIntegrationAccess + +`GET` `/{version}/code/marketplace/integrations/{IntegrationViewId}/access` + +Fetch a single item by ID. + +**Request DTO**: `CodeMashHub2.GetMarketplaceIntegrationAccess` +**Response**: `CodeMashHub2.GetMarketplaceIntegrationAccessResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.code.getMarketplaceIntegrationAccess({ + IntegrationViewId: 'IntegrationViewId-here', + // Other fields: see CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.GetMarketplaceIntegrationAccessResponse +``` + +[↑ Top](#endpoints) + +### setMarketplaceIntegrationRoleAccess + +`PUT` `/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles` + +**Request DTO**: `CodeMashHub2.SetMarketplaceIntegrationRoleAccess` +**Response**: `CodeMashHub2.IdResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.code.setMarketplaceIntegrationRoleAccess({ + IntegrationViewId: 'IntegrationViewId-here', + // Other fields: see CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.IdResponse +``` + +[↑ Top](#endpoints) + +### saveMarketplaceIntegrationCustomAccess + +`POST` `/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom` + +Upsert an item (create or update). + +**Request DTO**: `CodeMashHub2.SaveMarketplaceIntegrationCustomAccess` +**Response**: `CodeMashHub2.SaveMarketplaceIntegrationCustomAccessResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.code.saveMarketplaceIntegrationCustomAccess({ + IntegrationViewId: 'IntegrationViewId-here', + // Other fields: see CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.SaveMarketplaceIntegrationCustomAccessResponse +``` + +[↑ Top](#endpoints) + ### getMarketplaceIntegrationTools `GET` `/{version}/code/marketplace/integrations/{IntegrationViewId}/tools` diff --git a/src/hub/code.ts b/src/hub/code.ts index e676306..b65477b 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: 38 + * Endpoints: 42 */ export class CodeModule { constructor(private readonly transport: Transport) {} @@ -545,6 +545,63 @@ export class CodeModule { }); }; + /** + * GET /{version}/code/marketplace/integrations/{IntegrationViewId}/access + * Request DTO: GetMarketplaceIntegrationAccess + */ + getMarketplaceIntegrationAccess = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/code/marketplace/integrations/{IntegrationViewId}/access', + method: 'GET', + request, + pathParams: ['IntegrationViewId'], + scope: 'project', + ...options, + }); + }; + + /** + * PUT /{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles + * Request DTO: SetMarketplaceIntegrationRoleAccess + */ + setMarketplaceIntegrationRoleAccess = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles', + method: 'PUT', + request, + pathParams: ['IntegrationViewId'], + scope: 'project', + ...options, + }); + }; + + /** + * POST /{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom + * Request DTO: SaveMarketplaceIntegrationCustomAccess + */ + saveMarketplaceIntegrationCustomAccess = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom', + method: 'POST', + request, + pathParams: ['IntegrationViewId'], + scope: 'project', + ...options, + }); + }; + /** * GET /{version}/code/marketplace/integrations/{IntegrationViewId}/tools * Request DTO: GetMarketplaceIntegrationTools diff --git a/tests/hub/code.test.ts b/tests/hub/code.test.ts index 9861562..d8f4dcd 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 (38 endpoints). + * Tests for hub.code (42 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 38 method(s)', () => { + it('module exposes 42 method(s)', () => { const mock = createMockFetch(); const mod = new CodeModule({} as never); void mod; // silence unused — we only need the type @@ -57,6 +57,9 @@ describe('hub.code', () => { expect(typeof ns['invokeMarketplaceFunction']).toBe('function'); expect(typeof ns['getMarketplaceListing']).toBe('function'); expect(typeof ns['testMarketplaceIntegration']).toBe('function'); + expect(typeof ns['getMarketplaceIntegrationAccess']).toBe('function'); + expect(typeof ns['setMarketplaceIntegrationRoleAccess']).toBe('function'); + expect(typeof ns['saveMarketplaceIntegrationCustomAccess']).toBe('function'); expect(typeof ns['getMarketplaceIntegrationTools']).toBe('function'); expect(typeof ns['replaceMarketplaceIntegrationSecrets']).toBe('function'); expect(typeof ns['revealMarketplaceIntegrationSecrets']).toBe('function'); @@ -738,6 +741,81 @@ describe('hub.code', () => { expect(mock.lastCall?.headers.get('norbix-project-id')).toBe('test-project'); }); + it('getMarketplaceIntegrationAccess: GET /{version}/code/marketplace/integrations/{IntegrationViewId}/access', async () => { + const stub = stubRequestForPath( + '/{version}/code/marketplace/integrations/{IntegrationViewId}/access', + ); + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/code/marketplace/integrations/{IntegrationViewId}/access', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['code']!['getMarketplaceIntegrationAccess']!; + await fn(stub); + expect(mock.lastCall).toBeDefined(); + expect(mock.lastCall?.method).toBe('GET'); + 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('setMarketplaceIntegrationRoleAccess: PUT /{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles', async () => { + const stub = stubRequestForPath( + '/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles', + ); + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['code']!['setMarketplaceIntegrationRoleAccess']!; + await fn(stub); + expect(mock.lastCall).toBeDefined(); + expect(mock.lastCall?.method).toBe('PUT'); + 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('saveMarketplaceIntegrationCustomAccess: POST /{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom', async () => { + const stub = stubRequestForPath( + '/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom', + ); + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['code']!['saveMarketplaceIntegrationCustomAccess']!; + 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('getMarketplaceIntegrationTools: GET /{version}/code/marketplace/integrations/{IntegrationViewId}/tools', async () => { const stub = stubRequestForPath( '/{version}/code/marketplace/integrations/{IntegrationViewId}/tools', diff --git a/tests/hub/marketplace-access.test.ts b/tests/hub/marketplace-access.test.ts new file mode 100644 index 0000000..aff3f7f --- /dev/null +++ b/tests/hub/marketplace-access.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, it } from 'vitest'; + +import type { CodeMashHub2 } from '../../src/types/hub2.dtos.js'; +import { createMockFetch, makeClient } from '../_helpers.js'; + +/** + * Routes 25–27 of the marketplace contract (gateway item access): the access + * matrix of a connection, a role's package and a custom access policy. + * + * The generated tests in code.test.ts send a stub request; these cases pin + * that the connection id goes into the path (never the query or the body), + * that the GET sends no body, and that the PUT / POST carry the role and the + * ticks in the JSON body. + */ +const INTEGRATION = 'int_6Qp1dW2kRm8NsT4vYc0Hb'; +const ROLE = 'acc_6Qp1dW2kRm8NsT4vYc0Hb_nr_3kLm9QwErTyU'; +const BASE = `/v2/code/marketplace/integrations/${INTEGRATION}/access`; + +describe('hub.code — marketplace connection access (routes 25–27)', () => { + it('getMarketplaceIntegrationAccess: GET with the id in the path, no body, matrix read back', async () => { + const role = { + roleId: ROLE, + name: 'Projects Developer', + scope: 'account', + isSystem: true, + fixed: false, + package: 'None', + isCustom: false, + manage: false, + custom: [], + defaultNotApplied: 'The person who enabled the connection may not manage policies.', + } as unknown as CodeMashHub2.MarketplaceAccessRoleDto; + const mock = createMockFetch({ + body: { + roles: [role], + packages: [ + { kind: 'ReadOnly', policyId: 'pol_1', name: 'GitHub — Read-only', exists: true }, + ], + tools: [{ name: 'get_me', readOnly: true, grantable: true }], + functions: [{ functionId: 'func_1', name: 'Create issue', functionKey: 'create_issue' }], + }, + }); + const { norbix } = makeClient({ fetch: mock.fetch }); + + const response = await norbix.hub.code.getMarketplaceIntegrationAccess({ + integrationViewId: INTEGRATION, + }); + + const call = mock.lastCall!; + const url = new URL(call.url); + expect(call.method).toBe('GET'); + expect(url.pathname).toBe(BASE); + expect(url.searchParams.has('integrationViewId')).toBe(false); + expect(call.body).toBeUndefined(); + expect(call.headers.get('norbix-project-id')).toBe('test-project'); + expect(response.roles).toEqual([role]); + expect(response.roles?.[0]?.defaultNotApplied).toContain('manage policies'); + expect(response.tools?.[0]?.grantable).toBe(true); + }); + + it('setMarketplaceIntegrationRoleAccess: PUT with the id in the path and the role in the body', async () => { + const mock = createMockFetch({ body: { id: ROLE } }); + const { norbix } = makeClient({ fetch: mock.fetch }); + + const response = await norbix.hub.code.setMarketplaceIntegrationRoleAccess({ + integrationViewId: INTEGRATION, + roleId: ROLE, + package: 'ReadOnly' as CodeMashHub2.MarketplaceAccessLevel, + manage: false, + }); + + const call = mock.lastCall!; + expect(call.method).toBe('PUT'); + expect(new URL(call.url).pathname).toBe(`${BASE}/roles`); + expect(JSON.parse(call.body as string)).toEqual({ + roleId: ROLE, + package: 'ReadOnly', + manage: false, + }); + expect(response.id).toBe(ROLE); + }); + + it('saveMarketplaceIntegrationCustomAccess: POST with the id in the path and the ticks in the body', async () => { + const mock = createMockFetch({ body: { policyId: 'pol_9', created: true, usedByRoles: 1 } }); + const { norbix } = makeClient({ fetch: mock.fetch }); + + const response = await norbix.hub.code.saveMarketplaceIntegrationCustomAccess({ + integrationViewId: INTEGRATION, + roleId: ROLE, + basePackage: 'ReadOnly' as CodeMashHub2.MarketplaceAccessLevel, + tools: ['create_issue'], + functions: ['func_4hT2b9QkLm3XcT1yRfGh7a'], + manage: false, + newPolicyForThisRoleOnly: true, + }); + + const call = mock.lastCall!; + expect(call.method).toBe('POST'); + expect(new URL(call.url).pathname).toBe(`${BASE}/roles/custom`); + expect(JSON.parse(call.body as string)).toEqual({ + roleId: ROLE, + basePackage: 'ReadOnly', + tools: ['create_issue'], + functions: ['func_4hT2b9QkLm3XcT1yRfGh7a'], + manage: false, + newPolicyForThisRoleOnly: true, + }); + expect(response).toMatchObject({ policyId: 'pol_9', created: true, usedByRoles: 1 }); + }); + + it('testMarketplaceIntegration: accessChanges and the step errorCode come back as sent', async () => { + const mock = createMockFetch({ + body: { + steps: [ + { + key: 'mcp.tools', + label: 'Tools listed', + requiredForFunctions: true, + succeeded: false, + skipped: false, + message: 'The server listed no tools.', + errorCode: 'CM-ERRORS-MARKETPLACE-018', + }, + ], + accessChanges: { + readOnlyAdded: ['get_me'], + readOnlyRemoved: [], + notGrantable: ['bad name'], + }, + }, + }); + const { norbix } = makeClient({ fetch: mock.fetch }); + + const response: CodeMashHub2.TestMarketplaceIntegrationResponse = + await norbix.hub.code.testMarketplaceIntegration({ integrationViewId: INTEGRATION }); + + expect(response.steps?.[0]?.errorCode).toBe('CM-ERRORS-MARKETPLACE-018'); + expect(response.accessChanges).toEqual({ + readOnlyAdded: ['get_me'], + readOnlyRemoved: [], + notGrantable: ['bad name'], + }); + }); +}); From 701a6cf56dda9b6b603201c56ada19b9abd9846a Mon Sep 17 00:00:00 2001 From: CodeMash Date: Tue, 13 Oct 2026 16:23:46 +0300 Subject: [PATCH 3/4] chore(types): regenerate hub types from gateway refactoringV2 515275038 (oauth, route 28) Adds StartMarketplaceIntegrationSignIn / StartMarketplaceIntegrationSignInResponse (authorizeUrl, redirectUrl, expiresAt) and the additive fields MarketplaceIntegrationDto.signedIn / signedInBy / signedInAt / tokenExpiresAt, MarketplaceIntegrationListProjection.signedIn / signedInAt and MarketplaceListingDto.connectSteps / oauthRedirectUrl. Route 29 (the OAuth callback) is not in the type metadata. Co-Authored-By: Claude Opus 5.5 --- src/types/hub2.contract.json | 4 +++ src/types/hub2.dtos.ts | 61 +++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/types/hub2.contract.json b/src/types/hub2.contract.json index 668e0bf..8545506 100644 --- a/src/types/hub2.contract.json +++ b/src/types/hub2.contract.json @@ -1971,6 +1971,10 @@ "requiresAuth": true, "returnsBytes": false }, + "StartMarketplaceIntegrationSignIn": { + "requiresAuth": true, + "returnsBytes": false + }, "StopChatRunStepRequest": { "requiresAuth": true, "returnsBytes": false diff --git a/src/types/hub2.dtos.ts b/src/types/hub2.dtos.ts index aeb9209..5875a3f 100644 --- a/src/types/hub2.dtos.ts +++ b/src/types/hub2.dtos.ts @@ -1,6 +1,6 @@ // @ts-nocheck /* Options: -Date: 2026-10-13 12:56:15 +Date: 2026-10-13 13:23:35 Version: 10.20 Tip: To override a DTO option, remove "//" prefix before updating BaseUrl: http://localhost:5001 @@ -3908,6 +3908,18 @@ export module CodeMashHub2 { // @DataMember public lastValidationSteps?: MarketplaceValidationStepResultDto[]; + // @DataMember + public signedIn: boolean; + + // @DataMember + public signedInBy?: string; + + // @DataMember + public signedInAt?: string; + + // @DataMember + public tokenExpiresAt?: string; + public constructor(init?: Partial) { super(init); (Object as any).assign(this, init); @@ -4208,6 +4220,12 @@ export module CodeMashHub2 { // @DataMember public availability: MarketplaceAvailability; + // @DataMember + public connectSteps: IReadOnlyList; + + // @DataMember + public oAuthRedirectUrl?: string; + // @DataMember public isOfficial: boolean; @@ -9541,6 +9559,12 @@ export module CodeMashHub2 { // @DataMember public requiresHumanDeliveryConfirmation: boolean; + // @DataMember + public signedIn: boolean; + + // @DataMember + public signedInAt?: string; + public constructor(init?: Partial) { (Object as any).assign(this, init); } @@ -12927,6 +12951,17 @@ export module CodeMashHub2 { } } + export class StartMarketplaceIntegrationSignInResponse extends ResponseBase { + public authorizeUrl?: string; + public redirectUrl?: string; + public expiresAt?: string; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + } + export class EmptyMarketplaceSecretsResponse extends ResponseBase { public constructor(init?: Partial) { super(init); @@ -23969,6 +24004,30 @@ export module CodeMashHub2 { } } + // @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start", "POST") + export class StartMarketplaceIntegrationSignIn + 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 'StartMarketplaceIntegrationSignIn'; + } + public getMethod() { + return 'POST'; + } + public createResponse() { + return new StartMarketplaceIntegrationSignInResponse(); + } + } + // @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets", "PUT") // @DataContract export class ReplaceMarketplaceIntegrationSecretsRequest From f2aeea4fe46432be9cb19db7a4166f57b9e8d336 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Tue, 13 Oct 2026 16:24:49 +0300 Subject: [PATCH 4/4] feat(marketplace): startMarketplaceIntegrationSignIn, the OAuth sign-in start (route 28) CodeModule gains startMarketplaceIntegrationSignIn (POST .../integrations/{IntegrationViewId}/oauth/start), spliced from the generate-endpoints output with its generated test and docs section. marketplace-oauth.test.ts pins the id in the path only and reads authorizeUrl / redirectUrl, and the new signedIn / signedInBy / tokenExpiresAt fields of a connection. Route 29 (the callback) is not in the type metadata and has no method. Co-Authored-By: Claude Opus 5.5 --- docs/hub/code.md | 22 ++++++++++ src/hub/code.ts | 21 +++++++++- tests/hub/code.test.ts | 30 +++++++++++++- tests/hub/marketplace-oauth.test.ts | 62 +++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 tests/hub/marketplace-oauth.test.ts diff --git a/docs/hub/code.md b/docs/hub/code.md index fec1359..17314a6 100644 --- a/docs/hub/code.md +++ b/docs/hub/code.md @@ -40,6 +40,7 @@ Accessed as `norbix.hub.code` on the [`Norbix`](../../README.md#authentication) | [`setMarketplaceIntegrationRoleAccess`](#setmarketplaceintegrationroleaccess) | `PUT` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles` | `project` | | [`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` | | [`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` | @@ -780,6 +781,27 @@ const result = await norbix.hub.code.getMarketplaceIntegrationTools({ [↑ Top](#endpoints) +### startMarketplaceIntegrationSignIn + +`POST` `/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start` + +**Request DTO**: `CodeMashHub2.StartMarketplaceIntegrationSignIn` +**Response**: `CodeMashHub2.StartMarketplaceIntegrationSignInResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.code.startMarketplaceIntegrationSignIn({ + IntegrationViewId: 'IntegrationViewId-here', + // Other fields: see CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.StartMarketplaceIntegrationSignInResponse +``` + +[↑ Top](#endpoints) + ### replaceMarketplaceIntegrationSecrets `PUT` `/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets` diff --git a/src/hub/code.ts b/src/hub/code.ts index b65477b..49e4864 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: 42 + * Endpoints: 43 */ export class CodeModule { constructor(private readonly transport: Transport) {} @@ -621,6 +621,25 @@ export class CodeModule { }); }; + /** + * POST /{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start + * Request DTO: StartMarketplaceIntegrationSignIn + */ + startMarketplaceIntegrationSignIn = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start', + method: 'POST', + request, + pathParams: ['IntegrationViewId'], + scope: 'project', + ...options, + }); + }; + /** * PUT /{version}/code/marketplace/integrations/{IntegrationViewId}/secrets * Request DTO: ReplaceMarketplaceIntegrationSecretsRequest diff --git a/tests/hub/code.test.ts b/tests/hub/code.test.ts index d8f4dcd..f7dfd18 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 (42 endpoints). + * Tests for hub.code (43 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 42 method(s)', () => { + it('module exposes 43 method(s)', () => { const mock = createMockFetch(); const mod = new CodeModule({} as never); void mod; // silence unused — we only need the type @@ -61,6 +61,7 @@ describe('hub.code', () => { expect(typeof ns['setMarketplaceIntegrationRoleAccess']).toBe('function'); expect(typeof ns['saveMarketplaceIntegrationCustomAccess']).toBe('function'); expect(typeof ns['getMarketplaceIntegrationTools']).toBe('function'); + expect(typeof ns['startMarketplaceIntegrationSignIn']).toBe('function'); expect(typeof ns['replaceMarketplaceIntegrationSecrets']).toBe('function'); expect(typeof ns['revealMarketplaceIntegrationSecrets']).toBe('function'); expect(typeof ns['setMarketplaceIntegrationTokenMappings']).toBe('function'); @@ -841,6 +842,31 @@ describe('hub.code', () => { expect(mock.lastCall?.headers.get('norbix-project-id')).toBe('test-project'); }); + it('startMarketplaceIntegrationSignIn: POST /{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start', async () => { + const stub = stubRequestForPath( + '/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start', + ); + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['code']!['startMarketplaceIntegrationSignIn']!; + 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-oauth.test.ts b/tests/hub/marketplace-oauth.test.ts new file mode 100644 index 0000000..db9a28d --- /dev/null +++ b/tests/hub/marketplace-oauth.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest'; + +import { createMockFetch, makeClient } from '../_helpers.js'; + +/** + * Route 28 of the marketplace contract (gateway item oauth): start the OAuth + * sign-in of an MCP connection. The answer carries the vendor's authorize URL + * to open in a browser; route 29 (the callback) is not in the type metadata + * and has no SDK method. + */ +const INTEGRATION = 'int_6Qp1dW2kRm8NsT4vYc0Hb'; + +describe('hub.code.startMarketplaceIntegrationSignIn', () => { + it('POSTs with the connection id in the path only and returns the authorize URL', async () => { + const mock = createMockFetch({ + body: { + authorizeUrl: 'https://slack.com/oauth/v2/authorize?client_id=1&state=s1', + redirectUrl: 'https://hub.norbix.ai/v3/code/marketplace/oauth/callback', + expiresAt: '2026-10-13T08:10:00Z', + }, + }); + const { norbix } = makeClient({ fetch: mock.fetch }); + + const response = await norbix.hub.code.startMarketplaceIntegrationSignIn({ + 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/start`); + expect(url.searchParams.has('integrationViewId')).toBe(false); + expect(JSON.parse((call.body as string | undefined) ?? '{}')).not.toHaveProperty( + 'integrationViewId', + ); + expect(response.authorizeUrl).toContain('slack.com/oauth/v2/authorize'); + expect(response.redirectUrl).toBe('https://hub.norbix.ai/v3/code/marketplace/oauth/callback'); + }); + + it('reads the sign-in fields of a connection and a listing', async () => { + const mock = createMockFetch({ + body: { + integration: { + viewId: INTEGRATION, + signedIn: true, + signedInBy: 'ada@example.test', + signedInAt: '2026-10-13T08:00:00Z', + tokenExpiresAt: '2026-10-13T20:00:00Z', + }, + }, + }); + const { norbix } = makeClient({ fetch: mock.fetch }); + + const response = await norbix.hub.code.getMarketplaceIntegration({ + integrationViewId: INTEGRATION, + }); + + expect(response.integration?.signedIn).toBe(true); + expect(response.integration?.signedInBy).toBe('ada@example.test'); + expect(response.integration?.tokenExpiresAt).toBe('2026-10-13T20:00:00Z'); + }); +});