Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,37 @@ project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [2.7.1] - 2026-09-19

### Fixed
- `client.ai` and `client.mockups` are pinned to the paths they were published
on: `/api/v1/sudoai/2d-mockups` and `/api/v1/mockups`. 2.7.0 routed them to
the family paths, so upgrading the package alone moved a caller's new jobs to
the `photo_mockup_*` kinds without an error anywhere, and code branching on
`job.kind === '2d_create'` stopped matching in silence. An async job opened
through an earlier name again resolves with `2d_create` / `2d_render`, as
2.6.x documented. `client.photoMockups` and `client.psdMockups` keep the
family paths and the `photo_mockup_*` kinds; move to them to adopt the new
names deliberately.
- `client.ai` and `client.mockups` accept assignment. They were getter-only, so
injecting a test double threw a `TypeError` in an ES module and was dropped
without a word in plain CommonJS -- where the call then left the test and
reached the live API. Assigning to either now replaces only that accessor;
`client.photoMockups` / `client.psdMockups` are untouched.

### Documentation
- The deprecation warning no longer calls an earlier name and its family name
"the same object". They are two objects, each on the path its own name was
published on, and the warning now says so -- the earlier wording read as a
free swap and sent readers into the `2d_*` / `photo_mockup_*` kind change the
release above is about.
- `WebhookEventNaming` and `CreateWebhookEndpointParams.eventNaming` describe
the real default. An endpoint you do not pin yourself follows the spelling of
the `eventTypes` you subscribe in: family names give `current`, the earlier
`2d_mockup.*` / `2d_render.*` names give `legacy`, and an empty or mixed list
gives `legacy`. The docs said a new endpoint was always pinned to `current`.
Behaviour is the API's; nothing in this package changed.

## [2.7.0] - 2026-09-19

### Added
Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ The API key can be passed as the first argument or via the `SUDOMOCK_API_KEY` en
Every request identifies itself with `X-SudoMock-Client: node-sdk/<version>` (and the same value as `User-Agent` where the runtime allows it), so you can pick the SDK's traffic out of your own logs and proxy rules.

`client.ai` and `client.mockups` are the earlier names of `client.photoMockups`
and `client.psdMockups`. They return the same objects and warn once per process.
and `client.psdMockups`. Each stays on the endpoint it was published on, so an
async job you open through an earlier name comes back under the `2d_*` kind it
always did, and each warns once per process. Switch to `client.photoMockups` /
`client.psdMockups` to move to the family paths and the `photo_mockup_*` kinds.
Both earlier names accept assignment, so a test double can replace them.

### PSD Mockups (`client.psdMockups`)

