@immagin/client 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,12 +17,12 @@ import { Immagin } from '@immagin/client'
17
17
 
18
18
  const client = new Immagin({ apiKey: 'imk_...' })
19
19
 
20
- // Get a processed image URL (via API)
21
- const url = await client.images.url('photos/hero.jpg', {
22
- size: [800, 600],
23
- })
20
+ // Generate a signed image URL (auto-fetches tenant credentials)
21
+ const url = await client.images.url('photos/hero.jpg', [
22
+ { resize: { width: 800, height: 600 } },
23
+ ])
24
24
 
25
- // Upload an image
25
+ // Upload an image (auto-detects MIME type)
26
26
  import { readFileSync } from 'node:fs'
27
27
  const buffer = readFileSync('photo.jpg')
28
28
  await client.images.upload(buffer, 'photos/hero.jpg')
@@ -30,62 +30,59 @@ await client.images.upload(buffer, 'photos/hero.jpg')
30
30
 
31
31
  ## Images
32
32
 
33
- ### Get a processed URL (via API)
33
+ ### Generate image URLs
34
34
 
35
- Returns a signed URL that serves the image through Immagin's processing pipeline.
35
+ Generate signed image URLs for serving processed images. All URLs are automatically signed — the signature locks transformation parameters so they can't be tampered with.
36
36
 
37
37
  ```ts
38
+ // Original image
38
39
  const url = await client.images.url('photo.jpg')
39
- ```
40
-
41
- With transformations:
42
-
43
- ```ts
44
- const url = await client.images.url('photo.jpg', {
45
- size: [400, 300],
46
- text: {
47
- text: 'Hello',
48
- position: 'bottom-right', // top-left, top-right, bottom-left, bottom-right, center
49
- fontSize: 24,
50
- color: '#ffffff',
51
- opacity: 0.8,
52
- },
53
- })
54
- ```
55
-
56
- ### Sign URLs locally (without API call)
57
-
58
- Generate signed image URLs on the server without making an API request. Requires `tenantId` and `tenantSecret` in the constructor.
59
-
60
- ```ts
61
- const client = new Immagin({
62
- apiKey: 'imk_...',
63
- tenantId: 'your-tenant-id',
64
- tenantSecret: 'your-tenant-secret',
65
- })
66
-
67
- // Synchronous - no network request
68
- const url = client.images.signedUrl('photo.jpg')
69
40
 
70
41
  // With transformations
71
- const thumb = client.images.signedUrl('photo.jpg', {
72
- size: [800, 600],
73
- text: { text: '© My Company', position: 'bottom-right', opacity: 0.5 },
74
- })
42
+ const thumb = await client.images.url('photo.jpg', [
43
+ { resize: { width: 800, height: 600 } },
44
+ { text: { text: '© My Company', position: 'bottom-right', opacity: 0.5 } },
45
+ ])
46
+
47
+ // Rotate, blur, grayscale
48
+ const edited = await client.images.url('photo.jpg', [
49
+ { rotate: { angle: 90 } },
50
+ { blur: 3 },
51
+ { grayscale: true },
52
+ ])
53
+
54
+ // Crop a region
55
+ const cropped = await client.images.url('photo.jpg', [
56
+ { crop: { left: 100, top: 50, width: 400, height: 300 } },
57
+ ])
58
+
59
+ // Flip, sharpen, adjust colors
60
+ const adjusted = await client.images.url('photo.jpg', [
61
+ { flip: true },
62
+ { sharpen: 2 },
63
+ { modulate: { brightness: 1.2, saturation: 0.8 } },
64
+ ])
65
+
66
+ // Custom output format (default is WebP at quality 90)
67
+ const jpeg = await client.images.url(
68
+ 'photo.jpg',
69
+ [{ resize: { width: 800 } }],
70
+ { format: 'jpeg', quality: 85 },
71
+ )
75
72
  ```
76
73
 
