From 0cb356b6ccb8e35c593c9e62f4b008354937e11a Mon Sep 17 00:00:00 2001 From: CodeMash Date: Wed, 21 Oct 2026 21:18:40 +0300 Subject: [PATCH 1/4] feat(types): regenerate api2/hub2 types from gateway audit/logging Generated from the private stack (Hub 127.0.0.1:15101, Api 127.0.0.1:15102), gateway branch audit/logging (includes refactoringV2 as of 2026-10-21). (Type `gen` is not in this repo's commitlint list; `feat(types)` as in 019b278.) Logs: WriteLogs / WriteLogEntry / WriteLogsResponse (Api), GetLog / GetLogResponse (Hub), SaveLogSettings fields now optional (absent = unchanged), AwsCloudWatchLoggingIntegrationRequest/Dto, TenantLogEntryDto.origin. Also brought in from refactoringV2: RegisterFilesRequest (+ items/results), ownerUserId / reassignOwner on Files requests, endUserOwnRecords on schema DTOs. Nothing removed. Co-Authored-By: Claude Opus 5.5 --- src/types/api2.contract.json | 8 ++ src/types/api2.dtos.ts | 176 ++++++++++++++++++++++++++++++++++- src/types/hub2.contract.json | 4 + src/types/hub2.dtos.ts | 82 ++++++++++++++-- 4 files changed, 259 insertions(+), 11 deletions(-) diff --git a/src/types/api2.contract.json b/src/types/api2.contract.json index 047e3d5..29a79c8 100644 --- a/src/types/api2.contract.json +++ b/src/types/api2.contract.json @@ -327,6 +327,10 @@ "requiresAuth": false, "returnsBytes": false }, + "RegisterFilesRequest": { + "requiresAuth": true, + "returnsBytes": false + }, "RegisterOwnPushDeviceRequest": { "requiresAuth": true, "returnsBytes": false @@ -466,5 +470,9 @@ "VerifyPasskeyRegistrationRequest": { "requiresAuth": false, "returnsBytes": false + }, + "WriteLogs": { + "requiresAuth": true, + "returnsBytes": false } } diff --git a/src/types/api2.dtos.ts b/src/types/api2.dtos.ts index f5ebb16..aba32c7 100644 --- a/src/types/api2.dtos.ts +++ b/src/types/api2.dtos.ts @@ -1,6 +1,6 @@ // @ts-nocheck /* Options: -Date: 2026-10-19 10:55:02 +Date: 2026-10-21 18:12:02 Version: 10.20 Tip: To override a DTO option, remove "//" prefix before updating BaseUrl: http://localhost:5002 @@ -548,6 +548,62 @@ export module CodeMashApi2 { } } + // @DataContract + export class RegisterFileItem { + // @DataMember + public path?: string; + + // @DataMember + public ownerUserId?: string; + + // @DataMember + public contentType?: string; + + // @DataMember + public sizeBytes?: number; + + // @DataMember + public fileName?: string; + + public constructor(init?: Partial) { + (Object as any).assign(this, init); + } + } + + export class WriteLogEntry { + /** @description information (default), warning or error. */ + // @ApiMember(Description="information (default), warning or error.") + public level?: string; + + /** @description A short line, 1–200 characters — what the Logs list shows. */ + // @ApiMember(Description="A short line, 1–200 characters — what the Logs list shows.", IsRequired=true) + public title: string; + + /** @description Details, at most 8 000 characters. */ + // @ApiMember(Description="Details, at most 8 000 characters.") + public message?: string; + + /** @description Your own code for this kind of line, e.g. order:paid — lower case letters, digits and _ . : -, at most 64. Stored as app:; app:log when empty. */ + // @ApiMember(Description="Your own code for this kind of line, e.g. order:paid — lower case letters, digits and _ . : -, at most 64. Stored as app:; app:log when empty.") + public eventCode?: string; + + /** @description Groups your lines into one flow in the Logs details view: at most 64 letters, digits, _ or -. Empty = the id of this request. */ + // @ApiMember(Description="Groups your lines into one flow in the Logs details view: at most 64 letters, digits, _ or -. Empty = the id of this request.") + public correlationId?: string; + + /** @description When it happened (UTC). Empty = when Norbix received it. At most 7 days old and at most 5 minutes ahead. */ + // @ApiMember(Description="When it happened (UTC). Empty = when Norbix received it. At most 7 days old and at most 5 minutes ahead.") + public timestamp?: string; + + /** @description Extra fields, at most 20; key at most 64, value at most 2 000 characters. Values under secret-looking keys (password, token, apiKey …) are masked. */ + // @ApiMember(Description="Extra fields, at most 20; key at most 64, value at most 2 000 characters. Values under secret-looking keys (password, token, apiKey …) are masked.") + public meta?: { [index: string]: string }; + + public constructor(init?: Partial) { + (Object as any).assign(this, init); + } + } + // @DataContract export class PaymentOrderLineInput { // @DataMember @@ -1102,6 +1158,9 @@ export module CodeMashApi2 { // @DataMember public uniqueIndexes?: SchemaUniqueIndexDto[]; + // @DataMember + public endUserOwnRecords?: boolean; + public constructor(init?: Partial) { (Object as any).assign(this, init); } @@ -1355,6 +1414,23 @@ export module CodeMashApi2 { } } + export class RegisterFileResult { + public index: number; + public path?: string; + public ownerUserId?: string; + public status?: string; + public fileId?: string; + public verified?: boolean; + public warning?: string; + public errorCode?: string; + public errorStatus?: number; + public error?: string; + + public constructor(init?: Partial) { + (Object as any).assign(this, init); + } + } + // @DataContract export class IntegrationTestResultItemDto { // @DataMember @@ -2045,6 +2121,19 @@ export module CodeMashApi2 { } } + export class RegisterFilesResponse extends ResponseBase { + public dryRun: boolean; + public registered: number; + public unchanged: number; + public failed: number; + public items?: RegisterFileResult[]; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + } + export class RequestUploadUrlResponse extends ResponseBase { public url?: string; @@ -2083,6 +2172,15 @@ export module CodeMashApi2 { } } + export class WriteLogsResponse extends ResponseBase { + public accepted: number; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + } + export class GetPaymentOrderResponse extends ResponseBase { public item?: PaymentOrderDto; @@ -4944,6 +5042,12 @@ export module CodeMashApi2 { // @DataMember public fileName?: string; + // @DataMember + public ownerUserId?: string; + + // @DataMember + public reassignOwner?: boolean; + public constructor(init?: Partial) { super(init); (Object as any).assign(this, init); @@ -5243,6 +5347,41 @@ export module CodeMashApi2 { } } + /** @description Files */ + // @Route("/{version}/files/{filesIntegrationId}/register", "POST") + // @Api(Description="Files") + // @DataContract + export class RegisterFilesRequest + extends CodeMashRequestBase + implements IReturn + { + // @DataMember + public filesIntegrationId: string; + + // @DataMember + public items?: RegisterFileItem[]; + + // @DataMember + public reassignOwner?: boolean; + + // @DataMember + public dryRun?: boolean; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + public getTypeName() { + return 'RegisterFilesRequest'; + } + public getMethod() { + return 'POST'; + } + public createResponse() { + return new RegisterFilesResponse(); + } + } + /** @description Files */ // @Route("/{version}/files/{filesIntegrationId}/upload-url", "POST") // @Api(Description="Files") @@ -5263,6 +5402,12 @@ export module CodeMashApi2 { // @DataMember public expirationSeconds?: number; + // @DataMember + public ownerUserId?: string; + + // @DataMember + public reassignOwner?: boolean; + public constructor(init?: Partial) { super(init); (Object as any).assign(this, init); @@ -5315,6 +5460,12 @@ export module CodeMashApi2 { // @DataMember public path: string; + // @DataMember + public ownerUserId?: string; + + // @DataMember + public reassignOwner?: boolean; + public constructor(init?: Partial) { super(init); (Object as any).assign(this, init); @@ -5546,6 +5697,29 @@ export module CodeMashApi2 { } } + /** @description Send your app's own log entries to the project's logs. */ + // @Route("/{version}/logs", "POST") + // @Api(Description="Send your app's own log entries to the project's logs.") + export class WriteLogs extends CodeMashRequestBase implements IReturn { + /** @description 1–100 entries. */ + // @ApiMember(Description="1–100 entries.", IsRequired=true) + public entries: WriteLogEntry[] = []; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + public getTypeName() { + return 'WriteLogs'; + } + public getMethod() { + return 'POST'; + } + public createResponse() { + return new WriteLogsResponse(); + } + } + /** @description Stores a payment order with a fixed price for one of the project's users. Server only (service user API key). */ // @Route("/{version}/payments/orders", "POST") // @Api(Description="Stores a payment order with a fixed price for one of the project's users. Server only (service user API key).") diff --git a/src/types/hub2.contract.json b/src/types/hub2.contract.json index 01d9bb3..f7cccaf 100644 --- a/src/types/hub2.contract.json +++ b/src/types/hub2.contract.json @@ -1119,6 +1119,10 @@ "requiresAuth": true, "returnsBytes": false }, + "GetLog": { + "requiresAuth": true, + "returnsBytes": false + }, "GetLogSettings": { "requiresAuth": true, "returnsBytes": false diff --git a/src/types/hub2.dtos.ts b/src/types/hub2.dtos.ts index 45c62a6..1c9f7f6 100644 --- a/src/types/hub2.dtos.ts +++ b/src/types/hub2.dtos.ts @@ -1,6 +1,6 @@ // @ts-nocheck /* Options: -Date: 2026-10-21 09:44:23 +Date: 2026-10-21 18:12:03 Version: 10.20 Tip: To override a DTO option, remove "//" prefix before updating BaseUrl: http://localhost:5001 @@ -5211,6 +5211,31 @@ export module CodeMashHub2 { } } + export class AwsCloudWatchLoggingIntegrationRequest extends LoggingIntegrationRequest { + public provider: LoggingProvider; + public region: string; + public logGroup: string; + public logStream: string; + public accessKey: string; + public secretKey: string; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + } + + export class AwsCloudWatchLoggingIntegrationDto extends LoggingIntegrationDto { + public region: string; + public logGroup: string; + public logStream: string; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + } + export class JsonSchemaFieldDto { // @DataMember public fieldName: string; @@ -10997,6 +11022,9 @@ export module CodeMashHub2 { // @DataMember public env: string; + // @DataMember + public origin: string; + // @DataMember public meta?: IReadOnlyDictionary; @@ -14556,6 +14584,15 @@ export module CodeMashHub2 { } } + export class GetLogResponse extends ResponseBase { + public item?: TenantLogEntryDto; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + } + export class GetLogsResponse extends ResponseBase { public list?: PaginatedResponse; @@ -16257,6 +16294,8 @@ export module CodeMashHub2 { public typegen_282_FakeCodeIntegrationRequest?: FakeCodeIntegrationRequest; public typegen_283_CodeFunctionSchedulerTaskRequest?: CodeFunctionSchedulerTaskRequest; public typegen_284_MarketplaceFunctionSchedulerTaskRequest?: MarketplaceFunctionSchedulerTaskRequest; + public typegen_285_AwsCloudWatchLoggingIntegrationRequest?: AwsCloudWatchLoggingIntegrationRequest; + public typegen_286_AwsCloudWatchLoggingIntegrationDto?: AwsCloudWatchLoggingIntegrationDto; public constructor(init?: Partial) { (Object as any).assign(this, init); @@ -27856,6 +27895,29 @@ export module CodeMashHub2 { } } + /** @description Fetch one tenant log entry by id */ + // @Route("/{version}/logs/{Id}", "GET") + // @Api(Description="Fetch one tenant log entry by id") + export class GetLog extends CodeMashRequestBase implements IReturn { + /** @description The log entry id (24 hex characters), from a get_logs row. */ + // @ApiMember(Description="The log entry id (24 hex characters), from a get_logs row.", IsRequired=true) + public id: string; + + public constructor(init?: Partial) { + super(init); + (Object as any).assign(this, init); + } + public getTypeName() { + return 'GetLog'; + } + public getMethod() { + return 'GET'; + } + public createResponse() { + return new GetLogResponse(); + } + } + /** @description Fetch a filtered, cursor-paged list of tenant log entries */ // @Route("/{version}/logs", "GET") // @Api(Description="Fetch a filtered, cursor-paged list of tenant log entries") @@ -27943,17 +28005,17 @@ export module CodeMashHub2 { extends CodeMashRequestBase implements IReturn { - /** @description Drop log entries from requests originating from the Norbix studio (cloud dashboard). */ - // @ApiMember(Description="Drop log entries from requests originating from the Norbix studio (cloud dashboard).") - public skipCloudDashboardLogs: boolean; + /** @description Drop log entries from requests originating from the Norbix studio (cloud dashboard). Not sent = unchanged. */ + // @ApiMember(Description="Drop log entries from requests originating from the Norbix studio (cloud dashboard). Not sent = unchanged.") + public skipCloudDashboardLogs?: boolean; - /** @description Strip request/response body meta off http:request / http:response log entries. */ - // @ApiMember(Description="Strip request/response body meta off http:request / http:response log entries.") - public skipHttpBodyMeta: boolean; + /** @description Strip request/response body meta off http:request / http:response log entries. Not sent = unchanged. */ + // @ApiMember(Description="Strip request/response body meta off http:request / http:response log entries. Not sent = unchanged.") + public skipHttpBodyMeta?: boolean; - /** @description Turn on tenant-visible log entries for AI chat turns. Default false. */ - // @ApiMember(Description="Turn on tenant-visible log entries for AI chat turns. Default false.") - public aiChatLoggingEnabled: boolean; + /** @description Turn on tenant-visible log entries for AI chat turns. Default false. Not sent = unchanged. */ + // @ApiMember(Description="Turn on tenant-visible log entries for AI chat turns. Default false. Not sent = unchanged.") + public aiChatLoggingEnabled?: boolean; public constructor(init?: Partial) { super(init); From 85eca0033079579c3879d1c0f25198f0ed858840 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Wed, 21 Oct 2026 21:18:58 +0300 Subject: [PATCH 2/4] feat(logs): api.logs.write / writeLogs, hub.logs.getLog, Retry-After on NorbixError - norbix.api.logs.writeLogs({ entries }) (generated, POST /{version}/logs on the Api host) and the hand-written norbix.api.logs.write(line | lines). - norbix.hub.logs.getLog({ id }) (generated, GET /{version}/logs/{Id}). - NorbixError.retryAfterSeconds: the answer's Retry-After header in seconds (a 429 CM-ERRORS-LOGGING-003 from writeLogs). New optional field; nothing else on NorbixError changes. The SDK still never retries a POST. - Tests with a fake fetch: every entry field in the body, one line wrapped in a list, accepted count, 429 thrown once with retryAfterSeconds = 7, 400 CM-ERRORS-LOGGING-002, getLog id in the path, saveLogSettings sends only the given fields, MongoDB integration save then testLoggingIntegration. Co-Authored-By: Claude Opus 5.5 --- src/api/index.ts | 3 + src/api/logs.ts | 66 ++++++++++++++++++ src/client/errors.ts | 51 +++++++++++++- src/hub/logs.ts | 21 +++++- tests/api/logs.test.ts | 147 +++++++++++++++++++++++++++++++++++++++++ tests/hub/logs.test.ts | 65 +++++++++++++++++- 6 files changed, 347 insertions(+), 6 deletions(-) create mode 100644 src/api/logs.ts create mode 100644 tests/api/logs.test.ts diff --git a/src/api/index.ts b/src/api/index.ts index 7d78f59..2134b24 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -7,6 +7,7 @@ import { AuthModule } from './auth.js'; import { DatabaseModule } from './database.js'; import { EchoModule } from './echo.js'; import { FilesModule } from './files.js'; +import { LogsModule } from './logs.js'; import { MembershipModule } from './membership.js'; import { NotificationsModule } from './notifications.js'; import { PaymentsModule } from './payments.js'; @@ -24,6 +25,7 @@ export class ApiNamespace { public readonly database: DatabaseModule; public readonly echo: EchoModule; public readonly files: FilesModule; + public readonly logs: LogsModule; public readonly membership: MembershipModule; public readonly notifications: NotificationsModule; public readonly payments: PaymentsModule; @@ -37,6 +39,7 @@ export class ApiNamespace { this.database = new DatabaseModule(transport); this.echo = new EchoModule(transport); this.files = new FilesModule(transport); + this.logs = new LogsModule(transport); this.membership = new MembershipModule(transport); this.notifications = new NotificationsModule(transport); this.payments = new PaymentsModule(transport); diff --git a/src/api/logs.ts b/src/api/logs.ts new file mode 100644 index 0000000..8f31d73 --- /dev/null +++ b/src/api/logs.ts @@ -0,0 +1,66 @@ +import type { RequestOverrideOptions, Transport } from '../client/transport.js'; +import type { CodeMashApi2 } from '../types/api2.dtos.js'; + +/** + * Auto-generated. Do not edit by hand — run `npm run generate-endpoints` + * to refresh this file from the DTO definitions. + * + * Group: logs + * Endpoints: 1 + */ +export class LogsModule { + constructor(private readonly transport: Transport) {} + + /** + * POST /{version}/logs + * Request DTO: WriteLogs + */ + writeLogs = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'api', + path: '/{version}/logs', + method: 'POST', + request, + pathParams: [], + scope: 'project', + ...options, + }); + }; + + // @hand-written:begin + /** + * Hand-written helper (not from `generate-endpoints`; the generator keeps + * the `@hand-written` block when it regenerates this module). + * + * Send your app's own log lines to the project's Logs — one line or a list + * of 1–100. Same call as `writeLogs({ entries })` (POST /{version}/logs on + * the API host). The answer comes at once with `{ accepted }`: the lines are + * queued, not yet stored. + * + * Each line: `title` (required, ≤ 200), `level` (`information` default, + * `warning`, `error`), `message` (≤ 8000), `eventCode` (a-z 0-9 _ . : -, + * ≤ 64, stored as `app:`), `correlationId` (letters digits _ -, ≤ 64), + * `timestamp` (UTC, ≤ 7 days old, ≤ 5 minutes ahead), `meta` (≤ 20 keys). + * + * Refusals throw a `NorbixError`: 400 for a broken line, 400 + * `CM-ERRORS-LOGGING-002` when the Logs module is off, 429 + * `CM-ERRORS-LOGGING-003` when the project's per-minute quota is used up — + * then `err.retryAfterSeconds` says when to send again. + */ + write = ( + entries: + | (Omit & { + level?: 'information' | 'warning' | 'error'; + }) + | (Omit & { + level?: 'information' | 'warning' | 'error'; + })[], + options: RequestOverrideOptions = {}, + ): Promise => { + return this.writeLogs({ entries: Array.isArray(entries) ? entries : [entries] }, options); + }; + // @hand-written:end +} diff --git a/src/client/errors.ts b/src/client/errors.ts index b43354f..3a7ab04 100644 --- a/src/client/errors.ts +++ b/src/client/errors.ts @@ -56,6 +56,14 @@ export class NorbixError extends Error { public readonly fieldErrors: NorbixErrorItem[]; public readonly raw?: unknown; public readonly url?: string; + /** + * Seconds to wait before trying again, from the answer's `Retry-After` + * header. Set when the gateway sent one — for example a 429 + * `CM-ERRORS-LOGGING-003` from `norbix.api.logs.writeLogs` when the + * project's per-minute quota is used up. A POST is never retried by the SDK, + * so the caller decides when to send again. + */ + public readonly retryAfterSeconds?: number; constructor(opts: { message: string; @@ -64,6 +72,7 @@ export class NorbixError extends Error { fieldErrors?: NorbixErrorItem[]; raw?: unknown; url?: string; + retryAfterSeconds?: number; }) { super(opts.message); this.name = 'NorbixError'; @@ -72,6 +81,7 @@ export class NorbixError extends Error { this.fieldErrors = opts.fieldErrors ?? []; this.raw = opts.raw; this.url = opts.url; + this.retryAfterSeconds = opts.retryAfterSeconds; } /** Same value as `status`. The name every Norbix SDK uses for it. */ @@ -129,6 +139,7 @@ export class NorbixAuthError extends NorbixError { fieldErrors?: NorbixErrorItem[]; url?: string; raw?: unknown; + retryAfterSeconds?: number; }) { super({ message: opts.message, @@ -137,6 +148,7 @@ export class NorbixAuthError extends NorbixError { fieldErrors: opts.fieldErrors ?? [], raw: opts.raw, url: opts.url, + retryAfterSeconds: opts.retryAfterSeconds, }); this.name = 'NorbixAuthError'; } @@ -150,6 +162,7 @@ export class NorbixValidationError extends NorbixError { fieldErrors?: NorbixErrorItem[]; url?: string; raw?: unknown; + retryAfterSeconds?: number; }) { super({ message: opts.message, @@ -158,6 +171,7 @@ export class NorbixValidationError extends NorbixError { fieldErrors: opts.fieldErrors ?? [], raw: opts.raw, url: opts.url, + retryAfterSeconds: opts.retryAfterSeconds, }); this.name = 'NorbixValidationError'; } @@ -238,9 +252,22 @@ export function readErrorBody( } /** Build the right NorbixError for a status + an already parsed body. */ -export function errorFromBody(args: { status: number; raw: unknown; url?: string }): NorbixError { +export function errorFromBody(args: { + status: number; + raw: unknown; + url?: string; + retryAfterSeconds?: number; +}): NorbixError { const { message, code, fieldErrors } = readErrorBody(args.raw, args.status); - const opts = { message, status: args.status, code, fieldErrors, raw: args.raw, url: args.url }; + const opts = { + message, + status: args.status, + code, + fieldErrors, + raw: args.raw, + url: args.url, + retryAfterSeconds: args.retryAfterSeconds, + }; if (args.status === 401 || args.status === 403) return new NorbixAuthError(opts); if (args.status === 400) return new NorbixValidationError(opts); @@ -264,5 +291,23 @@ export async function fromResponse(res: Response, url: string): Promise= 0) return Math.ceil(s); + const at = Date.parse(value); + if (!Number.isNaN(at)) return Math.max(0, Math.ceil((at - Date.now()) / 1000)); + return undefined; } diff --git a/src/hub/logs.ts b/src/hub/logs.ts index b8238af..f43c7a6 100644 --- a/src/hub/logs.ts +++ b/src/hub/logs.ts @@ -6,7 +6,7 @@ import type { CodeMashHub2 } from '../types/hub2.dtos.js'; * to refresh this file from the DTO definitions. * * Group: logs - * Endpoints: 14 + * Endpoints: 15 */ export class LogsModule { constructor(private readonly transport: Transport) {} @@ -220,6 +220,25 @@ export class LogsModule { }); }; + /** + * GET /{version}/logs/{Id} + * Request DTO: GetLog + */ + getLog = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/logs/{Id}', + method: 'GET', + request, + pathParams: ['Id'], + scope: 'project', + ...options, + }); + }; + /** * GET /{version}/logs * Request DTO: GetLogs diff --git a/tests/api/logs.test.ts b/tests/api/logs.test.ts new file mode 100644 index 0000000..e868b2d --- /dev/null +++ b/tests/api/logs.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, it } from 'vitest'; + +import { LogsModule } from '../../src/api/logs.js'; +import { createMockFetch, expectedUrl, makeClient } from '../_helpers.js'; + +/** + * Auto-generated. Do not edit by hand — run `npm run generate-endpoints` + * to refresh this file from the DTO definitions. + * + * Tests for api.logs (1 endpoints). + * + * Each method is asserted against: + * - presence on the module (smoke check) + * - issued HTTP verb + URL with version segment substituted and path + * tokens interpolated from a stubbed request + * - auth, project, and (when applicable) account headers + * - account-scope guard: throws NORBIX_ACCOUNT_SCOPE_REQUIRED without accountId + */ +describe('api.logs', () => { + it('module exposes 1 method(s)', () => { + const mock = createMockFetch(); + const mod = new LogsModule({} as never); + void mod; // silence unused — we only need the type + // Sanity check the auto-mapped surface exists on the namespaced client. + const { norbix } = makeClient(); + const ns = (norbix.api as unknown as Record)['logs'] as Record< + string, + unknown + >; + expect(ns).toBeDefined(); + void mock; + expect(typeof ns['writeLogs']).toBe('function'); + }); + + it('writeLogs: POST /{version}/logs', async () => { + const stub = {}; + const expected = expectedUrl({ + baseUrl: 'https://api.norbix.io', + path: '/{version}/logs', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.api as unknown as Record< + string, + Record Promise> + > + )['logs']!['writeLogs']!; + 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'); + }); + + // @hand-written:begin + /** A fetch that answers every call with one fixed response and counts the calls. */ + function fixedAnswer(status: number, body: unknown, headers: Record = {}) { + const sent: RequestInit[] = []; + const fetch = (async (_url: string, init?: RequestInit) => { + sent.push(init ?? {}); + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json', ...headers }, + }); + }) as unknown as typeof globalThis.fetch; + return { norbix: makeClient({ fetch }).norbix, sent }; + } + + /** What a refused call throws — the fields every Norbix SDK error carries. */ + type Thrown = { + name: string; + httpStatus: number; + errorCode?: string; + retryAfterSeconds?: number; + }; + it('writeLogs: every entry field goes in the JSON body on the API host', async () => { + const { norbix, mock } = makeClient({}); + const entry = { + level: 'warning', + title: 'Payment retried', + message: 'Card declined once, second try passed', + eventCode: 'order:paid', + correlationId: 'ord_123', + timestamp: '2026-10-21T10:00:00Z', + meta: { orderId: 'ord_123', attempt: '2' }, + }; + await norbix.api.logs.writeLogs({ entries: [entry] }); + expect(mock.lastCall?.method).toBe('POST'); + expect(mock.lastCall?.url).toBe('https://api.norbix.io/v2/logs'); + expect(JSON.parse(mock.lastCall!.body!)).toEqual({ entries: [entry] }); + }); + + it('write: one line is sent as a one-item entries list', async () => { + const { norbix, mock } = makeClient({}); + await norbix.api.logs.write({ title: 'Signed up', eventCode: 'user:signup' }); + expect(mock.lastCall?.url).toBe('https://api.norbix.io/v2/logs'); + expect(JSON.parse(mock.lastCall!.body!)).toEqual({ + entries: [{ title: 'Signed up', eventCode: 'user:signup' }], + }); + }); + + it('write: a list is sent as it is and the accepted count comes back', async () => { + const { norbix, sent } = fixedAnswer(200, { accepted: 2 }); + const res = await norbix.api.logs.write([{ title: 'a' }, { title: 'b', level: 'error' }]); + expect(res.accepted).toBe(2); + expect(JSON.parse(sent[0]!.body as string)).toEqual({ + entries: [{ title: 'a' }, { title: 'b', level: 'error' }], + }); + }); + + it('writeLogs: a 429 throws once (no retry) with the Retry-After seconds', async () => { + const { norbix, sent } = fixedAnswer( + 429, + { + responseStatus: { + isSuccess: false, + errors: [{ errorCode: 'CM-ERRORS-LOGGING-003', message: 'Log quota used up' }], + }, + }, + { 'Retry-After': '7' }, + ); + const err = (await norbix.api.logs.write({ title: 'x' }).catch((e: unknown) => e)) as Thrown; + expect(err.name).toBe('NorbixError'); + expect(err.httpStatus).toBe(429); + expect(err.errorCode).toBe('CM-ERRORS-LOGGING-003'); + expect(err.retryAfterSeconds).toBe(7); + expect(sent).toHaveLength(1); + }); + + it('writeLogs: Logs module off is a 400 validation error with its code', async () => { + const { norbix } = fixedAnswer(400, { + responseStatus: { + isSuccess: false, + errors: [{ errorCode: 'CM-ERRORS-LOGGING-002', message: 'Logs module is off' }], + }, + }); + const err = (await norbix.api.logs.write({ title: 'x' }).catch((e: unknown) => e)) as Thrown; + expect(err.name).toBe('NorbixValidationError'); + expect(err.httpStatus).toBe(400); + expect(err.errorCode).toBe('CM-ERRORS-LOGGING-002'); + expect(err.retryAfterSeconds).toBeUndefined(); + }); + // @hand-written:end +}); diff --git a/tests/hub/logs.test.ts b/tests/hub/logs.test.ts index 86fe0ba..6d6f5ff 100644 --- a/tests/hub/logs.test.ts +++ b/tests/hub/logs.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.logs (14 endpoints). + * Tests for hub.logs (15 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.logs', () => { - it('module exposes 14 method(s)', () => { + it('module exposes 15 method(s)', () => { const mock = createMockFetch(); const mod = new LogsModule({} as never); void mod; // silence unused — we only need the type @@ -40,6 +40,7 @@ describe('hub.logs', () => { expect(typeof ns['testLoggingIntegration']).toBe('function'); expect(typeof ns['cleanLogs']).toBe('function'); expect(typeof ns['getLogsByCorrelationId']).toBe('function'); + expect(typeof ns['getLog']).toBe('function'); expect(typeof ns['getLogs']).toBe('function'); expect(typeof ns['getLogSettings']).toBe('function'); expect(typeof ns['saveLogSettings']).toBe('function'); @@ -298,6 +299,29 @@ describe('hub.logs', () => { expect(mock.lastCall?.headers.get('norbix-project-id')).toBe('test-project'); }); + it('getLog: GET /{version}/logs/{Id}', async () => { + const stub = stubRequestForPath('/{version}/logs/{Id}'); + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/logs/{Id}', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['logs']!['getLog']!; + 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('getLogs: GET /{version}/logs', async () => { const stub = {}; const expected = expectedUrl({ @@ -383,5 +407,42 @@ describe('hub.logs', () => { expect(url.searchParams.get('metaValue')).toBe('tsk_123'); expect(mock.lastCall?.body).toBeUndefined(); }); + it('getLog: the id goes in the path, not in the query', async () => { + const { norbix, mock } = makeClient({}); + await norbix.hub.logs.getLog({ id: '66b2f0a1c2d3e4f5a6b7c8d9' }); + expect(mock.lastCall?.method).toBe('GET'); + const url = new URL(mock.lastCall!.url); + expect(`${url.origin}${url.pathname}`).toBe( + 'https://hub.norbix.io/v2/logs/66b2f0a1c2d3e4f5a6b7c8d9', + ); + expect(url.searchParams.has('id')).toBe(false); + }); + + it('saveLogSettings: only the settings given are sent (absent = unchanged)', async () => { + const { norbix, mock } = makeClient({}); + await norbix.hub.logs.saveLogSettings({ skipHttpBodyMeta: true }); + expect(mock.lastCall?.method).toBe('POST'); + expect(JSON.parse(mock.lastCall!.body!)).toEqual({ skipHttpBodyMeta: true }); + }); + + it('saveLoggingIntegration (MongoDB) then testLoggingIntegration with the new id', async () => { + const { norbix, mock } = makeClient({}); + await norbix.hub.logs.saveLoggingIntegration({ + integration: { + provider: 'MongoDB', + integrationName: 'Audit store', + isEnabled: true, + connectionString: 'mongodb://db.example:27017', + databaseName: 'logs', + } as never, + }); + expect(mock.lastCall?.url).toBe('https://hub.norbix.io/v2/logs/integrations'); + expect(JSON.parse(mock.lastCall!.body!).integration.provider).toBe('MongoDB'); + + await norbix.hub.logs.testLoggingIntegration({ integrationId: 'lgi_1' }); + expect(mock.lastCall?.method).toBe('POST'); + expect(mock.lastCall?.url).toBe('https://hub.norbix.io/v2/logs/integrations/test'); + expect(JSON.parse(mock.lastCall!.body!)).toEqual({ integrationId: 'lgi_1' }); + }); // @hand-written:end }); From e49a19a9ffd362aa3576deaf2c8b8cf0320f8842 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Wed, 21 Oct 2026 21:19:01 +0300 Subject: [PATCH 3/4] docs(logs): writing app log lines, getLog, settings absent = unchanged, test after save docs/api/logs.md: write / writeLogs with every field, the refusals, and retryAfterSeconds on a 429. docs/hub/logs.md: getLog, saveLogSettings sends only what you give, and nothing is delivered to a logging integration until its last test passed (call testLoggingIntegration after every save). README: api.logs row, Retry-After in Error handling. Co-Authored-By: Claude Opus 5.5 --- README.md | 8 ++++- docs/api/logs.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/hub/logs.md | 73 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 docs/api/logs.md diff --git a/README.md b/README.md index f81fdef..f38c773 100644 --- a/README.md +++ b/README.md @@ -351,6 +351,7 @@ Default base URL: `https://api.norbix.ai`. **8 modules · 66 endpoints.** | 🔑 `apikeys` | List + regenerate per-environment API keys | [`docs/api/apikeys.md`](./docs/api/apikeys.md) | | 🪪 `accessToken` | Refresh-token exchange | [`docs/api/access_token.md`](./docs/api/access_token.md) | | 🩺 `echo` | Gateway smoke check | [`docs/api/echo.md`](./docs/api/echo.md) | +| 📊 `logs` | Write your app's own log lines (`write` / `writeLogs`) to the project's Logs | [`docs/api/logs.md`](./docs/api/logs.md) | > Email/push/SMS notifications, payments, and code execution are configured on the **Hub** surface and surfaced at runtime through integration triggers — they are not separate API modules. File storage now has its own `files` API module: it issues signed upload URLs so clients upload binary content directly to storage, never streaming bytes through the Norbix API. @@ -372,7 +373,7 @@ Default base URL: `https://hub.norbix.ai`. **18 modules · 315 endpoints.** | 📨 `email` | Email module switches | [`docs/hub/email.md`](./docs/hub/email.md) | | 💳 `payments` | Payment integrations, triggers, tests, module settings | [`docs/hub/payments.md`](./docs/hub/payments.md) | | 🤖 `ai` | LLM and MCP integration configuration and tests | [`docs/hub/ai.md`](./docs/hub/ai.md) | -| 📊 `logs` | Logging integrations and module settings | [`docs/hub/logs.md`](./docs/hub/logs.md) | +| 📊 `logs` | Read logs (list, one entry, trail), settings, integrations and tests | [`docs/hub/logs.md`](./docs/hub/logs.md) | | 👥 `membership` | Roles, policies, users, preferences, integrations, triggers | [`docs/hub/membership.md`](./docs/hub/membership.md) | | ⏰ `scheduler` | Scheduler module, tasks and their run history | [`docs/hub/scheduler.md`](./docs/hub/scheduler.md) | | 🪝 `webhooks` | Webhook integrations, destinations, tests, module settings | [`docs/hub/webhooks.md`](./docs/hub/webhooks.md) | @@ -758,6 +759,11 @@ try { } ``` +When the answer carries a `Retry-After` header (a 429 from +`norbix.api.logs.write` when the project's per-minute log quota is used up), +`err.retryAfterSeconds` holds it in seconds. The SDK retries a 429 by itself +only for GET and DELETE; anything else is yours to send again. + `message` and `errorCode` are the gateway's own. The gateway puts them inside `responseStatus.errors[]`, so a `NorbixError` reads that list first, takes the first entry for `message` / `errorCode`, and keeps every entry in `errors`. diff --git a/docs/api/logs.md b/docs/api/logs.md new file mode 100644 index 0000000..2d8a087 --- /dev/null +++ b/docs/api/logs.md @@ -0,0 +1,90 @@ +# API · Logs + +[← Back to API index](./_index.md) · [↑ Back to project README](../../README.md) + +Logging integrations and module on/off switches. + +Accessed as `norbix.api.logs` on the [`Norbix`](../../README.md#authentication) client. + +## Endpoints + +| Method | Verb | Path | Scope | +| ------------------------- | ------ | ----------------- | --------- | +| [`writeLogs`](#writelogs) | `POST` | `/{version}/logs` | `project` | + +## Reference + +### writeLogs + +`POST` `/{version}/logs` + +**Request DTO**: `CodeMashApi2.WriteLogs` +**Response**: `CodeMashApi2.WriteLogsResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.api.logs.writeLogs({ + // See CodeMash type for the full request shape. +}); +// → typed as CodeMashApi2.WriteLogsResponse +``` + +[↑ Top](#endpoints) + + + +## Writing your app's own log lines + +`norbix.api.logs` is not about integrations (those are on the Hub, see +[`docs/hub/logs.md`](../hub/logs.md)): it sends **your app's own lines** to the +project's Logs, where they show next to the lines Norbix writes. + +```ts +import { Norbix, NorbixError } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +// One line … +await norbix.api.logs.write({ title: 'Order paid', eventCode: 'order:paid' }); + +// … or up to 100 at once. `writeLogs({ entries })` is the same call. +const { accepted } = await norbix.api.logs.write([ + { + level: 'warning', // information (default) | warning | error + title: 'Payment retried', // required, at most 200 characters + message: 'Card declined once, second try passed', // at most 8 000 + eventCode: 'order:retry', // a-z 0-9 _ . : -, at most 64 — stored as app:order:retry + correlationId: 'ord_123', // letters, digits, _ or -, at most 64 — groups lines into one flow + timestamp: '2026-10-21T10:00:00Z', // UTC, at most 7 days old, at most 5 minutes ahead + meta: { orderId: 'ord_123' }, // at most 20 keys; secret-looking keys are masked + }, +]); +``` + +The answer comes at once: `accepted` lines are queued, not yet stored. + +| Refusal | When | +| ----------------------------- | ------------------------------------------ | +| 400 (`NorbixValidationError`) | A line breaks one of the rules above. | +| 400 `CM-ERRORS-LOGGING-002` | The Logs module is off for the project. | +| 429 `CM-ERRORS-LOGGING-003` | The project's per-minute quota is used up. | + +On a 429 the SDK does not send again by itself (a POST is never retried). +`err.retryAfterSeconds` holds the gateway's `Retry-After` value: + +```ts +try { + await norbix.api.logs.write(lines); +} catch (err) { + if (err instanceof NorbixError && err.httpStatus === 429) { + setTimeout(() => norbix.api.logs.write(lines), (err.retryAfterSeconds ?? 60) * 1000); + } else throw err; +} +``` + +Tests: `tests/api/logs.test.ts`. + + diff --git a/docs/hub/logs.md b/docs/hub/logs.md index 1679dc1..2d632b2 100644 --- a/docs/hub/logs.md +++ b/docs/hub/logs.md @@ -21,6 +21,7 @@ Accessed as `norbix.hub.logs` on the [`Norbix`](../../README.md#authentication) | [`testLoggingIntegration`](#testloggingintegration) | `POST` | `/{version}/logs/integrations/test` | `project` | | [`cleanLogs`](#cleanlogs) | `POST` | `/{version}/logs/clean` | `project` | | [`getLogsByCorrelationId`](#getlogsbycorrelationid) | `GET` | `/{version}/logs/audit` | `project` | +| [`getLog`](#getlog) | `GET` | `/{version}/logs/{Id}` | `project` | | [`getLogs`](#getlogs) | `GET` | `/{version}/logs` | `project` | | [`getLogSettings`](#getlogsettings) | `GET` | `/{version}/logs/settings` | `project` | | [`saveLogSettings`](#savelogsettings) | `POST` | `/{version}/logs/settings` | `project` | @@ -271,6 +272,29 @@ const result = await norbix.hub.logs.getLogsByCorrelationId({ [↑ Top](#endpoints) +### getLog + +`GET` `/{version}/logs/{Id}` + +Fetch a single item by ID. + +**Request DTO**: `CodeMashHub2.GetLog` +**Response**: `CodeMashHub2.GetLogResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.logs.getLog({ + Id: 'Id-here', + // Other fields: see CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.GetLogResponse +``` + +[↑ Top](#endpoints) + ### getLogs `GET` `/{version}/logs` @@ -336,3 +360,52 @@ const result = await norbix.hub.logs.saveLogSettings({ ``` [↑ Top](#endpoints) + + + +## Reading one entry + +`getLog({ id })` returns one entry (`item`) by the `id` of a `getLogs` row. + +```ts +const { item } = await norbix.hub.logs.getLog({ id: row.id }); +``` + +## Log settings: absent = unchanged + +`saveLogSettings` changes only the settings you send. A field you leave out +keeps its current value: + +```ts +await norbix.hub.logs.saveLogSettings({ skipHttpBodyMeta: true }); // the other two stay as they are +``` + +## Logging integrations: test after you save + +**Nothing is delivered to a logging integration until its last test passed.** +After `saveLoggingIntegration` (a new one or a change), call +`testLoggingIntegration` with its id; send the test again after every change. + +```ts +const { id } = await norbix.hub.logs.saveLoggingIntegration({ + integration: { + provider: 'MongoDB', + integrationName: 'Audit store', + isEnabled: true, + connectionString: 'mongodb://db.example:27017', + databaseName: 'logs', // optional + }, +}); +const test = await norbix.hub.logs.testLoggingIntegration({ integrationId: id }); +``` + +Providers you can save include MongoDB (`connectionString`, optional +`databaseName`), Datadog (`site` is one of the seven Datadog sites) and AWS +CloudWatch (`region`, `logGroup`, `logStream`, `accessKey`, `secretKey`); the +request types are in `CodeMashHub2.*LoggingIntegrationRequest`. + +To write your app's own lines, see [`docs/api/logs.md`](../api/logs.md). + +Tests: `tests/hub/logs.test.ts`. + + From f40d0ded0875ea7739dbdd18f49dd20be9909d52 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Wed, 21 Oct 2026 21:19:03 +0300 Subject: [PATCH 4/4] feat(hub): regenerated modules for endpoints already in the types generate-endpoints run over the regenerated types adds the methods whose DTOs arrived from gateway refactoringV2: hub.account repairEnvironmentBuiltIns, removeTeamMember, setTeamMemberRoleEnvironments, getAccountTeamInvitations, cancelTeamInvitation, updateTeamMember; hub.database endUserOwnRecordsReport; api.files registerFiles. Generated tests and reference pages only; no hand-written helpers for them in this change. Co-Authored-By: Claude Opus 5.5 --- docs/README.md | 4 +- docs/api/_index.md | 5 +- docs/api/files.md | 24 ++++++ docs/hub/_index.md | 8 +- docs/hub/account.md | 132 +++++++++++++++++++++++++++++++++ docs/hub/database.md | 21 ++++++ src/api/files.ts | 21 +++++- src/hub/account.ts | 116 ++++++++++++++++++++++++++++- src/hub/database.ts | 21 +++++- tests/api/files.test.ts | 28 ++++++- tests/hub/account.test.ts | 148 ++++++++++++++++++++++++++++++++++++- tests/hub/database.test.ts | 28 ++++++- 12 files changed, 539 insertions(+), 17 deletions(-) diff --git a/docs/README.md b/docs/README.md index 7e1630d..a808552 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,8 +8,8 @@ integration guides live under `integrations/`. | Surface | Modules | Description | | ------------------------ | -------------------------: | ------------------------------------------------------------------------------------------------ | -| [`API`](./api/_index.md) | 11 modules · 120 endpoints | Project-scoped data operations: database collections, users, AI chat, auth, API keys. | -| [`Hub`](./hub/_index.md) | 27 modules · 582 endpoints | Project & account configuration: schemas, integrations, team management, billing, observability. | +| [`API`](./api/_index.md) | 12 modules · 122 endpoints | Project-scoped data operations: database collections, users, AI chat, auth, API keys. | +| [`Hub`](./hub/_index.md) | 27 modules · 590 endpoints | Project & account configuration: schemas, integrations, team management, billing, observability. | ## Integration guides diff --git a/docs/api/_index.md b/docs/api/_index.md index e49468c..97499f9 100644 --- a/docs/api/_index.md +++ b/docs/api/_index.md @@ -4,7 +4,7 @@ Project-scoped data operations: database collections, users, AI chat, auth, API keys. -**120 endpoints across 11 modules.** Click a module +**122 endpoints across 12 modules.** Click a module name for the full method reference and TypeScript examples. | Module | Endpoints | Description | @@ -15,7 +15,8 @@ name for the full method reference and TypeScript examples. | [`auth`](./auth.md) | 1 | Sign-in / sign-out and federated provider flows. Most apps prefer `norbix.login(...)` over calling these directly. | | [`database`](./database.md) | 22 | Database schemas, integrations, triggers, taxonomies, and module on/off switches. For data-level CRUD on collections see `api.database`. | | [`echo`](./echo.md) | 1 | Echo helpers used by the gateway smoke checks. | -| [`files`](./files.md) | 14 | File storage integrations and triggers. Upload + download is in `api.database` (FileResource fields). | +| [`files`](./files.md) | 15 | File storage integrations and triggers. Upload + download is in `api.database` (FileResource fields). | +| [`logs`](./logs.md) | 1 | Logging integrations and module on/off switches. | | [`membership`](./membership.md) | 43 | Roles, policies, and user preferences (Hub side). For user CRUD and registration see `api.membership`. | | [`notifications`](./notifications.md) | 11 | Email and push templates, integrations, campaigns, devices, signatures, footers, and one-click unsubscribe. | | [`payments`](./payments.md) | 4 | Payment provider integrations and triggers (Stripe, etc.). | diff --git a/docs/api/files.md b/docs/api/files.md index dd184d2..168815e 100644 --- a/docs/api/files.md +++ b/docs/api/files.md @@ -21,6 +21,7 @@ Accessed as `norbix.api.files` on the [`Norbix`](../../README.md#authentication) | [`getSignedUrl`](#getsignedurl) | `GET` | `/{version}/files/{filesIntegrationId}/sign` | `project` | | [`listFiles`](#listfiles) | `GET` | `/{version}/files/{filesIntegrationId}` | `project` | | [`getPublicFile`](#getpublicfile) | `GET` | `/{version}/files/public/{PublicId}/{Name*}` | `project` | +| [`registerFiles`](#registerfiles) | `POST` | `/{version}/files/{filesIntegrationId}/register` | `project` | | [`requestUploadUrl`](#requestuploadurl) | `POST` | `/{version}/files/{filesIntegrationId}/upload-url` | `project` | | [`testFilesIntegration`](#testfilesintegration) | `POST` | `/{version}/files/{filesIntegrationId}/test` | `project` | | [`useFile`](#usefile) | `POST` | `/{version}/files/{filesIntegrationId}/use` | `project` | @@ -274,6 +275,29 @@ const result = await norbix.api.files.getPublicFile({ [↑ Top](#endpoints) +### registerFiles + +`POST` `/{version}/files/{filesIntegrationId}/register` + +Register a new entry. + +**Request DTO**: `CodeMashApi2.RegisterFilesRequest` +**Response**: `CodeMashApi2.RegisterFilesResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.api.files.registerFiles({ + filesIntegrationId: 'filesIntegrationId-here', + // Other fields: see CodeMash type for the full request shape. +}); +// → typed as CodeMashApi2.RegisterFilesResponse +``` + +[↑ Top](#endpoints) + ### requestUploadUrl `POST` `/{version}/files/{filesIntegrationId}/upload-url` diff --git a/docs/hub/_index.md b/docs/hub/_index.md index cbc28a4..939f1b0 100644 --- a/docs/hub/_index.md +++ b/docs/hub/_index.md @@ -4,26 +4,26 @@ Project & account configuration: schemas, integrations, team management, billing, observability. -**582 endpoints across 27 modules.** Click a module +**590 endpoints across 27 modules.** Click a module name for the full method reference and TypeScript examples. | Module | Endpoints | Description | | ------------------------------------- | --------: | ---------------------------------------------------------------------------------------------------------------------------------------- | | [`accessToken`](./access_token.md) | 1 | Refresh-token exchange to get a new bearer token. | -| [`account`](./account.md) | 122 | Account profile, status, verification, team invites, and Stripe billing portal. Most write endpoints require `accountId` on the client. | +| [`account`](./account.md) | 128 | Account profile, status, verification, team invites, and Stripe billing portal. Most write endpoints require `accountId` on the client. | | [`ai`](./ai.md) | 26 | | | [`apikeys`](./apikeys.md) | 2 | List and regenerate per-environment API keys for service auth. | | [`auth`](./auth.md) | 5 | Sign-in / sign-out and federated provider flows. Most apps prefer `norbix.login(...)` over calling these directly. | | [`code`](./code.md) | 44 | | | [`compliance`](./compliance.md) | 18 | | -| [`database`](./database.md) | 74 | Database schemas, integrations, triggers, taxonomies, and module on/off switches. For data-level CRUD on collections see `api.database`. | +| [`database`](./database.md) | 75 | Database schemas, integrations, triggers, taxonomies, and module on/off switches. For data-level CRUD on collections see `api.database`. | | [`diagnostics`](./diagnostics.md) | 7 | | | [`echo`](./echo.md) | 1 | Echo helpers used by the gateway smoke checks. | | [`email`](./email.md) | 2 | | | [`files`](./files.md) | 24 | File storage integrations and triggers. Upload + download is in `api.database` (FileResource fields). | | [`internal`](./internal.md) | 1 | | | [`licensing`](./licensing.md) | 3 | | -| [`logs`](./logs.md) | 14 | Logging integrations and module on/off switches. | +| [`logs`](./logs.md) | 15 | Logging integrations and module on/off switches. | | [`membership`](./membership.md) | 48 | Roles, policies, and user preferences (Hub side). For user CRUD and registration see `api.membership`. | | [`notifications`](./notifications.md) | 128 | Email and push templates, integrations, campaigns, devices, signatures, footers, and one-click unsubscribe. | | [`oauth`](./oauth.md) | 6 | | diff --git a/docs/hub/account.md b/docs/hub/account.md index 559429a..d693efb 100644 --- a/docs/hub/account.md +++ b/docs/hub/account.md @@ -31,6 +31,7 @@ Accessed as `norbix.hub.account` on the [`Norbix`](../../README.md#authenticatio | [`setEnvironmentRank`](#setenvironmentrank) | `PATCH` | `/{version}/account/projects/environments/{environmentName}/rank` | `project` | | [`promoteEnvironment`](#promoteenvironment) | `POST` | `/{version}/account/projects/environments/promote` | `project` | | [`rollbackPromotion`](#rollbackpromotion) | `POST` | `/{version}/account/projects/environments/promote/rollback` | `project` | +| [`repairEnvironmentBuiltIns`](#repairenvironmentbuiltins) | `POST` | `/{version}/account/projects/environments/built-ins/repair` | `project` | | [`getProjectEnvironments`](#getprojectenvironments) | `GET` | `/{version}/account/projects/environments` | `project` | | [`getProject`](#getproject) | `GET` | `/{version}/account/projects/{projectId}` | `project` | | [`getProjects`](#getprojects) | `GET` | `/{version}/account/projects` | `project` | @@ -81,8 +82,13 @@ Accessed as `norbix.hub.account` on the [`Norbix`](../../README.md#authenticatio | [`getAccountCollaborators`](#getaccountcollaborators) | `GET` | `/{version}/account/collaborators` | `project` | | [`getAccountPasswordPolicy`](#getaccountpasswordpolicy) | `GET` | `/{version}/account/team/password-policy` | `project` | | [`getAccountTeamPolicies`](#getaccountteampolicies) | `GET` | `/{version}/account/team/policies` | `project` | +| [`removeTeamMember`](#removeteammember) | `DELETE` | `/{version}/account/team/member/{MemberId}` | `project` | +| [`setTeamMemberRoleEnvironments`](#setteammemberroleenvironments) | `PUT` | `/{version}/account/team/member/role-environments` | `project` | | [`getAccountTeamRoles`](#getaccountteamroles) | `GET` | `/{version}/account/team/roles` | `project` | | [`sendInviteToTeamMember`](#sendinvitetoteammember) | `POST` | `/{version}/account/team/member/invite` | `project` | +| [`getAccountTeamInvitations`](#getaccountteaminvitations) | `GET` | `/{version}/account/team/invitations` | `project` | +| [`cancelTeamInvitation`](#cancelteaminvitation) | `DELETE` | `/{version}/account/team/invitations/{InvitationId}` | `project` | +| [`updateTeamMember`](#updateteammember) | `PUT` | `/{version}/account/team/member` | `project` | | [`updateAccountPolicy`](#updateaccountpolicy) | `PUT` | `/{version}/account/team/policies` | `project` | | [`updateAccountRole`](#updateaccountrole) | `PUT` | `/{version}/account/team/roles` | `project` | | [`accountHasPasskey`](#accounthaspasskey) | `POST` | `/{version}/account/userauth/has-passkey` | `project` | @@ -595,6 +601,26 @@ const result = await norbix.hub.account.rollbackPromotion({ [↑ Top](#endpoints) +### repairEnvironmentBuiltIns + +`POST` `/{version}/account/projects/environments/built-ins/repair` + +**Request DTO**: `CodeMashHub2.RepairEnvironmentBuiltInsRequest` +**Response**: `CodeMashHub2.RepairEnvironmentBuiltInsResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.account.repairEnvironmentBuiltIns({ + // See CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.RepairEnvironmentBuiltInsResponse +``` + +[↑ Top](#endpoints) + ### getProjectEnvironments `GET` `/{version}/account/projects/environments` @@ -1730,6 +1756,47 @@ const result = await norbix.hub.account.getAccountTeamPolicies({ [↑ Top](#endpoints) +### removeTeamMember + +`DELETE` `/{version}/account/team/member/{MemberId}` + +**Request DTO**: `CodeMashHub2.RemoveTeamMember` +**Response**: `CodeMashHub2.IdResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.account.removeTeamMember({ + MemberId: 'MemberId-here', + // Other fields: see CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.IdResponse +``` + +[↑ Top](#endpoints) + +### setTeamMemberRoleEnvironments + +`PUT` `/{version}/account/team/member/role-environments` + +**Request DTO**: `CodeMashHub2.SetTeamMemberRoleEnvironmentsRequest` +**Response**: `CodeMashHub2.IdResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.account.setTeamMemberRoleEnvironments({ + // See CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.IdResponse +``` + +[↑ Top](#endpoints) + ### getAccountTeamRoles `GET` `/{version}/account/team/roles` @@ -1774,6 +1841,71 @@ const result = await norbix.hub.account.sendInviteToTeamMember({ [↑ Top](#endpoints) +### getAccountTeamInvitations + +`GET` `/{version}/account/team/invitations` + +Fetch a single item by ID. + +**Request DTO**: `CodeMashHub2.GetAccountTeamInvitations` +**Response**: `CodeMashHub2.GetAccountTeamInvitationsResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.account.getAccountTeamInvitations({ + // See CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.GetAccountTeamInvitationsResponse +``` + +[↑ Top](#endpoints) + +### cancelTeamInvitation + +`DELETE` `/{version}/account/team/invitations/{InvitationId}` + +**Request DTO**: `CodeMashHub2.CancelTeamInvitation` +**Response**: `CodeMashHub2.IdResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.account.cancelTeamInvitation({ + InvitationId: 'InvitationId-here', + // Other fields: see CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.IdResponse +``` + +[↑ Top](#endpoints) + +### updateTeamMember + +`PUT` `/{version}/account/team/member` + +Update an existing item. + +**Request DTO**: `CodeMashHub2.UpdateTeamMember` +**Response**: `CodeMashHub2.IdResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.account.updateTeamMember({ + // See CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.IdResponse +``` + +[↑ Top](#endpoints) + ### updateAccountPolicy `PUT` `/{version}/account/team/policies` diff --git a/docs/hub/database.md b/docs/hub/database.md index 4fe206f..800aa52 100644 --- a/docs/hub/database.md +++ b/docs/hub/database.md @@ -33,6 +33,7 @@ Accessed as `norbix.hub.database` on the [`Norbix`](../../README.md#authenticati | [`applyDatabaseSchemaBundle`](#applydatabaseschemabundle) | `POST` | `/{version}/database/schemas/apply-bundle` | `project` | | [`deleteDatabaseSchema`](#deletedatabaseschema) | `DELETE` | `/{version}/database/schemas/{Id}` | `project` | | [`discardDatabaseSchemaDraft`](#discarddatabaseschemadraft) | `DELETE` | `/{version}/database/schemas/{Id}/draft` | `project` | +| [`endUserOwnRecordsReport`](#enduserownrecordsreport) | `POST` | `/{version}/database/schemas/end-user-own-records` | `project` | | [`getDatabaseSchema`](#getdatabaseschema) | `GET` | `/{version}/database/schemas/{id}` | `project` | | [`getDatabaseSchemas`](#getdatabaseschemas) | `GET` | `/{version}/database/schemas` | `project` | | [`getDatabaseSchemaDraft`](#getdatabaseschemadraft) | `GET` | `/{version}/database/schemas/{Id}/draft` | `project` | @@ -607,6 +608,26 @@ const result = await norbix.hub.database.discardDatabaseSchemaDraft({ [↑ Top](#endpoints) +### endUserOwnRecordsReport + +`POST` `/{version}/database/schemas/end-user-own-records` + +**Request DTO**: `CodeMashHub2.EndUserOwnRecordsReportRequest` +**Response**: `CodeMashHub2.EndUserOwnRecordsReportResponse` + +```ts +import { Norbix } from '@norbix.ai/ts'; + +const norbix = new Norbix(); + +const result = await norbix.hub.database.endUserOwnRecordsReport({ + // See CodeMash type for the full request shape. +}); +// → typed as CodeMashHub2.EndUserOwnRecordsReportResponse +``` + +[↑ Top](#endpoints) + ### getDatabaseSchema `GET` `/{version}/database/schemas/{id}` diff --git a/src/api/files.ts b/src/api/files.ts index b2456a1..2ab7fc1 100644 --- a/src/api/files.ts +++ b/src/api/files.ts @@ -6,7 +6,7 @@ import type { CodeMashApi2 } from '../types/api2.dtos.js'; * to refresh this file from the DTO definitions. * * Group: files - * Endpoints: 14 + * Endpoints: 15 */ export class FilesModule { constructor(private readonly transport: Transport) {} @@ -223,6 +223,25 @@ export class FilesModule { }); }; + /** + * POST /{version}/files/{filesIntegrationId}/register + * Request DTO: RegisterFilesRequest + */ + registerFiles = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'api', + path: '/{version}/files/{filesIntegrationId}/register', + method: 'POST', + request, + pathParams: ['filesIntegrationId'], + scope: 'project', + ...options, + }); + }; + /** * POST /{version}/files/{filesIntegrationId}/upload-url * Request DTO: RequestUploadUrlRequest diff --git a/src/hub/account.ts b/src/hub/account.ts index 8a8e5c2..bc2ec8d 100644 --- a/src/hub/account.ts +++ b/src/hub/account.ts @@ -6,7 +6,7 @@ import type { CodeMashHub2 } from '../types/hub2.dtos.js'; * to refresh this file from the DTO definitions. * * Group: account - * Endpoints: 122 + * Endpoints: 128 */ export class AccountModule { constructor(private readonly transport: Transport) {} @@ -413,6 +413,25 @@ export class AccountModule { }); }; + /** + * POST /{version}/account/projects/environments/built-ins/repair + * Request DTO: RepairEnvironmentBuiltInsRequest + */ + repairEnvironmentBuiltIns = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/account/projects/environments/built-ins/repair', + method: 'POST', + request, + pathParams: [], + scope: 'project', + ...options, + }); + }; + /** * GET /{version}/account/projects/environments * Request DTO: GetProjectEnvironments @@ -1365,6 +1384,44 @@ export class AccountModule { }); }; + /** + * DELETE /{version}/account/team/member/{MemberId} + * Request DTO: RemoveTeamMember + */ + removeTeamMember = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/account/team/member/{MemberId}', + method: 'DELETE', + request, + pathParams: ['MemberId'], + scope: 'project', + ...options, + }); + }; + + /** + * PUT /{version}/account/team/member/role-environments + * Request DTO: SetTeamMemberRoleEnvironmentsRequest + */ + setTeamMemberRoleEnvironments = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/account/team/member/role-environments', + method: 'PUT', + request, + pathParams: [], + scope: 'project', + ...options, + }); + }; + /** * GET /{version}/account/team/roles * Request DTO: GetAccountTeamRoles @@ -1403,6 +1460,63 @@ export class AccountModule { }); }; + /** + * GET /{version}/account/team/invitations + * Request DTO: GetAccountTeamInvitations + */ + getAccountTeamInvitations = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/account/team/invitations', + method: 'GET', + request, + pathParams: [], + scope: 'project', + ...options, + }); + }; + + /** + * DELETE /{version}/account/team/invitations/{InvitationId} + * Request DTO: CancelTeamInvitation + */ + cancelTeamInvitation = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/account/team/invitations/{InvitationId}', + method: 'DELETE', + request, + pathParams: ['InvitationId'], + scope: 'project', + ...options, + }); + }; + + /** + * PUT /{version}/account/team/member + * Request DTO: UpdateTeamMember + */ + updateTeamMember = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/account/team/member', + method: 'PUT', + request, + pathParams: [], + scope: 'project', + ...options, + }); + }; + /** * PUT /{version}/account/team/policies * Request DTO: UpdateAccountPolicy diff --git a/src/hub/database.ts b/src/hub/database.ts index eb84838..112ae38 100644 --- a/src/hub/database.ts +++ b/src/hub/database.ts @@ -6,7 +6,7 @@ import type { CodeMashHub2 } from '../types/hub2.dtos.js'; * to refresh this file from the DTO definitions. * * Group: database - * Endpoints: 74 + * Endpoints: 75 */ export class DatabaseModule { constructor(private readonly transport: Transport) {} @@ -448,6 +448,25 @@ export class DatabaseModule { }); }; + /** + * POST /{version}/database/schemas/end-user-own-records + * Request DTO: EndUserOwnRecordsReportRequest + */ + endUserOwnRecordsReport = ( + request: Partial = {} as Partial, + options: RequestOverrideOptions = {}, + ): Promise => { + return this.transport.send({ + target: 'hub', + path: '/{version}/database/schemas/end-user-own-records', + method: 'POST', + request, + pathParams: [], + scope: 'project', + ...options, + }); + }; + /** * GET /{version}/database/schemas/{id} * Request DTO: GetDatabaseSchema diff --git a/tests/api/files.test.ts b/tests/api/files.test.ts index 4c215f1..d478a6d 100644 --- a/tests/api/files.test.ts +++ b/tests/api/files.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 api.files (14 endpoints). + * Tests for api.files (15 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('api.files', () => { - it('module exposes 14 method(s)', () => { + it('module exposes 15 method(s)', () => { const mock = createMockFetch(); const mod = new FilesModule({} as never); void mod; // silence unused — we only need the type @@ -40,6 +40,7 @@ describe('api.files', () => { expect(typeof ns['getSignedUrl']).toBe('function'); expect(typeof ns['listFiles']).toBe('function'); expect(typeof ns['getPublicFile']).toBe('function'); + expect(typeof ns['registerFiles']).toBe('function'); expect(typeof ns['requestUploadUrl']).toBe('function'); expect(typeof ns['testFilesIntegration']).toBe('function'); expect(typeof ns['useFile']).toBe('function'); @@ -298,6 +299,29 @@ describe('api.files', () => { expect(mock.lastCall?.headers.get('norbix-project-id')).toBe('test-project'); }); + it('registerFiles: POST /{version}/files/{filesIntegrationId}/register', async () => { + const stub = stubRequestForPath('/{version}/files/{filesIntegrationId}/register'); + const expected = expectedUrl({ + baseUrl: 'https://api.norbix.io', + path: '/{version}/files/{filesIntegrationId}/register', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.api as unknown as Record< + string, + Record Promise> + > + )['files']!['registerFiles']!; + 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('requestUploadUrl: POST /{version}/files/{filesIntegrationId}/upload-url', async () => { const stub = stubRequestForPath('/{version}/files/{filesIntegrationId}/upload-url'); const expected = expectedUrl({ diff --git a/tests/hub/account.test.ts b/tests/hub/account.test.ts index 505e07b..58b49e3 100644 --- a/tests/hub/account.test.ts +++ b/tests/hub/account.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.account (122 endpoints). + * Tests for hub.account (128 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.account', () => { - it('module exposes 122 method(s)', () => { + it('module exposes 128 method(s)', () => { const mock = createMockFetch(); const mod = new AccountModule({} as never); void mod; // silence unused — we only need the type @@ -50,6 +50,7 @@ describe('hub.account', () => { expect(typeof ns['setEnvironmentRank']).toBe('function'); expect(typeof ns['promoteEnvironment']).toBe('function'); expect(typeof ns['rollbackPromotion']).toBe('function'); + expect(typeof ns['repairEnvironmentBuiltIns']).toBe('function'); expect(typeof ns['getProjectEnvironments']).toBe('function'); expect(typeof ns['getProject']).toBe('function'); expect(typeof ns['getProjects']).toBe('function'); @@ -100,8 +101,13 @@ describe('hub.account', () => { expect(typeof ns['getAccountCollaborators']).toBe('function'); expect(typeof ns['getAccountPasswordPolicy']).toBe('function'); expect(typeof ns['getAccountTeamPolicies']).toBe('function'); + expect(typeof ns['removeTeamMember']).toBe('function'); + expect(typeof ns['setTeamMemberRoleEnvironments']).toBe('function'); expect(typeof ns['getAccountTeamRoles']).toBe('function'); expect(typeof ns['sendInviteToTeamMember']).toBe('function'); + expect(typeof ns['getAccountTeamInvitations']).toBe('function'); + expect(typeof ns['cancelTeamInvitation']).toBe('function'); + expect(typeof ns['updateTeamMember']).toBe('function'); expect(typeof ns['updateAccountPolicy']).toBe('function'); expect(typeof ns['updateAccountRole']).toBe('function'); expect(typeof ns['accountHasPasskey']).toBe('function'); @@ -648,6 +654,29 @@ describe('hub.account', () => { expect(mock.lastCall?.headers.get('norbix-project-id')).toBe('test-project'); }); + it('repairEnvironmentBuiltIns: POST /{version}/account/projects/environments/built-ins/repair', async () => { + const stub = {}; + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/account/projects/environments/built-ins/repair', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['account']!['repairEnvironmentBuiltIns']!; + 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('getProjectEnvironments: GET /{version}/account/projects/environments', async () => { const stub = {}; const expected = expectedUrl({ @@ -1816,6 +1845,52 @@ describe('hub.account', () => { expect(mock.lastCall?.headers.get('norbix-project-id')).toBe('test-project'); }); + it('removeTeamMember: DELETE /{version}/account/team/member/{MemberId}', async () => { + const stub = stubRequestForPath('/{version}/account/team/member/{MemberId}'); + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/account/team/member/{MemberId}', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['account']!['removeTeamMember']!; + await fn(stub); + expect(mock.lastCall).toBeDefined(); + expect(mock.lastCall?.method).toBe('DELETE'); + 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('setTeamMemberRoleEnvironments: PUT /{version}/account/team/member/role-environments', async () => { + const stub = {}; + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/account/team/member/role-environments', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['account']!['setTeamMemberRoleEnvironments']!; + 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('getAccountTeamRoles: GET /{version}/account/team/roles', async () => { const stub = {}; const expected = expectedUrl({ @@ -1862,6 +1937,75 @@ describe('hub.account', () => { expect(mock.lastCall?.headers.get('norbix-project-id')).toBe('test-project'); }); + it('getAccountTeamInvitations: GET /{version}/account/team/invitations', async () => { + const stub = {}; + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/account/team/invitations', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['account']!['getAccountTeamInvitations']!; + 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('cancelTeamInvitation: DELETE /{version}/account/team/invitations/{InvitationId}', async () => { + const stub = stubRequestForPath('/{version}/account/team/invitations/{InvitationId}'); + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/account/team/invitations/{InvitationId}', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['account']!['cancelTeamInvitation']!; + await fn(stub); + expect(mock.lastCall).toBeDefined(); + expect(mock.lastCall?.method).toBe('DELETE'); + 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('updateTeamMember: PUT /{version}/account/team/member', async () => { + const stub = {}; + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/account/team/member', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['account']!['updateTeamMember']!; + 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('updateAccountPolicy: PUT /{version}/account/team/policies', async () => { const stub = {}; const expected = expectedUrl({ diff --git a/tests/hub/database.test.ts b/tests/hub/database.test.ts index 19b75fc..cbb1eaa 100644 --- a/tests/hub/database.test.ts +++ b/tests/hub/database.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.database (74 endpoints). + * Tests for hub.database (75 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.database', () => { - it('module exposes 74 method(s)', () => { + it('module exposes 75 method(s)', () => { const mock = createMockFetch(); const mod = new DatabaseModule({} as never); void mod; // silence unused — we only need the type @@ -52,6 +52,7 @@ describe('hub.database', () => { expect(typeof ns['applyDatabaseSchemaBundle']).toBe('function'); expect(typeof ns['deleteDatabaseSchema']).toBe('function'); expect(typeof ns['discardDatabaseSchemaDraft']).toBe('function'); + expect(typeof ns['endUserOwnRecordsReport']).toBe('function'); expect(typeof ns['getDatabaseSchema']).toBe('function'); expect(typeof ns['getDatabaseSchemas']).toBe('function'); expect(typeof ns['getDatabaseSchemaDraft']).toBe('function'); @@ -634,6 +635,29 @@ describe('hub.database', () => { expect(mock.lastCall?.headers.get('norbix-project-id')).toBe('test-project'); }); + it('endUserOwnRecordsReport: POST /{version}/database/schemas/end-user-own-records', async () => { + const stub = {}; + const expected = expectedUrl({ + baseUrl: 'https://hub.norbix.io', + path: '/{version}/database/schemas/end-user-own-records', + version: 'v2', + stub, + }); + const { norbix, mock } = makeClient({}); + const fn = ( + norbix.hub as unknown as Record< + string, + Record Promise> + > + )['database']!['endUserOwnRecordsReport']!; + 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('getDatabaseSchema: GET /{version}/database/schemas/{id}', async () => { const stub = stubRequestForPath('/{version}/database/schemas/{id}'); const expected = expectedUrl({