From c2a6a649519ec47e17f86b7ad9e1059dfe2122f9 Mon Sep 17 00:00:00 2001 From: vishal Date: Mon, 29 Jun 2026 16:19:08 +0530 Subject: [PATCH 1/5] feat: add OAuth Bearer access token authentication - add accessToken client configuration option - send Authorization: Bearer when accessToken is provided - keep existing x-api-key authentication unchanged - allow client initialization without an API key when using OAuth --- src/core/SVGMakerClient.ts | 8 +++++--- src/types/config.ts | 7 +++++++ src/utils/httpClient.ts | 13 +++++++++++-- tests/SVGMakerClient.test.ts | 10 ++++++++-- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/core/SVGMakerClient.ts b/src/core/SVGMakerClient.ts index af57c1b..5099ece 100644 --- a/src/core/SVGMakerClient.ts +++ b/src/core/SVGMakerClient.ts @@ -113,11 +113,13 @@ export class SVGMakerClient { * @param config Additional configuration options */ constructor(apiKey: string, config: Partial = {}) { - if (!apiKey) { - throw new ValidationError('API key is required'); + if (!apiKey && !config.accessToken) { + throw new ValidationError('Either an API key or an access token is required'); } - // Merge default config with provided config + // Merge default config with provided config. + // The explicit `apiKey` argument wins over any `apiKey` in `config`, + // while an OAuth `accessToken` from `config` is preserved (different key). this.config = { ...DEFAULT_CONFIG, ...config, diff --git a/src/types/config.ts b/src/types/config.ts index d5cc074..3fe233d 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -7,6 +7,13 @@ export interface SVGMakerConfig { */ apiKey: string; + /** + * OAuth Bearer access token (JWT). + * When set, this takes precedence over `apiKey` and is sent as an + * `Authorization: Bearer ` header instead of `x-api-key`. + */ + accessToken?: string; + /** * Base URL for the SVGMaker API * @default "https://api.svgmaker.io" diff --git a/src/utils/httpClient.ts b/src/utils/httpClient.ts index 0bc8e38..913dc94 100644 --- a/src/utils/httpClient.ts +++ b/src/utils/httpClient.ts @@ -295,10 +295,19 @@ export class HttpClient { const headers: Record = { Accept: 'application/json', 'Content-Type': 'application/json', - 'x-api-key': this.config.apiKey, - ...(options.headers as Record), }; + // Prefer the OAuth Bearer access token when present; otherwise fall back + // to the API key sent via the `x-api-key` header. + if (this.config.accessToken) { + headers['Authorization'] = `Bearer ${this.config.accessToken}`; + } else { + headers['x-api-key'] = this.config.apiKey; + } + + // Let per-request headers override the defaults above. + Object.assign(headers, options.headers as Record); + return headers; } diff --git a/tests/SVGMakerClient.test.ts b/tests/SVGMakerClient.test.ts index 8135017..bd45af9 100644 --- a/tests/SVGMakerClient.test.ts +++ b/tests/SVGMakerClient.test.ts @@ -8,9 +8,15 @@ describe('SVGMakerClient', () => { }); describe('constructor', () => { - it('should throw an error if no API key is provided', () => { + it('should throw an error if no API key or access token is provided', () => { expect(() => new SVGMakerClient('')).toThrow(ValidationError); - expect(() => new SVGMakerClient('')).toThrow('API key is required'); + expect(() => new SVGMakerClient('')).toThrow('Either an API key or an access token is required'); + }); + + it('should create a client when only an access token is provided', () => { + const client = new SVGMakerClient('', { accessToken: 'test-access-token' }); + expect(client).toBeInstanceOf(SVGMakerClient); + expect(client.getConfig().accessToken).toBe('test-access-token'); }); it('should create a client with default configuration', () => { From f1e4fc9bd801da59f691e2e390bd19cd56b475fe Mon Sep 17 00:00:00 2001 From: vishal Date: Fri, 10 Jul 2026 15:14:12 +0530 Subject: [PATCH 2/5] feat: add removeBackground client and bearer auth for form-data endpoints Add a top-level removeBackground client (c.removeBackground) that posts to /v1/remove-background and returns a transparent SVG, mirroring the convert/aiVectorize builder (configure/execute + stream). Complete OAuth bearer support for form-data endpoints: the earlier accessToken work only reached the JSON path (HttpClient.buildHeaders), so form-data requests (edit, convert, remove-background) still sent an empty x-api-key and no Authorization header over bearer. Add a shared BaseClient.buildAuthHeaders() (bearer when accessToken is set, else x-api-key) and use it in executeFormDataRequest and the streaming path, so all transports authenticate consistently. - add RemoveBackgroundClient + RemoveBackground types + core wiring/export - add BaseClient.buildAuthHeaders() and apply to form-data + stream paths - bump version to 1.2.0 --- package-lock.json | 4 +- package.json | 2 +- src/clients/BaseClient.ts | 15 +- src/clients/RemoveBackgroundClient.ts | 249 ++++++++++++++++++++++++++ src/core/SVGMakerClient.ts | 7 + src/index.ts | 2 + src/types/api.ts | 33 ++++ 7 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 src/clients/RemoveBackgroundClient.ts diff --git a/package-lock.json b/package-lock.json index 8b4a6ef..2a0ac3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@genwave/svgmaker-sdk", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@genwave/svgmaker-sdk", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "async-retry": "^1.3.3", diff --git a/package.json b/package.json index dcaa0d6..a6c1330 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@genwave/svgmaker-sdk", - "version": "1.1.0", + "version": "1.2.0", "description": "Official Node.js SDK for SVGMaker API", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/src/clients/BaseClient.ts b/src/clients/BaseClient.ts index 923dec0..7003df7 100644 --- a/src/clients/BaseClient.ts +++ b/src/clients/BaseClient.ts @@ -170,6 +170,19 @@ export abstract class BaseClient { return mimeTypes[ext] || 'application/octet-stream'; } + /** + * Build the authentication header for raw fetch calls (form-data and streaming + * paths that bypass HttpClient.buildHeaders). Prefers the OAuth Bearer access + * token when present; otherwise falls back to the `x-api-key` header. Mirrors + * the logic in HttpClient so all transports authenticate consistently. + */ + protected buildAuthHeaders(): Record { + if (this.config.accessToken) { + return { Authorization: `Bearer ${this.config.accessToken}` }; + } + return { 'x-api-key': this.config.apiKey }; + } + /** * Execute a POST request with FormData body and unwrap the v1 API envelope * @param endpoint API endpoint path (e.g., '/v1/convert/trace') @@ -183,7 +196,7 @@ export abstract class BaseClient { const response = await fetch(`${this.config.baseUrl}${endpoint}`, { method: 'POST', headers: { - 'x-api-key': this.config.apiKey, + ...this.buildAuthHeaders(), }, body: formData, }); diff --git a/src/clients/RemoveBackgroundClient.ts b/src/clients/RemoveBackgroundClient.ts new file mode 100644 index 0000000..e4d9b44 --- /dev/null +++ b/src/clients/RemoveBackgroundClient.ts @@ -0,0 +1,249 @@ +import { BaseClient } from './BaseClient'; +import { + RemoveBackgroundParams, + RemoveBackgroundResponse, + RemoveBackgroundStreamEvent, +} from '../types/api'; +import { SVGMakerClient } from '../core/SVGMakerClient'; +import { z } from 'zod'; +import { Readable } from 'stream'; +import { decodeSvgContent } from '../utils/base64'; + +/** + * Schema for validating remove background parameters + */ +const removeBackgroundParamsSchema = z.object({ + file: z.union([z.string(), z.instanceof(Buffer), z.instanceof(Readable)]), + stream: z.boolean().optional(), + svgText: z.boolean().optional(), + storage: z.boolean().optional(), +}); + +/** + * Client for the Remove Background API + * + * Removes the background from an image and returns the result as an SVG with + * transparency. Accepts any raster image (PNG, JPEG, WebP, etc.) or SVG. + */ +export class RemoveBackgroundClient extends BaseClient { + private params: Partial = {}; + + /** + * Create a new Remove Background client + * @param client Parent SVGMaker client + */ + constructor(client: SVGMakerClient) { + super(client); + } + + /** + * Execute the Remove Background request + * @returns Remove Background response + */ + public async execute(): Promise { + this.logger.debug('Starting background removal', { + hasFile: !!this.params.file, + svgTextRequested: !!this.params.svgText, + }); + + // Validate parameters + this.validateRequest(this.params, removeBackgroundParamsSchema); + + // Prepare form data + const formData = new FormData(); + + // Add file + await this.addFileToForm(formData, 'file', this.params.file!); + + // Add optional parameters + this.appendOptionalParams(formData, this.params as Record, [ + 'storage', + 'stream', + 'svgText', + ]); + + // Execute request + const { data, metadata: responseMetadata } = await this.executeFormDataRequest( + '/v1/remove-background', + formData + ); + + this.logger.debug('Background removal completed', { + creditCost: data.creditCost, + hasSvgText: !!data.svgText, + }); + + // Normalize svgText (API now sends raw SVG text, but we handle legacy base64 too) + let svgText: string | undefined = undefined; + if (data.svgText && typeof data.svgText === 'string') { + svgText = decodeSvgContent(data.svgText); + } + + return { + svgUrl: data.svgUrl, + creditCost: data.creditCost, + message: data.message ?? '', + svgUrlExpiresIn: data.svgUrlExpiresIn, + generationId: data.generationId, + metadata: responseMetadata, + svgText, + } as RemoveBackgroundResponse; + } + + /** + * Configure the remove background parameters + * @param config Configuration object with remove background parameters + * @returns New client instance + */ + public configure(config: Partial): RemoveBackgroundClient { + this.logger.debug('Configuring remove background parameters', { config }); + + const client = this.clone(); + client.params = { ...client.params, ...config }; + return client; + } + + /** + * Stream the remove background response + * @returns Readable stream of events + */ + public stream(): Readable { + // Create a clone with streaming enabled + const client = this.clone(); + client.params.stream = true; + + // Validate parameters + this.validateRequest(client.params, removeBackgroundParamsSchema); + + // Create a readable stream for the events + const stream = new Readable({ + objectMode: true, + read() {}, + }); + + // Execute the request and handle streaming + (async () => { + try { + // Prepare form data + const formData = new FormData(); + + // Add file + await this.addFileToForm(formData, 'file', client.params.file!); + + // Add storage option if present + if (client.params.storage !== undefined) { + formData.append('storage', String(client.params.storage)); + } + + // Add stream option + formData.append('stream', 'true'); + + if (client.params.svgText) { + formData.append('svgText', String(client.params.svgText)); + } + + // Make request to the streaming endpoint using native fetch. Auth headers + // prefer the OAuth Bearer token when present, else the x-api-key. + const response = await fetch(`${this.config.baseUrl}/v1/remove-background`, { + method: 'POST', + headers: { + Accept: 'text/event-stream', + ...this.buildAuthHeaders(), + }, + body: formData, + }); + + if (!response.ok) { + await this.handleFetchErrorResponse(response); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('Response body is not readable'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + const accumulated: Record = {}; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + // Decode the chunk and add to buffer + buffer += decoder.decode(value, { stream: true }); + + // Split by newlines to get individual JSON objects + const lines = buffer.split('\n'); + + // Keep the last incomplete line in the buffer + buffer = lines.pop() || ''; + + // Process each complete line + for (const line of lines) { + const trimmedLine = line.trim(); + if (trimmedLine === '') continue; + + try { + // Parse the JSON chunk directly (no "data:" prefix like SSE) + const event = JSON.parse(trimmedLine) as RemoveBackgroundStreamEvent; + // Normalize svgText (API now sends raw SVG text, but we handle legacy base64 too) + if (event.svgText && typeof event.svgText === 'string') { + event.svgText = decodeSvgContent(event.svgText); + } + // Accumulate fields from all events + for (const [key, value] of Object.entries(event)) { + if (value !== undefined && key !== 'status' && key !== 'message') { + accumulated[key] = value; + } + } + + // When complete, merge accumulated fields into the event + if (event.status === 'complete' || event.status === 'error') { + const mergedEvent = { ...accumulated, ...event }; + stream.push(mergedEvent); + stream.push(null); + return; + } + + stream.push(event); + } catch (e) { + console.error('Error parsing streaming chunk:', e); + console.error('Problematic line:', trimmedLine); + } + } + } + + // Process any remaining data in buffer + if (buffer.trim()) { + try { + const event = JSON.parse(buffer.trim()) as RemoveBackgroundStreamEvent; + stream.push(event); + } catch (e) { + console.error('Error parsing final chunk:', e); + } + } + + // End of stream + stream.push(null); + } catch (error) { + console.error('Streaming error:', error); + stream.emit('error', error); + stream.push(null); + } + })(); + + return stream; + } + + /** + * Create a clone of this client + * @returns New client instance + */ + protected clone(): RemoveBackgroundClient { + const client = new RemoveBackgroundClient(this.client); + this.copyTo(client); + client.params = { ...this.params }; + return client; + } +} diff --git a/src/core/SVGMakerClient.ts b/src/core/SVGMakerClient.ts index 5099ece..b294f81 100644 --- a/src/core/SVGMakerClient.ts +++ b/src/core/SVGMakerClient.ts @@ -13,6 +13,7 @@ import { GenerationsClient } from '../clients/GenerationsClient'; import { GalleryClient } from '../clients/GalleryClient'; import { AccountClient } from '../clients/AccountClient'; import { OptimizeSvgClient } from '../clients/OptimizeSvgClient'; +import { RemoveBackgroundClient } from '../clients/RemoveBackgroundClient'; import { createRetryWrapper } from '../utils/retry'; import { createRateLimiter } from '../utils/rateLimit'; import { Logger, createLogger } from '../utils/logger'; @@ -66,6 +67,11 @@ export class SVGMakerClient { */ public readonly edit: EditClient; + /** + * Remove Background client — removes an image's background and returns an SVG + */ + public readonly removeBackground: RemoveBackgroundClient; + /** * Convert namespace — contains AI vectorize and future conversion clients */ @@ -144,6 +150,7 @@ export class SVGMakerClient { // Create API clients this.generate = new GenerateClient(this); this.edit = new EditClient(this); + this.removeBackground = new RemoveBackgroundClient(this); this.convert = { aiVectorize: new AIVectorizeClient(this), trace: new TraceClient(this), diff --git a/src/index.ts b/src/index.ts index 1b5f22c..f995a0e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { SvgToVectorClient } from './clients/convert/SvgToVectorClient'; import { RasterToRasterClient } from './clients/convert/RasterToRasterClient'; import { BatchConvertClient } from './clients/convert/BatchConvertClient'; import { EnhancePromptClient } from './clients/EnhancePromptClient'; +import { RemoveBackgroundClient } from './clients/RemoveBackgroundClient'; // Export error classes import * as Errors from './errors/CustomErrors'; @@ -44,6 +45,7 @@ export { RasterToRasterClient, BatchConvertClient, EnhancePromptClient, + RemoveBackgroundClient, // Utils HttpClient, diff --git a/src/types/api.ts b/src/types/api.ts index 8bcd989..2c5da0e 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -161,6 +161,26 @@ export interface AiVectorizeParams { */ export type ConvertParams = AiVectorizeParams; +/** + * Remove Background request parameters + * + * Removes the background from an image and returns the result as an SVG with + * transparency. Accepts any raster image (PNG, JPEG, WebP, etc.) or SVG. + */ +export interface RemoveBackgroundParams { + /** Required: Image file to remove the background from */ + file: string | Buffer | Readable; + + /** Optional: Enable streaming response (default: false) */ + stream?: boolean; + + /** Optional: Include SVG source code as text in response (default: false) */ + svgText?: boolean; + + /** Optional: Store the resulting SVG on SVGMaker servers (default: false) */ + storage?: boolean; +} + /** * Base SVGMaker API response */ @@ -232,6 +252,14 @@ export interface AiVectorizeResponse extends BaseResponse { */ export type ConvertResponse = AiVectorizeResponse; +/** + * Remove Background response + */ +export interface RemoveBackgroundResponse extends BaseResponse { + /** SVG source code as text - only when svgText=true */ + svgText?: string; +} + // --- Optimize SVG Types --- export interface OptimizeSvgParams { @@ -446,6 +474,11 @@ export type ConvertStreamEvent = StreamEvent; */ export type AiVectorizeStreamEvent = StreamEvent; +/** + * Remove Background stream event + */ +export type RemoveBackgroundStreamEvent = StreamEvent; + // --- Generations Management Types --- export interface GenerationsListParams { From 8e782f8145164546c6e31b1986b49966debc3f8d Mon Sep 17 00:00:00 2001 From: vishal Date: Wed, 15 Jul 2026 10:44:23 +0530 Subject: [PATCH 3/5] style: fix prettier formatting in SVGMakerClient test --- tests/SVGMakerClient.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/SVGMakerClient.test.ts b/tests/SVGMakerClient.test.ts index bd45af9..2e85269 100644 --- a/tests/SVGMakerClient.test.ts +++ b/tests/SVGMakerClient.test.ts @@ -10,7 +10,9 @@ describe('SVGMakerClient', () => { describe('constructor', () => { it('should throw an error if no API key or access token is provided', () => { expect(() => new SVGMakerClient('')).toThrow(ValidationError); - expect(() => new SVGMakerClient('')).toThrow('Either an API key or an access token is required'); + expect(() => new SVGMakerClient('')).toThrow( + 'Either an API key or an access token is required' + ); }); it('should create a client when only an access token is provided', () => { From 3321bf1932e5146b30b6932dee4570ef261bdf62 Mon Sep 17 00:00:00 2001 From: vishal Date: Fri, 17 Jul 2026 10:39:42 +0530 Subject: [PATCH 4/5] fix(clients): send Bearer token in gallery/generations get() The get() methods bypassed handleRequest() with a raw fetch that hardcoded x-api-key, so OAuth (Bearer) sessions sent no credential and got 401. Use buildAuthHeaders() to match the working list/download paths. --- src/clients/GalleryClient.ts | 2 +- src/clients/GenerationsClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clients/GalleryClient.ts b/src/clients/GalleryClient.ts index 0f0bd2f..91d57ee 100644 --- a/src/clients/GalleryClient.ts +++ b/src/clients/GalleryClient.ts @@ -109,7 +109,7 @@ export class GalleryClient extends BaseClient { method: 'GET', headers: { Accept: 'application/json', - 'x-api-key': this.config.apiKey, + ...this.buildAuthHeaders(), }, }); diff --git a/src/clients/GenerationsClient.ts b/src/clients/GenerationsClient.ts index 55c97b0..dda8bbb 100644 --- a/src/clients/GenerationsClient.ts +++ b/src/clients/GenerationsClient.ts @@ -107,7 +107,7 @@ export class GenerationsClient extends BaseClient { method: 'GET', headers: { Accept: 'application/json', - 'x-api-key': this.config.apiKey, + ...this.buildAuthHeaders(), }, }); From 02c49eed04651a95764263da4cef88d184fbe8ab Mon Sep 17 00:00:00 2001 From: vishal Date: Fri, 17 Jul 2026 11:26:37 +0530 Subject: [PATCH 5/5] docs: add removeBackground to README clients table --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8ff43eb..e49683f 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ console.log('Expires in:', result.imageUrlExpiresIn); |--------|--------|-------------| | `generate` | `client.generate` | Create SVGs from text prompts | | `edit` | `client.edit` | Modify existing images/SVGs with AI | +| `removeBackground` | `client.removeBackground` | Remove an image's background and return an SVG | | `convert.aiVectorize` | `client.convert.aiVectorize` | AI-powered raster to SVG conversion | | `convert.trace` | `client.convert.trace` | Algorithmic raster to SVG tracing | | `convert.svgToVector` | `client.convert.svgToVector` | SVG to vector formats (PDF, EPS, DXF, AI, PS) |