77
- > **Note:** This uses `node:crypto` and is intended for server-side use only. Never expose your tenant secret in client-side code.
74
+ > **Note:** `url()` uses `node:crypto` for signing and is intended for server-side use only. Tenant credentials are auto-fetched and cached on initialization.
78
75
 
79
- ### Get a pre-signed upload URL
76
+ ### Get a signed upload URL
80
77
 
81
- Returns a pre-signed S3 URL for direct upload (expires in 5 minutes). Useful for browser uploads where you don't want to expose your API key.
78
+ Returns a CloudFront signed URL for direct upload (expires in 5 minutes). Useful for browser uploads where you don't want to expose your API key. The URL points to a CloudFront distribution (not S3 directly), so the underlying infrastructure is not exposed.
82
79
 
83
80
  ```ts
84
- // Server: get the presigned URL
81
+ // Server: get the signed upload URL
85
82
  const { uploadUrl, key } = await client.images.signUrl('photos/hero.jpg')
86
83
  // Return uploadUrl to the browser
87
84
 
88
- // Browser: upload directly to S3
85
+ // Browser: upload directly
89
86
  await fetch(uploadUrl, {
90
87
  method: 'PUT',
91
88
  body: file,
@@ -94,7 +91,7 @@ await fetch(uploadUrl, {
94
91
 
95
92
  ### Upload (Node.js)
96
93
 
97
- Convenience method that gets a signed URL and uploads in one call. The file goes directly to S3 and never passes through the API.
94
+ Convenience method that gets a signed URL and uploads in one call. The file goes directly to CloudFront/S3 and never passes through the API. MIME type is auto-detected from file bytes (magic bytes for JPEG, PNG, GIF, WebP, BMP, AVIF, HEIC, HEIF) or file extension.
98
95
 
99
96
  ```ts
100
97
  import { readFileSync } from 'node:fs'
