@donotlb/imagen-sdk 0.2.0

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.
@@ -0,0 +1,13 @@
1
+ import type { ImagenCallbackPayload } from './contracts.js';
2
+ export declare class ImagenCallbackVerificationError extends Error {
3
+ constructor(message?: string, options?: {
4
+ cause?: unknown;
5
+ });
6
+ }
7
+ export declare class ImagenCallbackParseError extends Error {
8
+ constructor(message?: string, options?: {
9
+ cause?: unknown;
10
+ });
11
+ }
12
+ export declare function parseImagenCallback(body: unknown): ImagenCallbackPayload;
13
+ export declare function verifyImagenCallbackRequest(headers: Headers | Record<string, string | undefined>, secret: string, clientName?: string): Promise<void>;
@@ -0,0 +1,46 @@
1
+ import { imagenCallbackPayloadSchema } from './contracts.js';
2
+ import { verifyImagenCallbackJwt } from './service-jwt.js';
3
+ export class ImagenCallbackVerificationError extends Error {
4
+ constructor(message = 'Invalid Imagen callback request', options = {}) {
5
+ super(message, options);
6
+ this.name = 'ImagenCallbackVerificationError';
7
+ }
8
+ }
9
+ export class ImagenCallbackParseError extends Error {
10
+ constructor(message = 'Invalid Imagen callback payload', options = {}) {
11
+ super(message, options);
12
+ this.name = 'ImagenCallbackParseError';
13
+ }
14
+ }
15
+ function getHeader(headers, name) {
16
+ if (headers instanceof Headers)
17
+ return headers.get(name);
18
+ const lower = name.toLowerCase();
19
+ for (const [key, value] of Object.entries(headers)) {
20
+ if (key.toLowerCase() === lower)
21
+ return value ?? null;
22
+ }
23
+ return null;
24
+ }
25
+ function bearerToken(authorization) {
26
+ const [scheme, token] = authorization?.split(/\s+/u) ?? [];
27
+ if (scheme?.toLowerCase() !== 'bearer' || !token)
28
+ throw new ImagenCallbackVerificationError('Imagen callback bearer token is required');
29
+ return token;
30
+ }
31
+ export function parseImagenCallback(body) {
32
+ const parsed = imagenCallbackPayloadSchema.safeParse(body);
33
+ if (!parsed.success)
34
+ throw new ImagenCallbackParseError('Invalid Imagen callback payload', { cause: parsed.error });
35
+ return parsed.data;
36
+ }
37
+ export async function verifyImagenCallbackRequest(headers, secret, clientName) {
38
+ try {
39
+ await verifyImagenCallbackJwt(bearerToken(getHeader(headers, 'authorization')), secret, clientName);
40
+ }
41
+ catch (error) {
42
+ if (error instanceof ImagenCallbackVerificationError)
43
+ throw error;
44
+ throw new ImagenCallbackVerificationError('Invalid Imagen callback token', { cause: error });
45
+ }
46
+ }
@@ -0,0 +1,33 @@
1
+ import type { ImagenGenerationAccepted, ImagenGenerationCancelAccepted, ImagenGenerationCancelRequest, ImagenGenerationRequest } from './contracts.js';
2
+ export interface ImagenClientOptions {
3
+ baseUrl: string;
4
+ serviceJwtSecret: string;
5
+ /** This caller's service identity (JWT `iss`). Defaults to `tutu`. */
6
+ clientName?: string;
7
+ fetch?: typeof fetch;
8
+ }
9
+ export declare class ImagenClientError extends Error {
10
+ readonly status: number | null;
11
+ readonly retryable: boolean;
12
+ readonly responseBody: string | null;
13
+ constructor(message: string, options: {
14
+ status?: number | null;
15
+ retryable: boolean;
16
+ responseBody?: string | null;
17
+ cause?: unknown;
18
+ });
19
+ }
20
+ export declare class ImagenPayloadMismatchError extends ImagenClientError {
21
+ constructor(message: string, options?: {
22
+ responseBody?: string | null;
23
+ });
24
+ }
25
+ export declare class ImagenClient {
26
+ private readonly baseUrl;
27
+ private readonly serviceJwtSecret;
28
+ private readonly clientName;
29
+ private readonly fetchImpl;
30
+ constructor(options: ImagenClientOptions);
31
+ createGeneration(request: ImagenGenerationRequest): Promise<ImagenGenerationAccepted>;
32
+ cancelGeneration(request: ImagenGenerationCancelRequest): Promise<ImagenGenerationCancelAccepted>;
33
+ }
package/dist/client.js ADDED
@@ -0,0 +1,111 @@
1
+ import { imagenGenerationAcceptedSchema, imagenGenerationCancelAcceptedSchema } from './contracts.js';
2
+ import { DEFAULT_SERVICE_CLIENT_NAME, signClientToImagenJwt } from './service-jwt.js';
3
+ export class ImagenClientError extends Error {
4
+ status;
5
+ retryable;
6
+ responseBody;
7
+ constructor(message, options) {
8
+ super(message, { cause: options.cause });
9
+ this.name = 'ImagenClientError';
10
+ this.status = options.status ?? null;
11
+ this.retryable = options.retryable;
12
+ this.responseBody = options.responseBody ?? null;
13
+ }
14
+ }
15
+ export class ImagenPayloadMismatchError extends ImagenClientError {
16
+ constructor(message, options = {}) {
17
+ super(message, {
18
+ status: 409,
19
+ retryable: false,
20
+ responseBody: options.responseBody ?? null,
21
+ });
22
+ this.name = 'ImagenPayloadMismatchError';
23
+ }
24
+ }
25
+ export class ImagenClient {
26
+ baseUrl;
27
+ serviceJwtSecret;
28
+ clientName;
29
+ fetchImpl;
30
+ constructor(options) {
31
+ this.baseUrl = options.baseUrl.replace(/\/+$/u, '');
32
+ this.serviceJwtSecret = options.serviceJwtSecret;
33
+ this.clientName = options.clientName ?? DEFAULT_SERVICE_CLIENT_NAME;
34
+ this.fetchImpl = options.fetch ?? fetch;
35
+ }
36
+ async createGeneration(request) {
37
+ const token = await signClientToImagenJwt(this.serviceJwtSecret, this.clientName);
38
+ let response;
39
+ try {
40
+ response = await this.fetchImpl(`${this.baseUrl}/api/generations`, {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Authorization': `Bearer ${token}`,
44
+ 'Content-Type': 'application/json',
45
+ },
46
+ body: JSON.stringify(request),
47
+ });
48
+ }
49
+ catch (error) {
50
+ throw new ImagenClientError('Imagen generation request failed', {
51
+ retryable: true,
52
+ cause: error,
53
+ });
54
+ }
55
+ if (response.status === 200 || response.status === 202) {
56
+ const parsed = imagenGenerationAcceptedSchema.safeParse(await response.json());
57
+ if (!parsed.success) {
58
+ throw new ImagenClientError('Imagen accepted the generation but returned an invalid response', {
59
+ status: response.status,
60
+ retryable: true,
61
+ });
62
+ }
63
+ return parsed.data;
64
+ }
65
+ const text = await response.text();
66
+ const message = `Imagen generation request failed (${response.status}): ${text.slice(0, 500)}`;
67
+ if (response.status === 409)
68
+ throw new ImagenPayloadMismatchError(message, { responseBody: text });
69
+ throw new ImagenClientError(message, {
70
+ status: response.status,
71
+ retryable: response.status >= 500,
72
+ responseBody: text,
73
+ });
74
+ }
75
+ async cancelGeneration(request) {
76
+ const token = await signClientToImagenJwt(this.serviceJwtSecret, this.clientName);
77
+ let response;
78
+ try {
79
+ response = await this.fetchImpl(`${this.baseUrl}/api/generations/cancel`, {
80
+ method: 'POST',
81
+ headers: {
82
+ 'Authorization': `Bearer ${token}`,
83
+ 'Content-Type': 'application/json',
84
+ },
85
+ body: JSON.stringify(request),
86
+ });
87
+ }
88
+ catch (error) {
89
+ throw new ImagenClientError('Imagen generation cancellation request failed', {
90
+ retryable: true,
91
+ cause: error,
92
+ });
93
+ }
94
+ if (response.status === 200 || response.status === 202) {
95
+ const parsed = imagenGenerationCancelAcceptedSchema.safeParse(await response.json());
96
+ if (!parsed.success) {
97
+ throw new ImagenClientError('Imagen accepted the cancellation but returned an invalid response', {
98
+ status: response.status,
99
+ retryable: true,
100
+ });
101
+ }
102
+ return parsed.data;
103
+ }
104
+ const text = await response.text();
105
+ throw new ImagenClientError(`Imagen generation cancellation request failed (${response.status}): ${text.slice(0, 500)}`, {
106
+ status: response.status,
107
+ retryable: response.status >= 500,
108
+ responseBody: text,
109
+ });
110
+ }
111
+ }
@@ -0,0 +1,113 @@
1
+ import { z } from 'zod';
2
+ export declare const imageGenerationTypeSchema: z.ZodEnum<{
3
+ "image.generate": "image.generate";
4
+ "image.edit": "image.edit";
5
+ }>;
6
+ export declare const imagenGenerationRequestSchema: z.ZodObject<{
7
+ idempotencyKey: z.ZodString;
8
+ externalJobId: z.ZodString;
9
+ type: z.ZodEnum<{
10
+ "image.generate": "image.generate";
11
+ "image.edit": "image.edit";
12
+ }>;
13
+ input: z.ZodObject<{
14
+ prompt: z.ZodString;
15
+ model: z.ZodEnum<{
16
+ "openai/gpt-image-2": "openai/gpt-image-2";
17
+ }>;
18
+ quality: z.ZodEnum<{
19
+ low: "low";
20
+ medium: "medium";
21
+ high: "high";
22
+ }>;
23
+ resolution: z.ZodEnum<{
24
+ "1k": "1k";
25
+ "2k": "2k";
26
+ "4k": "4k";
27
+ }>;
28
+ aspectRatio: z.ZodEnum<{
29
+ auto: "auto";
30
+ "1:1": "1:1";
31
+ "2:3": "2:3";
32
+ "3:2": "3:2";
33
+ "3:4": "3:4";
34
+ "4:3": "4:3";
35
+ "4:5": "4:5";
36
+ "5:4": "5:4";
37
+ "9:16": "9:16";
38
+ "16:9": "16:9";
39
+ "21:9": "21:9";
40
+ }>;
41
+ inputImages: z.ZodOptional<z.ZodArray<z.ZodObject<{
42
+ url: z.ZodURL;
43
+ contentType: z.ZodOptional<z.ZodString>;
44
+ }, z.core.$strict>>>;
45
+ count: z.ZodOptional<z.ZodInt>;
46
+ }, z.core.$strict>;
47
+ output: z.ZodObject<{
48
+ storageKeyPrefix: z.ZodString;
49
+ }, z.core.$strict>;
50
+ callback: z.ZodObject<{
51
+ url: z.ZodURL;
52
+ }, z.core.$strict>;
53
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
54
+ }, z.core.$strict>;
55
+ export declare const imagenGenerationAcceptedSchema: z.ZodObject<{
56
+ jobId: z.ZodString;
57
+ externalJobId: z.ZodString;
58
+ status: z.ZodEnum<{
59
+ accepted: "accepted";
60
+ duplicate: "duplicate";
61
+ }>;
62
+ }, z.core.$strict>;
63
+ export declare const imagenGenerationCancelRequestSchema: z.ZodObject<{
64
+ externalJobId: z.ZodString;
65
+ reason: z.ZodOptional<z.ZodString>;
66
+ }, z.core.$strict>;
67
+ export declare const imagenGenerationCancelAcceptedSchema: z.ZodObject<{
68
+ jobId: z.ZodNullable<z.ZodString>;
69
+ externalJobId: z.ZodString;
70
+ status: z.ZodEnum<{
71
+ cancelled: "cancelled";
72
+ already_terminal: "already_terminal";
73
+ not_found: "not_found";
74
+ }>;
75
+ }, z.core.$strict>;
76
+ export declare const imagenCallbackPayloadSchema: z.ZodObject<{
77
+ jobId: z.ZodString;
78
+ externalJobId: z.ZodString;
79
+ status: z.ZodEnum<{
80
+ succeeded: "succeeded";
81
+ failed: "failed";
82
+ }>;
83
+ outputs: z.ZodOptional<z.ZodArray<z.ZodObject<{
84
+ storageKey: z.ZodString;
85
+ url: z.ZodURL;
86
+ contentType: z.ZodString;
87
+ width: z.ZodNullable<z.ZodNumber>;
88
+ height: z.ZodNullable<z.ZodNumber>;
89
+ }, z.core.$strict>>>;
90
+ provider: z.ZodOptional<z.ZodObject<{
91
+ name: z.ZodString;
92
+ model: z.ZodString;
93
+ responseId: z.ZodOptional<z.ZodString>;
94
+ costUsdMicros: z.ZodOptional<z.ZodNumber>;
95
+ }, z.core.$strict>>;
96
+ error: z.ZodOptional<z.ZodObject<{
97
+ code: z.ZodString;
98
+ message: z.ZodString;
99
+ retryable: z.ZodBoolean;
100
+ }, z.core.$strict>>;
101
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
102
+ }, z.core.$strict>;
103
+ export type ImageGenerationType = z.infer<typeof imageGenerationTypeSchema>;
104
+ export type ImagenGenerationRequest = z.infer<typeof imagenGenerationRequestSchema>;
105
+ export type ImagenGenerationAccepted = z.infer<typeof imagenGenerationAcceptedSchema>;
106
+ export type ImagenGenerationCancelRequest = z.infer<typeof imagenGenerationCancelRequestSchema>;
107
+ export type ImagenGenerationCancelAccepted = z.infer<typeof imagenGenerationCancelAcceptedSchema>;
108
+ export type ImagenCallbackPayload = z.infer<typeof imagenCallbackPayloadSchema>;
109
+ export interface CallbackJobPayload {
110
+ jobId: string;
111
+ url: string;
112
+ body: ImagenCallbackPayload;
113
+ }
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+ import { ASPECT_RATIOS, IMAGE_MODEL_IDS, IMAGE_QUALITIES, RESOLUTIONS, } from './image-models/index.js';
3
+ export const imageGenerationTypeSchema = z.enum(['image.generate', 'image.edit']);
4
+ export const imagenGenerationRequestSchema = z.object({
5
+ idempotencyKey: z.string().trim().min(1).max(200),
6
+ externalJobId: z.string().trim().min(1).max(200),
7
+ type: imageGenerationTypeSchema,
8
+ input: z.object({
9
+ prompt: z.string().trim().min(1).max(20_000),
10
+ model: z.enum(IMAGE_MODEL_IDS),
11
+ quality: z.enum(IMAGE_QUALITIES),
12
+ resolution: z.enum(RESOLUTIONS),
13
+ aspectRatio: z.enum(ASPECT_RATIOS),
14
+ inputImages: z.array(z.object({
15
+ url: z.url(),
16
+ contentType: z.string().trim().min(1).max(200).optional(),
17
+ }).strict()).max(16).optional(),
18
+ count: z.int().min(1).max(4).optional(),
19
+ }).strict(),
20
+ output: z.object({
21
+ // Opaque namespace owned by the caller. Imagen does not interpret its
22
+ // structure; it only normalizes it and owns the leaf filename
23
+ // (`${jobId}-${index}.${ext}`). Callers should treat the returned
24
+ // `storageKey` as a read-only opaque value.
25
+ storageKeyPrefix: z.string().trim().min(1).max(1024),
26
+ }).strict(),
27
+ callback: z.object({
28
+ url: z.url(),
29
+ }).strict(),
30
+ metadata: z.record(z.string(), z.unknown()).optional(),
31
+ }).strict();
32
+ export const imagenGenerationAcceptedSchema = z.object({
33
+ jobId: z.string(),
34
+ externalJobId: z.string(),
35
+ status: z.enum(['accepted', 'duplicate']),
36
+ }).strict();
37
+ export const imagenGenerationCancelRequestSchema = z.object({
38
+ externalJobId: z.string().trim().min(1).max(200),
39
+ reason: z.string().trim().min(1).max(200).optional(),
40
+ }).strict();
41
+ export const imagenGenerationCancelAcceptedSchema = z.object({
42
+ jobId: z.string().nullable(),
43
+ externalJobId: z.string(),
44
+ status: z.enum(['cancelled', 'already_terminal', 'not_found']),
45
+ }).strict();
46
+ export const imagenCallbackPayloadSchema = z.object({
47
+ jobId: z.string(),
48
+ externalJobId: z.string(),
49
+ status: z.enum(['succeeded', 'failed']),
50
+ outputs: z.array(z.object({
51
+ storageKey: z.string(),
52
+ url: z.url(),
53
+ contentType: z.string(),
54
+ width: z.number().int().positive().nullable(),
55
+ height: z.number().int().positive().nullable(),
56
+ }).strict()).optional(),
57
+ provider: z.object({
58
+ name: z.string(),
59
+ model: z.string(),
60
+ responseId: z.string().optional(),
61
+ costUsdMicros: z.number().int().nonnegative().optional(),
62
+ }).strict().optional(),
63
+ error: z.object({
64
+ code: z.string(),
65
+ message: z.string(),
66
+ retryable: z.boolean(),
67
+ }).strict().optional(),
68
+ metadata: z.record(z.string(), z.unknown()).optional(),
69
+ }).strict();
@@ -0,0 +1,6 @@
1
+ export type { ImageModelSpec, SizeConstraints } from './registry.js';
2
+ export { getModelSpec } from './registry.js';
3
+ export type { ResolvedSize } from './size.js';
4
+ export { formatSize, resolveImageSize } from './size.js';
5
+ export type { AspectRatio, ImageModelId, ImageQuality, Resolution, } from './types.js';
6
+ export { ASPECT_RATIOS, IMAGE_MODEL_IDS, IMAGE_QUALITIES, RESOLUTIONS, } from './types.js';
@@ -0,0 +1,3 @@
1
+ export { getModelSpec } from './registry.js';
2
+ export { formatSize, resolveImageSize } from './size.js';
3
+ export { ASPECT_RATIOS, IMAGE_MODEL_IDS, IMAGE_QUALITIES, RESOLUTIONS, } from './types.js';
@@ -0,0 +1,30 @@
1
+ import type { AspectRatio, ImageModelId, ImageQuality, Resolution } from './types.js';
2
+ /**
3
+ * The provider's hard limits on an output size; size derivation honours all of
4
+ * these. {@link ImageModelSpec.resolutionLongEdge} and {@link SizeConstraints.maxPixels}
5
+ * together bound the worst-case (largest) pixel count of a resolution tier.
6
+ */
7
+ export interface SizeConstraints {
8
+ /** Longest edge the provider accepts, in pixels. */
9
+ maxEdge: number;
10
+ /** Both edges must be a multiple of this. */
11
+ edgeMultiple: number;
12
+ minPixels: number;
13
+ maxPixels: number;
14
+ /** Long edge / short edge must not exceed this. */
15
+ maxAspectRatio: number;
16
+ }
17
+ export interface ImageModelSpec {
18
+ id: ImageModelId;
19
+ qualities: readonly ImageQuality[];
20
+ resolutions: readonly Resolution[];
21
+ aspectRatios: readonly AspectRatio[];
22
+ constraints: SizeConstraints;
23
+ /**
24
+ * Target long-edge (px) each resolution tier aims for before the constraints
25
+ * clamp it. Square/near-square 4K is pulled below 3840 by the pixel ceiling;
26
+ * thin ratios at 1K are pushed up by the pixel floor.
27
+ */
28
+ resolutionLongEdge: Record<Resolution, number>;
29
+ }
30
+ export declare function getModelSpec(model: ImageModelId): ImageModelSpec;
@@ -0,0 +1,27 @@
1
+ import { ASPECT_RATIOS, IMAGE_QUALITIES, RESOLUTIONS } from './types.js';
2
+ // gpt-image-2 constraints, from OpenAI's image generation guide: max edge 3840,
3
+ // edges multiple of 16, long:short <= 3:1, total pixels in [655_360, 8_294_400].
4
+ const GPT_IMAGE_2 = {
5
+ id: 'openai/gpt-image-2',
6
+ qualities: IMAGE_QUALITIES,
7
+ resolutions: RESOLUTIONS,
8
+ aspectRatios: ASPECT_RATIOS,
9
+ constraints: {
10
+ maxEdge: 3840,
11
+ edgeMultiple: 16,
12
+ minPixels: 655_360,
13
+ maxPixels: 8_294_400,
14
+ maxAspectRatio: 3,
15
+ },
16
+ resolutionLongEdge: {
17
+ '1k': 1024,
18
+ '2k': 2048,
19
+ '4k': 3840,
20
+ },
21
+ };
22
+ const SPECS = {
23
+ 'openai/gpt-image-2': GPT_IMAGE_2,
24
+ };
25
+ export function getModelSpec(model) {
26
+ return SPECS[model];
27
+ }
@@ -0,0 +1,23 @@
1
+ import type { AspectRatio, ImageModelId, Resolution } from './types.js';
2
+ export interface ResolvedSize {
3
+ width: number;
4
+ height: number;
5
+ }
6
+ /**
7
+ * Derive the concrete pixel size to request for a (ratio, resolution) choice,
8
+ * honouring every model constraint: the resolution tier sets a target long
9
+ * edge, which is then clamped into the pixel floor/ceiling and rounded so both
10
+ * edges are a valid multiple. The long edge is anchored to the tier, so the
11
+ * canonical OpenAI sizes fall out exactly (e.g. 2K 16:9 -> 2048x1152, 4K 16:9
12
+ * -> 3840x2160), while squarer ratios at 4K are pulled down by the 8.29MP
13
+ * ceiling and thin ratios at 1K are pushed up by the 655K floor.
14
+ *
15
+ * `auto` ratio resolves to a square (1:1) at the chosen resolution: the
16
+ * resolution selector is always honoured and the billed tier always matches the
17
+ * pixels actually requested. (gpt-image-2's own `size: auto` would let the model
18
+ * pick the aspect but ignore our resolution choice, so we default the aspect
19
+ * ourselves instead.)
20
+ */
21
+ export declare function resolveImageSize(model: ImageModelId, aspectRatio: AspectRatio, resolution: Resolution): ResolvedSize;
22
+ /** Format a resolved size as the provider's `WIDTHxHEIGHT` size string. */
23
+ export declare function formatSize(size: ResolvedSize | 'auto'): string;
@@ -0,0 +1,64 @@
1
+ import { getModelSpec } from './registry.js';
2
+ function roundToMultiple(value, multiple) {
3
+ return Math.max(multiple, Math.round(value / multiple) * multiple);
4
+ }
5
+ /**
6
+ * Derive the concrete pixel size to request for a (ratio, resolution) choice,
7
+ * honouring every model constraint: the resolution tier sets a target long
8
+ * edge, which is then clamped into the pixel floor/ceiling and rounded so both
9
+ * edges are a valid multiple. The long edge is anchored to the tier, so the
10
+ * canonical OpenAI sizes fall out exactly (e.g. 2K 16:9 -> 2048x1152, 4K 16:9
11
+ * -> 3840x2160), while squarer ratios at 4K are pulled down by the 8.29MP
12
+ * ceiling and thin ratios at 1K are pushed up by the 655K floor.
13
+ *
14
+ * `auto` ratio resolves to a square (1:1) at the chosen resolution: the
15
+ * resolution selector is always honoured and the billed tier always matches the
16
+ * pixels actually requested. (gpt-image-2's own `size: auto` would let the model
17
+ * pick the aspect but ignore our resolution choice, so we default the aspect
18
+ * ourselves instead.)
19
+ */
20
+ export function resolveImageSize(model, aspectRatio, resolution) {
21
+ const { constraints: c, resolutionLongEdge } = getModelSpec(model);
22
+ const [rw, rh] = aspectRatio === 'auto'
23
+ ? [1, 1]
24
+ : aspectRatio.split(':').map(Number);
25
+ const ratioLong = Math.max(rw, rh);
26
+ const ratioShort = Math.min(rw, rh);
27
+ let longEdge = resolutionLongEdge[resolution];
28
+ let shortEdge = (longEdge * ratioShort) / ratioLong;
29
+ // Scale into the pixel band before rounding.
30
+ const pixels = longEdge * shortEdge;
31
+ if (pixels > c.maxPixels) {
32
+ const scale = Math.sqrt(c.maxPixels / pixels);
33
+ longEdge *= scale;
34
+ shortEdge *= scale;
35
+ }
36
+ else if (pixels < c.minPixels) {
37
+ const scale = Math.sqrt(c.minPixels / pixels);
38
+ longEdge *= scale;
39
+ shortEdge *= scale;
40
+ }
41
+ longEdge = roundToMultiple(Math.min(longEdge, c.maxEdge), c.edgeMultiple);
42
+ shortEdge = roundToMultiple(shortEdge, c.edgeMultiple);
43
+ // Rounding can nudge past the ceiling/floor; step back into range.
44
+ while (longEdge * shortEdge > c.maxPixels) {
45
+ if (longEdge >= shortEdge)
46
+ longEdge -= c.edgeMultiple;
47
+ else
48
+ shortEdge -= c.edgeMultiple;
49
+ }
50
+ while (longEdge * shortEdge < c.minPixels) {
51
+ shortEdge += c.edgeMultiple;
52
+ if (shortEdge > longEdge && ratioLong !== ratioShort) {
53
+ shortEdge -= c.edgeMultiple;
54
+ longEdge += c.edgeMultiple;
55
+ }
56
+ }
57
+ return rw >= rh
58
+ ? { width: longEdge, height: shortEdge }
59
+ : { width: shortEdge, height: longEdge };
60
+ }
61
+ /** Format a resolved size as the provider's `WIDTHxHEIGHT` size string. */
62
+ export function formatSize(size) {
63
+ return size === 'auto' ? 'auto' : `${size.width}x${size.height}`;
64
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Canonical image-model vocabulary for the Imagen service contract — the
3
+ * aspect-ratio / resolution / quality unions defined exactly once and shared by
4
+ * every consumer (the Imagen service and its SDK clients).
5
+ *
6
+ * `quality` is deliberately three tiers — `auto` was dropped at the product
7
+ * layer (callers should pick a concrete tier before submitting work to Imagen).
8
+ * A consumer's database may keep a legacy `auto` enum value for historical rows,
9
+ * but nothing new is written with it.
10
+ */
11
+ export declare const IMAGE_MODEL_IDS: readonly ["openai/gpt-image-2"];
12
+ export type ImageModelId = typeof IMAGE_MODEL_IDS[number];
13
+ export declare const IMAGE_QUALITIES: readonly ["low", "medium", "high"];
14
+ export type ImageQuality = typeof IMAGE_QUALITIES[number];
15
+ export declare const RESOLUTIONS: readonly ["1k", "2k", "4k"];
16
+ export type Resolution = typeof RESOLUTIONS[number];
17
+ export declare const ASPECT_RATIOS: readonly ["auto", "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
18
+ export type AspectRatio = typeof ASPECT_RATIOS[number];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Canonical image-model vocabulary for the Imagen service contract — the
3
+ * aspect-ratio / resolution / quality unions defined exactly once and shared by
4
+ * every consumer (the Imagen service and its SDK clients).
5
+ *
6
+ * `quality` is deliberately three tiers — `auto` was dropped at the product
7
+ * layer (callers should pick a concrete tier before submitting work to Imagen).
8
+ * A consumer's database may keep a legacy `auto` enum value for historical rows,
9
+ * but nothing new is written with it.
10
+ */
11
+ export const IMAGE_MODEL_IDS = ['openai/gpt-image-2'];
12
+ export const IMAGE_QUALITIES = ['low', 'medium', 'high'];
13
+ export const RESOLUTIONS = ['1k', '2k', '4k'];
14
+ export const ASPECT_RATIOS = [
15
+ 'auto',
16
+ '1:1',
17
+ '2:3',
18
+ '3:2',
19
+ '3:4',
20
+ '4:3',
21
+ '4:5',
22
+ '5:4',
23
+ '9:16',
24
+ '16:9',
25
+ '21:9',
26
+ ];
@@ -0,0 +1,6 @@
1
+ export * from './callback.js';
2
+ export * from './client.js';
3
+ export * from './contracts.js';
4
+ export * from './image-models/index.js';
5
+ export * from './service-jwt.js';
6
+ export * from './stable-hash.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './callback.js';
2
+ export * from './client.js';
3
+ export * from './contracts.js';
4
+ export * from './image-models/index.js';
5
+ export * from './service-jwt.js';
6
+ export * from './stable-hash.js';
@@ -0,0 +1,31 @@
1
+ import type { JWTPayload } from 'jose';
2
+ /**
3
+ * Identity of the service that calls Imagen. Imagen is generic infrastructure;
4
+ * the caller's name is configurable. `tutu` is the default because it is the
5
+ * first consumer, but any client can set its own name via `SERVICE_CLIENT_NAME`.
6
+ */
7
+ export declare const DEFAULT_SERVICE_CLIENT_NAME = "tutu";
8
+ export declare class ServiceJwtError extends Error {
9
+ constructor(message?: string);
10
+ }
11
+ export interface SignServiceJwtInput {
12
+ secret: string;
13
+ issuer: string;
14
+ audience: string;
15
+ subject: string;
16
+ expiresInSeconds?: number;
17
+ }
18
+ export interface VerifyServiceJwtInput {
19
+ token: string;
20
+ secret: string;
21
+ issuer: string;
22
+ audience: string;
23
+ subject?: string;
24
+ maxTtlSeconds?: number;
25
+ }
26
+ export declare function signServiceJwt({ secret, issuer, audience, subject, expiresInSeconds, }: SignServiceJwtInput): Promise<string>;
27
+ export declare function signClientToImagenJwt(secret: string, clientName?: string): Promise<string>;
28
+ export declare function signImagenToClientJwt(secret: string, clientName?: string): Promise<string>;
29
+ export declare function verifyServiceJwt({ token, secret, issuer, audience, subject, maxTtlSeconds, }: VerifyServiceJwtInput): Promise<JWTPayload>;
30
+ export declare function verifyClientToImagenJwt(token: string, secret: string, clientName?: string): Promise<JWTPayload>;
31
+ export declare function verifyImagenCallbackJwt(token: string, secret: string, clientName?: string): Promise<JWTPayload>;
@@ -0,0 +1,93 @@
1
+ import { jwtVerify, SignJWT } from 'jose';
2
+ const DEFAULT_MAX_TTL_SECONDS = 5 * 60;
3
+ const CLOCK_TOLERANCE_SECONDS = 5;
4
+ /**
5
+ * Identity of the service that calls Imagen. Imagen is generic infrastructure;
6
+ * the caller's name is configurable. `tutu` is the default because it is the
7
+ * first consumer, but any client can set its own name via `SERVICE_CLIENT_NAME`.
8
+ */
9
+ export const DEFAULT_SERVICE_CLIENT_NAME = 'tutu';
10
+ export class ServiceJwtError extends Error {
11
+ constructor(message = 'Invalid service token') {
12
+ super(message);
13
+ this.name = 'ServiceJwtError';
14
+ }
15
+ }
16
+ function secretKey(secret) {
17
+ return new TextEncoder().encode(secret);
18
+ }
19
+ function assertSecret(secret) {
20
+ if (secret.length < 32)
21
+ throw new ServiceJwtError('SERVICE_JWT_SECRET must be at least 32 characters');
22
+ }
23
+ export async function signServiceJwt({ secret, issuer, audience, subject, expiresInSeconds = DEFAULT_MAX_TTL_SECONDS, }) {
24
+ assertSecret(secret);
25
+ if (expiresInSeconds > DEFAULT_MAX_TTL_SECONDS)
26
+ throw new ServiceJwtError('service token ttl must not exceed 5 minutes');
27
+ return new SignJWT({})
28
+ .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
29
+ .setIssuer(issuer)
30
+ .setAudience(audience)
31
+ .setSubject(subject)
32
+ .setIssuedAt()
33
+ .setExpirationTime(`${expiresInSeconds}s`)
34
+ .sign(secretKey(secret));
35
+ }
36
+ export function signClientToImagenJwt(secret, clientName = DEFAULT_SERVICE_CLIENT_NAME) {
37
+ return signServiceJwt({
38
+ secret,
39
+ issuer: clientName,
40
+ audience: 'imagen',
41
+ subject: `service:${clientName}`,
42
+ });
43
+ }
44
+ export function signImagenToClientJwt(secret, clientName = DEFAULT_SERVICE_CLIENT_NAME) {
45
+ return signServiceJwt({
46
+ secret,
47
+ issuer: 'imagen',
48
+ audience: clientName,
49
+ subject: 'service:imagen',
50
+ });
51
+ }
52
+ export async function verifyServiceJwt({ token, secret, issuer, audience, subject, maxTtlSeconds = DEFAULT_MAX_TTL_SECONDS, }) {
53
+ try {
54
+ assertSecret(secret);
55
+ const { payload } = await jwtVerify(token, secretKey(secret), {
56
+ issuer,
57
+ audience,
58
+ algorithms: ['HS256'],
59
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
60
+ });
61
+ if (subject && payload.sub !== subject)
62
+ throw new ServiceJwtError('service token subject mismatch');
63
+ if (payload.exp && payload.iat) {
64
+ const ttl = payload.exp - payload.iat;
65
+ if (ttl > maxTtlSeconds + CLOCK_TOLERANCE_SECONDS)
66
+ throw new ServiceJwtError('service token ttl is too long');
67
+ }
68
+ return payload;
69
+ }
70
+ catch (error) {
71
+ if (error instanceof ServiceJwtError)
72
+ throw error;
73
+ throw new ServiceJwtError();
74
+ }
75
+ }
76
+ export async function verifyClientToImagenJwt(token, secret, clientName = DEFAULT_SERVICE_CLIENT_NAME) {
77
+ return verifyServiceJwt({
78
+ token,
79
+ secret,
80
+ issuer: clientName,
81
+ audience: 'imagen',
82
+ subject: `service:${clientName}`,
83
+ });
84
+ }
85
+ export async function verifyImagenCallbackJwt(token, secret, clientName = DEFAULT_SERVICE_CLIENT_NAME) {
86
+ return verifyServiceJwt({
87
+ token,
88
+ secret,
89
+ issuer: 'imagen',
90
+ audience: clientName,
91
+ subject: 'service:imagen',
92
+ });
93
+ }
@@ -0,0 +1,2 @@
1
+ export declare function stableStringify(value: unknown): string;
2
+ export declare function sha256StableHash(value: unknown): string;
@@ -0,0 +1,18 @@
1
+ import { createHash } from 'node:crypto';
2
+ function normalize(value) {
3
+ if (Array.isArray(value))
4
+ return value.map(normalize);
5
+ if (value && typeof value === 'object') {
6
+ const record = value;
7
+ return Object.fromEntries(Object.keys(record)
8
+ .sort()
9
+ .map(key => [key, normalize(record[key])]));
10
+ }
11
+ return value;
12
+ }
13
+ export function stableStringify(value) {
14
+ return JSON.stringify(normalize(value));
15
+ }
16
+ export function sha256StableHash(value) {
17
+ return createHash('sha256').update(stableStringify(value)).digest('hex');
18
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@donotlb/imagen-sdk",
3
+ "type": "module",
4
+ "version": "0.2.0",
5
+ "private": false,
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ },
11
+ "./image-models": {
12
+ "types": "./dist/image-models/index.d.ts",
13
+ "default": "./dist/image-models/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "jose": "^6.2.0",
24
+ "zod": "^4.3.5"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^24.12.4",
28
+ "typescript": "~5.9.3",
29
+ "vitest": "~4.1.8",
30
+ "@pi/tsconfig": "0.0.0"
31
+ },
32
+ "scripts": {
33
+ "typecheck": "tsc --noEmit",
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest"
37
+ }
38
+ }