Expand Down Expand Up @@ -553,10 +557,13 @@ const events = await client.webhooks.listEvents({ status: 'failed', limit: 100 }
Photo-mockup events are `photo_mockup.ready` / `photo_mockup.rejected` /
`photo_mockup.failed` and `photo_mockup_render.succeeded` /
`photo_mockup_render.failed`. Each endpoint is pinned to one spelling
(`eventNaming`): a new endpoint receives these names; an endpoint created
before they existed keeps receiving `2d_mockup.*` / `2d_render.*` until you
re-pin it. Subscribe with either spelling and the delivery carries the
endpoint's own.
(`eventNaming`). Pin a new endpoint yourself on create, or leave `eventNaming`
out and the pin follows the spelling you subscribed in: a list in these names
pins the endpoint to `current`, a list in the earlier `2d_mockup.*` /
`2d_render.*` names pins it to `legacy`, and an empty or mixed list pins it to
`legacy`. An endpoint created before these names existed keeps receiving
`2d_mockup.*` / `2d_render.*` until you re-pin it. Subscribe with either
spelling and the delivery carries the endpoint's own.

```typescript
// A handler that still expects the legacy names:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sudomock",
"version": "2.7.0",
"version": "2.7.1",
"description": "Official Node.js/TypeScript SDK for the SudoMock API - mockup generation from PSD files",
"type": "module",
"main": "./dist/index.cjs",
Expand Down
43 changes: 33 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { deprecate } from 'node:util'
import { HttpClient } from './client'
import { SudoMockError } from './errors'
import { MockupsResource } from './resources/mockups'
import { EARLIER_PSD_MOCKUPS_PATH, MockupsResource } from './resources/mockups'
import { RendersResource } from './resources/renders'
import { AIResource } from './resources/ai'
import { AIResource, EARLIER_PHOTO_MOCKUPS_PATH } from './resources/ai'
import { ImagesResource } from './resources/images'
import { UploadsResource } from './resources/uploads'
import { AccountResource } from './resources/account'
Expand All @@ -16,15 +16,15 @@ const DEFAULT_BASE_URL = 'https://api.sudomock.com'
const DEFAULT_TIMEOUT = 30_000
const DEFAULT_MAX_RETRIES = 2

// Earlier accessor names. Each warns once per process, then behaves exactly
// like the name it stands for.
// Earlier accessor names. Each warns once per process, then serves the same
// methods on the path its own name was published on.
const warnAi = deprecate(
() => {},
'client.ai is deprecated, use client.photoMockups (the same object).',
'client.ai is deprecated, use client.photoMockups. client.ai keeps the path it was published on and the 2d_* job kinds; client.photoMockups is the photo-mockup family path.',
)
const warnMockups = deprecate(
() => {},
'client.mockups is deprecated, use client.psdMockups (the same object).',
'client.mockups is deprecated, use client.psdMockups. client.mockups keeps the path it was published on; client.psdMockups is the PSD-mockup family path.',
)

/**
Expand Down Expand Up @@ -70,6 +70,12 @@ class SudoMock {
/** Manage webhook endpoints and their deliveries */
readonly webhooks: WebhooksResource

// Backing objects for the earlier accessor names. They speak to the paths
// those names were published on, so an upgrade alone never moves a caller's
// work to a different family. Writable, so a test double can replace them.
private earlierPhotoMockups: AIResource
private earlierPsdMockups: MockupsResource

constructor(apiKey?: string, options: SudoMockOptions = {}) {
const resolvedKey = apiKey ?? options.apiKey ?? process.env['SUDOMOCK_API_KEY'] ?? ''

Expand All @@ -95,18 +101,35 @@ class SudoMock {
this.studio = new StudioResource(client)
this.jobs = new JobsResource(client)
this.webhooks = new WebhooksResource(client)

this.earlierPhotoMockups = new AIResource(client, EARLIER_PHOTO_MOCKUPS_PATH)
this.earlierPsdMockups = new MockupsResource(client, EARLIER_PSD_MOCKUPS_PATH)
}

/** @deprecated Earlier name of {@link SudoMock.psdMockups}. The same object; warns once per process. */
/**
* @deprecated Earlier name of {@link SudoMock.psdMockups}. Same methods, on
* the path this name was published on; warns once per process.
*/
get mockups(): MockupsResource {
warnMockups()
return this.psdMockups
return this.earlierPsdMockups
}

set mockups(resource: MockupsResource) {
this.earlierPsdMockups = resource
}

/** @deprecated Earlier name of {@link SudoMock.photoMockups}. The same object; warns once per process. */
/**
* @deprecated Earlier name of {@link SudoMock.photoMockups}. Same methods, on
* the path this name was published on; warns once per process.
*/
get ai(): AIResource {
warnAi()
return this.photoMockups
return this.earlierPhotoMockups
}

set ai(resource: AIResource) {
this.earlierPhotoMockups = resource
}
}

Expand Down
23 changes: 16 additions & 7 deletions src/resources/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,17 @@ function wirePlacement(
* manage that catalog. Creation costs 25 credits. Rendering costs 5 credits
* per call.
*/
/** Path the photo-mockup family is served on. */
export const PHOTO_MOCKUPS_PATH = '/api/v1/photo-mockups'
/** Path `client.ai` was published on; it still answers, with the job kinds and
* event names a caller pinned to that accessor already branches on. */
export const EARLIER_PHOTO_MOCKUPS_PATH = '/api/v1/sudoai/2d-mockups'

export class AIResource {
constructor(private readonly client: HttpClient) {}
constructor(
private readonly client: HttpClient,
private readonly basePath: string = PHOTO_MOCKUPS_PATH,
) {}

/**
* Create a reusable 2D mockup from a public URL or base64 image.
Expand Down Expand Up @@ -211,7 +220,7 @@ export class AIResource {
TwoDMockupDetails | Job
>({
method: 'POST',
path: '/api/v1/photo-mockups',
path: this.basePath,
body,
headers: { 'Idempotency-Key': idempotencyKey },
})
Expand Down Expand Up @@ -352,7 +361,7 @@ export class AIResource {
AIRenderResult | Job
>({
method: 'POST',
path: `/api/v1/photo-mockups/${params.mockupId}/render`,
path: `${this.basePath}/${params.mockupId}/render`,
body,
timeout: AI_RENDER_TIMEOUT,
})
Expand Down Expand Up @@ -400,7 +409,7 @@ export class AIResource {
offset?: number
}>({
method: 'GET',
path: '/api/v1/photo-mockups',
path: this.basePath,
query: {
limit: params.limit,
offset: params.offset,
Expand All @@ -423,7 +432,7 @@ export class AIResource {
async get(mockupId: string): Promise<TwoDMockupDetails> {
const mockup = await this.client.request<TwoDMockupDetails>({
method: 'GET',
path: `/api/v1/photo-mockups/${mockupId}`,
path: `${this.basePath}/${mockupId}`,
})
return publicMockupDetails(mockup)
}
Expand All @@ -444,7 +453,7 @@ export class AIResource {

const result = await this.client.request<Update2DPrintAreasResult>({
method: 'PUT',
path: `/api/v1/photo-mockups/${mockupId}/print-areas`,
path: `${this.basePath}/${mockupId}/print-areas`,
body: { printAreas },
})
return {
Expand All @@ -459,7 +468,7 @@ export class AIResource {
async delete(mockupId: string): Promise<void> {
await this.client.request<void>({
method: 'DELETE',
path: `/api/v1/photo-mockups/${mockupId}`,
path: `${this.basePath}/${mockupId}`,
})
}
}
18 changes: 13 additions & 5 deletions src/resources/mockups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,16 @@ export function toPublicMockup(mockup: Mockup): Mockup {
}
}

/** Path the PSD-mockup family is served on. */
export const PSD_MOCKUPS_PATH = '/api/v1/psd-mockups'
/** Path `client.mockups` was published on; it still answers. */
export const EARLIER_PSD_MOCKUPS_PATH = '/api/v1/mockups'

export class MockupsResource {
constructor(private readonly client: HttpClient) {}
constructor(
private readonly client: HttpClient,
private readonly basePath: string = PSD_MOCKUPS_PATH,
) {}

/**
* List mockups with pagination and filtering.
Expand All @@ -88,7 +96,7 @@ export class MockupsResource {
async list(params: ListMockupsParams = {}): Promise<MockupListResult> {
const result = await this.client.request<MockupListResult>({
method: 'GET',
path: '/api/v1/psd-mockups',
path: this.basePath,
query: {
limit: params.limit,
offset: params.offset,
Expand Down Expand Up @@ -119,7 +127,7 @@ export class MockupsResource {
async get(uuid: string): Promise<Mockup> {
const mockup = await this.client.request<Mockup>({
method: 'GET',
path: `/api/v1/psd-mockups/${uuid}`,
path: `${this.basePath}/${uuid}`,
})
return toPublicMockup(mockup)
}
Expand All @@ -135,7 +143,7 @@ export class MockupsResource {
async update(uuid: string, params: { name: string }): Promise<Mockup> {
const mockup = await this.client.request<Mockup>({
method: 'PATCH',
path: `/api/v1/psd-mockups/${uuid}`,
path: `${this.basePath}/${uuid}`,
body: params,
})
return toPublicMockup(mockup)
Expand All @@ -152,7 +160,7 @@ export class MockupsResource {
async delete(uuid: string): Promise<void> {
await this.client.request<void>({
method: 'DELETE',
path: `/api/v1/psd-mockups/${uuid}`,
path: `${this.basePath}/${uuid}`,
})
}
}
16 changes: 11 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1023,8 +1023,13 @@ export type WebhookEvent =
* - `'legacy'` -- `2d_mockup.*` / `2d_render.*`; the payload's `kind` is
* `2d_create` / `2d_render`.
*
* A new endpoint is pinned to `'current'` unless created otherwise; an endpoint
* that predates the current names stays on `'legacy'` until re-pinned.
* Pass it on create to pin an endpoint yourself. Left out, the pin follows the
* spelling of the event types you subscribe to: a list written in the family
* names (`photo_mockup.*` / `photo_mockup_render.*`) pins the endpoint to
* `'current'`, a list written in the earlier names (`2d_mockup.*` /
* `2d_render.*`) to `'legacy'`, and an empty or mixed list to `'legacy'` --
* so subscribing the way your handler already reads keeps it reading. An
* endpoint that predates the current names stays on `'legacy'` until re-pinned.
*/
export type WebhookEventNaming = 'legacy' | 'current'

Expand Down Expand Up @@ -1064,9 +1069,10 @@ export interface CreateWebhookEndpointParams {
/** Optional description. */
description?: string
/**
* Which spelling of the photo-mockup events this endpoint receives. Omit to
* take the API default (`'current'`); pass `'legacy'` for a handler that
* still expects `2d_mockup.*` / `2d_render.*`.
* Which spelling of the photo-mockup events this endpoint receives. Omit it
* and the pin follows `eventTypes`: family names give `'current'`, the
* earlier `2d_mockup.*` / `2d_render.*` names give `'legacy'`, an empty or
* mixed list gives `'legacy'`. Pass it to pin the endpoint yourself.
*/
eventNaming?: WebhookEventNaming
}
Expand Down
Loading
Loading