@@ -123,6 +120,27 @@ const page = await client.images.list({
123
120
  await client.images.delete('uploads/photo.jpg')
124
121
  ```
125
122
 
123
+ ### Metadata
124
+
125
+ Inspect image properties (dimensions, format, color space, etc.) without downloading the full image. The request goes through Luna, which extracts metadata using Sharp.
126
+
127
+ ```ts
128
+ const meta = await client.images.metadata('photo.jpg')
129
+ // {
130
+ // width: 1920,
131
+ // height: 1080,
132
+ // format: 'jpeg',
133
+ // space: 'srgb',
134
+ // channels: 3,
135
+ // hasAlpha: false,
136
+ // density: 72,
137
+ // isProgressive: false,
138
+ // size: 204800,
139
+ // }
140
+ ```
141
+
142
+ > **Note:** `metadata()` uses `node:crypto` for signing and is intended for server-side use only.
143
+
126
144
  ## API Keys
127
145
 
128
146
  Manage API keys programmatically.
@@ -171,8 +189,6 @@ try {
171
189
  const client = new Immagin({
172
190
  apiKey: 'imk_...', // Required
173
191
  baseUrl: 'https://...', // Optional, defaults to https://gateway.immag.in
174
- tenantId: 'your-tenant-id', // Optional, required for signedUrl()
175
- tenantSecret: 'your-secret', // Optional, required for signedUrl()
176
192
  })
177
193
  ```
178
194
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region src/types.d.ts
2
- type TextPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
2
+ type TextPosition = 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
3
3
  interface TextOverlay {
4
4
  text: string;
5
5
  fontSize?: number;
@@ -8,11 +8,111 @@ interface TextOverlay {
8
8
  position?: TextPosition;
9
9
  padding?: number;
10
10
  }
11
- interface ImageEdits {
12
- size?: [number] | [number, number];
13
- text?: TextOverlay;
11
+ type ResizeFit = 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
12
+ type ResizePosition = 'top' | 'right top' | 'right' | 'right bottom' | 'bottom' | 'left bottom' | 'left' | 'left top' | 'centre' | 'center' | 'entropy' | 'attention';
13
+ type ResizeKernel = 'nearest' | 'cubic' | 'mitchell' | 'lanczos2' | 'lanczos3';
14
+ interface ResizeOptions {
15
+ width: number;
16
+ height?: number;
17
+ fit?: ResizeFit;
18
+ position?: ResizePosition;
19
+ kernel?: ResizeKernel;
20
+ background?: string;
21
+ upscale?: boolean;
22
+ downscale?: boolean;
23
+ }
24
+ interface RotateOptions {
25
+ angle: number;
26
+ background?: string;
27
+ }
28
+ interface CropOptions {
29
+ left: number;
30
+ top: number;
31
+ width: number;
32
+ height: number;
33
+ }
34
+ interface ExtendOptions {
35
+ top?: number;
36
+ bottom?: number;
37
+ left?: number;
38
+ right?: number;
39
+ background?: string;
40
+ }
41
+ interface ModulateOptions {
42
+ brightness?: number;
43
+ saturation?: number;
44
+ hue?: number;
45
+ lightness?: number;
46
+ }
47
+ interface FlattenOptions {
48
+ background?: string;
49
+ }
50
+ interface TintOptions {
51
+ r: number;
52
+ g: number;
53
+ b: number;
54
+ }
55
+ interface NormalizeOptions {
56
+ lower?: number;
57
+ upper?: number;
58
+ }
59
+ interface TrimOptions {
60
+ background?: string;
61
+ threshold?: number;
62
+ }
63
+ type ImageOperation = {
64
+ resize: ResizeOptions;
65
+ } | {
66
+ text: TextOverlay;
67
+ } | {
68
+ rotate: RotateOptions;
69
+ } | {
70
+ flip: true;
71
+ } | {
72
+ flop: true;
73
+ } | {
74
+ blur: number;
75
+ } | {
76
+ sharpen: number;
77
+ } | {
78
+ grayscale: true;
79
+ } | {
80
+ crop: CropOptions;
81
+ } | {
82
+ extend: ExtendOptions;
83
+ } | {
84
+ modulate: ModulateOptions;
85
+ } | {
86
+ flatten: FlattenOptions;
87
+ } | {
88
+ tint: TintOptions;
89
+ } | {
90
+ invert: boolean;
91
+ } | {
92
+ normalize: NormalizeOptions;
93
+ } | {
94
+ trim: TrimOptions;
95
+ };
96
+ type OutputFormat = 'webp' | 'jpeg' | 'png' | 'gif' | 'jp2' | 'tiff' | 'avif' | 'heif';
97
+ interface OutputOptions {
98
+ format?: OutputFormat;
99
+ quality?: number;
100
+ progressive?: boolean;
101
+ lossless?: boolean;
102
+ }
103
+ interface ImageMetadata {
104
+ width: number;
105
+ height: number;
106
+ format: string;
107
+ space: string;
108
+ channels: number;
109
+ hasAlpha: boolean;
110
+ orientation?: number;
111
+ density?: number;
112
+ isProgressive?: boolean;
113
+ pages?: number;
114
+ size: number;
14
115
  }
15
- interface ImageUrlOptions extends ImageEdits {}
16
116
  interface UploadResult {
17
117
  uploadUrl: string;
18
118
  key: string;
@@ -43,6 +143,10 @@ interface CreateKeyResult {
43
143
  prefix: string;
44
144
  name: string;
45
145
  }
146
+ interface ProjectInfo {
147
+ tenantId: string;
148
+ tenantSecret: string;
149
+ }
46
150
  interface ImmaginConfig {
47
151
  apiKey: string;
48
152
  baseUrl?: string;
@@ -54,12 +158,12 @@ interface ImmaginConfig {
54
158
  declare class ImagesResource {
55
159
  private client;
56
160
  constructor(client: Immagin);
57
- signedUrl(key: string, edits?: ImageEdits): string;
58
- url(key: string, options?: ImageUrlOptions): Promise<string>;
161
+ url(key: string, edits?: ImageOperation | ImageOperation[], output?: OutputOptions): Promise<string>;
59
162
  signUrl(key: string): Promise<UploadResult>;
60
163
  upload(file: Blob | Buffer | ReadableStream, key: string): Promise<UploadResult>;
61
164
  list(options?: ListImagesOptions): Promise<ListImagesResult>;
62
165
  delete(key: string): Promise<void>;
166
+ metadata(key: string): Promise<ImageMetadata>;
63
167
  }
64
168
  //#endregion
65
169
  //#region src/resources/keys.d.ts
@@ -81,10 +185,16 @@ declare class Immagin {
81
185
  tenantId?: string;
82
186
  /** @internal */
83
187
  tenantSecret?: string;
188
+ private _tenantInfoPromise?;
84
189
  images: ImagesResource;
85
190
  keys: KeysResource;
86
191
  constructor(config: ImmaginConfig);
87
192
  /** @internal */
193
+ resolveTenantInfo(): Promise<{
194
+ tenantId: string;
195
+ tenantSecret: string;
196
+ }>;
197
+ /** @internal */
88
198
  request<T>(method: string, path: string, options?: {
89
199
  body?: unknown;
90
200
  params?: Record<string, string>;
@@ -98,4 +208,4 @@ declare class ImmaginError extends Error {
98
208
  constructor(message: string, status: number, body?: unknown | undefined);
99
209
  }
100
210
  //#endregion
101
- export { type ApiKey, type CreateKeyResult, type ImageEdits, type ImageEntry, type ImageUrlOptions, Immagin, type ImmaginConfig, ImmaginError, type ListImagesOptions, type ListImagesResult, type TextOverlay, type TextPosition, type UploadResult };
211
+ export { type ApiKey, type CreateKeyResult, type CropOptions, type ExtendOptions, type FlattenOptions, type ImageEntry, type ImageMetadata, type ImageOperation, Immagin, type ImmaginConfig, ImmaginError, type ListImagesOptions, type ListImagesResult, type ModulateOptions, type NormalizeOptions, type OutputFormat, type OutputOptions, type ProjectInfo, type ResizeFit, type ResizeKernel, type ResizeOptions, type ResizePosition, type RotateOptions, type TextOverlay, type TextPosition, type TintOptions, type TrimOptions, type UploadResult };
package/dist/index.mjs CHANGED
@@ -1,5 +1,16 @@
1
1
  import { createHmac } from "node:crypto";
2
2
 
3
+ //#region src/errors.ts
4
+ var ImmaginError = class extends Error {
5
+ constructor(message, status, body) {
6
+ super(message);
7
+ this.status = status;
8
+ this.body = body;
9
+ this.name = "ImmaginError";
10
+ }
11
+ };
12
+
13
+ //#endregion
3
14
  //#region src/resources/images.ts
4
15
  const MIME_TYPES = {
5
16
  jpg: "image/jpeg",
@@ -42,29 +53,17 @@ var ImagesResource = class {
42
53
  constructor(client) {
43
54
  this.client = client;
44
55
  }
45
- signedUrl(key, edits) {
46
- const { tenantId, tenantSecret } = this.client;
47
- if (!tenantId || !tenantSecret) throw new Error("tenantId and tenantSecret are required for signedUrl(). Pass them in the Immagin constructor.");
56
+ async url(key, edits, output) {
57
+ const { tenantId, tenantSecret } = await this.client.resolveTenantInfo();
48
58
  const payload = { key };
49
- if (edits && Object.keys(edits).length > 0) payload.edits = edits;
59
+ if (edits) {
60
+ const editsArray = Array.isArray(edits) ? edits : [edits];
61
+ if (editsArray.length > 0) payload.edits = editsArray;
62
+ }
63
+ if (output) payload.output = output;
50
64
  const base64 = Buffer.from(JSON.stringify(payload)).toString("base64");
51
65
  return `https://${tenantId}.immag.in/${base64}?sig=${createHmac("sha256", tenantSecret).update(base64).digest("hex").slice(0, 16)}`;
52
66
  }
53
- async url(key, options) {
54
- const params = { key };
55
- if (options?.size) {
56
- params.width = String(options.size[0]);
57
- if (options.size.length === 2) params.height = String(options.size[1]);
58
- }
59
- if (options?.text) {
60
- params["text.text"] = options.text.text;
61
- if (options.text.position) params["text.position"] = options.text.position;
62
- if (options.text.fontSize) params["text.fontSize"] = String(options.text.fontSize);
63
- if (options.text.opacity) params["text.opacity"] = String(options.text.opacity);
64
- if (options.text.color) params["text.color"] = options.text.color;
65
- }
66
- return (await this.client.request("GET", "/v1/images/url", { params })).url;
67
- }
68
67
  async signUrl(key) {
69
68
  const contentType = mimeFromKey(key);
70
69
  return this.client.request("POST", "/v1/images/sign-url", { body: contentType ? {
@@ -95,6 +94,21 @@ var ImagesResource = class {
95
94
  async delete(key) {
96
95
  await this.client.request("DELETE", `/v1/images/${key}`);
97
96
  }
97
+ async metadata(key) {
98
+ const { tenantId, tenantSecret } = await this.client.resolveTenantInfo();
99
+ const payload = JSON.stringify({
100
+ key,
101
+ metadata: true
102
+ });
103
+ const base64 = Buffer.from(payload).toString("base64");
104
+ const url = `https://${tenantId}.immag.in/${base64}?sig=${createHmac("sha256", tenantSecret).update(base64).digest("hex").slice(0, 16)}`;
105
+ const response = await fetch(url);
106
+ if (!response.ok) {
107
+ const body = await response.text();
108
+ throw new ImmaginError(`HTTP ${response.status}`, response.status, body);
109
+ }
110
+ return response.json();
111
+ }
98
112
  };
99
113
 
100
114
  //#endregion
@@ -114,17 +128,6 @@ var KeysResource = class {
114
128
  }
115
129
  };
116
130
 
117
- //#endregion
118
- //#region src/errors.ts
119
- var ImmaginError = class extends Error {
120
- constructor(message, status, body) {
121
- super(message);
122
- this.status = status;
123
- this.body = body;
124
- this.name = "ImmaginError";
125
- }
126
- };
127
-
128
131
  //#endregion
129
132
  //#region src/client.ts
130
133
  const DEFAULT_BASE_URL = "https://gateway.immag.in";
@@ -135,6 +138,7 @@ var Immagin = class {
135
138
  tenantId;
136
139
  /** @internal */
137
140
  tenantSecret;
141
+ _tenantInfoPromise;
138
142
  images;
139
143
  keys;
140
144
  constructor(config) {
@@ -144,6 +148,23 @@ var Immagin = class {
144
148
  this.tenantSecret = config.tenantSecret;
145
149
  this.images = new ImagesResource(this);
146
150
  this.keys = new KeysResource(this);
151
+ if (!this.tenantId || !this.tenantSecret) this.resolveTenantInfo().catch(() => {});
152
+ }
153
+ /** @internal */
154
+ async resolveTenantInfo() {
155
+ if (this.tenantId && this.tenantSecret) return {
156
+ tenantId: this.tenantId,
157
+ tenantSecret: this.tenantSecret
158
+ };
159
+ if (!this._tenantInfoPromise) this._tenantInfoPromise = this.request("GET", "/v1/project").then((info) => {
160
+ this.tenantId = info.tenantId;
161
+ this.tenantSecret = info.tenantSecret;
162
+ return info;
163
+ }).catch((err) => {
164
+ this._tenantInfoPromise = void 0;
165
+ throw err;
166
+ });
167
+ return this._tenantInfoPromise;
147
168
  }
148
169
  /** @internal */
149
170
  async request(method, path, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immagin/client",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Node.js and browser client for the Immagin image processing API",
5
5
  "type": "module",
6
6
  "license": "MIT",