bannerify-js 0.0.28 → 0.1.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.
package/dist/index.cjs CHANGED
@@ -18,39 +18,24 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
20
  // src/index.ts
21
- var src_exports = {};
22
- __export(src_exports, {
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
23
  Bannerify: () => Bannerify,
24
24
  createClient: () => createClient
25
25
  });
26
- module.exports = __toCommonJS(src_exports);
26
+ module.exports = __toCommonJS(index_exports);
27
27
 
28
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/errors/HTTPError.js
28
+ // ../../../node_modules/ky/distribution/errors/HTTPError.js
29
29
  var HTTPError = class extends Error {
30
+ response;
31
+ request;
32
+ options;
30
33
  constructor(response, request, options) {
31
34
  const code = response.status || response.status === 0 ? response.status : "";
32
35
  const title = response.statusText || "";
33
36
  const status = `${code} ${title}`.trim();
34
37
  const reason = status ? `status code ${status}` : "an unknown error";
35
- super(`Request failed with ${reason}`);
36
- Object.defineProperty(this, "response", {
37
- enumerable: true,
38
- configurable: true,
39
- writable: true,
40
- value: void 0
41
- });
42
- Object.defineProperty(this, "request", {
43
- enumerable: true,
44
- configurable: true,
45
- writable: true,
46
- value: void 0
47
- });
48
- Object.defineProperty(this, "options", {
49
- enumerable: true,
50
- configurable: true,
51
- writable: true,
52
- value: void 0
53
- });
38
+ super(`Request failed with ${reason}: ${request.method} ${request.url}`);
54
39
  this.name = "HTTPError";
55
40
  this.response = response;
56
41
  this.request = request;
@@ -58,25 +43,20 @@ var HTTPError = class extends Error {
58
43
  }
59
44
  };
60
45
 
61
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/errors/TimeoutError.js
46
+ // ../../../node_modules/ky/distribution/errors/TimeoutError.js
62
47
  var TimeoutError = class extends Error {
48
+ request;
63
49
  constructor(request) {
64
- super("Request timed out");
65
- Object.defineProperty(this, "request", {
66
- enumerable: true,
67
- configurable: true,
68
- writable: true,
69
- value: void 0
70
- });
50
+ super(`Request timed out: ${request.method} ${request.url}`);
71
51
  this.name = "TimeoutError";
72
52
  this.request = request;
73
53
  }
74
54
  };
75
55
 
76
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/is.js
56
+ // ../../../node_modules/ky/distribution/utils/is.js
77
57
  var isObject = (value) => value !== null && typeof value === "object";
78
58
 
79
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/merge.js
59
+ // ../../../node_modules/ky/distribution/utils/merge.js
80
60
  var validateAndMerge = (...sources) => {
81
61
  for (const source of sources) {
82
62
  if ((!isObject(source) || Array.isArray(source)) && source !== void 0) {
@@ -98,9 +78,19 @@ var mergeHeaders = (source1 = {}, source2 = {}) => {
98
78
  }
99
79
  return result;
100
80
  };
81
+ function newHookValue(original, incoming, property) {
82
+ return Object.hasOwn(incoming, property) && incoming[property] === void 0 ? [] : deepMerge(original[property] ?? [], incoming[property] ?? []);
83
+ }
84
+ var mergeHooks = (original = {}, incoming = {}) => ({
85
+ beforeRequest: newHookValue(original, incoming, "beforeRequest"),
86
+ beforeRetry: newHookValue(original, incoming, "beforeRetry"),
87
+ afterResponse: newHookValue(original, incoming, "afterResponse"),
88
+ beforeError: newHookValue(original, incoming, "beforeError")
89
+ });
101
90
  var deepMerge = (...sources) => {
102
91
  let returnValue = {};
103
92
  let headers = {};
93
+ let hooks = {};
104
94
  for (const source of sources) {
105
95
  if (Array.isArray(source)) {
106
96
  if (!Array.isArray(returnValue)) {
@@ -114,6 +104,10 @@ var deepMerge = (...sources) => {
114
104
  }
115
105
  returnValue = { ...returnValue, [key]: value };
116
106
  }
107
+ if (isObject(source.hooks)) {
108
+ hooks = mergeHooks(hooks, source.hooks);
109
+ returnValue.hooks = hooks;
110
+ }
117
111
  if (isObject(source.headers)) {
118
112
  headers = mergeHeaders(headers, source.headers);
119
113
  returnValue.headers = headers;
@@ -123,22 +117,29 @@ var deepMerge = (...sources) => {
123
117
  return returnValue;
124
118
  };
125
119
 
126
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/core/constants.js
120
+ // ../../../node_modules/ky/distribution/core/constants.js
127
121
  var supportsRequestStreams = (() => {
128
122
  let duplexAccessed = false;
129
123
  let hasContentType = false;
130
124
  const supportsReadableStream = typeof globalThis.ReadableStream === "function";
131
125
  const supportsRequest = typeof globalThis.Request === "function";
132
126
  if (supportsReadableStream && supportsRequest) {
133
- hasContentType = new globalThis.Request("https://empty.invalid", {
134
- body: new globalThis.ReadableStream(),
135
- method: "POST",
136
- // @ts-expect-error - Types are outdated.
137
- get duplex() {
138
- duplexAccessed = true;
139
- return "half";
140
- }
141
- }).headers.has("Content-Type");
127
+ try {
128
+ hasContentType = new globalThis.Request("https://empty.invalid", {
129
+ body: new globalThis.ReadableStream(),
130
+ method: "POST",
131
+ // @ts-expect-error - Types are outdated.
132
+ get duplex() {
133
+ duplexAccessed = true;
134
+ return "half";
135
+ }
136
+ }).headers.has("Content-Type");
137
+ } catch (error) {
138
+ if (error instanceof Error && error.message === "unsupported BodyInit type") {
139
+ return false;
140
+ }
141
+ throw error;
142
+ }
142
143
  }
143
144
  return duplexAccessed && !hasContentType;
144
145
  })();
@@ -160,6 +161,7 @@ var stop = Symbol("stop");
160
161
  var kyOptionKeys = {
161
162
  json: true,
162
163
  parseJson: true,
164
+ stringifyJson: true,
163
165
  searchParams: true,
164
166
  prefixUrl: true,
165
167
  retry: true,
@@ -184,10 +186,11 @@ var requestOptionsRegistry = {
184
186
  signal: true,
185
187
  window: true,
186
188
  dispatcher: true,
187
- duplex: true
189
+ duplex: true,
190
+ priority: true
188
191
  };
189
192
 
190
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/normalize.js
193
+ // ../../../node_modules/ky/distribution/utils/normalize.js
191
194
  var normalizeRequestMethod = (input) => requestMethods.includes(input) ? input.toUpperCase() : input;
192
195
  var retryMethods = ["get", "put", "head", "delete", "options", "trace"];
193
196
  var retryStatusCodes = [408, 413, 429, 500, 502, 503, 504];
@@ -216,12 +219,11 @@ var normalizeRetryOptions = (retry = {}) => {
216
219
  }
217
220
  return {
218
221
  ...defaultRetryOptions,
219
- ...retry,
220
- afterStatusCodes: retryAfterStatusCodes
222
+ ...retry
221
223
  };
222
224
  };
223
225
 
224
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/timeout.js
226
+ // ../../../node_modules/ky/distribution/utils/timeout.js
225
227
  async function timeout(request, init, abortController, options) {
226
228
  return new Promise((resolve, reject) => {
227
229
  const timeoutId = setTimeout(() => {
@@ -236,7 +238,7 @@ async function timeout(request, init, abortController, options) {
236
238
  });
237
239
  }
238
240
 
239
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/delay.js
241
+ // ../../../node_modules/ky/distribution/utils/delay.js
240
242
  async function delay(ms, { signal }) {
241
243
  return new Promise((resolve, reject) => {
242
244
  if (signal) {
@@ -254,7 +256,7 @@ async function delay(ms, { signal }) {
254
256
  });
255
257
  }
256
258
 
257
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/options.js
259
+ // ../../../node_modules/ky/distribution/utils/options.js
258
260
  var findUnknownOptions = (request, options) => {
259
261
  const unknownOptions = {};
260
262
  for (const key in options) {
@@ -265,11 +267,11 @@ var findUnknownOptions = (request, options) => {
265
267
  return unknownOptions;
266
268
  };
267
269
 
268
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/core/Ky.js
270
+ // ../../../node_modules/ky/distribution/core/Ky.js
269
271
  var Ky = class _Ky {
270
272
  static create(input, options) {
271
273
  const ky2 = new _Ky(input, options);
272
- const fn = async () => {
274
+ const function_ = async () => {
273
275
  if (typeof ky2._options.timeout === "number" && ky2._options.timeout > maxSafeTimeout) {
274
276
  throw new RangeError(`The \`timeout\` option cannot be greater than ${maxSafeTimeout}`);
275
277
  }
@@ -301,12 +303,11 @@ var Ky = class _Ky {
301
303
  return response;
302
304
  };
303
305
  const isRetriableMethod = ky2._options.retry.methods.includes(ky2.request.method.toLowerCase());
304
- const result = isRetriableMethod ? ky2._retry(fn) : fn();
306
+ const result = isRetriableMethod ? ky2._retry(function_) : function_();
305
307
  for (const [type, mimeType] of Object.entries(responseTypes)) {
306
308
  result[type] = async () => {
307
309
  ky2.request.headers.set("accept", ky2.request.headers.get("accept") || mimeType);
308
- const awaitedResult = await result;
309
- const response = awaitedResult.clone();
310
+ const response = await result;
310
311
  if (type === "json") {
311
312
  if (response.status === 204) {
312
313
  return "";
@@ -325,51 +326,24 @@ var Ky = class _Ky {
325
326
  }
326
327
  return result;
327
328
  }
329
+ request;
330
+ abortController;
331
+ _retryCount = 0;
332
+ _input;
333
+ _options;
328
334
  // eslint-disable-next-line complexity
329
335
  constructor(input, options = {}) {
330
- Object.defineProperty(this, "request", {
331
- enumerable: true,
332
- configurable: true,
333
- writable: true,
334
- value: void 0
335
- });
336
- Object.defineProperty(this, "abortController", {
337
- enumerable: true,
338
- configurable: true,
339
- writable: true,
340
- value: void 0
341
- });
342
- Object.defineProperty(this, "_retryCount", {
343
- enumerable: true,
344
- configurable: true,
345
- writable: true,
346
- value: 0
347
- });
348
- Object.defineProperty(this, "_input", {
349
- enumerable: true,
350
- configurable: true,
351
- writable: true,
352
- value: void 0
353
- });
354
- Object.defineProperty(this, "_options", {
355
- enumerable: true,
356
- configurable: true,
357
- writable: true,
358
- value: void 0
359
- });
360
336
  this._input = input;
361
337
  this._options = {
362
- // TODO: credentials can be removed when the spec change is implemented in all browsers. Context: https://www.chromestatus.com/feature/4539473312350208
363
- credentials: this._input.credentials || "same-origin",
364
338
  ...options,
365
339
  headers: mergeHeaders(this._input.headers, options.headers),
366
- hooks: deepMerge({
340
+ hooks: mergeHooks({
367
341
  beforeRequest: [],
368
342
  beforeRetry: [],
369
343
  beforeError: [],
370
344
  afterResponse: []
371
345
  }, options.hooks),
372
- method: normalizeRequestMethod(options.method ?? this._input.method),
346
+ method: normalizeRequestMethod(options.method ?? this._input.method ?? "GET"),
373
347
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
374
348
  prefixUrl: String(options.prefixUrl || ""),
375
349
  retry: normalizeRetryOptions(options.retry),
@@ -391,17 +365,22 @@ var Ky = class _Ky {
391
365
  }
392
366
  if (supportsAbortController) {
393
367
  this.abortController = new globalThis.AbortController();
394
- if (this._options.signal) {
395
- const originalSignal = this._options.signal;
396
- this._options.signal.addEventListener("abort", () => {
397
- this.abortController.abort(originalSignal.reason);
398
- });
368
+ const originalSignal = this._options.signal ?? this._input.signal;
369
+ if (originalSignal?.aborted) {
370
+ this.abortController.abort(originalSignal?.reason);
399
371
  }
372
+ originalSignal?.addEventListener("abort", () => {
373
+ this.abortController.abort(originalSignal.reason);
374
+ });
400
375
  this._options.signal = this.abortController.signal;
401
376
  }
402
377
  if (supportsRequestStreams) {
403
378
  this._options.duplex = "half";
404
379
  }
380
+ if (this._options.json !== void 0) {
381
+ this._options.body = this._options.stringifyJson?.(this._options.json) ?? JSON.stringify(this._options.json);
382
+ this._options.headers.set("content-type", this._options.headers.get("content-type") ?? "application/json");
383
+ }
405
384
  this.request = new globalThis.Request(this._input, this._options);
406
385
  if (this._options.searchParams) {
407
386
  const textSearchParams = typeof this._options.searchParams === "string" ? this._options.searchParams.replace(/^\?/, "") : new URLSearchParams(this._options.searchParams).toString();
@@ -412,40 +391,33 @@ var Ky = class _Ky {
412
391
  }
413
392
  this.request = new globalThis.Request(new globalThis.Request(url, { ...this.request }), this._options);
414
393
  }
415
- if (this._options.json !== void 0) {
416
- this._options.body = JSON.stringify(this._options.json);
417
- this.request.headers.set("content-type", this._options.headers.get("content-type") ?? "application/json");
418
- this.request = new globalThis.Request(this.request, { body: this._options.body });
419
- }
420
394
  }
421
395
  _calculateRetryDelay(error) {
422
396
  this._retryCount++;
423
- if (this._retryCount < this._options.retry.limit && !(error instanceof TimeoutError)) {
424
- if (error instanceof HTTPError) {
425
- if (!this._options.retry.statusCodes.includes(error.response.status)) {
426
- return 0;
427
- }
428
- const retryAfter = error.response.headers.get("Retry-After");
429
- if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
430
- let after = Number(retryAfter);
431
- if (Number.isNaN(after)) {
432
- after = Date.parse(retryAfter) - Date.now();
433
- } else {
434
- after *= 1e3;
435
- }
436
- if (this._options.retry.maxRetryAfter !== void 0 && after > this._options.retry.maxRetryAfter) {
437
- return 0;
438
- }
439
- return after;
440
- }
441
- if (error.response.status === 413) {
442
- return 0;
397
+ if (this._retryCount > this._options.retry.limit || error instanceof TimeoutError) {
398
+ throw error;
399
+ }
400
+ if (error instanceof HTTPError) {
401
+ if (!this._options.retry.statusCodes.includes(error.response.status)) {
402
+ throw error;
403
+ }
404
+ const retryAfter = error.response.headers.get("Retry-After") ?? error.response.headers.get("RateLimit-Reset") ?? error.response.headers.get("X-RateLimit-Reset") ?? error.response.headers.get("X-Rate-Limit-Reset");
405
+ if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
406
+ let after = Number(retryAfter) * 1e3;
407
+ if (Number.isNaN(after)) {
408
+ after = Date.parse(retryAfter) - Date.now();
409
+ } else if (after >= Date.parse("2024-01-01")) {
410
+ after -= Date.now();
443
411
  }
412
+ const max = this._options.retry.maxRetryAfter ?? after;
413
+ return after < max ? after : max;
414
+ }
415
+ if (error.response.status === 413) {
416
+ throw error;
444
417
  }
445
- const retryDelay = this._options.retry.delay(this._retryCount);
446
- return Math.min(this._options.retry.backoffLimit, retryDelay);
447
418
  }
448
- return 0;
419
+ const retryDelay = this._options.retry.delay(this._retryCount);
420
+ return Math.min(this._options.retry.backoffLimit, retryDelay);
449
421
  }
450
422
  _decorateResponse(response) {
451
423
  if (this._options.parseJson) {
@@ -453,27 +425,27 @@ var Ky = class _Ky {
453
425
  }
454
426
  return response;
455
427
  }
456
- async _retry(fn) {
428
+ async _retry(function_) {
457
429
  try {
458
- return await fn();
430
+ return await function_();
459
431
  } catch (error) {
460
432
  const ms = Math.min(this._calculateRetryDelay(error), maxSafeTimeout);
461
- if (ms !== 0 && this._retryCount > 0) {
462
- await delay(ms, { signal: this._options.signal });
463
- for (const hook of this._options.hooks.beforeRetry) {
464
- const hookResult = await hook({
465
- request: this.request,
466
- options: this._options,
467
- error,
468
- retryCount: this._retryCount
469
- });
470
- if (hookResult === stop) {
471
- return;
472
- }
433
+ if (this._retryCount < 1) {
434
+ throw error;
435
+ }
436
+ await delay(ms, { signal: this._options.signal });
437
+ for (const hook of this._options.hooks.beforeRetry) {
438
+ const hookResult = await hook({
439
+ request: this.request,
440
+ options: this._options,
441
+ error,
442
+ retryCount: this._retryCount
443
+ });
444
+ if (hookResult === stop) {
445
+ return;
473
446
  }
474
- return this._retry(fn);
475
447
  }
476
- throw error;
448
+ return this._retry(function_);
477
449
  }
478
450
  }
479
451
  async _fetch() {
@@ -488,10 +460,12 @@ var Ky = class _Ky {
488
460
  }
489
461
  }
490
462
  const nonRequestOptions = findUnknownOptions(this.request, this._options);
463
+ const mainRequest = this.request;
464
+ this.request = mainRequest.clone();
491
465
  if (this._options.timeout === false) {
492
- return this._options.fetch(this.request.clone(), nonRequestOptions);
466
+ return this._options.fetch(mainRequest, nonRequestOptions);
493
467
  }
494
- return timeout(this.request.clone(), nonRequestOptions, this.abortController, this._options);
468
+ return timeout(mainRequest, nonRequestOptions, this.abortController, this._options);
495
469
  }
496
470
  /* istanbul ignore next */
497
471
  _stream(response, onDownloadProgress) {
@@ -537,14 +511,19 @@ var Ky = class _Ky {
537
511
  }
538
512
  };
539
513
 
540
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/index.js
514
+ // ../../../node_modules/ky/distribution/index.js
541
515
  var createInstance = (defaults) => {
542
516
  const ky2 = (input, options) => Ky.create(input, validateAndMerge(defaults, options));
543
517
  for (const method of requestMethods) {
544
518
  ky2[method] = (input, options) => Ky.create(input, validateAndMerge(defaults, options, { method }));
545
519
  }
546
520
  ky2.create = (newDefaults) => createInstance(validateAndMerge(newDefaults));
547
- ky2.extend = (newDefaults) => createInstance(validateAndMerge(defaults, newDefaults));
521
+ ky2.extend = (newDefaults) => {
522
+ if (typeof newDefaults === "function") {
523
+ newDefaults = newDefaults(defaults ?? {});
524
+ }
525
+ return createInstance(validateAndMerge(defaults, newDefaults));
526
+ };
548
527
  ky2.stop = stop;
549
528
  return ky2;
550
529
  };
@@ -578,6 +557,24 @@ function getTelemetry() {
578
557
  return { platform, runtime, sdkVersions };
579
558
  }
580
559
 
560
+ // src/format.ts
561
+ var SUPPORTED_FORMATS = ["png", "jpeg", "webp"];
562
+ var isSupportedFormat = (value) => SUPPORTED_FORMATS.includes(value);
563
+ var validateImageFormat = (value) => {
564
+ if (!value) {
565
+ return { ok: true, format: "png" };
566
+ }
567
+ if (isSupportedFormat(value)) {
568
+ return { ok: true, format: value };
569
+ }
570
+ return {
571
+ ok: false,
572
+ error: {
573
+ message: `Unsupported format "${value}". Valid formats are png, jpeg, or webp.`
574
+ }
575
+ };
576
+ };
577
+
581
578
  // src/types.ts
582
579
  var timeoutError = {
583
580
  error: {
@@ -590,6 +587,15 @@ var timeoutError = {
590
587
  };
591
588
 
592
589
  // src/client.ts
590
+ var invalidFormatResult = (value) => ({
591
+ error: {
592
+ // @ts-ignore – local validation error
593
+ code: "INVALID_FORMAT",
594
+ docs: "https://bannerify.co/docs",
595
+ message: `Unsupported format "${value ?? "undefined"}". Valid formats are png, jpeg, or webp.`,
596
+ requestId: "local"
597
+ }
598
+ });
593
599
  function createClient(apiKey, opts) {
594
600
  return new Bannerify(apiKey, opts);
595
601
  }
@@ -608,32 +614,35 @@ var Bannerify = class {
608
614
  "X-Platform": telemetry?.platform,
609
615
  "X-Runtime": telemetry?.runtime
610
616
  },
611
- timeout: opts?.timeout ?? 1e4
617
+ timeout: opts?.timeout ?? 6e4
612
618
  });
613
619
  }
614
620
  client;
615
621
  baseUrl;
616
622
  async createImage(templateId, options) {
617
623
  try {
624
+ const normalizedFormat = validateImageFormat(options?.format);
625
+ if (!normalizedFormat.ok) {
626
+ return invalidFormatResult(options?.format);
627
+ }
618
628
  const res = await this.client.post("templates/createImage", {
619
629
  json: {
620
630
  modifications: options?.modifications ?? [],
621
631
  // template: JSON.stringify(options?.template ?? {}),
622
632
  templateId,
623
633
  apiKey: this.apiKey,
624
- format: options?.format
634
+ format: normalizedFormat.format,
635
+ thumbnail: options?.thumbnail ?? false
625
636
  }
626
637
  });
627
- if (options?.format === "svg") {
628
- return { result: await res.text() };
629
- }
630
638
  return { result: await res.arrayBuffer() };
631
639
  } catch (e) {
632
640
  if (e instanceof HTTPError) {
633
- return (await e.response.json())["error"];
641
+ const json = await e.response.json();
642
+ return { error: json.error };
634
643
  }
635
644
  if (e instanceof TimeoutError) {
636
- return timeoutError;
645
+ return { error: timeoutError.error };
637
646
  }
638
647
  throw e;
639
648
  }
@@ -645,40 +654,47 @@ var Bannerify = class {
645
654
  modifications: options?.modifications ?? [],
646
655
  // template: JSON.stringify(options?.template ?? {}),
647
656
  templateId,
648
- apiKey: this.apiKey,
649
- format: options?.format
657
+ apiKey: this.apiKey
650
658
  }
651
659
  });
652
660
  return { result: await res.arrayBuffer() };
653
661
  } catch (e) {
654
662
  if (e instanceof HTTPError) {
655
- return (await e.response.json())["error"];
663
+ const json = await e.response.json();
664
+ return { error: json.error };
656
665
  }
657
666
  if (e instanceof TimeoutError) {
658
- return timeoutError;
667
+ return { error: timeoutError.error };
659
668
  }
660
669
  throw e;
661
670
  }
662
671
  }
663
672
  async createStoredImage(templateId, options) {
664
673
  try {
674
+ const normalizedFormat = validateImageFormat(options?.format);
675
+ if (!normalizedFormat.ok) {
676
+ return invalidFormatResult(options?.format);
677
+ }
665
678
  const res = await this.client.post("templates/createStoredImage", {
666
679
  json: {
667
680
  modifications: options?.modifications ?? [],
668
681
  // template: JSON.stringify(options?.template ?? {}),
669
682
  templateId,
670
683
  apiKey: this.apiKey,
671
- format: options?.format
684
+ format: normalizedFormat.format,
685
+ thumbnail: options?.thumbnail ?? false,
686
+ s3Config: options?.s3Config
672
687
  }
673
688
  });
674
689
  const json = await res.json();
675
690
  return { result: json.url };
676
691
  } catch (e) {
677
692
  if (e instanceof HTTPError) {
678
- return (await e.response.json())["error"];
693
+ const json = await e.response.json();
694
+ return { error: json.error };
679
695
  }
680
696
  if (e instanceof TimeoutError) {
681
- return timeoutError;
697
+ return { error: timeoutError.error };
682
698
  }
683
699
  throw e;
684
700
  }
@@ -690,12 +706,33 @@ var Bannerify = class {
690
706
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
691
707
  return hashHex;
692
708
  };
709
+ // The signature is an HMAC keyed with the API key itself, so a signed URL can
710
+ // be shared without exposing anything that can sign a different one. The
711
+ // apiKeyHashed in the URL only tells the server which key to verify with.
712
+ #signText = async (text) => {
713
+ const key = await crypto.subtle.importKey(
714
+ "raw",
715
+ new TextEncoder().encode(this.apiKey),
716
+ { name: "HMAC", hash: "SHA-256" },
717
+ false,
718
+ ["sign"]
719
+ );
720
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(text));
721
+ return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
722
+ };
693
723
  async generateImageSignedUrl(templateId, options) {
694
724
  const apiKeyHashed = await this.#hashText(this.apiKey);
695
725
  const searchParams = new URLSearchParams();
696
726
  searchParams.set("apiKeyHashed", apiKeyHashed);
697
- if (options?.format === "svg") {
698
- searchParams.set("format", "svg");
727
+ if (options?.format) {
728
+ const normalizedFormat = validateImageFormat(options.format);
729
+ if (!normalizedFormat.ok) {
730
+ throw new Error(normalizedFormat.error.message);
731
+ }
732
+ searchParams.set("format", normalizedFormat.format);
733
+ }
734
+ if (options?.quality !== void 0) {
735
+ searchParams.set("quality", String(options.quality));
699
736
  }
700
737
  if (options?.modifications) {
701
738
  searchParams.set("modifications", JSON.stringify(options?.modifications));
@@ -703,12 +740,12 @@ var Bannerify = class {
703
740
  if (options?.nocache) {
704
741
  searchParams.set("nocache", "true");
705
742
  }
743
+ if (options?.thumbnail) {
744
+ searchParams.set("thumbnail", "true");
745
+ }
706
746
  searchParams.set("templateId", templateId);
707
747
  searchParams.sort();
708
- searchParams.set(
709
- "sign",
710
- await this.#hashText(searchParams.toString() + searchParams.get("apiKeyHashed"))
711
- );
748
+ searchParams.set("sign", await this.#signText(searchParams.toString()));
712
749
  return `${this.baseUrl}/templates/signedurl?${searchParams.toString()}`;
713
750
  }
714
751
  };