From 69ea8afd20b24771129ce317a3bd9197082df3e5 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 12:53:36 +0200 Subject: [PATCH 01/16] docs: add private spaces SDK design spec (ORB-1919) Co-Authored-By: Claude Fable 5 --- specs/2026-07-27-private-spaces-design.md | 184 ++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 specs/2026-07-27-private-spaces-design.md diff --git a/specs/2026-07-27-private-spaces-design.md b/specs/2026-07-27-private-spaces-design.md new file mode 100644 index 0000000..73a2d5b --- /dev/null +++ b/specs/2026-07-27-private-spaces-design.md @@ -0,0 +1,184 @@ +# Private Spaces SDK Support — Design + +Date: 2026-07-27 +Driver: [ORB-1919](https://make.atlassian.net/browse/ORB-1919) (MCP support of Private spaces), part of epic [ORB-843](https://make.atlassian.net/browse/ORB-843) (Private Spaces, Phase 1: Personal Teams) + +## Background + +A private space is a per-user personal workspace inside an organization. Under the hood it +is a team with `type: 'personal'`, but the platform exposes it through a dedicated +`/private-spaces` API. The endpoints were published in the Make OpenAPI docs (ORB-1914) and +received dedicated OAuth scopes `private-spaces:read` / `private-spaces:write` (ORB-1919). + +Key platform facts that constrain this design: + +- **No create/delete endpoints.** `POST /private-spaces` and `DELETE /private-spaces/{id}` + existed during development but were removed (ORB-1061). Lifecycle is all-or-nothing per + organization via `/organizations/{id}/private-spaces-settings` (out of scope here). +- **Feature-flagged and cloud-only.** When the org flag is off, endpoints fail with + `IM903`. There is no way to provision a private space through the public API. +- The SDK already covers the two teams touchpoints: `includePrivateSpaces` on + `teams.list()` and `Team.type` (`'personal' | 'standard'`). + +## Scope + +In scope (option A, agreed 2026-07-27): + +1. New `PrivateSpaces` endpoint class: `list`, `get`, `update`. +2. New `private-spaces` tool definitions (3 tools). +3. `privateSpaces` field on the `Organization` type. +4. Registration (`make.ts`, `index.ts`, `src/tools.ts`), tests, mocks, README. + +Out of scope (candidates for follow-up tickets): + +- `GET`/`PATCH /organizations/{organizationId}/private-spaces-settings` (admin bulk + operations; disabling auto-creation bulk-deletes all private spaces). +- `GET /users` list (not in the SDK at all) and its `privateSpace` col; `GET /users/by-permission`. +- Team-variables and llm-configuration route aliases mounted under `/private-spaces/{id}/…`. +- Phase 2 "Locked connections" ACL endpoints (flag-gated, not GA). + +## API contract (verified against imt-web-api code and OpenAPI spec) + +### GET /private-spaces — scope `private-spaces:read` + +Query: `organizationId` (number, **required**), `externalId` (string, optional filter), +`cols[]`, `pg[sortBy|sortDir|offset|limit]` (sortable by `name` only). +Response: `{ privateSpaces: PrivateSpace[], pg }`. + +Requires org permission `personal team manage`. + +### GET /private-spaces/{privateSpaceId} — scope `private-spaces:read` + +Query: `cols[]` — list cols plus `operations`, `transfer`, `centicredits` (usage totals +since last reset, computed from Elasticsearch, returned as strings; 503 when ES fails). +Response: `{ privateSpace }`. + +Requires org permission `personal team own view`; non-admin callers must be a member of +the space, otherwise **404**. + +### PATCH /private-spaces/{privateSpaceId} — scope `private-spaces:write` + +Body: `{ operationsLimit?: number | null }` — min 0; `null` removes the limit +(unlimited); omitted = unchanged; `transferLimit` is derived server-side. +Query: `confirmed` (boolean) — **required (else `IM004`) when the new limit is below the +space's current consumption; confirming pauses the space.** +Response: `{ privateSpace }` including `deleted` and `externalId`. + +Requires org permission `personal team manage`. + +### PrivateSpace fields + +| Field | Type | Availability | +|---|---|---| +| `id` | number | default col | +| `name` | string | default col | +| `organizationId` | number | default col | +| `globalAgentsEnabled` | boolean | default col | +| `type` | `'personal'` | default col | +| `privateSpaceOwnerName` | string | default col | +| `privateSpaceOwnerEmail` | string | default col | +| `privateSpaceOwnerId` | number | default col | +| `operationsLimit` | number \| null | cols; null = unlimited | +| `transferLimit` | string \| null | cols; bytes | +| `consumedOperations` | number \| null | cols | +| `consumedTransfer` | string \| null | cols | +| `isPaused` | boolean \| null | cols; paused due to exceeded limits | +| `consumedCenticredits` | number \| null | cols | +| `operations` | string | `get()` cols only (ES totals) | +| `transfer` | string | `get()` cols only (ES totals) | +| `centicredits` | string | `get()` cols only (ES totals) | +| `deleted` | boolean | admin col; present in PATCH response | +| `externalId` | string \| null | admin col; present in PATCH response | + +## Design + +### 1. `src/endpoints/private-spaces.ts` + +Follows the standard endpoint template (closest precedents: `teams.ts`, `scenarios.ts`). + +Types (exported unless noted): + +- `PrivateSpace` — **one entity type** for list/get/update. `id`, `name`, + `organizationId` required; everything else optional. Get-only usage cols and + admin/PATCH-only fields live on the same type with JSDoc noting availability. + Decision: matches the `Team` convention (one entity type mixing list and detail + fields); a split `PrivateSpaceWithUsage` type was considered and rejected as + non-idiomatic for this repo. +- `ListPrivateSpacesOptions` — `cols`, `pg` + (`Partial>`), `externalId?: string`. +- `GetPrivateSpaceOptions` — `cols`. +- `UpdatePrivateSpaceBody` — `{ operationsLimit?: number | null }`. +- `UpdatePrivateSpaceOptions` — `{ confirmed?: boolean }`. +- Internal (not exported): `ListPrivateSpacesResponse`, `GetPrivateSpaceResponse`, + `UpdatePrivateSpaceResponse`. + +Class `PrivateSpaces`: + +- `list(organizationId: number, options?: ListPrivateSpacesOptions): Promise[]>` + — GET `/private-spaces` with query `{ organizationId, externalId, cols, pg }`. +- `get(privateSpaceId: number, options?: GetPrivateSpaceOptions): Promise>` + — GET `/private-spaces/{privateSpaceId}`. +- `update(privateSpaceId: number, body: UpdatePrivateSpaceBody, options?: UpdatePrivateSpaceOptions): Promise` + — PATCH with query `{ confirmed: options?.confirmed }`; signature follows + `scenarios.update(id, body, { confirmed })`. + +JSDoc documents: no create/delete (org-settings-driven lifecycle), the `confirmed` +trap, the 404-for-non-members behavior, and null-vs-omitted `operationsLimit` semantics. + +### 2. `src/endpoints/private-spaces.tools.ts` + +Category `private-spaces`. All tools set explicit `readOnlyHint` / `destructiveHint` / +`openWorldHint` and document state traps in descriptions (repo convention since WM-4172). + +| Tool | Scope | scopeId / resourceId | Hints | Notes | +|---|---|---|---|---| +| `private-spaces_list` | `private-spaces:read` | `organizationId` / — | read-only | Params: `organizationId` (required), `externalId`. Description: requires the org's private-spaces feature and `personal team manage` permission. Executes with `cols: ['*']`. | +| `private-spaces_get` | `private-spaces:read` | `privateSpaceId` / `privateSpaceId` | read-only | Description: non-members receive 404; usage totals (`operations`, `transfer`, `centicredits`) come from analytics storage. Executes with `cols: ['*']`. | +| `private-spaces_update` | `private-spaces:write` | `privateSpaceId` / `privateSpaceId` | not read-only, not destructive, idempotent | Params: `privateSpaceId` (required), `operationsLimit` (`type: ['number', 'null']`, null = unlimited), `confirmed` (boolean; description: required when lowering the limit below current consumption — confirming pauses the space). | + +### 3. `src/endpoints/organizations.ts` + +Add to `Organization`: + +```ts +/** Private spaces the requesting user is a member of (cols-selectable; cloud only). + * `hasAdminVisibility` mirrors the organization's "add admins as observers" setting. */ +privateSpaces?: { id: number; name: string; isOwner: boolean; hasAdminVisibility: boolean }[]; +``` + +Type-only change; exercised by extending the organizations get mock + a cols assertion. + +### 4. Registration and docs + +- `src/make.ts`: import, `public readonly privateSpaces: PrivateSpaces`, constructor + init, JSDoc. +- `src/index.ts`: export `PrivateSpace`, `PrivateSpaces`, `ListPrivateSpacesOptions`, + `GetPrivateSpaceOptions`, `UpdatePrivateSpaceBody`, `UpdatePrivateSpaceOptions`. +- `src/tools.ts`: import and spread `PrivateSpacesTools` into `MakeTools`. +- `README.md`: add `privateSpaces` to the endpoint list and `private-spaces` to the tool + categories. + +## Testing + +TDD throughout (red → green per behavior). + +- `test/private-spaces.spec.ts` + `test/mocks/private-spaces/{list,get,update}.json` + (realistic data matching the field table above): + - list: response unwrapping; query assertion for `organizationId` and `externalId`. + - list: column selection (`cols`) round-trip. + - get: response unwrapping; get-only usage cols present in mock. + - update: body assertion (`operationsLimit`, including `null`), `confirmed=true` in + query, `content-type: application/json`. +- `test/organizations.spec.ts` + mock: extend get mock with `privateSpaces` and assert + it round-trips. +- `test/private-spaces.integration.test.ts`: lists spaces for `MAKE_ORGANIZATION`; + **skips gracefully when none exist** (no public API to provision one; feature is + flag-gated). When a space exists: `get()` it, `update()` the operations limit and + restore the original value. +- Coverage floor: ≥90% line/branch on touched files. + +## Error handling + +No special handling — `IM903` (feature disabled), `IM004` (confirmation required), 404 +(non-member), and 503 (ES unavailable) bubble up as `MakeError`, per repo convention. +The tool descriptions carry the guidance instead. From 900ded323c551619e8728c839c9a112dc87eb283 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 13:12:43 +0200 Subject: [PATCH 02/16] docs: add private spaces implementation plan (ORB-1919) Co-Authored-By: Claude Fable 5 --- plans/2026-07-27-private-spaces.md | 1163 ++++++++++++++++++++++++++++ 1 file changed, 1163 insertions(+) create mode 100644 plans/2026-07-27-private-spaces.md diff --git a/plans/2026-07-27-private-spaces.md b/plans/2026-07-27-private-spaces.md new file mode 100644 index 0000000..7734fa8 --- /dev/null +++ b/plans/2026-07-27-private-spaces.md @@ -0,0 +1,1163 @@ +# Private Spaces SDK Support Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add private-spaces support to the Make TypeScript SDK: a `PrivateSpaces` endpoint class (`list`/`get`/`update`), matching tool definitions, and the `privateSpaces` field on `Organization`, per `specs/2026-07-27-private-spaces-design.md` (ORB-1919). + +**Architecture:** One new endpoint file + one new tools file following the repo's exact endpoint template (closest precedents: `src/endpoints/teams.ts`, `src/endpoints/scenarios.ts` for the `confirmed` option). Registration in `make.ts`, `index.ts`, `src/tools.ts`. No create/delete methods — those API endpoints were removed upstream (ORB-1061). + +**Tech Stack:** TypeScript (strict, ES modules with `.js` import extensions), Jest + jest-fetch-mock (`test/test.utils.ts` `mockFetch`), tsup build. + +## Global Constraints + +- TDD: write the failing test first, watch it fail for the right reason, then implement. Never modify a test to make the implementation pass. +- Strict typing: no `any`, no `@ts-ignore`. All public types/methods carry JSDoc. +- Imports always use `.js` extensions (`../types.js`, `./endpoints/private-spaces.js`). +- Internal response types (`*Response`) are NOT exported. +- Tool names: `private-spaces_list`, `private-spaces_get`, `private-spaces_update`; category `private-spaces`; scopes `private-spaces:read` / `private-spaces:write`. Every tool sets explicit `readOnlyHint`, `destructiveHint`, `openWorldHint`. +- The repo's `JSONSchema.type` is a single string union — nullable params use `oneOf: [{ type: 'number' }, { type: 'null' }]`, NOT `type: ['number', 'null']`. +- Do NOT add create/delete methods or tools for private spaces. +- `mockFetch` mock URLs must match the built URL exactly (query-param insertion order; `[` `]` encode as `%5B` `%5D`; `*` stays literal). +- Run a single spec file with: `npx jest --runInBand --forceExit --testMatch "**/test/"`. +- Full suite: `npm test` (includes text coverage). Lint: `npm run lint`. Format: `npm run format`. +- Long test output goes to the scratchpad dir via `> file 2>&1`, never piped through `tail`/`grep` directly. +- Coverage floor: ≥90% line and branch on touched files. +- Every commit message ends with `Co-Authored-By: Claude Fable 5 `. +- Baseline before Task 1: run `npm test`, record pass count; every GREEN step must be baseline + new tests, no regressions. + +--- + +### Task 1: `PrivateSpace` type, `PrivateSpaces.list()`, client registration + +**Files:** +- Create: `src/endpoints/private-spaces.ts` +- Create: `test/mocks/private-spaces/list.json` +- Create: `test/private-spaces.spec.ts` +- Modify: `src/make.ts` (import ~line 20, property ~line 186, constructor ~line 281) + +**Interfaces:** +- Consumes: `FetchFunction`, `Pagination`, `PickColumns` from `src/types.js` (existing). +- Produces: `PrivateSpace` type; `ListPrivateSpacesOptions`; class `PrivateSpaces` with `list(organizationId: number, options?: ListPrivateSpacesOptions): Promise[]>`; `make.privateSpaces: PrivateSpaces` on the `Make` client. Tasks 2–7 rely on all of these names exactly. + +- [ ] **Step 1: Record the baseline** + +Run: `npm test > /private/tmp/claude-501/-Users-jankulhavy-Projects-Make-make-typescript-sdk/767a9850-fc5b-4c6a-a17a-a686a8b29d30/scratchpad/baseline.txt 2>&1` +Then inspect the tail of that file for the totals line. Record the number of passing tests/suites. + +- [ ] **Step 2: Create the list mock** + +`test/mocks/private-spaces/list.json`: + +```json +{ + "privateSpaces": [ + { + "id": 101, + "name": "Becca's space", + "organizationId": 5, + "globalAgentsEnabled": false, + "type": "personal", + "privateSpaceOwnerName": "Becca Smith", + "privateSpaceOwnerEmail": "becca.smith@example.com", + "privateSpaceOwnerId": 42 + }, + { + "id": 102, + "name": "Jan's space", + "organizationId": 5, + "globalAgentsEnabled": true, + "type": "personal", + "privateSpaceOwnerName": "Jan Novak", + "privateSpaceOwnerEmail": "jan.novak@example.com", + "privateSpaceOwnerId": 43 + } + ], + "pg": { + "sortBy": "name", + "sortDir": "asc", + "offset": 0, + "limit": 100 + } +} +``` + +- [ ] **Step 3: Write the failing tests** + +`test/private-spaces.spec.ts`: + +```typescript +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { mockFetch } from './test.utils.js'; +import type { PrivateSpace } from '../src/endpoints/private-spaces.js'; + +import * as privateSpacesListMock from './mocks/private-spaces/list.json'; + +const MAKE_API_KEY = 'api-key'; +const MAKE_ZONE = 'make.local'; + +describe('Endpoints: PrivateSpaces', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + it('Should list private spaces', async () => { + mockFetch('GET https://make.local/api/v2/private-spaces?organizationId=5', privateSpacesListMock); + + const result = await make.privateSpaces.list(5); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should list private spaces filtered by externalId with pagination', async () => { + mockFetch( + 'GET https://make.local/api/v2/private-spaces?organizationId=5&externalId=ext-1&pg%5BsortBy%5D=name&pg%5BsortDir%5D=asc', + privateSpacesListMock, + ); + + const result = await make.privateSpaces.list(5, { + externalId: 'ext-1', + pg: { + sortBy: 'name', + sortDir: 'asc', + }, + }); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should list private spaces with selected columns', async () => { + const cols: (keyof PrivateSpace)[] = ['id', 'name', 'isPaused']; + mockFetch( + `GET https://make.local/api/v2/private-spaces?organizationId=5&cols%5B%5D=${cols.join('&cols%5B%5D=')}`, + privateSpacesListMock, + ); + + const result = await make.privateSpaces.list(5, { + cols, + }); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); +}); +``` + +- [ ] **Step 4: Run the tests to verify they fail for the right reason** + +Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` +Expected: FAIL — cannot find module `'../src/endpoints/private-spaces.js'` (the module does not exist yet), and `make.privateSpaces` does not exist. Import/compile failure caused by the missing feature is the correct RED here; a typo in an existing path is not. + +- [ ] **Step 5: Implement the endpoint file with `list()`** + +`src/endpoints/private-spaces.ts`: + +```typescript +import type { FetchFunction, Pagination, PickColumns } from '../types.js'; + +/** + * Represents a private space in Make. + * A private space is a per-user personal workspace inside an organization where + * scenarios and related entities (including connections) stay visible only to the + * owner. Private spaces cannot be created or deleted through the API — they are + * provisioned automatically based on the organization's private-spaces settings. + */ +export type PrivateSpace = { + /** Unique identifier of the private space */ + id: number; + /** Name of the private space */ + name: string; + /** ID of the organization this private space belongs to */ + organizationId: number; + /** Whether Make global AI agents are enabled for the space */ + globalAgentsEnabled?: boolean; + /** Type of the underlying team; always `personal` for private spaces */ + type?: 'personal'; + /** Name of the space owner */ + privateSpaceOwnerName?: string; + /** Email of the space owner */ + privateSpaceOwnerEmail?: string; + /** User ID of the space owner */ + privateSpaceOwnerId?: number; + /** Maximum operations limit; null means unlimited */ + operationsLimit?: number | null; + /** Maximum data transfer limit in bytes; derived from the operations limit */ + transferLimit?: string | null; + /** Number of operations consumed in the current period */ + consumedOperations?: number | null; + /** Amount of data transfer consumed in the current period, in bytes */ + consumedTransfer?: string | null; + /** Whether the space is paused due to exceeded limits */ + isPaused?: boolean | null; + /** Number of centicredits consumed in the current period */ + consumedCenticredits?: number | null; + /** Total operations since the last reset; only selectable via `cols` on `get()` */ + operations?: string; + /** Total data transfer since the last reset, in bytes; only selectable via `cols` on `get()` */ + transfer?: string; + /** Total centicredits since the last reset; only selectable via `cols` on `get()` */ + centicredits?: string; + /** Whether the space is deleted; returned by `update()` */ + deleted?: boolean; + /** External identifier of the space; returned by `update()` */ + externalId?: string | null; +}; + +/** + * Options for listing private spaces. + * @template C Keys of the PrivateSpace type to include in the response + */ +export type ListPrivateSpacesOptions = { + /** Specific columns/fields to include in the response */ + cols?: C[] | ['*']; + /** Pagination options (the API supports sorting by `name` only) */ + pg?: Partial>; + /** Filter spaces by their external ID */ + externalId?: string; +}; + +/** + * Response format for listing private spaces. + */ +type ListPrivateSpacesResponse = { + /** List of private spaces matching the query */ + privateSpaces: PickColumns[]; + /** Pagination information */ + pg: Pagination; +}; + +/** + * Class providing methods for working with Make private spaces. + * Requires the organization's private-spaces feature to be enabled; the API + * responds with error IM903 when it is not. + */ +export class PrivateSpaces { + readonly #fetch: FetchFunction; + + /** + * Create a new PrivateSpaces instance. + * @param fetch Function for making API requests + */ + constructor(fetch: FetchFunction) { + this.#fetch = fetch; + } + + /** + * List private spaces of an organization. + * Requires the `personal team manage` organization permission. + * @param organizationId The organization ID to list private spaces for + * @param options Optional parameters for filtering and pagination + * @returns Promise with the list of private spaces + * + * @example + * ```typescript + * const spaces = await make.privateSpaces.list(123); + * ``` + */ + async list( + organizationId: number, + options?: ListPrivateSpacesOptions, + ): Promise[]> { + return ( + await this.#fetch>('/private-spaces', { + query: { + organizationId, + externalId: options?.externalId, + cols: options?.cols, + pg: options?.pg, + }, + }) + ).privateSpaces; + } +} +``` + +- [ ] **Step 6: Register the endpoint on the `Make` client** + +In `src/make.ts`, three edits: + +After the `PublicTemplates` import (line ~20): + +```typescript +import { PrivateSpaces } from './endpoints/private-spaces.js'; +``` + +After the `publicTemplates` property declaration (line ~186): + +```typescript +/** + * Access to private space endpoints. + * Private spaces are per-user personal workspaces within an organization; they are + * provisioned automatically and cannot be created or deleted through the API. + */ +public readonly privateSpaces: PrivateSpaces; +``` + +After `this.publicTemplates = new PublicTemplates(this.fetch.bind(this));` in the constructor (line ~281): + +```typescript +this.privateSpaces = new PrivateSpaces(this.fetch.bind(this)); +``` + +- [ ] **Step 7: Run the tests to verify they pass** + +Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` +Expected: PASS (3 tests). + +- [ ] **Step 8: Run lint and the full suite** + +Run: `npm run lint && npm test > /private/tmp/claude-501/-Users-jankulhavy-Projects-Make-make-typescript-sdk/767a9850-fc5b-4c6a-a17a-a686a8b29d30/scratchpad/task1.txt 2>&1` +Inspect the file tail: totals must equal baseline + 3 new passing tests, zero failures. + +- [ ] **Step 9: Commit** + +```bash +git add src/endpoints/private-spaces.ts src/make.ts test/private-spaces.spec.ts test/mocks/private-spaces/list.json +git commit -m "feat(private-spaces): add PrivateSpaces endpoint with list() (ORB-1919) + +Co-Authored-By: Claude Fable 5 " +``` + +--- + +### Task 2: `PrivateSpaces.get()` + +**Files:** +- Create: `test/mocks/private-spaces/get.json` +- Modify: `src/endpoints/private-spaces.ts` (add `GetPrivateSpaceOptions`, `GetPrivateSpaceResponse`, `get()`) +- Modify: `test/private-spaces.spec.ts` (extend the existing describe block) + +**Interfaces:** +- Consumes: `PrivateSpace`, `PrivateSpaces` class from Task 1. +- Produces: `GetPrivateSpaceOptions`; `get(privateSpaceId: number, options?: GetPrivateSpaceOptions): Promise>`. Tasks 6–7 call `make.privateSpaces.get(...)` with this exact signature. + +- [ ] **Step 1: Create the get mock** + +`test/mocks/private-spaces/get.json` (includes the get-only usage columns): + +```json +{ + "privateSpace": { + "id": 101, + "name": "Becca's space", + "organizationId": 5, + "globalAgentsEnabled": false, + "type": "personal", + "privateSpaceOwnerName": "Becca Smith", + "privateSpaceOwnerEmail": "becca.smith@example.com", + "privateSpaceOwnerId": 42, + "operationsLimit": 1000, + "transferLimit": "1073741824", + "consumedOperations": 250, + "consumedTransfer": "52428800", + "isPaused": false, + "consumedCenticredits": 12345, + "operations": "250", + "transfer": "52428800", + "centicredits": "12345" + } +} +``` + +- [ ] **Step 2: Write the failing tests** + +Add to the describe block in `test/private-spaces.spec.ts` (and add the import at the top with the other mock imports): + +```typescript +import * as privateSpaceGetMock from './mocks/private-spaces/get.json'; +``` + +```typescript + it('Should get a private space', async () => { + mockFetch('GET https://make.local/api/v2/private-spaces/101', privateSpaceGetMock); + + const result = await make.privateSpaces.get(101); + + expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); + }); + + it('Should get a private space with usage columns', async () => { + const cols: (keyof PrivateSpace)[] = ['id', 'operations', 'transfer', 'centicredits']; + mockFetch( + `GET https://make.local/api/v2/private-spaces/101?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, + privateSpaceGetMock, + ); + + const result = await make.privateSpaces.get(101, { + cols, + }); + + expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); + }); +``` + +- [ ] **Step 3: Run the tests to verify they fail** + +Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` +Expected: FAIL — `make.privateSpaces.get` is not a function (TypeScript: property `get` does not exist on `PrivateSpaces`). + +- [ ] **Step 4: Implement `get()`** + +In `src/endpoints/private-spaces.ts`, add after `ListPrivateSpacesOptions`: + +```typescript +/** + * Options for retrieving a private space. + * @template C Keys of the PrivateSpace type to include in the response + */ +export type GetPrivateSpaceOptions = { + /** + * Specific columns/fields to include in the response. In addition to the list + * columns, `get()` supports the usage totals `operations`, `transfer` and + * `centicredits` (computed from analytics storage; the API responds with 503 + * when that storage is unavailable). + */ + cols?: C[] | ['*']; +}; +``` + +Add after `ListPrivateSpacesResponse`: + +```typescript +/** + * Response format for getting a private space. + */ +type GetPrivateSpaceResponse = { + /** The requested private space */ + privateSpace: PickColumns; +}; +``` + +Add to the `PrivateSpaces` class after `list()`: + +```typescript + /** + * Get details of a specific private space. + * Requires the `personal team own view` organization permission; callers who are + * not members of the space receive a 404 even when the space exists. + * @param privateSpaceId The private space ID to get + * @param options Optional parameters for filtering returned fields + * @returns Promise with the private space information + * + * @example + * ```typescript + * const space = await make.privateSpaces.get(101); + * ``` + */ + async get( + privateSpaceId: number, + options?: GetPrivateSpaceOptions, + ): Promise> { + return ( + await this.#fetch>(`/private-spaces/${privateSpaceId}`, { + query: { + cols: options?.cols, + }, + }) + ).privateSpace; + } +``` + +- [ ] **Step 5: Run the tests to verify they pass** + +Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` +Expected: PASS (5 tests). + +- [ ] **Step 6: Commit** + +```bash +git add src/endpoints/private-spaces.ts test/private-spaces.spec.ts test/mocks/private-spaces/get.json +git commit -m "feat(private-spaces): add get() with usage columns (ORB-1919) + +Co-Authored-By: Claude Fable 5 " +``` + +--- + +### Task 3: `PrivateSpaces.update()` + +**Files:** +- Create: `test/mocks/private-spaces/update.json` +- Modify: `src/endpoints/private-spaces.ts` (add `UpdatePrivateSpaceBody`, `UpdatePrivateSpaceOptions`, `UpdatePrivateSpaceResponse`, `update()`) +- Modify: `test/private-spaces.spec.ts` + +**Interfaces:** +- Consumes: `PrivateSpace`, `PrivateSpaces` class from Tasks 1–2. +- Produces: `UpdatePrivateSpaceBody = { operationsLimit?: number | null }`; `UpdatePrivateSpaceOptions = { confirmed?: boolean }`; `update(privateSpaceId: number, body: UpdatePrivateSpaceBody, options?: UpdatePrivateSpaceOptions): Promise`. Tasks 6–7 call `make.privateSpaces.update(...)` with this exact signature. + +- [ ] **Step 1: Create the update mock** + +`test/mocks/private-spaces/update.json` (PATCH responses additionally include `deleted` and `externalId`; this one shows the confirmed-below-consumption outcome — space paused): + +```json +{ + "privateSpace": { + "id": 101, + "name": "Becca's space", + "organizationId": 5, + "globalAgentsEnabled": false, + "type": "personal", + "privateSpaceOwnerName": "Becca Smith", + "privateSpaceOwnerEmail": "becca.smith@example.com", + "privateSpaceOwnerId": 42, + "operationsLimit": 100, + "transferLimit": "107374182", + "consumedOperations": 250, + "consumedTransfer": "52428800", + "isPaused": true, + "consumedCenticredits": 12345, + "deleted": false, + "externalId": null + } +} +``` + +- [ ] **Step 2: Write the failing tests** + +Add the mock import to `test/private-spaces.spec.ts`: + +```typescript +import * as privateSpaceUpdateMock from './mocks/private-spaces/update.json'; +``` + +Add to the describe block: + +```typescript + it('Should update a private space', async () => { + const body = { + operationsLimit: 100, + }; + + mockFetch('PATCH https://make.local/api/v2/private-spaces/101', privateSpaceUpdateMock, req => { + expect(req.body).toStrictEqual(body); + expect(req.headers.get('content-type')).toBe('application/json'); + }); + + const result = await make.privateSpaces.update(101, body); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + }); + + it('Should update a private space with confirmation and null limit', async () => { + const body = { + operationsLimit: null, + }; + + mockFetch('PATCH https://make.local/api/v2/private-spaces/101?confirmed=true', privateSpaceUpdateMock, req => { + expect(req.body).toStrictEqual(body); + }); + + const result = await make.privateSpaces.update(101, body, { confirmed: true }); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + }); +``` + +- [ ] **Step 3: Run the tests to verify they fail** + +Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` +Expected: FAIL — `make.privateSpaces.update` is not a function. + +- [ ] **Step 4: Implement `update()`** + +In `src/endpoints/private-spaces.ts`, add after `GetPrivateSpaceOptions`: + +```typescript +/** + * Body for updating a private space. + */ +export type UpdatePrivateSpaceBody = { + /** + * Maximum operations limit (minimum 0). Set to `null` to remove the limit + * (unlimited); omit to leave unchanged. The transfer limit is derived from + * this value by the API. + */ + operationsLimit?: number | null; +}; + +/** + * Options for updating a private space. + */ +export type UpdatePrivateSpaceOptions = { + /** + * Confirmation of the update. Required (the API fails with IM004 otherwise) + * when the new operations limit is below the space's current consumption; + * confirming pauses the space. + */ + confirmed?: boolean; +}; +``` + +Add after `GetPrivateSpaceResponse`: + +```typescript +/** + * Response format for updating a private space. + */ +type UpdatePrivateSpaceResponse = { + /** The updated private space */ + privateSpace: PrivateSpace; +}; +``` + +Add to the `PrivateSpaces` class after `get()`: + +```typescript + /** + * Update a private space. + * Requires the `personal team manage` organization permission. + * @param privateSpaceId The private space ID to update + * @param body The fields to update + * @param options Optional update options + * @returns Promise with the updated private space + * + * @example + * ```typescript + * // Remove the operations limit + * const space = await make.privateSpaces.update(101, { operationsLimit: null }); + * ``` + */ + async update( + privateSpaceId: number, + body: UpdatePrivateSpaceBody, + options?: UpdatePrivateSpaceOptions, + ): Promise { + return ( + await this.#fetch(`/private-spaces/${privateSpaceId}`, { + method: 'PATCH', + query: { + confirmed: options?.confirmed, + }, + body, + }) + ).privateSpace; + } +``` + +- [ ] **Step 5: Run the tests to verify they pass** + +Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` +Expected: PASS (7 tests). + +- [ ] **Step 6: Commit** + +```bash +git add src/endpoints/private-spaces.ts test/private-spaces.spec.ts test/mocks/private-spaces/update.json +git commit -m "feat(private-spaces): add update() with confirmed option (ORB-1919) + +Co-Authored-By: Claude Fable 5 " +``` + +--- + +### Task 4: `Organization.privateSpaces` field + +**Files:** +- Modify: `src/endpoints/organizations.ts` (add field to the `Organization` type, after the `license` block ends, ~line 100+ — place it with the other optional top-level fields) +- Modify: `test/organizations.spec.ts` (extend the existing describe block) +- Modify: `test/mocks/organizations/get.json` (add `privateSpaces` to the organization object) + +**Interfaces:** +- Consumes: existing `Organization` type and `organizations.list()` / `organizations.get()`. +- Produces: `Organization.privateSpaces?: { id: number; name: string; isOwner: boolean; hasAdminVisibility: boolean }[]` — selectable via `cols` because option types use `keyof Organization`. + +- [ ] **Step 1: Write the failing test** + +Add to the describe block in `test/organizations.spec.ts`: + +```typescript + it('Should list organizations with the privateSpaces column', async () => { + const cols: (keyof Organization)[] = ['id', 'name', 'privateSpaces']; + mockFetch( + `GET https://make.local/api/v2/organizations?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, + organizationsListMock, + ); + + const result = await make.organizations.list({ + cols, + }); + + expect(result).toStrictEqual(organizationsListMock.organizations); + }); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `npx jest --runInBand --forceExit --testMatch "**/test/organizations.spec.ts"` +Expected: FAIL — TypeScript error: `'privateSpaces'` is not assignable to `keyof Organization` (the field does not exist yet). + +- [ ] **Step 3: Add the field to the `Organization` type** + +In `src/endpoints/organizations.ts`, inside the `Organization` type, after the `license` object closes (keep it alongside the other optional top-level fields): + +```typescript + /** + * Private spaces the requesting user is a member of within this organization. + * Only returned when requested via `cols`; available on public cloud only. + */ + privateSpaces?: { + /** Unique identifier of the private space */ + id: number; + /** Name of the private space */ + name: string; + /** Whether the requesting user owns the space */ + isOwner: boolean; + /** Whether org admins can see into the space (mirrors the organization's "add admins as observers" setting) */ + hasAdminVisibility: boolean; + }[]; +``` + +- [ ] **Step 4: Extend the get mock so the field shape is exercised** + +In `test/mocks/organizations/get.json`, add to the `organization` object (keep all existing fields): + +```json + "privateSpaces": [ + { + "id": 101, + "name": "Becca's space", + "isOwner": true, + "hasAdminVisibility": false + } + ] +``` + +- [ ] **Step 5: Run the organizations spec to verify all tests pass** + +Run: `npx jest --runInBand --forceExit --testMatch "**/test/organizations.spec.ts"` +Expected: PASS — the new cols test plus all pre-existing organization tests (the extended get mock must not break `Should get an organization with wait option`, which compares against the same mock object). + +- [ ] **Step 6: Commit** + +```bash +git add src/endpoints/organizations.ts test/organizations.spec.ts test/mocks/organizations/get.json +git commit -m "feat(organizations): add privateSpaces column to Organization type (ORB-1919) + +Co-Authored-By: Claude Fable 5 " +``` + +--- + +### Task 5: Public exports and README endpoint list + +**Files:** +- Modify: `src/index.ts` (after the `public-templates.js` export block, ~line 202) +- Modify: `README.md` (endpoint list, ~line 66) + +**Interfaces:** +- Consumes: all types from Tasks 1–3. +- Produces: package-level exports `PrivateSpace`, `PrivateSpaces`, `ListPrivateSpacesOptions`, `GetPrivateSpaceOptions`, `UpdatePrivateSpaceBody`, `UpdatePrivateSpaceOptions`. + +- [ ] **Step 1: Add the type exports** + +In `src/index.ts`, after the `public-templates.js` export block: + +```typescript +export type { + PrivateSpace, + PrivateSpaces, + ListPrivateSpacesOptions, + GetPrivateSpaceOptions, + UpdatePrivateSpaceBody, + UpdatePrivateSpaceOptions, +} from './endpoints/private-spaces.js'; +``` + +- [ ] **Step 2: Add the README endpoint bullet** + +In `README.md`, in the endpoints list, insert between the `**Organizations**` and `**Scenarios**` bullets: + +```markdown +- **Private Spaces** - Per-user private workspaces within an organization (list, get, update) +``` + +- [ ] **Step 3: Verify with lint and build** + +Run: `npm run lint && npm run build` +Expected: both succeed with no errors (tsc validates the export names exist). + +- [ ] **Step 4: Commit** + +```bash +git add src/index.ts README.md +git commit -m "feat(private-spaces): export public types and document endpoint (ORB-1919) + +Co-Authored-By: Claude Fable 5 " +``` + +--- + +### Task 6: Tool definitions + +**Files:** +- Create: `src/endpoints/private-spaces.tools.ts` +- Create: `test/private-spaces-tools.spec.ts` (mirrors `test/on-prem-tools.spec.ts`) +- Modify: `src/tools.ts` (import after `PublicTemplatesTools` import ~line 31; spread after `...PublicTemplatesTools,` ~line 233) +- Modify: `README.md` (tool categories list, ~line 214) + +**Interfaces:** +- Consumes: `make.privateSpaces.list/get/update` exactly as produced by Tasks 1–3; `MakeTool` type and `MakeTools` array from `src/tools.js`. +- Produces: tools `private-spaces_list`, `private-spaces_get`, `private-spaces_update` registered in `MakeTools`. + +- [ ] **Step 1: Write the failing tests** + +`test/private-spaces-tools.spec.ts`: + +```typescript +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { MakeTools } from '../src/tools.js'; +import { mockFetch } from './test.utils.js'; + +import * as privateSpacesListMock from './mocks/private-spaces/list.json'; +import * as privateSpaceGetMock from './mocks/private-spaces/get.json'; +import * as privateSpaceUpdateMock from './mocks/private-spaces/update.json'; + +const MAKE_API_KEY = 'api-key'; +const MAKE_ZONE = 'make.local'; +const ORGANIZATION_ID = 5; +const PRIVATE_SPACE_ID = 101; + +function getTool(name: string) { + const tool = MakeTools.find(entry => entry.name === name); + if (!tool) { + throw new Error(`Missing MCP tool: ${name}`); + } + return tool; +} + +describe('MCP tools: private-spaces', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + it('Should execute private-spaces_list', async () => { + mockFetch( + `GET https://make.local/api/v2/private-spaces?organizationId=${ORGANIZATION_ID}&cols%5B%5D=*`, + privateSpacesListMock, + ); + + const tool = getTool('private-spaces_list'); + const result = await tool.execute(make, { organizationId: ORGANIZATION_ID }); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should execute private-spaces_get', async () => { + mockFetch( + `GET https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?cols%5B%5D=*`, + privateSpaceGetMock, + ); + + const tool = getTool('private-spaces_get'); + const result = await tool.execute(make, { privateSpaceId: PRIVATE_SPACE_ID }); + + expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); + }); + + it('Should execute private-spaces_update', async () => { + mockFetch( + `PATCH https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?confirmed=true`, + privateSpaceUpdateMock, + req => { + expect(req.body).toStrictEqual({ operationsLimit: 100 }); + }, + ); + + const tool = getTool('private-spaces_update'); + const result = await tool.execute(make, { + privateSpaceId: PRIVATE_SPACE_ID, + operationsLimit: 100, + confirmed: true, + }); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + }); +}); +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces-tools.spec.ts"` +Expected: FAIL — `Missing MCP tool: private-spaces_list` (the tools are not defined/registered yet). + +- [ ] **Step 3: Implement the tools file** + +`src/endpoints/private-spaces.tools.ts`: + +```typescript +import type { Make } from '../make.js'; +import type { MakeTool } from '../tools.js'; + +export const tools: MakeTool[] = [ + { + name: 'private-spaces_list', + title: 'List private spaces', + description: + "List the private spaces of an organization. Requires the organization's private-spaces feature to be enabled (error IM903 otherwise) and the 'personal team manage' permission. Private spaces cannot be created or deleted through the API — they are provisioned automatically by organization settings.", + category: 'private-spaces', + scope: 'private-spaces:read', + scopeId: 'organizationId', + identifier: 'organizationId', + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID to list private spaces for' }, + externalId: { type: 'string', description: 'Filter private spaces by their external ID' }, + }, + required: ['organizationId'], + }, + examples: [{ organizationId: 5 }, { organizationId: 5, externalId: 'ext-1' }], + execute: async (make: Make, args: { organizationId: number; externalId?: string }) => { + const { organizationId, ...options } = args; + return await make.privateSpaces.list(organizationId, { ...options, cols: ['*'] }); + }, + }, + { + name: 'private-spaces_get', + title: 'Get private space', + description: + 'Get details of a specific private space, including usage totals (operations, transfer, centicredits). Callers who are not members of the space receive a 404 even when the space exists.', + category: 'private-spaces', + scope: 'private-spaces:read', + scopeId: 'privateSpaceId', + identifier: 'privateSpaceId', + resourceId: 'privateSpaceId', + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + privateSpaceId: { type: 'number', description: 'The private space ID to retrieve' }, + }, + required: ['privateSpaceId'], + }, + examples: [{ privateSpaceId: 101 }], + execute: async (make: Make, args: { privateSpaceId: number }) => { + return await make.privateSpaces.get(args.privateSpaceId, { cols: ['*'] }); + }, + }, + { + name: 'private-spaces_update', + title: 'Update private space', + description: + "Update a private space's operations limit. Set operationsLimit to null to remove the limit (unlimited); the transfer limit is derived automatically. When the new limit is below the space's current consumption the call fails with IM004 unless 'confirmed' is true — confirming pauses the space.", + category: 'private-spaces', + scope: 'private-spaces:write', + scopeId: 'privateSpaceId', + identifier: 'privateSpaceId', + resourceId: 'privateSpaceId', + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + privateSpaceId: { type: 'number', description: 'The private space ID to update' }, + operationsLimit: { + oneOf: [{ type: 'number' }, { type: 'null' }], + description: + 'Maximum operations limit (minimum 0). Pass null to remove the limit; omit to leave unchanged.', + }, + confirmed: { + type: 'boolean', + description: + "Confirmation of the update. Required when the new limit is below the space's current consumption; confirming pauses the space.", + }, + }, + required: ['privateSpaceId'], + }, + examples: [{ privateSpaceId: 101, operationsLimit: 10000 }, { privateSpaceId: 101, operationsLimit: null, confirmed: true }], + execute: async ( + make: Make, + args: { privateSpaceId: number; operationsLimit?: number | null; confirmed?: boolean }, + ) => { + const { privateSpaceId, confirmed, ...body } = args; + return await make.privateSpaces.update(privateSpaceId, body, { confirmed }); + }, + }, +]; +``` + +- [ ] **Step 4: Register the tools** + +In `src/tools.ts`, after the `PublicTemplatesTools` import: + +```typescript +import { tools as PrivateSpacesTools } from './endpoints/private-spaces.tools.js'; +``` + +In the `MakeTools` array, after `...PublicTemplatesTools,`: + +```typescript + ...PrivateSpacesTools, +``` + +- [ ] **Step 5: Run the tests to verify they pass** + +Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces-tools.spec.ts"` +Expected: PASS (3 tests). + +- [ ] **Step 6: Add the README tool category** + +In `README.md`, in the tool categories list, insert between `- \`organizations\`` and `- \`scenarios\``: + +```markdown +- `private-spaces` +``` + +- [ ] **Step 7: Run lint and the full suite** + +Run: `npm run lint && npm test > /private/tmp/claude-501/-Users-jankulhavy-Projects-Make-make-typescript-sdk/767a9850-fc5b-4c6a-a17a-a686a8b29d30/scratchpad/task6.txt 2>&1` +Inspect the file tail: all tests green (baseline + 11 new across Tasks 1–6: 7 endpoint + 1 organizations + 3 tools), zero failures. + +- [ ] **Step 8: Commit** + +```bash +git add src/endpoints/private-spaces.tools.ts src/tools.ts test/private-spaces-tools.spec.ts README.md +git commit -m "feat(private-spaces): add tool definitions (ORB-1919) + +Co-Authored-By: Claude Fable 5 " +``` + +--- + +### Task 7: Integration test + +**Files:** +- Create: `test/private-spaces.integration.test.ts` + +**Interfaces:** +- Consumes: `make.privateSpaces.list/get/update` from Tasks 1–3; env vars `MAKE_API_KEY`, `MAKE_ZONE`, `MAKE_ORGANIZATION` from `.env`. +- Produces: nothing consumed later. + +Integration tests run only via `npm run test:integration` (separate testMatch), so this file never affects `npm test`. There is no public API to provision a private space and the feature is flag-gated per org, so every test after the list guards with an early return when no space exists. + +- [ ] **Step 1: Write the integration test** + +`test/private-spaces.integration.test.ts`: + +```typescript +import 'dotenv/config'; +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; + +const MAKE_API_KEY = String(process.env.MAKE_API_KEY || ''); +const MAKE_ZONE = String(process.env.MAKE_ZONE || ''); +const MAKE_ORGANIZATION = Number(process.env.MAKE_ORGANIZATION || 0); + +describe('Integration: PrivateSpaces', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + let privateSpaceId: number | undefined; + let originalOperationsLimit: number | null | undefined; + let consumedOperations: number | null | undefined; + + it('Should list private spaces', async () => { + const spaces = await make.privateSpaces.list(MAKE_ORGANIZATION); + + expect(Array.isArray(spaces)).toBe(true); + + // No public API can provision a private space, so downstream tests are + // skipped (early return) when the organization has none. + privateSpaceId = spaces[0]?.id; + }); + + it('Should get a private space', async () => { + if (privateSpaceId === undefined) return; + + const space = await make.privateSpaces.get(privateSpaceId, { cols: ['*'] }); + + expect(space.id).toBe(privateSpaceId); + expect(space.organizationId).toBe(MAKE_ORGANIZATION); + expect(space.type).toBe('personal'); + + originalOperationsLimit = space.operationsLimit; + consumedOperations = space.consumedOperations; + }); + + it('Should update a private space and restore the original limit', async () => { + if (privateSpaceId === undefined) return; + + // Stay above current consumption so the update needs no confirmation + // and cannot pause the space. + const safeLimit = Math.max(consumedOperations ?? 0, originalOperationsLimit ?? 0) + 10000; + + const updated = await make.privateSpaces.update(privateSpaceId, { operationsLimit: safeLimit }); + expect(updated.operationsLimit).toBe(safeLimit); + + const restored = await make.privateSpaces.update( + privateSpaceId, + { operationsLimit: originalOperationsLimit ?? null }, + { confirmed: true }, + ); + expect(restored.operationsLimit).toBe(originalOperationsLimit ?? null); + }); +}); +``` + +- [ ] **Step 2: Verify it compiles and does not leak into the unit suite** + +Run: `npm run lint` +Expected: PASS. +Run: `npx jest --runInBand --forceExit --testMatch "**/test/**/*.spec.ts" --listTests | grep private-spaces` +Expected: only `test/private-spaces.spec.ts` and `test/private-spaces-tools.spec.ts` — NOT the integration file. + +- [ ] **Step 3: Run the integration test if `.env` is configured (skip this step when `.env` is absent)** + +Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.integration.test.ts" > /private/tmp/claude-501/-Users-jankulhavy-Projects-Make-make-typescript-sdk/767a9850-fc5b-4c6a-a17a-a686a8b29d30/scratchpad/task7.txt 2>&1` +Inspect the file: PASS, or an environment-related failure (missing env/feature flag) — report which. Do not mark this plan complete with an unexplained integration failure. + +- [ ] **Step 4: Commit** + +```bash +git add test/private-spaces.integration.test.ts +git commit -m "test(private-spaces): add integration tests (ORB-1919) + +Co-Authored-By: Claude Fable 5 " +``` + +--- + +### Task 8: Final verification + +**Files:** none new — verification only. + +- [ ] **Step 1: Full unit suite with coverage** + +Run: `npm test > /private/tmp/claude-501/-Users-jankulhavy-Projects-Make-make-typescript-sdk/767a9850-fc5b-4c6a-a17a-a686a8b29d30/scratchpad/final.txt 2>&1` +Inspect the file: zero failures; totals = baseline + 11 new tests. Read the coverage table rows for `private-spaces.ts`, `private-spaces.tools.ts`, `organizations.ts`, `make.ts`, `tools.ts`: each touched file must be ≥90% lines and branches. If below, add the missing test before proceeding (never assertion-free filler). + +- [ ] **Step 2: Lint, format, build** + +Run: `npm run lint && npm run format && npm run build` +Expected: all pass; `git status` after format shows no unexpected reformat of untouched files (if prettier changed only files from this plan, amend them into a `style:` commit or fold into Step 4). + +- [ ] **Step 3: README cross-check against the repo checklist** + +Confirm `README.md` shows the **Private Spaces** endpoint bullet (Task 5) and the `private-spaces` tool category (Task 6). Confirm no other README section (environment variables, configuration) is affected — this change adds no env vars or config options. + +- [ ] **Step 4: Commit any remaining changes and report** + +```bash +git status --short +``` + +If anything is uncommitted from steps above, commit it: + +```bash +git add -A -- ':!test-public-templates.ts' +git commit -m "chore(private-spaces): formatting and verification follow-ups (ORB-1919) + +Co-Authored-By: Claude Fable 5 " +``` + +Note: `test-public-templates.ts` in the repo root is an unrelated untracked scratch script — never stage it. + +Report: baseline vs final test counts, coverage numbers for the five touched files, lint/build status. From 31a2291ea01764f05a2bfe28cf71259109db540a Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 13:19:56 +0200 Subject: [PATCH 03/16] feat(private-spaces): add PrivateSpaces endpoint with list() (ORB-1919) Co-Authored-By: Claude Fable 5 --- src/endpoints/private-spaces.ts | 117 ++++++++++++++++++++++++++++ src/make.ts | 9 +++ test/mocks/private-spaces/list.json | 30 +++++++ test/private-spaces.spec.ts | 52 +++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 src/endpoints/private-spaces.ts create mode 100644 test/mocks/private-spaces/list.json create mode 100644 test/private-spaces.spec.ts diff --git a/src/endpoints/private-spaces.ts b/src/endpoints/private-spaces.ts new file mode 100644 index 0000000..5d2adbd --- /dev/null +++ b/src/endpoints/private-spaces.ts @@ -0,0 +1,117 @@ +import type { FetchFunction, Pagination, PickColumns } from '../types.js'; + +/** + * Represents a private space in Make. + * A private space is a per-user personal workspace inside an organization where + * scenarios and related entities (including connections) stay visible only to the + * owner. Private spaces cannot be created or deleted through the API — they are + * provisioned automatically based on the organization's private-spaces settings. + */ +export type PrivateSpace = { + /** Unique identifier of the private space */ + id: number; + /** Name of the private space */ + name: string; + /** ID of the organization this private space belongs to */ + organizationId: number; + /** Whether Make global AI agents are enabled for the space */ + globalAgentsEnabled?: boolean; + /** Type of the underlying team; always `personal` for private spaces */ + type?: 'personal'; + /** Name of the space owner */ + privateSpaceOwnerName?: string; + /** Email of the space owner */ + privateSpaceOwnerEmail?: string; + /** User ID of the space owner */ + privateSpaceOwnerId?: number; + /** Maximum operations limit; null means unlimited */ + operationsLimit?: number | null; + /** Maximum data transfer limit in bytes; derived from the operations limit */ + transferLimit?: string | null; + /** Number of operations consumed in the current period */ + consumedOperations?: number | null; + /** Amount of data transfer consumed in the current period, in bytes */ + consumedTransfer?: string | null; + /** Whether the space is paused due to exceeded limits */ + isPaused?: boolean | null; + /** Number of centicredits consumed in the current period */ + consumedCenticredits?: number | null; + /** Total operations since the last reset; only selectable via `cols` on `get()` */ + operations?: string; + /** Total data transfer since the last reset, in bytes; only selectable via `cols` on `get()` */ + transfer?: string; + /** Total centicredits since the last reset; only selectable via `cols` on `get()` */ + centicredits?: string; + /** Whether the space is deleted; returned by `update()` */ + deleted?: boolean; + /** External identifier of the space; returned by `update()` */ + externalId?: string | null; +}; + +/** + * Options for listing private spaces. + * @template C Keys of the PrivateSpace type to include in the response + */ +export type ListPrivateSpacesOptions = { + /** Specific columns/fields to include in the response */ + cols?: C[] | ['*']; + /** Pagination options (the API supports sorting by `name` only) */ + pg?: Partial>; + /** Filter spaces by their external ID */ + externalId?: string; +}; + +/** + * Response format for listing private spaces. + */ +type ListPrivateSpacesResponse = { + /** List of private spaces matching the query */ + privateSpaces: PickColumns[]; + /** Pagination information */ + pg: Pagination; +}; + +/** + * Class providing methods for working with Make private spaces. + * Requires the organization's private-spaces feature to be enabled; the API + * responds with error IM903 when it is not. + */ +export class PrivateSpaces { + readonly #fetch: FetchFunction; + + /** + * Create a new PrivateSpaces instance. + * @param fetch Function for making API requests + */ + constructor(fetch: FetchFunction) { + this.#fetch = fetch; + } + + /** + * List private spaces of an organization. + * Requires the `personal team manage` organization permission. + * @param organizationId The organization ID to list private spaces for + * @param options Optional parameters for filtering and pagination + * @returns Promise with the list of private spaces + * + * @example + * ```typescript + * const spaces = await make.privateSpaces.list(123); + * ``` + */ + async list( + organizationId: number, + options?: ListPrivateSpacesOptions, + ): Promise[]> { + return ( + await this.#fetch>('/private-spaces', { + query: { + organizationId, + externalId: options?.externalId, + cols: options?.cols, + pg: options?.pg, + }, + }) + ).privateSpaces; + } +} diff --git a/src/make.ts b/src/make.ts index 1920184..a8ad417 100644 --- a/src/make.ts +++ b/src/make.ts @@ -18,6 +18,7 @@ import { Enums } from './endpoints/enums.js'; import { OnPremAgents } from './endpoints/on-prem-agents.js'; import { ConnectedSystems } from './endpoints/connected-systems.js'; import { PublicTemplates } from './endpoints/public-templates.js'; +import { PrivateSpaces } from './endpoints/private-spaces.js'; import { SDKApps } from './endpoints/sdk/apps.js'; import { SDKModules } from './endpoints/sdk/modules.js'; import { SDKConnections } from './endpoints/sdk/connections.js'; @@ -185,6 +186,13 @@ export class Make { */ public readonly publicTemplates: PublicTemplates; + /** + * Access to private space endpoints. + * Private spaces are per-user personal workspaces within an organization; they are + * provisioned automatically and cannot be created or deleted through the API. + */ + public readonly privateSpaces: PrivateSpaces; + /** * Access to SDK-related endpoints */ @@ -278,6 +286,7 @@ export class Make { this.connectedSystems = new ConnectedSystems(this.fetch.bind(this)); this.credentialRequests = new CredentialRequests(this.fetch.bind(this)); this.publicTemplates = new PublicTemplates(this.fetch.bind(this)); + this.privateSpaces = new PrivateSpaces(this.fetch.bind(this)); this.sdk = { apps: new SDKApps(this.fetch.bind(this)), modules: new SDKModules(this.fetch.bind(this)), diff --git a/test/mocks/private-spaces/list.json b/test/mocks/private-spaces/list.json new file mode 100644 index 0000000..dd6b3aa --- /dev/null +++ b/test/mocks/private-spaces/list.json @@ -0,0 +1,30 @@ +{ + "privateSpaces": [ + { + "id": 101, + "name": "Becca's space", + "organizationId": 5, + "globalAgentsEnabled": false, + "type": "personal", + "privateSpaceOwnerName": "Becca Smith", + "privateSpaceOwnerEmail": "becca.smith@example.com", + "privateSpaceOwnerId": 42 + }, + { + "id": 102, + "name": "Jan's space", + "organizationId": 5, + "globalAgentsEnabled": true, + "type": "personal", + "privateSpaceOwnerName": "Jan Novak", + "privateSpaceOwnerEmail": "jan.novak@example.com", + "privateSpaceOwnerId": 43 + } + ], + "pg": { + "sortBy": "name", + "sortDir": "asc", + "offset": 0, + "limit": 100 + } +} diff --git a/test/private-spaces.spec.ts b/test/private-spaces.spec.ts new file mode 100644 index 0000000..66b2d19 --- /dev/null +++ b/test/private-spaces.spec.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { mockFetch } from './test.utils.js'; +import type { PrivateSpace } from '../src/endpoints/private-spaces.js'; + +import * as privateSpacesListMock from './mocks/private-spaces/list.json'; + +const MAKE_API_KEY = 'api-key'; +const MAKE_ZONE = 'make.local'; + +describe('Endpoints: PrivateSpaces', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + it('Should list private spaces', async () => { + mockFetch('GET https://make.local/api/v2/private-spaces?organizationId=5', privateSpacesListMock); + + const result = await make.privateSpaces.list(5); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should list private spaces filtered by externalId with pagination', async () => { + mockFetch( + 'GET https://make.local/api/v2/private-spaces?organizationId=5&externalId=ext-1&pg%5BsortBy%5D=name&pg%5BsortDir%5D=asc', + privateSpacesListMock, + ); + + const result = await make.privateSpaces.list(5, { + externalId: 'ext-1', + pg: { + sortBy: 'name', + sortDir: 'asc', + }, + }); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should list private spaces with selected columns', async () => { + const cols: (keyof PrivateSpace)[] = ['id', 'name', 'isPaused']; + mockFetch( + `GET https://make.local/api/v2/private-spaces?organizationId=5&cols%5B%5D=${cols.join('&cols%5B%5D=')}`, + privateSpacesListMock, + ); + + const result = await make.privateSpaces.list(5, { + cols, + }); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); +}); From 57a691784191c8de557d571265aefe5b21618db9 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 13:26:10 +0200 Subject: [PATCH 04/16] feat(private-spaces): add get() with usage columns (ORB-1919) Co-Authored-By: Claude Fable 5 --- src/endpoints/private-spaces.ts | 48 ++++++++++++++++++++++++++++++ test/mocks/private-spaces/get.json | 21 +++++++++++++ test/private-spaces.spec.ts | 23 ++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 test/mocks/private-spaces/get.json diff --git a/src/endpoints/private-spaces.ts b/src/endpoints/private-spaces.ts index 5d2adbd..dd74399 100644 --- a/src/endpoints/private-spaces.ts +++ b/src/endpoints/private-spaces.ts @@ -61,6 +61,20 @@ export type ListPrivateSpacesOptions = { externalId?: string; }; +/** + * Options for retrieving a private space. + * @template C Keys of the PrivateSpace type to include in the response + */ +export type GetPrivateSpaceOptions = { + /** + * Specific columns/fields to include in the response. In addition to the list + * columns, `get()` supports the usage totals `operations`, `transfer` and + * `centicredits` (computed from analytics storage; the API responds with 503 + * when that storage is unavailable). + */ + cols?: C[] | ['*']; +}; + /** * Response format for listing private spaces. */ @@ -71,6 +85,14 @@ type ListPrivateSpacesResponse = { pg: Pagination; }; +/** + * Response format for getting a private space. + */ +type GetPrivateSpaceResponse = { + /** The requested private space */ + privateSpace: PickColumns; +}; + /** * Class providing methods for working with Make private spaces. * Requires the organization's private-spaces feature to be enabled; the API @@ -114,4 +136,30 @@ export class PrivateSpaces { }) ).privateSpaces; } + + /** + * Get details of a specific private space. + * Requires the `personal team own view` organization permission; callers who are + * not members of the space receive a 404 even when the space exists. + * @param privateSpaceId The private space ID to get + * @param options Optional parameters for filtering returned fields + * @returns Promise with the private space information + * + * @example + * ```typescript + * const space = await make.privateSpaces.get(101); + * ``` + */ + async get( + privateSpaceId: number, + options?: GetPrivateSpaceOptions, + ): Promise> { + return ( + await this.#fetch>(`/private-spaces/${privateSpaceId}`, { + query: { + cols: options?.cols, + }, + }) + ).privateSpace; + } } diff --git a/test/mocks/private-spaces/get.json b/test/mocks/private-spaces/get.json new file mode 100644 index 0000000..ae05c93 --- /dev/null +++ b/test/mocks/private-spaces/get.json @@ -0,0 +1,21 @@ +{ + "privateSpace": { + "id": 101, + "name": "Becca's space", + "organizationId": 5, + "globalAgentsEnabled": false, + "type": "personal", + "privateSpaceOwnerName": "Becca Smith", + "privateSpaceOwnerEmail": "becca.smith@example.com", + "privateSpaceOwnerId": 42, + "operationsLimit": 1000, + "transferLimit": "1073741824", + "consumedOperations": 250, + "consumedTransfer": "52428800", + "isPaused": false, + "consumedCenticredits": 12345, + "operations": "250", + "transfer": "52428800", + "centicredits": "12345" + } +} diff --git a/test/private-spaces.spec.ts b/test/private-spaces.spec.ts index 66b2d19..4579c4c 100644 --- a/test/private-spaces.spec.ts +++ b/test/private-spaces.spec.ts @@ -4,6 +4,7 @@ import { mockFetch } from './test.utils.js'; import type { PrivateSpace } from '../src/endpoints/private-spaces.js'; import * as privateSpacesListMock from './mocks/private-spaces/list.json'; +import * as privateSpaceGetMock from './mocks/private-spaces/get.json'; const MAKE_API_KEY = 'api-key'; const MAKE_ZONE = 'make.local'; @@ -49,4 +50,26 @@ describe('Endpoints: PrivateSpaces', () => { expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); }); + + it('Should get a private space', async () => { + mockFetch('GET https://make.local/api/v2/private-spaces/101', privateSpaceGetMock); + + const result = await make.privateSpaces.get(101); + + expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); + }); + + it('Should get a private space with usage columns', async () => { + const cols: (keyof PrivateSpace)[] = ['id', 'operations', 'transfer', 'centicredits']; + mockFetch( + `GET https://make.local/api/v2/private-spaces/101?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, + privateSpaceGetMock, + ); + + const result = await make.privateSpaces.get(101, { + cols, + }); + + expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); + }); }); From a075f6f47e50807f04c3a3ec730d27f9d7afe59e Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 13:32:20 +0200 Subject: [PATCH 05/16] feat(private-spaces): add update() with confirmed option (ORB-1919) Co-Authored-By: Claude Fable 5 --- src/endpoints/private-spaces.ts | 62 +++++++++++++++++++++++++++ test/mocks/private-spaces/update.json | 20 +++++++++ test/private-spaces.spec.ts | 30 +++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 test/mocks/private-spaces/update.json diff --git a/src/endpoints/private-spaces.ts b/src/endpoints/private-spaces.ts index dd74399..f22c3e9 100644 --- a/src/endpoints/private-spaces.ts +++ b/src/endpoints/private-spaces.ts @@ -75,6 +75,30 @@ export type GetPrivateSpaceOptions = { cols?: C[] | ['*']; }; +/** + * Body for updating a private space. + */ +export type UpdatePrivateSpaceBody = { + /** + * Maximum operations limit (minimum 0). Set to `null` to remove the limit + * (unlimited); omit to leave unchanged. The transfer limit is derived from + * this value by the API. + */ + operationsLimit?: number | null; +}; + +/** + * Options for updating a private space. + */ +export type UpdatePrivateSpaceOptions = { + /** + * Confirmation of the update. Required (the API fails with IM004 otherwise) + * when the new operations limit is below the space's current consumption; + * confirming pauses the space. + */ + confirmed?: boolean; +}; + /** * Response format for listing private spaces. */ @@ -93,6 +117,14 @@ type GetPrivateSpaceResponse = { privateSpace: PickColumns; }; +/** + * Response format for updating a private space. + */ +type UpdatePrivateSpaceResponse = { + /** The updated private space */ + privateSpace: PrivateSpace; +}; + /** * Class providing methods for working with Make private spaces. * Requires the organization's private-spaces feature to be enabled; the API @@ -162,4 +194,34 @@ export class PrivateSpaces { }) ).privateSpace; } + + /** + * Update a private space. + * Requires the `personal team manage` organization permission. + * @param privateSpaceId The private space ID to update + * @param body The fields to update + * @param options Optional update options + * @returns Promise with the updated private space + * + * @example + * ```typescript + * // Remove the operations limit + * const space = await make.privateSpaces.update(101, { operationsLimit: null }); + * ``` + */ + async update( + privateSpaceId: number, + body: UpdatePrivateSpaceBody, + options?: UpdatePrivateSpaceOptions, + ): Promise { + return ( + await this.#fetch(`/private-spaces/${privateSpaceId}`, { + method: 'PATCH', + query: { + confirmed: options?.confirmed, + }, + body, + }) + ).privateSpace; + } } diff --git a/test/mocks/private-spaces/update.json b/test/mocks/private-spaces/update.json new file mode 100644 index 0000000..f07a818 --- /dev/null +++ b/test/mocks/private-spaces/update.json @@ -0,0 +1,20 @@ +{ + "privateSpace": { + "id": 101, + "name": "Becca's space", + "organizationId": 5, + "globalAgentsEnabled": false, + "type": "personal", + "privateSpaceOwnerName": "Becca Smith", + "privateSpaceOwnerEmail": "becca.smith@example.com", + "privateSpaceOwnerId": 42, + "operationsLimit": 100, + "transferLimit": "107374182", + "consumedOperations": 250, + "consumedTransfer": "52428800", + "isPaused": true, + "consumedCenticredits": 12345, + "deleted": false, + "externalId": null + } +} diff --git a/test/private-spaces.spec.ts b/test/private-spaces.spec.ts index 4579c4c..8cfbb5b 100644 --- a/test/private-spaces.spec.ts +++ b/test/private-spaces.spec.ts @@ -5,6 +5,7 @@ import type { PrivateSpace } from '../src/endpoints/private-spaces.js'; import * as privateSpacesListMock from './mocks/private-spaces/list.json'; import * as privateSpaceGetMock from './mocks/private-spaces/get.json'; +import * as privateSpaceUpdateMock from './mocks/private-spaces/update.json'; const MAKE_API_KEY = 'api-key'; const MAKE_ZONE = 'make.local'; @@ -72,4 +73,33 @@ describe('Endpoints: PrivateSpaces', () => { expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); }); + + it('Should update a private space', async () => { + const body = { + operationsLimit: 100, + }; + + mockFetch('PATCH https://make.local/api/v2/private-spaces/101', privateSpaceUpdateMock, req => { + expect(req.body).toStrictEqual(body); + expect(req.headers.get('content-type')).toBe('application/json'); + }); + + const result = await make.privateSpaces.update(101, body); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + }); + + it('Should update a private space with confirmation and null limit', async () => { + const body = { + operationsLimit: null, + }; + + mockFetch('PATCH https://make.local/api/v2/private-spaces/101?confirmed=true', privateSpaceUpdateMock, req => { + expect(req.body).toStrictEqual(body); + }); + + const result = await make.privateSpaces.update(101, body, { confirmed: true }); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + }); }); From 84d15723bf67fd3c203547d15b589572e0e06413 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 13:37:45 +0200 Subject: [PATCH 06/16] feat(organizations): add privateSpaces column to Organization type (ORB-1919) Co-Authored-By: Claude Fable 5 --- src/endpoints/organizations.ts | 14 ++++++++++++++ test/mocks/organizations/get.json | 8 ++++++++ test/organizations.spec.ts | 14 ++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/src/endpoints/organizations.ts b/src/endpoints/organizations.ts index ee19eae..826bc7e 100644 --- a/src/endpoints/organizations.ts +++ b/src/endpoints/organizations.ts @@ -99,6 +99,20 @@ export type Organization = { /** Webhook log retention period in days */ webhookLogRetentionDays?: number; }; + /** + * Private spaces the requesting user is a member of within this organization. + * Only returned when requested via `cols`; available on public cloud only. + */ + privateSpaces?: { + /** Unique identifier of the private space */ + id: number; + /** Name of the private space */ + name: string; + /** Whether the requesting user owns the space */ + isOwner: boolean; + /** Whether org admins can see into the space (mirrors the organization's "add admins as observers" setting) */ + hasAdminVisibility: boolean; + }[]; /** The name of the subscription for the organization */ serviceName: string; /** Teams in the organization */ diff --git a/test/mocks/organizations/get.json b/test/mocks/organizations/get.json index d446caa..93a0b71 100644 --- a/test/mocks/organizations/get.json +++ b/test/mocks/organizations/get.json @@ -33,6 +33,14 @@ "id": 16, "name": "My Team" } + ], + "privateSpaces": [ + { + "id": 101, + "name": "Becca's space", + "isOwner": true, + "hasAdminVisibility": false + } ] } } diff --git a/test/organizations.spec.ts b/test/organizations.spec.ts index eed27d6..8001503 100644 --- a/test/organizations.spec.ts +++ b/test/organizations.spec.ts @@ -96,4 +96,18 @@ describe('Endpoints: Organizations', () => { await make.organizations.delete(organizationId); }); + + it('Should list organizations with the privateSpaces column', async () => { + const cols: (keyof Organization)[] = ['id', 'name', 'privateSpaces']; + mockFetch( + `GET https://make.local/api/v2/organizations?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, + organizationsListMock, + ); + + const result = await make.organizations.list({ + cols, + }); + + expect(result).toStrictEqual(organizationsListMock.organizations); + }); }); From aea5e342cdcc7cbcd0e46e46bdc218dec7e7667b Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 13:42:19 +0200 Subject: [PATCH 07/16] feat(private-spaces): export public types and document endpoint (ORB-1919) Co-Authored-By: Claude Fable 5 --- README.md | 1 + src/index.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/README.md b/README.md index 3e2621d..0aba30a 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ const make = new Make('your-api-key', 'eu2.make.com', { - **Keys** - API keys and secrets - **On-Prem Agents** - On-prem bridge agents running on customer infrastructure - **Organizations** - Top-level account and billing management +- **Private Spaces** - Per-user private workspaces within an organization (list, get, update) - **Scenarios** - Scenario management - **Teams** - Team management and collaboration - **Public Templates** - Public template discovery and blueprint export (read-only) diff --git a/src/index.ts b/src/index.ts index b414cec..1759e96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -199,4 +199,12 @@ export type { ListPublicTemplatesOptions, GetPublicTemplateOptions, } from './endpoints/public-templates.js'; +export type { + PrivateSpace, + PrivateSpaces, + ListPrivateSpacesOptions, + GetPrivateSpaceOptions, + UpdatePrivateSpaceBody, + UpdatePrivateSpaceOptions, +} from './endpoints/private-spaces.js'; export type { User, Users } from './endpoints/users.js'; From 5f01c44d74d8ef2e2801445e9184126eff32d4d3 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 13:50:52 +0200 Subject: [PATCH 08/16] feat(private-spaces): add tool definitions (ORB-1919) Co-Authored-By: Claude Fable 5 --- README.md | 1 + src/endpoints/private-spaces.tools.ts | 102 ++++++++++++++++++++++++++ src/tools.ts | 2 + test/private-spaces-tools.spec.ts | 68 +++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 src/endpoints/private-spaces.tools.ts create mode 100644 test/private-spaces-tools.spec.ts diff --git a/README.md b/README.md index 0aba30a..8839376 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,7 @@ All tools are organized into the following categories: - `keys` - `on-prem-agent` - `organizations` +- `private-spaces` - `scenarios` - `teams` - `public-templates` diff --git a/src/endpoints/private-spaces.tools.ts b/src/endpoints/private-spaces.tools.ts new file mode 100644 index 0000000..d61bb5c --- /dev/null +++ b/src/endpoints/private-spaces.tools.ts @@ -0,0 +1,102 @@ +import type { Make } from '../make.js'; +import type { MakeTool } from '../tools.js'; + +export const tools: MakeTool[] = [ + { + name: 'private-spaces_list', + title: 'List private spaces', + description: + "List the private spaces of an organization. Requires the organization's private-spaces feature to be enabled (error IM903 otherwise) and the 'personal team manage' permission. Private spaces cannot be created or deleted through the API — they are provisioned automatically by organization settings.", + category: 'private-spaces', + scope: 'private-spaces:read', + scopeId: 'organizationId', + identifier: 'organizationId', + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID to list private spaces for' }, + externalId: { type: 'string', description: 'Filter private spaces by their external ID' }, + }, + required: ['organizationId'], + }, + examples: [{ organizationId: 5 }, { organizationId: 5, externalId: 'ext-1' }], + execute: async (make: Make, args: { organizationId: number; externalId?: string }) => { + const { organizationId, ...options } = args; + return await make.privateSpaces.list(organizationId, { ...options, cols: ['*'] }); + }, + }, + { + name: 'private-spaces_get', + title: 'Get private space', + description: + 'Get details of a specific private space, including usage totals (operations, transfer, centicredits). Callers who are not members of the space receive a 404 even when the space exists.', + category: 'private-spaces', + scope: 'private-spaces:read', + scopeId: 'privateSpaceId', + identifier: 'privateSpaceId', + resourceId: 'privateSpaceId', + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + privateSpaceId: { type: 'number', description: 'The private space ID to retrieve' }, + }, + required: ['privateSpaceId'], + }, + examples: [{ privateSpaceId: 101 }], + execute: async (make: Make, args: { privateSpaceId: number }) => { + return await make.privateSpaces.get(args.privateSpaceId, { cols: ['*'] }); + }, + }, + { + name: 'private-spaces_update', + title: 'Update private space', + description: + "Update a private space's operations limit. Set operationsLimit to null to remove the limit (unlimited); the transfer limit is derived automatically. When the new limit is below the space's current consumption the call fails with IM004 unless 'confirmed' is true — confirming pauses the space.", + category: 'private-spaces', + scope: 'private-spaces:write', + scopeId: 'privateSpaceId', + identifier: 'privateSpaceId', + resourceId: 'privateSpaceId', + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + privateSpaceId: { type: 'number', description: 'The private space ID to update' }, + operationsLimit: { + oneOf: [{ type: 'number' }, { type: 'null' }], + description: + 'Maximum operations limit (minimum 0). Pass null to remove the limit; omit to leave unchanged.', + }, + confirmed: { + type: 'boolean', + description: + "Confirmation of the update. Required when the new limit is below the space's current consumption; confirming pauses the space.", + }, + }, + required: ['privateSpaceId'], + }, + examples: [{ privateSpaceId: 101, operationsLimit: 10000 }, { privateSpaceId: 101, operationsLimit: null, confirmed: true }], + execute: async ( + make: Make, + args: { privateSpaceId: number; operationsLimit?: number | null; confirmed?: boolean }, + ) => { + const { privateSpaceId, confirmed, ...body } = args; + return await make.privateSpaces.update(privateSpaceId, body, { confirmed }); + }, + }, +]; diff --git a/src/tools.ts b/src/tools.ts index cd0ae82..5fc31ab 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -29,6 +29,7 @@ import { tools as EnumsTools } from './endpoints/enums.tools.js'; import { tools as OnPremAgentsTools } from './endpoints/on-prem-agents.tools.js'; import { tools as ConnectedSystemsTools } from './endpoints/connected-systems.tools.js'; import { tools as PublicTemplatesTools } from './endpoints/public-templates.tools.js'; +import { tools as PrivateSpacesTools } from './endpoints/private-spaces.tools.js'; /** * JSON Schema definition for input parameters. @@ -231,4 +232,5 @@ export const MakeTools = [ ...ConnectedSystemsTools, ...EnumsTools, ...PublicTemplatesTools, + ...PrivateSpacesTools, ] as MakeTool[]; diff --git a/test/private-spaces-tools.spec.ts b/test/private-spaces-tools.spec.ts new file mode 100644 index 0000000..4307b72 --- /dev/null +++ b/test/private-spaces-tools.spec.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { MakeTools } from '../src/tools.js'; +import { mockFetch } from './test.utils.js'; + +import * as privateSpacesListMock from './mocks/private-spaces/list.json'; +import * as privateSpaceGetMock from './mocks/private-spaces/get.json'; +import * as privateSpaceUpdateMock from './mocks/private-spaces/update.json'; + +const MAKE_API_KEY = 'api-key'; +const MAKE_ZONE = 'make.local'; +const ORGANIZATION_ID = 5; +const PRIVATE_SPACE_ID = 101; + +function getTool(name: string) { + const tool = MakeTools.find(entry => entry.name === name); + if (!tool) { + throw new Error(`Missing MCP tool: ${name}`); + } + return tool; +} + +describe('MCP tools: private-spaces', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + it('Should execute private-spaces_list', async () => { + mockFetch( + `GET https://make.local/api/v2/private-spaces?organizationId=${ORGANIZATION_ID}&cols%5B%5D=*`, + privateSpacesListMock, + ); + + const tool = getTool('private-spaces_list'); + const result = await tool.execute(make, { organizationId: ORGANIZATION_ID }); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should execute private-spaces_get', async () => { + mockFetch( + `GET https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?cols%5B%5D=*`, + privateSpaceGetMock, + ); + + const tool = getTool('private-spaces_get'); + const result = await tool.execute(make, { privateSpaceId: PRIVATE_SPACE_ID }); + + expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); + }); + + it('Should execute private-spaces_update', async () => { + mockFetch( + `PATCH https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?confirmed=true`, + privateSpaceUpdateMock, + req => { + expect(req.body).toStrictEqual({ operationsLimit: 100 }); + }, + ); + + const tool = getTool('private-spaces_update'); + const result = await tool.execute(make, { + privateSpaceId: PRIVATE_SPACE_ID, + operationsLimit: 100, + confirmed: true, + }); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + }); +}); From 45bc71195ecdafc976039323201c7a9f75a463ef Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 13:56:15 +0200 Subject: [PATCH 09/16] test(private-spaces): add integration tests (ORB-1919) Co-Authored-By: Claude Fable 5 --- test/private-spaces.integration.test.ts | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 test/private-spaces.integration.test.ts diff --git a/test/private-spaces.integration.test.ts b/test/private-spaces.integration.test.ts new file mode 100644 index 0000000..034b04d --- /dev/null +++ b/test/private-spaces.integration.test.ts @@ -0,0 +1,56 @@ +import 'dotenv/config'; +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; + +const MAKE_API_KEY = String(process.env.MAKE_API_KEY || ''); +const MAKE_ZONE = String(process.env.MAKE_ZONE || ''); +const MAKE_ORGANIZATION = Number(process.env.MAKE_ORGANIZATION || 0); + +describe('Integration: PrivateSpaces', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + let privateSpaceId: number | undefined; + let originalOperationsLimit: number | null | undefined; + let consumedOperations: number | null | undefined; + + it('Should list private spaces', async () => { + const spaces = await make.privateSpaces.list(MAKE_ORGANIZATION); + + expect(Array.isArray(spaces)).toBe(true); + + // No public API can provision a private space, so downstream tests are + // skipped (early return) when the organization has none. + privateSpaceId = spaces[0]?.id; + }); + + it('Should get a private space', async () => { + if (privateSpaceId === undefined) return; + + const space = await make.privateSpaces.get(privateSpaceId, { cols: ['*'] }); + + expect(space.id).toBe(privateSpaceId); + expect(space.organizationId).toBe(MAKE_ORGANIZATION); + expect(space.type).toBe('personal'); + + originalOperationsLimit = space.operationsLimit; + consumedOperations = space.consumedOperations; + }); + + it('Should update a private space and restore the original limit', async () => { + if (privateSpaceId === undefined) return; + + // Stay above current consumption so the update needs no confirmation + // and cannot pause the space. + const safeLimit = Math.max(consumedOperations ?? 0, originalOperationsLimit ?? 0) + 10000; + + const updated = await make.privateSpaces.update(privateSpaceId, { operationsLimit: safeLimit }); + expect(updated.operationsLimit).toBe(safeLimit); + + const restored = await make.privateSpaces.update( + privateSpaceId, + { operationsLimit: originalOperationsLimit ?? null }, + { confirmed: true }, + ); + expect(restored.operationsLimit).toBe(originalOperationsLimit ?? null); + }); +}); From bd8afd47ba86cfb2200c7f37e6f2d1d6508faa52 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 14:08:58 +0200 Subject: [PATCH 10/16] chore(private-spaces): formatting and verification follow-ups (ORB-1919) Co-Authored-By: Claude Fable 5 --- plans/2026-07-27-private-spaces.md | 138 ++++++++++++---------- specs/2026-07-27-private-spaces-design.md | 60 +++++----- src/endpoints/private-spaces.tools.ts | 5 +- test/private-spaces-tools.spec.ts | 5 +- 4 files changed, 111 insertions(+), 97 deletions(-) diff --git a/plans/2026-07-27-private-spaces.md b/plans/2026-07-27-private-spaces.md index 7734fa8..e23a570 100644 --- a/plans/2026-07-27-private-spaces.md +++ b/plans/2026-07-27-private-spaces.md @@ -30,12 +30,14 @@ ### Task 1: `PrivateSpace` type, `PrivateSpaces.list()`, client registration **Files:** + - Create: `src/endpoints/private-spaces.ts` - Create: `test/mocks/private-spaces/list.json` - Create: `test/private-spaces.spec.ts` - Modify: `src/make.ts` (import ~line 20, property ~line 186, constructor ~line 281) **Interfaces:** + - Consumes: `FetchFunction`, `Pagination`, `PickColumns` from `src/types.js` (existing). - Produces: `PrivateSpace` type; `ListPrivateSpacesOptions`; class `PrivateSpaces` with `list(organizationId: number, options?: ListPrivateSpacesOptions): Promise[]>`; `make.privateSpaces: PrivateSpaces` on the `Make` client. Tasks 2–7 rely on all of these names exactly. @@ -149,7 +151,7 @@ Expected: FAIL — cannot find module `'../src/endpoints/private-spaces.js'` (th `src/endpoints/private-spaces.ts`: -```typescript +````typescript import type { FetchFunction, Pagination, PickColumns } from '../types.js'; /** @@ -267,7 +269,7 @@ export class PrivateSpaces { ).privateSpaces; } } -``` +```` - [ ] **Step 6: Register the endpoint on the `Make` client** @@ -320,11 +322,13 @@ Co-Authored-By: Claude Fable 5 " ### Task 2: `PrivateSpaces.get()` **Files:** + - Create: `test/mocks/private-spaces/get.json` - Modify: `src/endpoints/private-spaces.ts` (add `GetPrivateSpaceOptions`, `GetPrivateSpaceResponse`, `get()`) - Modify: `test/private-spaces.spec.ts` (extend the existing describe block) **Interfaces:** + - Consumes: `PrivateSpace`, `PrivateSpaces` class from Task 1. - Produces: `GetPrivateSpaceOptions`; `get(privateSpaceId: number, options?: GetPrivateSpaceOptions): Promise>`. Tasks 6–7 call `make.privateSpaces.get(...)` with this exact signature. @@ -365,27 +369,27 @@ import * as privateSpaceGetMock from './mocks/private-spaces/get.json'; ``` ```typescript - it('Should get a private space', async () => { - mockFetch('GET https://make.local/api/v2/private-spaces/101', privateSpaceGetMock); +it('Should get a private space', async () => { + mockFetch('GET https://make.local/api/v2/private-spaces/101', privateSpaceGetMock); - const result = await make.privateSpaces.get(101); + const result = await make.privateSpaces.get(101); - expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); - }); + expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); +}); - it('Should get a private space with usage columns', async () => { - const cols: (keyof PrivateSpace)[] = ['id', 'operations', 'transfer', 'centicredits']; - mockFetch( - `GET https://make.local/api/v2/private-spaces/101?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, - privateSpaceGetMock, - ); +it('Should get a private space with usage columns', async () => { + const cols: (keyof PrivateSpace)[] = ['id', 'operations', 'transfer', 'centicredits']; + mockFetch( + `GET https://make.local/api/v2/private-spaces/101?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, + privateSpaceGetMock, + ); - const result = await make.privateSpaces.get(101, { - cols, - }); - - expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); + const result = await make.privateSpaces.get(101, { + cols, }); + + expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); +}); ``` - [ ] **Step 3: Run the tests to verify they fail** @@ -427,7 +431,7 @@ type GetPrivateSpaceResponse = { Add to the `PrivateSpaces` class after `list()`: -```typescript +````typescript /** * Get details of a specific private space. * Requires the `personal team own view` organization permission; callers who are @@ -453,7 +457,7 @@ Add to the `PrivateSpaces` class after `list()`: }) ).privateSpace; } -``` +```` - [ ] **Step 5: Run the tests to verify they pass** @@ -474,11 +478,13 @@ Co-Authored-By: Claude Fable 5 " ### Task 3: `PrivateSpaces.update()` **Files:** + - Create: `test/mocks/private-spaces/update.json` - Modify: `src/endpoints/private-spaces.ts` (add `UpdatePrivateSpaceBody`, `UpdatePrivateSpaceOptions`, `UpdatePrivateSpaceResponse`, `update()`) - Modify: `test/private-spaces.spec.ts` **Interfaces:** + - Consumes: `PrivateSpace`, `PrivateSpaces` class from Tasks 1–2. - Produces: `UpdatePrivateSpaceBody = { operationsLimit?: number | null }`; `UpdatePrivateSpaceOptions = { confirmed?: boolean }`; `update(privateSpaceId: number, body: UpdatePrivateSpaceBody, options?: UpdatePrivateSpaceOptions): Promise`. Tasks 6–7 call `make.privateSpaces.update(...)` with this exact signature. @@ -520,34 +526,34 @@ import * as privateSpaceUpdateMock from './mocks/private-spaces/update.json'; Add to the describe block: ```typescript - it('Should update a private space', async () => { - const body = { - operationsLimit: 100, - }; - - mockFetch('PATCH https://make.local/api/v2/private-spaces/101', privateSpaceUpdateMock, req => { - expect(req.body).toStrictEqual(body); - expect(req.headers.get('content-type')).toBe('application/json'); - }); - - const result = await make.privateSpaces.update(101, body); - - expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); +it('Should update a private space', async () => { + const body = { + operationsLimit: 100, + }; + + mockFetch('PATCH https://make.local/api/v2/private-spaces/101', privateSpaceUpdateMock, req => { + expect(req.body).toStrictEqual(body); + expect(req.headers.get('content-type')).toBe('application/json'); }); - it('Should update a private space with confirmation and null limit', async () => { - const body = { - operationsLimit: null, - }; + const result = await make.privateSpaces.update(101, body); - mockFetch('PATCH https://make.local/api/v2/private-spaces/101?confirmed=true', privateSpaceUpdateMock, req => { - expect(req.body).toStrictEqual(body); - }); + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); +}); - const result = await make.privateSpaces.update(101, body, { confirmed: true }); +it('Should update a private space with confirmation and null limit', async () => { + const body = { + operationsLimit: null, + }; - expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + mockFetch('PATCH https://make.local/api/v2/private-spaces/101?confirmed=true', privateSpaceUpdateMock, req => { + expect(req.body).toStrictEqual(body); }); + + const result = await make.privateSpaces.update(101, body, { confirmed: true }); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); +}); ``` - [ ] **Step 3: Run the tests to verify they fail** @@ -599,7 +605,7 @@ type UpdatePrivateSpaceResponse = { Add to the `PrivateSpaces` class after `get()`: -```typescript +````typescript /** * Update a private space. * Requires the `personal team manage` organization permission. @@ -629,7 +635,7 @@ Add to the `PrivateSpaces` class after `get()`: }) ).privateSpace; } -``` +```` - [ ] **Step 5: Run the tests to verify they pass** @@ -650,11 +656,13 @@ Co-Authored-By: Claude Fable 5 " ### Task 4: `Organization.privateSpaces` field **Files:** + - Modify: `src/endpoints/organizations.ts` (add field to the `Organization` type, after the `license` block ends, ~line 100+ — place it with the other optional top-level fields) - Modify: `test/organizations.spec.ts` (extend the existing describe block) - Modify: `test/mocks/organizations/get.json` (add `privateSpaces` to the organization object) **Interfaces:** + - Consumes: existing `Organization` type and `organizations.list()` / `organizations.get()`. - Produces: `Organization.privateSpaces?: { id: number; name: string; isOwner: boolean; hasAdminVisibility: boolean }[]` — selectable via `cols` because option types use `keyof Organization`. @@ -663,19 +671,19 @@ Co-Authored-By: Claude Fable 5 " Add to the describe block in `test/organizations.spec.ts`: ```typescript - it('Should list organizations with the privateSpaces column', async () => { - const cols: (keyof Organization)[] = ['id', 'name', 'privateSpaces']; - mockFetch( - `GET https://make.local/api/v2/organizations?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, - organizationsListMock, - ); - - const result = await make.organizations.list({ - cols, - }); - - expect(result).toStrictEqual(organizationsListMock.organizations); +it('Should list organizations with the privateSpaces column', async () => { + const cols: (keyof Organization)[] = ['id', 'name', 'privateSpaces']; + mockFetch( + `GET https://make.local/api/v2/organizations?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, + organizationsListMock, + ); + + const result = await make.organizations.list({ + cols, }); + + expect(result).toStrictEqual(organizationsListMock.organizations); +}); ``` - [ ] **Step 2: Run the test to verify it fails** @@ -738,10 +746,12 @@ Co-Authored-By: Claude Fable 5 " ### Task 5: Public exports and README endpoint list **Files:** + - Modify: `src/index.ts` (after the `public-templates.js` export block, ~line 202) - Modify: `README.md` (endpoint list, ~line 66) **Interfaces:** + - Consumes: all types from Tasks 1–3. - Produces: package-level exports `PrivateSpace`, `PrivateSpaces`, `ListPrivateSpacesOptions`, `GetPrivateSpaceOptions`, `UpdatePrivateSpaceBody`, `UpdatePrivateSpaceOptions`. @@ -787,12 +797,14 @@ Co-Authored-By: Claude Fable 5 " ### Task 6: Tool definitions **Files:** + - Create: `src/endpoints/private-spaces.tools.ts` - Create: `test/private-spaces-tools.spec.ts` (mirrors `test/on-prem-tools.spec.ts`) - Modify: `src/tools.ts` (import after `PublicTemplatesTools` import ~line 31; spread after `...PublicTemplatesTools,` ~line 233) - Modify: `README.md` (tool categories list, ~line 214) **Interfaces:** + - Consumes: `make.privateSpaces.list/get/update` exactly as produced by Tasks 1–3; `MakeTool` type and `MakeTools` array from `src/tools.js`. - Produces: tools `private-spaces_list`, `private-spaces_get`, `private-spaces_update` registered in `MakeTools`. @@ -839,10 +851,7 @@ describe('MCP tools: private-spaces', () => { }); it('Should execute private-spaces_get', async () => { - mockFetch( - `GET https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?cols%5B%5D=*`, - privateSpaceGetMock, - ); + mockFetch(`GET https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?cols%5B%5D=*`, privateSpaceGetMock); const tool = getTool('private-spaces_get'); const result = await tool.execute(make, { privateSpaceId: PRIVATE_SPACE_ID }); @@ -973,7 +982,10 @@ export const tools: MakeTool[] = [ }, required: ['privateSpaceId'], }, - examples: [{ privateSpaceId: 101, operationsLimit: 10000 }, { privateSpaceId: 101, operationsLimit: null, confirmed: true }], + examples: [ + { privateSpaceId: 101, operationsLimit: 10000 }, + { privateSpaceId: 101, operationsLimit: null, confirmed: true }, + ], execute: async ( make: Make, args: { privateSpaceId: number; operationsLimit?: number | null; confirmed?: boolean }, @@ -1006,7 +1018,7 @@ Expected: PASS (3 tests). - [ ] **Step 6: Add the README tool category** -In `README.md`, in the tool categories list, insert between `- \`organizations\`` and `- \`scenarios\``: +In `README.md`, in the tool categories list, insert between `- \`organizations\``and`- \`scenarios\``: ```markdown - `private-spaces` @@ -1031,9 +1043,11 @@ Co-Authored-By: Claude Fable 5 " ### Task 7: Integration test **Files:** + - Create: `test/private-spaces.integration.test.ts` **Interfaces:** + - Consumes: `make.privateSpaces.list/get/update` from Tasks 1–3; env vars `MAKE_API_KEY`, `MAKE_ZONE`, `MAKE_ORGANIZATION` from `.env`. - Produces: nothing consumed later. diff --git a/specs/2026-07-27-private-spaces-design.md b/specs/2026-07-27-private-spaces-design.md index 73a2d5b..d5b7683 100644 --- a/specs/2026-07-27-private-spaces-design.md +++ b/specs/2026-07-27-private-spaces-design.md @@ -68,27 +68,27 @@ Requires org permission `personal team manage`. ### PrivateSpace fields -| Field | Type | Availability | -|---|---|---| -| `id` | number | default col | -| `name` | string | default col | -| `organizationId` | number | default col | -| `globalAgentsEnabled` | boolean | default col | -| `type` | `'personal'` | default col | -| `privateSpaceOwnerName` | string | default col | -| `privateSpaceOwnerEmail` | string | default col | -| `privateSpaceOwnerId` | number | default col | -| `operationsLimit` | number \| null | cols; null = unlimited | -| `transferLimit` | string \| null | cols; bytes | -| `consumedOperations` | number \| null | cols | -| `consumedTransfer` | string \| null | cols | -| `isPaused` | boolean \| null | cols; paused due to exceeded limits | -| `consumedCenticredits` | number \| null | cols | -| `operations` | string | `get()` cols only (ES totals) | -| `transfer` | string | `get()` cols only (ES totals) | -| `centicredits` | string | `get()` cols only (ES totals) | -| `deleted` | boolean | admin col; present in PATCH response | -| `externalId` | string \| null | admin col; present in PATCH response | +| Field | Type | Availability | +| ------------------------ | --------------- | ------------------------------------ | +| `id` | number | default col | +| `name` | string | default col | +| `organizationId` | number | default col | +| `globalAgentsEnabled` | boolean | default col | +| `type` | `'personal'` | default col | +| `privateSpaceOwnerName` | string | default col | +| `privateSpaceOwnerEmail` | string | default col | +| `privateSpaceOwnerId` | number | default col | +| `operationsLimit` | number \| null | cols; null = unlimited | +| `transferLimit` | string \| null | cols; bytes | +| `consumedOperations` | number \| null | cols | +| `consumedTransfer` | string \| null | cols | +| `isPaused` | boolean \| null | cols; paused due to exceeded limits | +| `consumedCenticredits` | number \| null | cols | +| `operations` | string | `get()` cols only (ES totals) | +| `transfer` | string | `get()` cols only (ES totals) | +| `centicredits` | string | `get()` cols only (ES totals) | +| `deleted` | boolean | admin col; present in PATCH response | +| `externalId` | string \| null | admin col; present in PATCH response | ## Design @@ -130,10 +130,10 @@ trap, the 404-for-non-members behavior, and null-vs-omitted `operationsLimit` se Category `private-spaces`. All tools set explicit `readOnlyHint` / `destructiveHint` / `openWorldHint` and document state traps in descriptions (repo convention since WM-4172). -| Tool | Scope | scopeId / resourceId | Hints | Notes | -|---|---|---|---|---| -| `private-spaces_list` | `private-spaces:read` | `organizationId` / — | read-only | Params: `organizationId` (required), `externalId`. Description: requires the org's private-spaces feature and `personal team manage` permission. Executes with `cols: ['*']`. | -| `private-spaces_get` | `private-spaces:read` | `privateSpaceId` / `privateSpaceId` | read-only | Description: non-members receive 404; usage totals (`operations`, `transfer`, `centicredits`) come from analytics storage. Executes with `cols: ['*']`. | +| Tool | Scope | scopeId / resourceId | Hints | Notes | +| ----------------------- | ---------------------- | ----------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `private-spaces_list` | `private-spaces:read` | `organizationId` / — | read-only | Params: `organizationId` (required), `externalId`. Description: requires the org's private-spaces feature and `personal team manage` permission. Executes with `cols: ['*']`. | +| `private-spaces_get` | `private-spaces:read` | `privateSpaceId` / `privateSpaceId` | read-only | Description: non-members receive 404; usage totals (`operations`, `transfer`, `centicredits`) come from analytics storage. Executes with `cols: ['*']`. | | `private-spaces_update` | `private-spaces:write` | `privateSpaceId` / `privateSpaceId` | not read-only, not destructive, idempotent | Params: `privateSpaceId` (required), `operationsLimit` (`type: ['number', 'null']`, null = unlimited), `confirmed` (boolean; description: required when lowering the limit below current consumption — confirming pauses the space). | ### 3. `src/endpoints/organizations.ts` @@ -164,11 +164,11 @@ TDD throughout (red → green per behavior). - `test/private-spaces.spec.ts` + `test/mocks/private-spaces/{list,get,update}.json` (realistic data matching the field table above): - - list: response unwrapping; query assertion for `organizationId` and `externalId`. - - list: column selection (`cols`) round-trip. - - get: response unwrapping; get-only usage cols present in mock. - - update: body assertion (`operationsLimit`, including `null`), `confirmed=true` in - query, `content-type: application/json`. + - list: response unwrapping; query assertion for `organizationId` and `externalId`. + - list: column selection (`cols`) round-trip. + - get: response unwrapping; get-only usage cols present in mock. + - update: body assertion (`operationsLimit`, including `null`), `confirmed=true` in + query, `content-type: application/json`. - `test/organizations.spec.ts` + mock: extend get mock with `privateSpaces` and assert it round-trips. - `test/private-spaces.integration.test.ts`: lists spaces for `MAKE_ORGANIZATION`; diff --git a/src/endpoints/private-spaces.tools.ts b/src/endpoints/private-spaces.tools.ts index d61bb5c..0bae7bc 100644 --- a/src/endpoints/private-spaces.tools.ts +++ b/src/endpoints/private-spaces.tools.ts @@ -90,7 +90,10 @@ export const tools: MakeTool[] = [ }, required: ['privateSpaceId'], }, - examples: [{ privateSpaceId: 101, operationsLimit: 10000 }, { privateSpaceId: 101, operationsLimit: null, confirmed: true }], + examples: [ + { privateSpaceId: 101, operationsLimit: 10000 }, + { privateSpaceId: 101, operationsLimit: null, confirmed: true }, + ], execute: async ( make: Make, args: { privateSpaceId: number; operationsLimit?: number | null; confirmed?: boolean }, diff --git a/test/private-spaces-tools.spec.ts b/test/private-spaces-tools.spec.ts index 4307b72..b4735e0 100644 --- a/test/private-spaces-tools.spec.ts +++ b/test/private-spaces-tools.spec.ts @@ -36,10 +36,7 @@ describe('MCP tools: private-spaces', () => { }); it('Should execute private-spaces_get', async () => { - mockFetch( - `GET https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?cols%5B%5D=*`, - privateSpaceGetMock, - ); + mockFetch(`GET https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?cols%5B%5D=*`, privateSpaceGetMock); const tool = getTool('private-spaces_get'); const result = await tool.execute(make, { privateSpaceId: PRIVATE_SPACE_ID }); From dd66ec8b7a04f9caa037cdbf3cee580e0ee26c6d Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 14:25:20 +0200 Subject: [PATCH 11/16] test(private-spaces): skip restore when original limit was never observed (ORB-1919) Co-Authored-By: Claude Fable 5 --- test/private-spaces.integration.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/private-spaces.integration.test.ts b/test/private-spaces.integration.test.ts index 034b04d..433ba94 100644 --- a/test/private-spaces.integration.test.ts +++ b/test/private-spaces.integration.test.ts @@ -39,6 +39,9 @@ describe('Integration: PrivateSpaces', () => { it('Should update a private space and restore the original limit', async () => { if (privateSpaceId === undefined) return; + // get() never ran or failed — restoring would overwrite an unobserved limit. + if (originalOperationsLimit === undefined && consumedOperations === undefined) return; + // Stay above current consumption so the update needs no confirmation // and cannot pause the space. const safeLimit = Math.max(consumedOperations ?? 0, originalOperationsLimit ?? 0) + 10000; From 6fe92c5ec433c3db89dd02ce7270001defcfcf51 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 14:25:22 +0200 Subject: [PATCH 12/16] docs(private-spaces): correct nullable schema note in spec (ORB-1919) Co-Authored-By: Claude Fable 5 --- specs/2026-07-27-private-spaces-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/2026-07-27-private-spaces-design.md b/specs/2026-07-27-private-spaces-design.md index d5b7683..bc20f50 100644 --- a/specs/2026-07-27-private-spaces-design.md +++ b/specs/2026-07-27-private-spaces-design.md @@ -134,7 +134,7 @@ Category `private-spaces`. All tools set explicit `readOnlyHint` / `destructiveH | ----------------------- | ---------------------- | ----------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `private-spaces_list` | `private-spaces:read` | `organizationId` / — | read-only | Params: `organizationId` (required), `externalId`. Description: requires the org's private-spaces feature and `personal team manage` permission. Executes with `cols: ['*']`. | | `private-spaces_get` | `private-spaces:read` | `privateSpaceId` / `privateSpaceId` | read-only | Description: non-members receive 404; usage totals (`operations`, `transfer`, `centicredits`) come from analytics storage. Executes with `cols: ['*']`. | -| `private-spaces_update` | `private-spaces:write` | `privateSpaceId` / `privateSpaceId` | not read-only, not destructive, idempotent | Params: `privateSpaceId` (required), `operationsLimit` (`type: ['number', 'null']`, null = unlimited), `confirmed` (boolean; description: required when lowering the limit below current consumption — confirming pauses the space). | +| `private-spaces_update` | `private-spaces:write` | `privateSpaceId` / `privateSpaceId` | not read-only, not destructive, idempotent | Params: `privateSpaceId` (required), `operationsLimit` (`oneOf: [{ type: 'number' }, { type: 'null' }]`, null = unlimited), `confirmed` (boolean; description: required when lowering the limit below current consumption — confirming pauses the space). | ### 3. `src/endpoints/organizations.ts` From 598df553a0950f8ce5bca892b0d06f57093da9da Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 16:44:47 +0200 Subject: [PATCH 13/16] fix(private-spaces): align transferLimit and consumedCenticredits types with live API (ORB-1919) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live smoke test against eu2.make.com showed consumedCenticredits is serialized as a string on every route, and transferLimit as a string on list/get but a number on the update response — both diverging from the OpenAPI spec's integer/string declarations. Co-Authored-By: Claude Fable 5 --- specs/2026-07-27-private-spaces-design.md | 4 ++-- src/endpoints/private-spaces.ts | 11 +++++++---- test/mocks/private-spaces/get.json | 2 +- test/mocks/private-spaces/update.json | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/specs/2026-07-27-private-spaces-design.md b/specs/2026-07-27-private-spaces-design.md index bc20f50..cf00e30 100644 --- a/specs/2026-07-27-private-spaces-design.md +++ b/specs/2026-07-27-private-spaces-design.md @@ -79,11 +79,11 @@ Requires org permission `personal team manage`. | `privateSpaceOwnerEmail` | string | default col | | `privateSpaceOwnerId` | number | default col | | `operationsLimit` | number \| null | cols; null = unlimited | -| `transferLimit` | string \| null | cols; bytes | +| `transferLimit` | string \| number \| null | cols; bytes (string on list/get, number on update) | | `consumedOperations` | number \| null | cols | | `consumedTransfer` | string \| null | cols | | `isPaused` | boolean \| null | cols; paused due to exceeded limits | -| `consumedCenticredits` | number \| null | cols | +| `consumedCenticredits` | string \| null | cols; serialized as a string | | `operations` | string | `get()` cols only (ES totals) | | `transfer` | string | `get()` cols only (ES totals) | | `centicredits` | string | `get()` cols only (ES totals) | diff --git a/src/endpoints/private-spaces.ts b/src/endpoints/private-spaces.ts index f22c3e9..33e2b0b 100644 --- a/src/endpoints/private-spaces.ts +++ b/src/endpoints/private-spaces.ts @@ -26,16 +26,19 @@ export type PrivateSpace = { privateSpaceOwnerId?: number; /** Maximum operations limit; null means unlimited */ operationsLimit?: number | null; - /** Maximum data transfer limit in bytes; derived from the operations limit */ - transferLimit?: string | null; + /** + * Maximum data transfer limit in bytes; derived from the operations limit. + * Serialized as a string by `list()`/`get()` but as a number by `update()`. + */ + transferLimit?: string | number | null; /** Number of operations consumed in the current period */ consumedOperations?: number | null; /** Amount of data transfer consumed in the current period, in bytes */ consumedTransfer?: string | null; /** Whether the space is paused due to exceeded limits */ isPaused?: boolean | null; - /** Number of centicredits consumed in the current period */ - consumedCenticredits?: number | null; + /** Number of centicredits consumed in the current period, serialized as a string */ + consumedCenticredits?: string | null; /** Total operations since the last reset; only selectable via `cols` on `get()` */ operations?: string; /** Total data transfer since the last reset, in bytes; only selectable via `cols` on `get()` */ diff --git a/test/mocks/private-spaces/get.json b/test/mocks/private-spaces/get.json index ae05c93..6249ebc 100644 --- a/test/mocks/private-spaces/get.json +++ b/test/mocks/private-spaces/get.json @@ -13,7 +13,7 @@ "consumedOperations": 250, "consumedTransfer": "52428800", "isPaused": false, - "consumedCenticredits": 12345, + "consumedCenticredits": "12345", "operations": "250", "transfer": "52428800", "centicredits": "12345" diff --git a/test/mocks/private-spaces/update.json b/test/mocks/private-spaces/update.json index f07a818..144b15d 100644 --- a/test/mocks/private-spaces/update.json +++ b/test/mocks/private-spaces/update.json @@ -9,11 +9,11 @@ "privateSpaceOwnerEmail": "becca.smith@example.com", "privateSpaceOwnerId": 42, "operationsLimit": 100, - "transferLimit": "107374182", + "transferLimit": 107374182, "consumedOperations": 250, "consumedTransfer": "52428800", "isPaused": true, - "consumedCenticredits": 12345, + "consumedCenticredits": "12345", "deleted": false, "externalId": null } From d67d809806a79da2aa870dee09af12fe34817e99 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 16:47:43 +0200 Subject: [PATCH 14/16] feat: make JSONSchema type optional for enhanced flexibility --- plans/2026-07-27-private-spaces.md | 1177 --------------------- specs/2026-07-27-private-spaces-design.md | 184 ---- 2 files changed, 1361 deletions(-) delete mode 100644 plans/2026-07-27-private-spaces.md delete mode 100644 specs/2026-07-27-private-spaces-design.md diff --git a/plans/2026-07-27-private-spaces.md b/plans/2026-07-27-private-spaces.md deleted file mode 100644 index e23a570..0000000 --- a/plans/2026-07-27-private-spaces.md +++ /dev/null @@ -1,1177 +0,0 @@ -# Private Spaces SDK Support Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add private-spaces support to the Make TypeScript SDK: a `PrivateSpaces` endpoint class (`list`/`get`/`update`), matching tool definitions, and the `privateSpaces` field on `Organization`, per `specs/2026-07-27-private-spaces-design.md` (ORB-1919). - -**Architecture:** One new endpoint file + one new tools file following the repo's exact endpoint template (closest precedents: `src/endpoints/teams.ts`, `src/endpoints/scenarios.ts` for the `confirmed` option). Registration in `make.ts`, `index.ts`, `src/tools.ts`. No create/delete methods — those API endpoints were removed upstream (ORB-1061). - -**Tech Stack:** TypeScript (strict, ES modules with `.js` import extensions), Jest + jest-fetch-mock (`test/test.utils.ts` `mockFetch`), tsup build. - -## Global Constraints - -- TDD: write the failing test first, watch it fail for the right reason, then implement. Never modify a test to make the implementation pass. -- Strict typing: no `any`, no `@ts-ignore`. All public types/methods carry JSDoc. -- Imports always use `.js` extensions (`../types.js`, `./endpoints/private-spaces.js`). -- Internal response types (`*Response`) are NOT exported. -- Tool names: `private-spaces_list`, `private-spaces_get`, `private-spaces_update`; category `private-spaces`; scopes `private-spaces:read` / `private-spaces:write`. Every tool sets explicit `readOnlyHint`, `destructiveHint`, `openWorldHint`. -- The repo's `JSONSchema.type` is a single string union — nullable params use `oneOf: [{ type: 'number' }, { type: 'null' }]`, NOT `type: ['number', 'null']`. -- Do NOT add create/delete methods or tools for private spaces. -- `mockFetch` mock URLs must match the built URL exactly (query-param insertion order; `[` `]` encode as `%5B` `%5D`; `*` stays literal). -- Run a single spec file with: `npx jest --runInBand --forceExit --testMatch "**/test/"`. -- Full suite: `npm test` (includes text coverage). Lint: `npm run lint`. Format: `npm run format`. -- Long test output goes to the scratchpad dir via `> file 2>&1`, never piped through `tail`/`grep` directly. -- Coverage floor: ≥90% line and branch on touched files. -- Every commit message ends with `Co-Authored-By: Claude Fable 5 `. -- Baseline before Task 1: run `npm test`, record pass count; every GREEN step must be baseline + new tests, no regressions. - ---- - -### Task 1: `PrivateSpace` type, `PrivateSpaces.list()`, client registration - -**Files:** - -- Create: `src/endpoints/private-spaces.ts` -- Create: `test/mocks/private-spaces/list.json` -- Create: `test/private-spaces.spec.ts` -- Modify: `src/make.ts` (import ~line 20, property ~line 186, constructor ~line 281) - -**Interfaces:** - -- Consumes: `FetchFunction`, `Pagination`, `PickColumns` from `src/types.js` (existing). -- Produces: `PrivateSpace` type; `ListPrivateSpacesOptions`; class `PrivateSpaces` with `list(organizationId: number, options?: ListPrivateSpacesOptions): Promise[]>`; `make.privateSpaces: PrivateSpaces` on the `Make` client. Tasks 2–7 rely on all of these names exactly. - -- [ ] **Step 1: Record the baseline** - -Run: `npm test > /private/tmp/claude-501/-Users-jankulhavy-Projects-Make-make-typescript-sdk/767a9850-fc5b-4c6a-a17a-a686a8b29d30/scratchpad/baseline.txt 2>&1` -Then inspect the tail of that file for the totals line. Record the number of passing tests/suites. - -- [ ] **Step 2: Create the list mock** - -`test/mocks/private-spaces/list.json`: - -```json -{ - "privateSpaces": [ - { - "id": 101, - "name": "Becca's space", - "organizationId": 5, - "globalAgentsEnabled": false, - "type": "personal", - "privateSpaceOwnerName": "Becca Smith", - "privateSpaceOwnerEmail": "becca.smith@example.com", - "privateSpaceOwnerId": 42 - }, - { - "id": 102, - "name": "Jan's space", - "organizationId": 5, - "globalAgentsEnabled": true, - "type": "personal", - "privateSpaceOwnerName": "Jan Novak", - "privateSpaceOwnerEmail": "jan.novak@example.com", - "privateSpaceOwnerId": 43 - } - ], - "pg": { - "sortBy": "name", - "sortDir": "asc", - "offset": 0, - "limit": 100 - } -} -``` - -- [ ] **Step 3: Write the failing tests** - -`test/private-spaces.spec.ts`: - -```typescript -import { describe, expect, it } from '@jest/globals'; -import { Make } from '../src/make.js'; -import { mockFetch } from './test.utils.js'; -import type { PrivateSpace } from '../src/endpoints/private-spaces.js'; - -import * as privateSpacesListMock from './mocks/private-spaces/list.json'; - -const MAKE_API_KEY = 'api-key'; -const MAKE_ZONE = 'make.local'; - -describe('Endpoints: PrivateSpaces', () => { - const make = new Make(MAKE_API_KEY, MAKE_ZONE); - - it('Should list private spaces', async () => { - mockFetch('GET https://make.local/api/v2/private-spaces?organizationId=5', privateSpacesListMock); - - const result = await make.privateSpaces.list(5); - - expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); - }); - - it('Should list private spaces filtered by externalId with pagination', async () => { - mockFetch( - 'GET https://make.local/api/v2/private-spaces?organizationId=5&externalId=ext-1&pg%5BsortBy%5D=name&pg%5BsortDir%5D=asc', - privateSpacesListMock, - ); - - const result = await make.privateSpaces.list(5, { - externalId: 'ext-1', - pg: { - sortBy: 'name', - sortDir: 'asc', - }, - }); - - expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); - }); - - it('Should list private spaces with selected columns', async () => { - const cols: (keyof PrivateSpace)[] = ['id', 'name', 'isPaused']; - mockFetch( - `GET https://make.local/api/v2/private-spaces?organizationId=5&cols%5B%5D=${cols.join('&cols%5B%5D=')}`, - privateSpacesListMock, - ); - - const result = await make.privateSpaces.list(5, { - cols, - }); - - expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); - }); -}); -``` - -- [ ] **Step 4: Run the tests to verify they fail for the right reason** - -Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` -Expected: FAIL — cannot find module `'../src/endpoints/private-spaces.js'` (the module does not exist yet), and `make.privateSpaces` does not exist. Import/compile failure caused by the missing feature is the correct RED here; a typo in an existing path is not. - -- [ ] **Step 5: Implement the endpoint file with `list()`** - -`src/endpoints/private-spaces.ts`: - -````typescript -import type { FetchFunction, Pagination, PickColumns } from '../types.js'; - -/** - * Represents a private space in Make. - * A private space is a per-user personal workspace inside an organization where - * scenarios and related entities (including connections) stay visible only to the - * owner. Private spaces cannot be created or deleted through the API — they are - * provisioned automatically based on the organization's private-spaces settings. - */ -export type PrivateSpace = { - /** Unique identifier of the private space */ - id: number; - /** Name of the private space */ - name: string; - /** ID of the organization this private space belongs to */ - organizationId: number; - /** Whether Make global AI agents are enabled for the space */ - globalAgentsEnabled?: boolean; - /** Type of the underlying team; always `personal` for private spaces */ - type?: 'personal'; - /** Name of the space owner */ - privateSpaceOwnerName?: string; - /** Email of the space owner */ - privateSpaceOwnerEmail?: string; - /** User ID of the space owner */ - privateSpaceOwnerId?: number; - /** Maximum operations limit; null means unlimited */ - operationsLimit?: number | null; - /** Maximum data transfer limit in bytes; derived from the operations limit */ - transferLimit?: string | null; - /** Number of operations consumed in the current period */ - consumedOperations?: number | null; - /** Amount of data transfer consumed in the current period, in bytes */ - consumedTransfer?: string | null; - /** Whether the space is paused due to exceeded limits */ - isPaused?: boolean | null; - /** Number of centicredits consumed in the current period */ - consumedCenticredits?: number | null; - /** Total operations since the last reset; only selectable via `cols` on `get()` */ - operations?: string; - /** Total data transfer since the last reset, in bytes; only selectable via `cols` on `get()` */ - transfer?: string; - /** Total centicredits since the last reset; only selectable via `cols` on `get()` */ - centicredits?: string; - /** Whether the space is deleted; returned by `update()` */ - deleted?: boolean; - /** External identifier of the space; returned by `update()` */ - externalId?: string | null; -}; - -/** - * Options for listing private spaces. - * @template C Keys of the PrivateSpace type to include in the response - */ -export type ListPrivateSpacesOptions = { - /** Specific columns/fields to include in the response */ - cols?: C[] | ['*']; - /** Pagination options (the API supports sorting by `name` only) */ - pg?: Partial>; - /** Filter spaces by their external ID */ - externalId?: string; -}; - -/** - * Response format for listing private spaces. - */ -type ListPrivateSpacesResponse = { - /** List of private spaces matching the query */ - privateSpaces: PickColumns[]; - /** Pagination information */ - pg: Pagination; -}; - -/** - * Class providing methods for working with Make private spaces. - * Requires the organization's private-spaces feature to be enabled; the API - * responds with error IM903 when it is not. - */ -export class PrivateSpaces { - readonly #fetch: FetchFunction; - - /** - * Create a new PrivateSpaces instance. - * @param fetch Function for making API requests - */ - constructor(fetch: FetchFunction) { - this.#fetch = fetch; - } - - /** - * List private spaces of an organization. - * Requires the `personal team manage` organization permission. - * @param organizationId The organization ID to list private spaces for - * @param options Optional parameters for filtering and pagination - * @returns Promise with the list of private spaces - * - * @example - * ```typescript - * const spaces = await make.privateSpaces.list(123); - * ``` - */ - async list( - organizationId: number, - options?: ListPrivateSpacesOptions, - ): Promise[]> { - return ( - await this.#fetch>('/private-spaces', { - query: { - organizationId, - externalId: options?.externalId, - cols: options?.cols, - pg: options?.pg, - }, - }) - ).privateSpaces; - } -} -```` - -- [ ] **Step 6: Register the endpoint on the `Make` client** - -In `src/make.ts`, three edits: - -After the `PublicTemplates` import (line ~20): - -```typescript -import { PrivateSpaces } from './endpoints/private-spaces.js'; -``` - -After the `publicTemplates` property declaration (line ~186): - -```typescript -/** - * Access to private space endpoints. - * Private spaces are per-user personal workspaces within an organization; they are - * provisioned automatically and cannot be created or deleted through the API. - */ -public readonly privateSpaces: PrivateSpaces; -``` - -After `this.publicTemplates = new PublicTemplates(this.fetch.bind(this));` in the constructor (line ~281): - -```typescript -this.privateSpaces = new PrivateSpaces(this.fetch.bind(this)); -``` - -- [ ] **Step 7: Run the tests to verify they pass** - -Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` -Expected: PASS (3 tests). - -- [ ] **Step 8: Run lint and the full suite** - -Run: `npm run lint && npm test > /private/tmp/claude-501/-Users-jankulhavy-Projects-Make-make-typescript-sdk/767a9850-fc5b-4c6a-a17a-a686a8b29d30/scratchpad/task1.txt 2>&1` -Inspect the file tail: totals must equal baseline + 3 new passing tests, zero failures. - -- [ ] **Step 9: Commit** - -```bash -git add src/endpoints/private-spaces.ts src/make.ts test/private-spaces.spec.ts test/mocks/private-spaces/list.json -git commit -m "feat(private-spaces): add PrivateSpaces endpoint with list() (ORB-1919) - -Co-Authored-By: Claude Fable 5 " -``` - ---- - -### Task 2: `PrivateSpaces.get()` - -**Files:** - -- Create: `test/mocks/private-spaces/get.json` -- Modify: `src/endpoints/private-spaces.ts` (add `GetPrivateSpaceOptions`, `GetPrivateSpaceResponse`, `get()`) -- Modify: `test/private-spaces.spec.ts` (extend the existing describe block) - -**Interfaces:** - -- Consumes: `PrivateSpace`, `PrivateSpaces` class from Task 1. -- Produces: `GetPrivateSpaceOptions`; `get(privateSpaceId: number, options?: GetPrivateSpaceOptions): Promise>`. Tasks 6–7 call `make.privateSpaces.get(...)` with this exact signature. - -- [ ] **Step 1: Create the get mock** - -`test/mocks/private-spaces/get.json` (includes the get-only usage columns): - -```json -{ - "privateSpace": { - "id": 101, - "name": "Becca's space", - "organizationId": 5, - "globalAgentsEnabled": false, - "type": "personal", - "privateSpaceOwnerName": "Becca Smith", - "privateSpaceOwnerEmail": "becca.smith@example.com", - "privateSpaceOwnerId": 42, - "operationsLimit": 1000, - "transferLimit": "1073741824", - "consumedOperations": 250, - "consumedTransfer": "52428800", - "isPaused": false, - "consumedCenticredits": 12345, - "operations": "250", - "transfer": "52428800", - "centicredits": "12345" - } -} -``` - -- [ ] **Step 2: Write the failing tests** - -Add to the describe block in `test/private-spaces.spec.ts` (and add the import at the top with the other mock imports): - -```typescript -import * as privateSpaceGetMock from './mocks/private-spaces/get.json'; -``` - -```typescript -it('Should get a private space', async () => { - mockFetch('GET https://make.local/api/v2/private-spaces/101', privateSpaceGetMock); - - const result = await make.privateSpaces.get(101); - - expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); -}); - -it('Should get a private space with usage columns', async () => { - const cols: (keyof PrivateSpace)[] = ['id', 'operations', 'transfer', 'centicredits']; - mockFetch( - `GET https://make.local/api/v2/private-spaces/101?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, - privateSpaceGetMock, - ); - - const result = await make.privateSpaces.get(101, { - cols, - }); - - expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); -}); -``` - -- [ ] **Step 3: Run the tests to verify they fail** - -Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` -Expected: FAIL — `make.privateSpaces.get` is not a function (TypeScript: property `get` does not exist on `PrivateSpaces`). - -- [ ] **Step 4: Implement `get()`** - -In `src/endpoints/private-spaces.ts`, add after `ListPrivateSpacesOptions`: - -```typescript -/** - * Options for retrieving a private space. - * @template C Keys of the PrivateSpace type to include in the response - */ -export type GetPrivateSpaceOptions = { - /** - * Specific columns/fields to include in the response. In addition to the list - * columns, `get()` supports the usage totals `operations`, `transfer` and - * `centicredits` (computed from analytics storage; the API responds with 503 - * when that storage is unavailable). - */ - cols?: C[] | ['*']; -}; -``` - -Add after `ListPrivateSpacesResponse`: - -```typescript -/** - * Response format for getting a private space. - */ -type GetPrivateSpaceResponse = { - /** The requested private space */ - privateSpace: PickColumns; -}; -``` - -Add to the `PrivateSpaces` class after `list()`: - -````typescript - /** - * Get details of a specific private space. - * Requires the `personal team own view` organization permission; callers who are - * not members of the space receive a 404 even when the space exists. - * @param privateSpaceId The private space ID to get - * @param options Optional parameters for filtering returned fields - * @returns Promise with the private space information - * - * @example - * ```typescript - * const space = await make.privateSpaces.get(101); - * ``` - */ - async get( - privateSpaceId: number, - options?: GetPrivateSpaceOptions, - ): Promise> { - return ( - await this.#fetch>(`/private-spaces/${privateSpaceId}`, { - query: { - cols: options?.cols, - }, - }) - ).privateSpace; - } -```` - -- [ ] **Step 5: Run the tests to verify they pass** - -Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` -Expected: PASS (5 tests). - -- [ ] **Step 6: Commit** - -```bash -git add src/endpoints/private-spaces.ts test/private-spaces.spec.ts test/mocks/private-spaces/get.json -git commit -m "feat(private-spaces): add get() with usage columns (ORB-1919) - -Co-Authored-By: Claude Fable 5 " -``` - ---- - -### Task 3: `PrivateSpaces.update()` - -**Files:** - -- Create: `test/mocks/private-spaces/update.json` -- Modify: `src/endpoints/private-spaces.ts` (add `UpdatePrivateSpaceBody`, `UpdatePrivateSpaceOptions`, `UpdatePrivateSpaceResponse`, `update()`) -- Modify: `test/private-spaces.spec.ts` - -**Interfaces:** - -- Consumes: `PrivateSpace`, `PrivateSpaces` class from Tasks 1–2. -- Produces: `UpdatePrivateSpaceBody = { operationsLimit?: number | null }`; `UpdatePrivateSpaceOptions = { confirmed?: boolean }`; `update(privateSpaceId: number, body: UpdatePrivateSpaceBody, options?: UpdatePrivateSpaceOptions): Promise`. Tasks 6–7 call `make.privateSpaces.update(...)` with this exact signature. - -- [ ] **Step 1: Create the update mock** - -`test/mocks/private-spaces/update.json` (PATCH responses additionally include `deleted` and `externalId`; this one shows the confirmed-below-consumption outcome — space paused): - -```json -{ - "privateSpace": { - "id": 101, - "name": "Becca's space", - "organizationId": 5, - "globalAgentsEnabled": false, - "type": "personal", - "privateSpaceOwnerName": "Becca Smith", - "privateSpaceOwnerEmail": "becca.smith@example.com", - "privateSpaceOwnerId": 42, - "operationsLimit": 100, - "transferLimit": "107374182", - "consumedOperations": 250, - "consumedTransfer": "52428800", - "isPaused": true, - "consumedCenticredits": 12345, - "deleted": false, - "externalId": null - } -} -``` - -- [ ] **Step 2: Write the failing tests** - -Add the mock import to `test/private-spaces.spec.ts`: - -```typescript -import * as privateSpaceUpdateMock from './mocks/private-spaces/update.json'; -``` - -Add to the describe block: - -```typescript -it('Should update a private space', async () => { - const body = { - operationsLimit: 100, - }; - - mockFetch('PATCH https://make.local/api/v2/private-spaces/101', privateSpaceUpdateMock, req => { - expect(req.body).toStrictEqual(body); - expect(req.headers.get('content-type')).toBe('application/json'); - }); - - const result = await make.privateSpaces.update(101, body); - - expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); -}); - -it('Should update a private space with confirmation and null limit', async () => { - const body = { - operationsLimit: null, - }; - - mockFetch('PATCH https://make.local/api/v2/private-spaces/101?confirmed=true', privateSpaceUpdateMock, req => { - expect(req.body).toStrictEqual(body); - }); - - const result = await make.privateSpaces.update(101, body, { confirmed: true }); - - expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); -}); -``` - -- [ ] **Step 3: Run the tests to verify they fail** - -Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` -Expected: FAIL — `make.privateSpaces.update` is not a function. - -- [ ] **Step 4: Implement `update()`** - -In `src/endpoints/private-spaces.ts`, add after `GetPrivateSpaceOptions`: - -```typescript -/** - * Body for updating a private space. - */ -export type UpdatePrivateSpaceBody = { - /** - * Maximum operations limit (minimum 0). Set to `null` to remove the limit - * (unlimited); omit to leave unchanged. The transfer limit is derived from - * this value by the API. - */ - operationsLimit?: number | null; -}; - -/** - * Options for updating a private space. - */ -export type UpdatePrivateSpaceOptions = { - /** - * Confirmation of the update. Required (the API fails with IM004 otherwise) - * when the new operations limit is below the space's current consumption; - * confirming pauses the space. - */ - confirmed?: boolean; -}; -``` - -Add after `GetPrivateSpaceResponse`: - -```typescript -/** - * Response format for updating a private space. - */ -type UpdatePrivateSpaceResponse = { - /** The updated private space */ - privateSpace: PrivateSpace; -}; -``` - -Add to the `PrivateSpaces` class after `get()`: - -````typescript - /** - * Update a private space. - * Requires the `personal team manage` organization permission. - * @param privateSpaceId The private space ID to update - * @param body The fields to update - * @param options Optional update options - * @returns Promise with the updated private space - * - * @example - * ```typescript - * // Remove the operations limit - * const space = await make.privateSpaces.update(101, { operationsLimit: null }); - * ``` - */ - async update( - privateSpaceId: number, - body: UpdatePrivateSpaceBody, - options?: UpdatePrivateSpaceOptions, - ): Promise { - return ( - await this.#fetch(`/private-spaces/${privateSpaceId}`, { - method: 'PATCH', - query: { - confirmed: options?.confirmed, - }, - body, - }) - ).privateSpace; - } -```` - -- [ ] **Step 5: Run the tests to verify they pass** - -Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.spec.ts"` -Expected: PASS (7 tests). - -- [ ] **Step 6: Commit** - -```bash -git add src/endpoints/private-spaces.ts test/private-spaces.spec.ts test/mocks/private-spaces/update.json -git commit -m "feat(private-spaces): add update() with confirmed option (ORB-1919) - -Co-Authored-By: Claude Fable 5 " -``` - ---- - -### Task 4: `Organization.privateSpaces` field - -**Files:** - -- Modify: `src/endpoints/organizations.ts` (add field to the `Organization` type, after the `license` block ends, ~line 100+ — place it with the other optional top-level fields) -- Modify: `test/organizations.spec.ts` (extend the existing describe block) -- Modify: `test/mocks/organizations/get.json` (add `privateSpaces` to the organization object) - -**Interfaces:** - -- Consumes: existing `Organization` type and `organizations.list()` / `organizations.get()`. -- Produces: `Organization.privateSpaces?: { id: number; name: string; isOwner: boolean; hasAdminVisibility: boolean }[]` — selectable via `cols` because option types use `keyof Organization`. - -- [ ] **Step 1: Write the failing test** - -Add to the describe block in `test/organizations.spec.ts`: - -```typescript -it('Should list organizations with the privateSpaces column', async () => { - const cols: (keyof Organization)[] = ['id', 'name', 'privateSpaces']; - mockFetch( - `GET https://make.local/api/v2/organizations?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, - organizationsListMock, - ); - - const result = await make.organizations.list({ - cols, - }); - - expect(result).toStrictEqual(organizationsListMock.organizations); -}); -``` - -- [ ] **Step 2: Run the test to verify it fails** - -Run: `npx jest --runInBand --forceExit --testMatch "**/test/organizations.spec.ts"` -Expected: FAIL — TypeScript error: `'privateSpaces'` is not assignable to `keyof Organization` (the field does not exist yet). - -- [ ] **Step 3: Add the field to the `Organization` type** - -In `src/endpoints/organizations.ts`, inside the `Organization` type, after the `license` object closes (keep it alongside the other optional top-level fields): - -```typescript - /** - * Private spaces the requesting user is a member of within this organization. - * Only returned when requested via `cols`; available on public cloud only. - */ - privateSpaces?: { - /** Unique identifier of the private space */ - id: number; - /** Name of the private space */ - name: string; - /** Whether the requesting user owns the space */ - isOwner: boolean; - /** Whether org admins can see into the space (mirrors the organization's "add admins as observers" setting) */ - hasAdminVisibility: boolean; - }[]; -``` - -- [ ] **Step 4: Extend the get mock so the field shape is exercised** - -In `test/mocks/organizations/get.json`, add to the `organization` object (keep all existing fields): - -```json - "privateSpaces": [ - { - "id": 101, - "name": "Becca's space", - "isOwner": true, - "hasAdminVisibility": false - } - ] -``` - -- [ ] **Step 5: Run the organizations spec to verify all tests pass** - -Run: `npx jest --runInBand --forceExit --testMatch "**/test/organizations.spec.ts"` -Expected: PASS — the new cols test plus all pre-existing organization tests (the extended get mock must not break `Should get an organization with wait option`, which compares against the same mock object). - -- [ ] **Step 6: Commit** - -```bash -git add src/endpoints/organizations.ts test/organizations.spec.ts test/mocks/organizations/get.json -git commit -m "feat(organizations): add privateSpaces column to Organization type (ORB-1919) - -Co-Authored-By: Claude Fable 5 " -``` - ---- - -### Task 5: Public exports and README endpoint list - -**Files:** - -- Modify: `src/index.ts` (after the `public-templates.js` export block, ~line 202) -- Modify: `README.md` (endpoint list, ~line 66) - -**Interfaces:** - -- Consumes: all types from Tasks 1–3. -- Produces: package-level exports `PrivateSpace`, `PrivateSpaces`, `ListPrivateSpacesOptions`, `GetPrivateSpaceOptions`, `UpdatePrivateSpaceBody`, `UpdatePrivateSpaceOptions`. - -- [ ] **Step 1: Add the type exports** - -In `src/index.ts`, after the `public-templates.js` export block: - -```typescript -export type { - PrivateSpace, - PrivateSpaces, - ListPrivateSpacesOptions, - GetPrivateSpaceOptions, - UpdatePrivateSpaceBody, - UpdatePrivateSpaceOptions, -} from './endpoints/private-spaces.js'; -``` - -- [ ] **Step 2: Add the README endpoint bullet** - -In `README.md`, in the endpoints list, insert between the `**Organizations**` and `**Scenarios**` bullets: - -```markdown -- **Private Spaces** - Per-user private workspaces within an organization (list, get, update) -``` - -- [ ] **Step 3: Verify with lint and build** - -Run: `npm run lint && npm run build` -Expected: both succeed with no errors (tsc validates the export names exist). - -- [ ] **Step 4: Commit** - -```bash -git add src/index.ts README.md -git commit -m "feat(private-spaces): export public types and document endpoint (ORB-1919) - -Co-Authored-By: Claude Fable 5 " -``` - ---- - -### Task 6: Tool definitions - -**Files:** - -- Create: `src/endpoints/private-spaces.tools.ts` -- Create: `test/private-spaces-tools.spec.ts` (mirrors `test/on-prem-tools.spec.ts`) -- Modify: `src/tools.ts` (import after `PublicTemplatesTools` import ~line 31; spread after `...PublicTemplatesTools,` ~line 233) -- Modify: `README.md` (tool categories list, ~line 214) - -**Interfaces:** - -- Consumes: `make.privateSpaces.list/get/update` exactly as produced by Tasks 1–3; `MakeTool` type and `MakeTools` array from `src/tools.js`. -- Produces: tools `private-spaces_list`, `private-spaces_get`, `private-spaces_update` registered in `MakeTools`. - -- [ ] **Step 1: Write the failing tests** - -`test/private-spaces-tools.spec.ts`: - -```typescript -import { describe, expect, it } from '@jest/globals'; -import { Make } from '../src/make.js'; -import { MakeTools } from '../src/tools.js'; -import { mockFetch } from './test.utils.js'; - -import * as privateSpacesListMock from './mocks/private-spaces/list.json'; -import * as privateSpaceGetMock from './mocks/private-spaces/get.json'; -import * as privateSpaceUpdateMock from './mocks/private-spaces/update.json'; - -const MAKE_API_KEY = 'api-key'; -const MAKE_ZONE = 'make.local'; -const ORGANIZATION_ID = 5; -const PRIVATE_SPACE_ID = 101; - -function getTool(name: string) { - const tool = MakeTools.find(entry => entry.name === name); - if (!tool) { - throw new Error(`Missing MCP tool: ${name}`); - } - return tool; -} - -describe('MCP tools: private-spaces', () => { - const make = new Make(MAKE_API_KEY, MAKE_ZONE); - - it('Should execute private-spaces_list', async () => { - mockFetch( - `GET https://make.local/api/v2/private-spaces?organizationId=${ORGANIZATION_ID}&cols%5B%5D=*`, - privateSpacesListMock, - ); - - const tool = getTool('private-spaces_list'); - const result = await tool.execute(make, { organizationId: ORGANIZATION_ID }); - - expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); - }); - - it('Should execute private-spaces_get', async () => { - mockFetch(`GET https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?cols%5B%5D=*`, privateSpaceGetMock); - - const tool = getTool('private-spaces_get'); - const result = await tool.execute(make, { privateSpaceId: PRIVATE_SPACE_ID }); - - expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); - }); - - it('Should execute private-spaces_update', async () => { - mockFetch( - `PATCH https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?confirmed=true`, - privateSpaceUpdateMock, - req => { - expect(req.body).toStrictEqual({ operationsLimit: 100 }); - }, - ); - - const tool = getTool('private-spaces_update'); - const result = await tool.execute(make, { - privateSpaceId: PRIVATE_SPACE_ID, - operationsLimit: 100, - confirmed: true, - }); - - expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); - }); -}); -``` - -- [ ] **Step 2: Run the tests to verify they fail** - -Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces-tools.spec.ts"` -Expected: FAIL — `Missing MCP tool: private-spaces_list` (the tools are not defined/registered yet). - -- [ ] **Step 3: Implement the tools file** - -`src/endpoints/private-spaces.tools.ts`: - -```typescript -import type { Make } from '../make.js'; -import type { MakeTool } from '../tools.js'; - -export const tools: MakeTool[] = [ - { - name: 'private-spaces_list', - title: 'List private spaces', - description: - "List the private spaces of an organization. Requires the organization's private-spaces feature to be enabled (error IM903 otherwise) and the 'personal team manage' permission. Private spaces cannot be created or deleted through the API — they are provisioned automatically by organization settings.", - category: 'private-spaces', - scope: 'private-spaces:read', - scopeId: 'organizationId', - identifier: 'organizationId', - annotations: { - readOnlyHint: true, - destructiveHint: false, - openWorldHint: false, - }, - inputSchema: { - type: 'object', - properties: { - organizationId: { type: 'number', description: 'The organization ID to list private spaces for' }, - externalId: { type: 'string', description: 'Filter private spaces by their external ID' }, - }, - required: ['organizationId'], - }, - examples: [{ organizationId: 5 }, { organizationId: 5, externalId: 'ext-1' }], - execute: async (make: Make, args: { organizationId: number; externalId?: string }) => { - const { organizationId, ...options } = args; - return await make.privateSpaces.list(organizationId, { ...options, cols: ['*'] }); - }, - }, - { - name: 'private-spaces_get', - title: 'Get private space', - description: - 'Get details of a specific private space, including usage totals (operations, transfer, centicredits). Callers who are not members of the space receive a 404 even when the space exists.', - category: 'private-spaces', - scope: 'private-spaces:read', - scopeId: 'privateSpaceId', - identifier: 'privateSpaceId', - resourceId: 'privateSpaceId', - annotations: { - readOnlyHint: true, - destructiveHint: false, - openWorldHint: false, - }, - inputSchema: { - type: 'object', - properties: { - privateSpaceId: { type: 'number', description: 'The private space ID to retrieve' }, - }, - required: ['privateSpaceId'], - }, - examples: [{ privateSpaceId: 101 }], - execute: async (make: Make, args: { privateSpaceId: number }) => { - return await make.privateSpaces.get(args.privateSpaceId, { cols: ['*'] }); - }, - }, - { - name: 'private-spaces_update', - title: 'Update private space', - description: - "Update a private space's operations limit. Set operationsLimit to null to remove the limit (unlimited); the transfer limit is derived automatically. When the new limit is below the space's current consumption the call fails with IM004 unless 'confirmed' is true — confirming pauses the space.", - category: 'private-spaces', - scope: 'private-spaces:write', - scopeId: 'privateSpaceId', - identifier: 'privateSpaceId', - resourceId: 'privateSpaceId', - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: 'object', - properties: { - privateSpaceId: { type: 'number', description: 'The private space ID to update' }, - operationsLimit: { - oneOf: [{ type: 'number' }, { type: 'null' }], - description: - 'Maximum operations limit (minimum 0). Pass null to remove the limit; omit to leave unchanged.', - }, - confirmed: { - type: 'boolean', - description: - "Confirmation of the update. Required when the new limit is below the space's current consumption; confirming pauses the space.", - }, - }, - required: ['privateSpaceId'], - }, - examples: [ - { privateSpaceId: 101, operationsLimit: 10000 }, - { privateSpaceId: 101, operationsLimit: null, confirmed: true }, - ], - execute: async ( - make: Make, - args: { privateSpaceId: number; operationsLimit?: number | null; confirmed?: boolean }, - ) => { - const { privateSpaceId, confirmed, ...body } = args; - return await make.privateSpaces.update(privateSpaceId, body, { confirmed }); - }, - }, -]; -``` - -- [ ] **Step 4: Register the tools** - -In `src/tools.ts`, after the `PublicTemplatesTools` import: - -```typescript -import { tools as PrivateSpacesTools } from './endpoints/private-spaces.tools.js'; -``` - -In the `MakeTools` array, after `...PublicTemplatesTools,`: - -```typescript - ...PrivateSpacesTools, -``` - -- [ ] **Step 5: Run the tests to verify they pass** - -Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces-tools.spec.ts"` -Expected: PASS (3 tests). - -- [ ] **Step 6: Add the README tool category** - -In `README.md`, in the tool categories list, insert between `- \`organizations\``and`- \`scenarios\``: - -```markdown -- `private-spaces` -``` - -- [ ] **Step 7: Run lint and the full suite** - -Run: `npm run lint && npm test > /private/tmp/claude-501/-Users-jankulhavy-Projects-Make-make-typescript-sdk/767a9850-fc5b-4c6a-a17a-a686a8b29d30/scratchpad/task6.txt 2>&1` -Inspect the file tail: all tests green (baseline + 11 new across Tasks 1–6: 7 endpoint + 1 organizations + 3 tools), zero failures. - -- [ ] **Step 8: Commit** - -```bash -git add src/endpoints/private-spaces.tools.ts src/tools.ts test/private-spaces-tools.spec.ts README.md -git commit -m "feat(private-spaces): add tool definitions (ORB-1919) - -Co-Authored-By: Claude Fable 5 " -``` - ---- - -### Task 7: Integration test - -**Files:** - -- Create: `test/private-spaces.integration.test.ts` - -**Interfaces:** - -- Consumes: `make.privateSpaces.list/get/update` from Tasks 1–3; env vars `MAKE_API_KEY`, `MAKE_ZONE`, `MAKE_ORGANIZATION` from `.env`. -- Produces: nothing consumed later. - -Integration tests run only via `npm run test:integration` (separate testMatch), so this file never affects `npm test`. There is no public API to provision a private space and the feature is flag-gated per org, so every test after the list guards with an early return when no space exists. - -- [ ] **Step 1: Write the integration test** - -`test/private-spaces.integration.test.ts`: - -```typescript -import 'dotenv/config'; -import { describe, expect, it } from '@jest/globals'; -import { Make } from '../src/make.js'; - -const MAKE_API_KEY = String(process.env.MAKE_API_KEY || ''); -const MAKE_ZONE = String(process.env.MAKE_ZONE || ''); -const MAKE_ORGANIZATION = Number(process.env.MAKE_ORGANIZATION || 0); - -describe('Integration: PrivateSpaces', () => { - const make = new Make(MAKE_API_KEY, MAKE_ZONE); - - let privateSpaceId: number | undefined; - let originalOperationsLimit: number | null | undefined; - let consumedOperations: number | null | undefined; - - it('Should list private spaces', async () => { - const spaces = await make.privateSpaces.list(MAKE_ORGANIZATION); - - expect(Array.isArray(spaces)).toBe(true); - - // No public API can provision a private space, so downstream tests are - // skipped (early return) when the organization has none. - privateSpaceId = spaces[0]?.id; - }); - - it('Should get a private space', async () => { - if (privateSpaceId === undefined) return; - - const space = await make.privateSpaces.get(privateSpaceId, { cols: ['*'] }); - - expect(space.id).toBe(privateSpaceId); - expect(space.organizationId).toBe(MAKE_ORGANIZATION); - expect(space.type).toBe('personal'); - - originalOperationsLimit = space.operationsLimit; - consumedOperations = space.consumedOperations; - }); - - it('Should update a private space and restore the original limit', async () => { - if (privateSpaceId === undefined) return; - - // Stay above current consumption so the update needs no confirmation - // and cannot pause the space. - const safeLimit = Math.max(consumedOperations ?? 0, originalOperationsLimit ?? 0) + 10000; - - const updated = await make.privateSpaces.update(privateSpaceId, { operationsLimit: safeLimit }); - expect(updated.operationsLimit).toBe(safeLimit); - - const restored = await make.privateSpaces.update( - privateSpaceId, - { operationsLimit: originalOperationsLimit ?? null }, - { confirmed: true }, - ); - expect(restored.operationsLimit).toBe(originalOperationsLimit ?? null); - }); -}); -``` - -- [ ] **Step 2: Verify it compiles and does not leak into the unit suite** - -Run: `npm run lint` -Expected: PASS. -Run: `npx jest --runInBand --forceExit --testMatch "**/test/**/*.spec.ts" --listTests | grep private-spaces` -Expected: only `test/private-spaces.spec.ts` and `test/private-spaces-tools.spec.ts` — NOT the integration file. - -- [ ] **Step 3: Run the integration test if `.env` is configured (skip this step when `.env` is absent)** - -Run: `npx jest --runInBand --forceExit --testMatch "**/test/private-spaces.integration.test.ts" > /private/tmp/claude-501/-Users-jankulhavy-Projects-Make-make-typescript-sdk/767a9850-fc5b-4c6a-a17a-a686a8b29d30/scratchpad/task7.txt 2>&1` -Inspect the file: PASS, or an environment-related failure (missing env/feature flag) — report which. Do not mark this plan complete with an unexplained integration failure. - -- [ ] **Step 4: Commit** - -```bash -git add test/private-spaces.integration.test.ts -git commit -m "test(private-spaces): add integration tests (ORB-1919) - -Co-Authored-By: Claude Fable 5 " -``` - ---- - -### Task 8: Final verification - -**Files:** none new — verification only. - -- [ ] **Step 1: Full unit suite with coverage** - -Run: `npm test > /private/tmp/claude-501/-Users-jankulhavy-Projects-Make-make-typescript-sdk/767a9850-fc5b-4c6a-a17a-a686a8b29d30/scratchpad/final.txt 2>&1` -Inspect the file: zero failures; totals = baseline + 11 new tests. Read the coverage table rows for `private-spaces.ts`, `private-spaces.tools.ts`, `organizations.ts`, `make.ts`, `tools.ts`: each touched file must be ≥90% lines and branches. If below, add the missing test before proceeding (never assertion-free filler). - -- [ ] **Step 2: Lint, format, build** - -Run: `npm run lint && npm run format && npm run build` -Expected: all pass; `git status` after format shows no unexpected reformat of untouched files (if prettier changed only files from this plan, amend them into a `style:` commit or fold into Step 4). - -- [ ] **Step 3: README cross-check against the repo checklist** - -Confirm `README.md` shows the **Private Spaces** endpoint bullet (Task 5) and the `private-spaces` tool category (Task 6). Confirm no other README section (environment variables, configuration) is affected — this change adds no env vars or config options. - -- [ ] **Step 4: Commit any remaining changes and report** - -```bash -git status --short -``` - -If anything is uncommitted from steps above, commit it: - -```bash -git add -A -- ':!test-public-templates.ts' -git commit -m "chore(private-spaces): formatting and verification follow-ups (ORB-1919) - -Co-Authored-By: Claude Fable 5 " -``` - -Note: `test-public-templates.ts` in the repo root is an unrelated untracked scratch script — never stage it. - -Report: baseline vs final test counts, coverage numbers for the five touched files, lint/build status. diff --git a/specs/2026-07-27-private-spaces-design.md b/specs/2026-07-27-private-spaces-design.md deleted file mode 100644 index cf00e30..0000000 --- a/specs/2026-07-27-private-spaces-design.md +++ /dev/null @@ -1,184 +0,0 @@ -# Private Spaces SDK Support — Design - -Date: 2026-07-27 -Driver: [ORB-1919](https://make.atlassian.net/browse/ORB-1919) (MCP support of Private spaces), part of epic [ORB-843](https://make.atlassian.net/browse/ORB-843) (Private Spaces, Phase 1: Personal Teams) - -## Background - -A private space is a per-user personal workspace inside an organization. Under the hood it -is a team with `type: 'personal'`, but the platform exposes it through a dedicated -`/private-spaces` API. The endpoints were published in the Make OpenAPI docs (ORB-1914) and -received dedicated OAuth scopes `private-spaces:read` / `private-spaces:write` (ORB-1919). - -Key platform facts that constrain this design: - -- **No create/delete endpoints.** `POST /private-spaces` and `DELETE /private-spaces/{id}` - existed during development but were removed (ORB-1061). Lifecycle is all-or-nothing per - organization via `/organizations/{id}/private-spaces-settings` (out of scope here). -- **Feature-flagged and cloud-only.** When the org flag is off, endpoints fail with - `IM903`. There is no way to provision a private space through the public API. -- The SDK already covers the two teams touchpoints: `includePrivateSpaces` on - `teams.list()` and `Team.type` (`'personal' | 'standard'`). - -## Scope - -In scope (option A, agreed 2026-07-27): - -1. New `PrivateSpaces` endpoint class: `list`, `get`, `update`. -2. New `private-spaces` tool definitions (3 tools). -3. `privateSpaces` field on the `Organization` type. -4. Registration (`make.ts`, `index.ts`, `src/tools.ts`), tests, mocks, README. - -Out of scope (candidates for follow-up tickets): - -- `GET`/`PATCH /organizations/{organizationId}/private-spaces-settings` (admin bulk - operations; disabling auto-creation bulk-deletes all private spaces). -- `GET /users` list (not in the SDK at all) and its `privateSpace` col; `GET /users/by-permission`. -- Team-variables and llm-configuration route aliases mounted under `/private-spaces/{id}/…`. -- Phase 2 "Locked connections" ACL endpoints (flag-gated, not GA). - -## API contract (verified against imt-web-api code and OpenAPI spec) - -### GET /private-spaces — scope `private-spaces:read` - -Query: `organizationId` (number, **required**), `externalId` (string, optional filter), -`cols[]`, `pg[sortBy|sortDir|offset|limit]` (sortable by `name` only). -Response: `{ privateSpaces: PrivateSpace[], pg }`. - -Requires org permission `personal team manage`. - -### GET /private-spaces/{privateSpaceId} — scope `private-spaces:read` - -Query: `cols[]` — list cols plus `operations`, `transfer`, `centicredits` (usage totals -since last reset, computed from Elasticsearch, returned as strings; 503 when ES fails). -Response: `{ privateSpace }`. - -Requires org permission `personal team own view`; non-admin callers must be a member of -the space, otherwise **404**. - -### PATCH /private-spaces/{privateSpaceId} — scope `private-spaces:write` - -Body: `{ operationsLimit?: number | null }` — min 0; `null` removes the limit -(unlimited); omitted = unchanged; `transferLimit` is derived server-side. -Query: `confirmed` (boolean) — **required (else `IM004`) when the new limit is below the -space's current consumption; confirming pauses the space.** -Response: `{ privateSpace }` including `deleted` and `externalId`. - -Requires org permission `personal team manage`. - -### PrivateSpace fields - -| Field | Type | Availability | -| ------------------------ | --------------- | ------------------------------------ | -| `id` | number | default col | -| `name` | string | default col | -| `organizationId` | number | default col | -| `globalAgentsEnabled` | boolean | default col | -| `type` | `'personal'` | default col | -| `privateSpaceOwnerName` | string | default col | -| `privateSpaceOwnerEmail` | string | default col | -| `privateSpaceOwnerId` | number | default col | -| `operationsLimit` | number \| null | cols; null = unlimited | -| `transferLimit` | string \| number \| null | cols; bytes (string on list/get, number on update) | -| `consumedOperations` | number \| null | cols | -| `consumedTransfer` | string \| null | cols | -| `isPaused` | boolean \| null | cols; paused due to exceeded limits | -| `consumedCenticredits` | string \| null | cols; serialized as a string | -| `operations` | string | `get()` cols only (ES totals) | -| `transfer` | string | `get()` cols only (ES totals) | -| `centicredits` | string | `get()` cols only (ES totals) | -| `deleted` | boolean | admin col; present in PATCH response | -| `externalId` | string \| null | admin col; present in PATCH response | - -## Design - -### 1. `src/endpoints/private-spaces.ts` - -Follows the standard endpoint template (closest precedents: `teams.ts`, `scenarios.ts`). - -Types (exported unless noted): - -- `PrivateSpace` — **one entity type** for list/get/update. `id`, `name`, - `organizationId` required; everything else optional. Get-only usage cols and - admin/PATCH-only fields live on the same type with JSDoc noting availability. - Decision: matches the `Team` convention (one entity type mixing list and detail - fields); a split `PrivateSpaceWithUsage` type was considered and rejected as - non-idiomatic for this repo. -- `ListPrivateSpacesOptions` — `cols`, `pg` - (`Partial>`), `externalId?: string`. -- `GetPrivateSpaceOptions` — `cols`. -- `UpdatePrivateSpaceBody` — `{ operationsLimit?: number | null }`. -- `UpdatePrivateSpaceOptions` — `{ confirmed?: boolean }`. -- Internal (not exported): `ListPrivateSpacesResponse`, `GetPrivateSpaceResponse`, - `UpdatePrivateSpaceResponse`. - -Class `PrivateSpaces`: - -- `list(organizationId: number, options?: ListPrivateSpacesOptions): Promise[]>` - — GET `/private-spaces` with query `{ organizationId, externalId, cols, pg }`. -- `get(privateSpaceId: number, options?: GetPrivateSpaceOptions): Promise>` - — GET `/private-spaces/{privateSpaceId}`. -- `update(privateSpaceId: number, body: UpdatePrivateSpaceBody, options?: UpdatePrivateSpaceOptions): Promise` - — PATCH with query `{ confirmed: options?.confirmed }`; signature follows - `scenarios.update(id, body, { confirmed })`. - -JSDoc documents: no create/delete (org-settings-driven lifecycle), the `confirmed` -trap, the 404-for-non-members behavior, and null-vs-omitted `operationsLimit` semantics. - -### 2. `src/endpoints/private-spaces.tools.ts` - -Category `private-spaces`. All tools set explicit `readOnlyHint` / `destructiveHint` / -`openWorldHint` and document state traps in descriptions (repo convention since WM-4172). - -| Tool | Scope | scopeId / resourceId | Hints | Notes | -| ----------------------- | ---------------------- | ----------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `private-spaces_list` | `private-spaces:read` | `organizationId` / — | read-only | Params: `organizationId` (required), `externalId`. Description: requires the org's private-spaces feature and `personal team manage` permission. Executes with `cols: ['*']`. | -| `private-spaces_get` | `private-spaces:read` | `privateSpaceId` / `privateSpaceId` | read-only | Description: non-members receive 404; usage totals (`operations`, `transfer`, `centicredits`) come from analytics storage. Executes with `cols: ['*']`. | -| `private-spaces_update` | `private-spaces:write` | `privateSpaceId` / `privateSpaceId` | not read-only, not destructive, idempotent | Params: `privateSpaceId` (required), `operationsLimit` (`oneOf: [{ type: 'number' }, { type: 'null' }]`, null = unlimited), `confirmed` (boolean; description: required when lowering the limit below current consumption — confirming pauses the space). | - -### 3. `src/endpoints/organizations.ts` - -Add to `Organization`: - -```ts -/** Private spaces the requesting user is a member of (cols-selectable; cloud only). - * `hasAdminVisibility` mirrors the organization's "add admins as observers" setting. */ -privateSpaces?: { id: number; name: string; isOwner: boolean; hasAdminVisibility: boolean }[]; -``` - -Type-only change; exercised by extending the organizations get mock + a cols assertion. - -### 4. Registration and docs - -- `src/make.ts`: import, `public readonly privateSpaces: PrivateSpaces`, constructor - init, JSDoc. -- `src/index.ts`: export `PrivateSpace`, `PrivateSpaces`, `ListPrivateSpacesOptions`, - `GetPrivateSpaceOptions`, `UpdatePrivateSpaceBody`, `UpdatePrivateSpaceOptions`. -- `src/tools.ts`: import and spread `PrivateSpacesTools` into `MakeTools`. -- `README.md`: add `privateSpaces` to the endpoint list and `private-spaces` to the tool - categories. - -## Testing - -TDD throughout (red → green per behavior). - -- `test/private-spaces.spec.ts` + `test/mocks/private-spaces/{list,get,update}.json` - (realistic data matching the field table above): - - list: response unwrapping; query assertion for `organizationId` and `externalId`. - - list: column selection (`cols`) round-trip. - - get: response unwrapping; get-only usage cols present in mock. - - update: body assertion (`operationsLimit`, including `null`), `confirmed=true` in - query, `content-type: application/json`. -- `test/organizations.spec.ts` + mock: extend get mock with `privateSpaces` and assert - it round-trips. -- `test/private-spaces.integration.test.ts`: lists spaces for `MAKE_ORGANIZATION`; - **skips gracefully when none exist** (no public API to provision one; feature is - flag-gated). When a space exists: `get()` it, `update()` the operations limit and - restore the original value. -- Coverage floor: ≥90% line/branch on touched files. - -## Error handling - -No special handling — `IM903` (feature disabled), `IM004` (confirmation required), 404 -(non-member), and 503 (ES unavailable) bubble up as `MakeError`, per repo convention. -The tool descriptions carry the guidance instead. From b70f4f029515a6a36715cf8cfb099e8d8bc0d0a1 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 17:03:30 +0200 Subject: [PATCH 15/16] fix(private-spaces): resolve round-1 Copilot PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - (r3658325414) private-spaces.integration.test.ts: skip restore when either original limit or consumption is unobserved (&& -> ||), and only send confirmed:true when the restored limit is below consumption - (r3658325454) private-spaces.tools.ts: mark private-spaces_update with destructiveHint:true — it can lower limits and pause a space, matching the repo convention for update tools Co-Authored-By: Claude Fable 5 --- src/endpoints/private-spaces.tools.ts | 2 +- test/private-spaces.integration.test.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/endpoints/private-spaces.tools.ts b/src/endpoints/private-spaces.tools.ts index 0bae7bc..da28162 100644 --- a/src/endpoints/private-spaces.tools.ts +++ b/src/endpoints/private-spaces.tools.ts @@ -69,7 +69,7 @@ export const tools: MakeTool[] = [ resourceId: 'privateSpaceId', annotations: { readOnlyHint: false, - destructiveHint: false, + destructiveHint: true, idempotentHint: true, openWorldHint: false, }, diff --git a/test/private-spaces.integration.test.ts b/test/private-spaces.integration.test.ts index 433ba94..c9bd0e4 100644 --- a/test/private-spaces.integration.test.ts +++ b/test/private-spaces.integration.test.ts @@ -39,8 +39,8 @@ describe('Integration: PrivateSpaces', () => { it('Should update a private space and restore the original limit', async () => { if (privateSpaceId === undefined) return; - // get() never ran or failed — restoring would overwrite an unobserved limit. - if (originalOperationsLimit === undefined && consumedOperations === undefined) return; + // Either value missing means get() never ran or failed — we can't safely reconstruct the original state. + if (originalOperationsLimit === undefined || consumedOperations === undefined) return; // Stay above current consumption so the update needs no confirmation // and cannot pause the space. @@ -49,10 +49,12 @@ describe('Integration: PrivateSpaces', () => { const updated = await make.privateSpaces.update(privateSpaceId, { operationsLimit: safeLimit }); expect(updated.operationsLimit).toBe(safeLimit); + // Confirmation is only needed (and only pauses) when the restored limit is below consumption. + const needsConfirm = (originalOperationsLimit ?? Infinity) < (consumedOperations ?? 0); const restored = await make.privateSpaces.update( privateSpaceId, { operationsLimit: originalOperationsLimit ?? null }, - { confirmed: true }, + needsConfirm ? { confirmed: true } : {}, ); expect(restored.operationsLimit).toBe(originalOperationsLimit ?? null); }); From e459b059b256c4f5eb2b80004271e559b92e91c9 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Mon, 27 Jul 2026 17:23:13 +0200 Subject: [PATCH 16/16] =?UTF-8?q?fix(private-spaces):=20checker=20findings?= =?UTF-8?q?=20=E2=80=94=20self-review=20round=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - integration test: try/finally guarantees limit restore even when the post-update assertion fails (live, billing-relevant state) - integration test: prefer the caller's own space via users.me() (get() 404s for non-members; update() mutates live state) - tools: fix misleading update example — confirmed:true now paired with a below-consumption limit, null-limit example standalone - mocks: realistic list.json shape (limits/consumption incl. string transferLimit/consumedCenticredits); organizations list mock now round-trips the privateSpaces column - tools spec: cover externalId filter and null-limit/no-confirm branches Co-Authored-By: Claude Fable 5 --- src/endpoints/private-spaces.tools.ts | 3 ++- test/mocks/organizations/list.json | 10 +++++++- test/mocks/private-spaces/list.json | 16 ++++++++++-- test/private-spaces-tools.spec.ts | 23 +++++++++++++++++ test/private-spaces.integration.test.ts | 33 +++++++++++++------------ 5 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/endpoints/private-spaces.tools.ts b/src/endpoints/private-spaces.tools.ts index da28162..7ea7e26 100644 --- a/src/endpoints/private-spaces.tools.ts +++ b/src/endpoints/private-spaces.tools.ts @@ -92,7 +92,8 @@ export const tools: MakeTool[] = [ }, examples: [ { privateSpaceId: 101, operationsLimit: 10000 }, - { privateSpaceId: 101, operationsLimit: null, confirmed: true }, + { privateSpaceId: 101, operationsLimit: 50, confirmed: true }, + { privateSpaceId: 101, operationsLimit: null }, ], execute: async ( make: Make, diff --git a/test/mocks/organizations/list.json b/test/mocks/organizations/list.json index 8995084..a1a18bc 100644 --- a/test/mocks/organizations/list.json +++ b/test/mocks/organizations/list.json @@ -3,7 +3,15 @@ { "id": 5, "name": "New organization", - "timezoneId": 113 + "timezoneId": 113, + "privateSpaces": [ + { + "id": 101, + "name": "Becca's space", + "isOwner": true, + "hasAdminVisibility": false + } + ] }, { "id": 6, diff --git a/test/mocks/private-spaces/list.json b/test/mocks/private-spaces/list.json index dd6b3aa..66f9fc8 100644 --- a/test/mocks/private-spaces/list.json +++ b/test/mocks/private-spaces/list.json @@ -8,7 +8,13 @@ "type": "personal", "privateSpaceOwnerName": "Becca Smith", "privateSpaceOwnerEmail": "becca.smith@example.com", - "privateSpaceOwnerId": 42 + "privateSpaceOwnerId": 42, + "operationsLimit": 1000, + "transferLimit": "536870912", + "consumedOperations": 250, + "consumedTransfer": "52428800", + "isPaused": false, + "consumedCenticredits": "12345" }, { "id": 102, @@ -18,7 +24,13 @@ "type": "personal", "privateSpaceOwnerName": "Jan Novak", "privateSpaceOwnerEmail": "jan.novak@example.com", - "privateSpaceOwnerId": 43 + "privateSpaceOwnerId": 43, + "operationsLimit": null, + "transferLimit": null, + "consumedOperations": 25, + "consumedTransfer": "1048576", + "isPaused": false, + "consumedCenticredits": "1200" } ], "pg": { diff --git a/test/private-spaces-tools.spec.ts b/test/private-spaces-tools.spec.ts index b4735e0..fc31802 100644 --- a/test/private-spaces-tools.spec.ts +++ b/test/private-spaces-tools.spec.ts @@ -62,4 +62,27 @@ describe('MCP tools: private-spaces', () => { expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); }); + + it('Should execute private-spaces_list with externalId filter', async () => { + mockFetch( + `GET https://make.local/api/v2/private-spaces?organizationId=${ORGANIZATION_ID}&externalId=ext-1&cols%5B%5D=*`, + privateSpacesListMock, + ); + + const tool = getTool('private-spaces_list'); + const result = await tool.execute(make, { organizationId: ORGANIZATION_ID, externalId: 'ext-1' }); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should execute private-spaces_update without confirmation and null limit', async () => { + mockFetch(`PATCH https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}`, privateSpaceUpdateMock, req => { + expect(req.body).toStrictEqual({ operationsLimit: null }); + }); + + const tool = getTool('private-spaces_update'); + const result = await tool.execute(make, { privateSpaceId: PRIVATE_SPACE_ID, operationsLimit: null }); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + }); }); diff --git a/test/private-spaces.integration.test.ts b/test/private-spaces.integration.test.ts index c9bd0e4..d34b40a 100644 --- a/test/private-spaces.integration.test.ts +++ b/test/private-spaces.integration.test.ts @@ -18,9 +18,9 @@ describe('Integration: PrivateSpaces', () => { expect(Array.isArray(spaces)).toBe(true); - // No public API can provision a private space, so downstream tests are - // skipped (early return) when the organization has none. - privateSpaceId = spaces[0]?.id; + // Prefer the caller's own space: get() returns 404 for non-admin non-members, and update() mutates live state. + const me = await make.users.me(); + privateSpaceId = (spaces.find(space => space.privateSpaceOwnerEmail === me.email) ?? spaces[0])?.id; }); it('Should get a private space', async () => { @@ -42,20 +42,21 @@ describe('Integration: PrivateSpaces', () => { // Either value missing means get() never ran or failed — we can't safely reconstruct the original state. if (originalOperationsLimit === undefined || consumedOperations === undefined) return; - // Stay above current consumption so the update needs no confirmation - // and cannot pause the space. + // Stay above current consumption so the update needs no confirmation and cannot pause the space. const safeLimit = Math.max(consumedOperations ?? 0, originalOperationsLimit ?? 0) + 10000; - const updated = await make.privateSpaces.update(privateSpaceId, { operationsLimit: safeLimit }); - expect(updated.operationsLimit).toBe(safeLimit); - - // Confirmation is only needed (and only pauses) when the restored limit is below consumption. - const needsConfirm = (originalOperationsLimit ?? Infinity) < (consumedOperations ?? 0); - const restored = await make.privateSpaces.update( - privateSpaceId, - { operationsLimit: originalOperationsLimit ?? null }, - needsConfirm ? { confirmed: true } : {}, - ); - expect(restored.operationsLimit).toBe(originalOperationsLimit ?? null); + try { + const updated = await make.privateSpaces.update(privateSpaceId, { operationsLimit: safeLimit }); + expect(updated.operationsLimit).toBe(safeLimit); + } finally { + // Restore even when the assertion above fails — the limit is live, billing-relevant state. + const needsConfirm = (originalOperationsLimit ?? Infinity) < (consumedOperations ?? 0); + const restored = await make.privateSpaces.update( + privateSpaceId, + { operationsLimit: originalOperationsLimit ?? null }, + needsConfirm ? { confirmed: true } : {}, + ); + expect(restored.operationsLimit).toBe(originalOperationsLimit ?? null); + } }); });