bannerify-js 0.0.27 → 0.0.29

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.js CHANGED
@@ -1,29 +1,14 @@
1
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/errors/HTTPError.js
1
+ // ../../../node_modules/ky/distribution/errors/HTTPError.js
2
2
  var HTTPError = class extends Error {
3
+ response;
4
+ request;
5
+ options;
3
6
  constructor(response, request, options) {
4
7
  const code = response.status || response.status === 0 ? response.status : "";
5
8
  const title = response.statusText || "";
6
9
  const status = `${code} ${title}`.trim();
7
10
  const reason = status ? `status code ${status}` : "an unknown error";
8
- super(`Request failed with ${reason}`);
9
- Object.defineProperty(this, "response", {
10
- enumerable: true,
11
- configurable: true,
12
- writable: true,
13
- value: void 0
14
- });
15
- Object.defineProperty(this, "request", {
16
- enumerable: true,
17
- configurable: true,
18
- writable: true,
19
- value: void 0
20
- });
21
- Object.defineProperty(this, "options", {
22
- enumerable: true,
23
- configurable: true,
24
- writable: true,
25
- value: void 0
26
- });
11
+ super(`Request failed with ${reason}: ${request.method} ${request.url}`);
27
12
  this.name = "HTTPError";
28
13
  this.response = response;
29
14
  this.request = request;
@@ -31,25 +16,20 @@ var HTTPError = class extends Error {
31
16
  }
32
17
  };
33
18
 
34
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/errors/TimeoutError.js
19
+ // ../../../node_modules/ky/distribution/errors/TimeoutError.js
35
20
  var TimeoutError = class extends Error {
21
+ request;
36
22
  constructor(request) {
37
- super("Request timed out");
38
- Object.defineProperty(this, "request", {
39
- enumerable: true,
40
- configurable: true,
41
- writable: true,
42
- value: void 0
43
- });
23
+ super(`Request timed out: ${request.method} ${request.url}`);
44
24
  this.name = "TimeoutError";
45
25
  this.request = request;
46
26
  }
47
27
  };
48
28
 
49
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/is.js
29
+ // ../../../node_modules/ky/distribution/utils/is.js
50
30
  var isObject = (value) => value !== null && typeof value === "object";
51
31
 
52
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/merge.js
32
+ // ../../../node_modules/ky/distribution/utils/merge.js
53
33
  var validateAndMerge = (...sources) => {
54
34
  for (const source of sources) {
55
35
  if ((!isObject(source) || Array.isArray(source)) && source !== void 0) {
@@ -71,9 +51,19 @@ var mergeHeaders = (source1 = {}, source2 = {}) => {
71
51
  }
72
52
  return result;
73
53
  };
54
+ function newHookValue(original, incoming, property) {
55
+ return Object.hasOwn(incoming, property) && incoming[property] === void 0 ? [] : deepMerge(original[property] ?? [], incoming[property] ?? []);
56
+ }
57
+ var mergeHooks = (original = {}, incoming = {}) => ({
58
+ beforeRequest: newHookValue(original, incoming, "beforeRequest"),
59
+ beforeRetry: newHookValue(original, incoming, "beforeRetry"),
60
+ afterResponse: newHookValue(original, incoming, "afterResponse"),
61
+ beforeError: newHookValue(original, incoming, "beforeError")
62
+ });
74
63
  var deepMerge = (...sources) => {
75
64
  let returnValue = {};
76
65
  let headers = {};
66
+ let hooks = {};
77
67
  for (const source of sources) {
78
68
  if (Array.isArray(source)) {
79
69
  if (!Array.isArray(returnValue)) {
@@ -87,6 +77,10 @@ var deepMerge = (...sources) => {
87
77
  }
88
78
  returnValue = { ...returnValue, [key]: value };
89
79
  }
80
+ if (isObject(source.hooks)) {
81
+ hooks = mergeHooks(hooks, source.hooks);
82
+ returnValue.hooks = hooks;
83
+ }
90
84
  if (isObject(source.headers)) {
91
85
  headers = mergeHeaders(headers, source.headers);
92
86
  returnValue.headers = headers;
@@ -96,22 +90,29 @@ var deepMerge = (...sources) => {
96
90
  return returnValue;
97
91
  };
98
92
 
99
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/core/constants.js
93
+ // ../../../node_modules/ky/distribution/core/constants.js
100
94
  var supportsRequestStreams = (() => {
101
95
  let duplexAccessed = false;
102
96
  let hasContentType = false;
103
97
  const supportsReadableStream = typeof globalThis.ReadableStream === "function";
104
98
  const supportsRequest = typeof globalThis.Request === "function";
105
99
  if (supportsReadableStream && supportsRequest) {
106
- hasContentType = new globalThis.Request("https://empty.invalid", {
107
- body: new globalThis.ReadableStream(),
108
- method: "POST",
109
- // @ts-expect-error - Types are outdated.
110
- get duplex() {
111
- duplexAccessed = true;
112
- return "half";
113
- }
114
- }).headers.has("Content-Type");
100
+ try {
101
+ hasContentType = new globalThis.Request("https://empty.invalid", {
102
+ body: new globalThis.ReadableStream(),
103
+ method: "POST",
104
+ // @ts-expect-error - Types are outdated.
105
+ get duplex() {
106
+ duplexAccessed = true;
107
+ return "half";
108
+ }
109
+ }).headers.has("Content-Type");
110
+ } catch (error) {
111
+ if (error instanceof Error && error.message === "unsupported BodyInit type") {
112
+ return false;
113
+ }
114
+ throw error;
115
+ }
115
116
  }
116
117
  return duplexAccessed && !hasContentType;
117
118
  })();
@@ -133,6 +134,7 @@ var stop = Symbol("stop");
133
134
  var kyOptionKeys = {
134
135
  json: true,
135
136
  parseJson: true,
137
+ stringifyJson: true,
136
138
  searchParams: true,
137
139
  prefixUrl: true,
138
140
  retry: true,
@@ -157,10 +159,11 @@ var requestOptionsRegistry = {
157
159
  signal: true,
158
160
  window: true,
159
161
  dispatcher: true,
160
- duplex: true
162
+ duplex: true,
163
+ priority: true
161
164
  };
162
165
 
163
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/normalize.js
166
+ // ../../../node_modules/ky/distribution/utils/normalize.js
164
167
  var normalizeRequestMethod = (input) => requestMethods.includes(input) ? input.toUpperCase() : input;
165
168
  var retryMethods = ["get", "put", "head", "delete", "options", "trace"];
166
169
  var retryStatusCodes = [408, 413, 429, 500, 502, 503, 504];
@@ -189,12 +192,11 @@ var normalizeRetryOptions = (retry = {}) => {
189
192
  }
190
193
  return {
191
194
  ...defaultRetryOptions,
192
- ...retry,
193
- afterStatusCodes: retryAfterStatusCodes
195
+ ...retry
194
196
  };
195
197
  };
196
198
 
197
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/timeout.js
199
+ // ../../../node_modules/ky/distribution/utils/timeout.js
198
200
  async function timeout(request, init, abortController, options) {
199
201
  return new Promise((resolve, reject) => {
200
202
  const timeoutId = setTimeout(() => {
@@ -209,7 +211,7 @@ async function timeout(request, init, abortController, options) {
209
211
  });
210
212
  }
211
213
 
212
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/delay.js
214
+ // ../../../node_modules/ky/distribution/utils/delay.js
213
215
  async function delay(ms, { signal }) {
214
216
  return new Promise((resolve, reject) => {
215
217
  if (signal) {
@@ -227,7 +229,7 @@ async function delay(ms, { signal }) {
227
229
  });
228
230
  }
229
231
 
230
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/utils/options.js
232
+ // ../../../node_modules/ky/distribution/utils/options.js
231
233
  var findUnknownOptions = (request, options) => {
232
234
  const unknownOptions = {};
233
235
  for (const key in options) {
@@ -238,11 +240,11 @@ var findUnknownOptions = (request, options) => {
238
240
  return unknownOptions;
239
241
  };
240
242
 
241
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/core/Ky.js
243
+ // ../../../node_modules/ky/distribution/core/Ky.js
242
244
  var Ky = class _Ky {
243
245
  static create(input, options) {
244
246
  const ky2 = new _Ky(input, options);
245
- const fn = async () => {
247
+ const function_ = async () => {
246
248
  if (typeof ky2._options.timeout === "number" && ky2._options.timeout > maxSafeTimeout) {
247
249
  throw new RangeError(`The \`timeout\` option cannot be greater than ${maxSafeTimeout}`);
248
250
  }
@@ -274,12 +276,11 @@ var Ky = class _Ky {
274
276
  return response;
275
277
  };
276
278
  const isRetriableMethod = ky2._options.retry.methods.includes(ky2.request.method.toLowerCase());
277
- const result = isRetriableMethod ? ky2._retry(fn) : fn();
279
+ const result = isRetriableMethod ? ky2._retry(function_) : function_();
278
280
  for (const [type, mimeType] of Object.entries(responseTypes)) {
279
281
  result[type] = async () => {
280
282
  ky2.request.headers.set("accept", ky2.request.headers.get("accept") || mimeType);
281
- const awaitedResult = await result;
282
- const response = awaitedResult.clone();
283
+ const response = await result;
283
284
  if (type === "json") {
284
285
  if (response.status === 204) {
285
286
  return "";
@@ -298,51 +299,24 @@ var Ky = class _Ky {
298
299
  }
299
300
  return result;
300
301
  }
302
+ request;
303
+ abortController;
304
+ _retryCount = 0;
305
+ _input;
306
+ _options;
301
307
  // eslint-disable-next-line complexity
302
308
  constructor(input, options = {}) {
303
- Object.defineProperty(this, "request", {
304
- enumerable: true,
305
- configurable: true,
306
- writable: true,
307
- value: void 0
308
- });
309
- Object.defineProperty(this, "abortController", {
310
- enumerable: true,
311
- configurable: true,
312
- writable: true,
313
- value: void 0
314
- });
315
- Object.defineProperty(this, "_retryCount", {
316
- enumerable: true,
317
- configurable: true,
318
- writable: true,
319
- value: 0
320
- });
321
- Object.defineProperty(this, "_input", {
322
- enumerable: true,
323
- configurable: true,
324
- writable: true,
325
- value: void 0
326
- });
327
- Object.defineProperty(this, "_options", {
328
- enumerable: true,
329
- configurable: true,
330
- writable: true,
331
- value: void 0
332
- });
333
309
  this._input = input;
334
310
  this._options = {
335
- // TODO: credentials can be removed when the spec change is implemented in all browsers. Context: https://www.chromestatus.com/feature/4539473312350208
336
- credentials: this._input.credentials || "same-origin",
337
311
  ...options,
338
312
  headers: mergeHeaders(this._input.headers, options.headers),
339
- hooks: deepMerge({
313
+ hooks: mergeHooks({
340
314
  beforeRequest: [],
341
315
  beforeRetry: [],
342
316
  beforeError: [],
343
317
  afterResponse: []
344
318
  }, options.hooks),
345
- method: normalizeRequestMethod(options.method ?? this._input.method),
319
+ method: normalizeRequestMethod(options.method ?? this._input.method ?? "GET"),
346
320
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
347
321
  prefixUrl: String(options.prefixUrl || ""),
348
322
  retry: normalizeRetryOptions(options.retry),
@@ -364,17 +338,22 @@ var Ky = class _Ky {
364
338
  }
365
339
  if (supportsAbortController) {
366
340
  this.abortController = new globalThis.AbortController();
367
- if (this._options.signal) {
368
- const originalSignal = this._options.signal;
369
- this._options.signal.addEventListener("abort", () => {
370
- this.abortController.abort(originalSignal.reason);
371
- });
341
+ const originalSignal = this._options.signal ?? this._input.signal;
342
+ if (originalSignal?.aborted) {
343
+ this.abortController.abort(originalSignal?.reason);
372
344
  }
345
+ originalSignal?.addEventListener("abort", () => {
346
+ this.abortController.abort(originalSignal.reason);
347
+ });
373
348
  this._options.signal = this.abortController.signal;
374
349
  }
375
350
  if (supportsRequestStreams) {
376
351
  this._options.duplex = "half";
377
352
  }
353
+ if (this._options.json !== void 0) {
354
+ this._options.body = this._options.stringifyJson?.(this._options.json) ?? JSON.stringify(this._options.json);
355
+ this._options.headers.set("content-type", this._options.headers.get("content-type") ?? "application/json");
356
+ }
378
357
  this.request = new globalThis.Request(this._input, this._options);
379
358
  if (this._options.searchParams) {
380
359
  const textSearchParams = typeof this._options.searchParams === "string" ? this._options.searchParams.replace(/^\?/, "") : new URLSearchParams(this._options.searchParams).toString();
@@ -385,40 +364,33 @@ var Ky = class _Ky {
385
364
  }
386
365
  this.request = new globalThis.Request(new globalThis.Request(url, { ...this.request }), this._options);
387
366
  }
388
- if (this._options.json !== void 0) {
389
- this._options.body = JSON.stringify(this._options.json);
390
- this.request.headers.set("content-type", this._options.headers.get("content-type") ?? "application/json");
391
- this.request = new globalThis.Request(this.request, { body: this._options.body });
392
- }
393
367
  }
394
368
  _calculateRetryDelay(error) {
395
369
  this._retryCount++;
396
- if (this._retryCount < this._options.retry.limit && !(error instanceof TimeoutError)) {
397
- if (error instanceof HTTPError) {
398
- if (!this._options.retry.statusCodes.includes(error.response.status)) {
399
- return 0;
400
- }
401
- const retryAfter = error.response.headers.get("Retry-After");
402
- if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
403
- let after = Number(retryAfter);
404
- if (Number.isNaN(after)) {
405
- after = Date.parse(retryAfter) - Date.now();
406
- } else {
407
- after *= 1e3;
408
- }
409
- if (this._options.retry.maxRetryAfter !== void 0 && after > this._options.retry.maxRetryAfter) {
410
- return 0;
411
- }
412
- return after;
413
- }
414
- if (error.response.status === 413) {
415
- return 0;
370
+ if (this._retryCount > this._options.retry.limit || error instanceof TimeoutError) {
371
+ throw error;
372
+ }
373
+ if (error instanceof HTTPError) {
374
+ if (!this._options.retry.statusCodes.includes(error.response.status)) {
375
+ throw error;
376
+ }
377
+ 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");
378
+ if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
379
+ let after = Number(retryAfter) * 1e3;
380
+ if (Number.isNaN(after)) {
381
+ after = Date.parse(retryAfter) - Date.now();
382
+ } else if (after >= Date.parse("2024-01-01")) {
383
+ after -= Date.now();
416
384
  }
385
+ const max = this._options.retry.maxRetryAfter ?? after;
386
+ return after < max ? after : max;
387
+ }
388
+ if (error.response.status === 413) {
389
+ throw error;
417
390
  }
418
- const retryDelay = this._options.retry.delay(this._retryCount);
419
- return Math.min(this._options.retry.backoffLimit, retryDelay);
420
391
  }
421
- return 0;
392
+ const retryDelay = this._options.retry.delay(this._retryCount);
393
+ return Math.min(this._options.retry.backoffLimit, retryDelay);
422
394
  }
423
395
  _decorateResponse(response) {
424
396
  if (this._options.parseJson) {
@@ -426,27 +398,27 @@ var Ky = class _Ky {
426
398
  }
427
399
  return response;
428
400
  }
429
- async _retry(fn) {
401
+ async _retry(function_) {
430
402
  try {
431
- return await fn();
403
+ return await function_();
432
404
  } catch (error) {
433
405
  const ms = Math.min(this._calculateRetryDelay(error), maxSafeTimeout);
434
- if (ms !== 0 && this._retryCount > 0) {
435
- await delay(ms, { signal: this._options.signal });
436
- for (const hook of this._options.hooks.beforeRetry) {
437
- const hookResult = await hook({
438
- request: this.request,
439
- options: this._options,
440
- error,
441
- retryCount: this._retryCount
442
- });
443
- if (hookResult === stop) {
444
- return;
445
- }
406
+ if (this._retryCount < 1) {
407
+ throw error;
408
+ }
409
+ await delay(ms, { signal: this._options.signal });
410
+ for (const hook of this._options.hooks.beforeRetry) {
411
+ const hookResult = await hook({
412
+ request: this.request,
413
+ options: this._options,
414
+ error,
415
+ retryCount: this._retryCount
416
+ });
417
+ if (hookResult === stop) {
418
+ return;
446
419
  }
447
- return this._retry(fn);
448
420
  }
449
- throw error;
421
+ return this._retry(function_);
450
422
  }
451
423
  }
452
424
  async _fetch() {
@@ -461,10 +433,12 @@ var Ky = class _Ky {
461
433
  }
462
434
  }
463
435
  const nonRequestOptions = findUnknownOptions(this.request, this._options);
436
+ const mainRequest = this.request;
437
+ this.request = mainRequest.clone();
464
438
  if (this._options.timeout === false) {
465
- return this._options.fetch(this.request.clone(), nonRequestOptions);
439
+ return this._options.fetch(mainRequest, nonRequestOptions);
466
440
  }
467
- return timeout(this.request.clone(), nonRequestOptions, this.abortController, this._options);
441
+ return timeout(mainRequest, nonRequestOptions, this.abortController, this._options);
468
442
  }
469
443
  /* istanbul ignore next */
470
444
  _stream(response, onDownloadProgress) {
@@ -510,14 +484,19 @@ var Ky = class _Ky {
510
484
  }
511
485
  };
512
486
 
513
- // node_modules/.pnpm/ky@1.1.3/node_modules/ky/distribution/index.js
487
+ // ../../../node_modules/ky/distribution/index.js
514
488
  var createInstance = (defaults) => {
515
489
  const ky2 = (input, options) => Ky.create(input, validateAndMerge(defaults, options));
516
490
  for (const method of requestMethods) {
517
491
  ky2[method] = (input, options) => Ky.create(input, validateAndMerge(defaults, options, { method }));
518
492
  }
519
493
  ky2.create = (newDefaults) => createInstance(validateAndMerge(newDefaults));
520
- ky2.extend = (newDefaults) => createInstance(validateAndMerge(defaults, newDefaults));
494
+ ky2.extend = (newDefaults) => {
495
+ if (typeof newDefaults === "function") {
496
+ newDefaults = newDefaults(defaults ?? {});
497
+ }
498
+ return createInstance(validateAndMerge(defaults, newDefaults));
499
+ };
521
500
  ky2.stop = stop;
522
501
  return ky2;
523
502
  };
@@ -525,7 +504,7 @@ var ky = createInstance();
525
504
  var distribution_default = ky;
526
505
 
527
506
  // package.json
528
- var version = "0.0.26";
507
+ var version = "0.0.28";
529
508
 
530
509
  // src/telemetry.ts
531
510
  function getTelemetry() {
@@ -551,6 +530,24 @@ function getTelemetry() {
551
530
  return { platform, runtime, sdkVersions };
552
531
  }
553
532
 
533
+ // src/format.ts
534
+ var SUPPORTED_FORMATS = ["png", "jpeg", "webp"];
535
+ var isSupportedFormat = (value) => SUPPORTED_FORMATS.includes(value);
536
+ var validateImageFormat = (value) => {
537
+ if (!value) {
538
+ return { ok: true, format: "png" };
539
+ }
540
+ if (isSupportedFormat(value)) {
541
+ return { ok: true, format: value };
542
+ }
543
+ return {
544
+ ok: false,
545
+ error: {
546
+ message: `Unsupported format "${value}". Valid formats are png, jpeg, or webp.`
547
+ }
548
+ };
549
+ };
550
+
554
551
  // src/types.ts
555
552
  var timeoutError = {
556
553
  error: {
@@ -563,6 +560,15 @@ var timeoutError = {
563
560
  };
564
561
 
565
562
  // src/client.ts
563
+ var invalidFormatResult = (value) => ({
564
+ error: {
565
+ // @ts-ignore – local validation error
566
+ code: "INVALID_FORMAT",
567
+ docs: "https://bannerify.co/docs",
568
+ message: `Unsupported format "${value ?? "undefined"}". Valid formats are png, jpeg, or webp.`,
569
+ requestId: "local"
570
+ }
571
+ });
566
572
  function createClient(apiKey, opts) {
567
573
  return new Bannerify(apiKey, opts);
568
574
  }
@@ -581,41 +587,90 @@ var Bannerify = class {
581
587
  "X-Platform": telemetry?.platform,
582
588
  "X-Runtime": telemetry?.runtime
583
589
  },
584
- timeout: opts?.timeout ?? 1e4
590
+ timeout: opts?.timeout ?? 6e4
585
591
  });
586
592
  }
587
593
  client;
588
594
  baseUrl;
589
595
  async createImage(templateId, options) {
590
596
  try {
597
+ const normalizedFormat = validateImageFormat(options?.format);
598
+ if (!normalizedFormat.ok) {
599
+ return invalidFormatResult(options?.format);
600
+ }
591
601
  const res = await this.client.post("templates/createImage", {
592
602
  json: {
593
603
  modifications: options?.modifications ?? [],
594
604
  // template: JSON.stringify(options?.template ?? {}),
595
605
  templateId,
596
606
  apiKey: this.apiKey,
597
- format: options?.format
607
+ format: normalizedFormat.format,
608
+ thumbnail: options?.thumbnail ?? false
598
609
  }
599
610
  });
600
- if (options?.format === "svg") {
601
- return { result: await res.text() };
602
- }
603
611
  return { result: await res.arrayBuffer() };
604
612
  } catch (e) {
605
613
  if (e instanceof HTTPError) {
606
- return (await e.response.json())["error"];
614
+ const json = await e.response.json();
615
+ return { error: json.error };
607
616
  }
608
617
  if (e instanceof TimeoutError) {
609
- return timeoutError;
618
+ return { error: timeoutError.error };
610
619
  }
611
620
  throw e;
612
621
  }
613
622
  }
614
623
  async createPdf(templateId, options) {
615
- throw new Error("Not yet implemented");
624
+ try {
625
+ const res = await this.client.post("templates/createPdf", {
626
+ json: {
627
+ modifications: options?.modifications ?? [],
628
+ // template: JSON.stringify(options?.template ?? {}),
629
+ templateId,
630
+ apiKey: this.apiKey
631
+ }
632
+ });
633
+ return { result: await res.arrayBuffer() };
634
+ } catch (e) {
635
+ if (e instanceof HTTPError) {
636
+ const json = await e.response.json();
637
+ return { error: json.error };
638
+ }
639
+ if (e instanceof TimeoutError) {
640
+ return { error: timeoutError.error };
641
+ }
642
+ throw e;
643
+ }
616
644
  }
617
- async createPermanentImage() {
618
- throw new Error("Not yet implemented");
645
+ async createStoredImage(templateId, options) {
646
+ try {
647
+ const normalizedFormat = validateImageFormat(options?.format);
648
+ if (!normalizedFormat.ok) {
649
+ return invalidFormatResult(options?.format);
650
+ }
651
+ const res = await this.client.post("templates/createStoredImage", {
652
+ json: {
653
+ modifications: options?.modifications ?? [],
654
+ // template: JSON.stringify(options?.template ?? {}),
655
+ templateId,
656
+ apiKey: this.apiKey,
657
+ format: normalizedFormat.format,
658
+ thumbnail: options?.thumbnail ?? false,
659
+ s3Config: options?.s3Config
660
+ }
661
+ });
662
+ const json = await res.json();
663
+ return { result: json.url };
664
+ } catch (e) {
665
+ if (e instanceof HTTPError) {
666
+ const json = await e.response.json();
667
+ return { error: json.error };
668
+ }
669
+ if (e instanceof TimeoutError) {
670
+ return { error: timeoutError.error };
671
+ }
672
+ throw e;
673
+ }
619
674
  }
620
675
  #hashText = async (text) => {
621
676
  const myText = new TextEncoder().encode(text);
@@ -624,12 +679,33 @@ var Bannerify = class {
624
679
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
625
680
  return hashHex;
626
681
  };
682
+ // The signature is an HMAC keyed with the API key itself, so a signed URL can
683
+ // be shared without exposing anything that can sign a different one. The
684
+ // apiKeyHashed in the URL only tells the server which key to verify with.
685
+ #signText = async (text) => {
686
+ const key = await crypto.subtle.importKey(
687
+ "raw",
688
+ new TextEncoder().encode(this.apiKey),
689
+ { name: "HMAC", hash: "SHA-256" },
690
+ false,
691
+ ["sign"]
692
+ );
693
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(text));
694
+ return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
695
+ };
627
696
  async generateImageSignedUrl(templateId, options) {
628
697
  const apiKeyHashed = await this.#hashText(this.apiKey);
629
698
  const searchParams = new URLSearchParams();
630
699
  searchParams.set("apiKeyHashed", apiKeyHashed);
631
- if (options?.format === "svg") {
632
- searchParams.set("format", "svg");
700
+ if (options?.format) {
701
+ const normalizedFormat = validateImageFormat(options.format);
702
+ if (!normalizedFormat.ok) {
703
+ throw new Error(normalizedFormat.error.message);
704
+ }
705
+ searchParams.set("format", normalizedFormat.format);
706
+ }
707
+ if (options?.quality !== void 0) {
708
+ searchParams.set("quality", String(options.quality));
633
709
  }
634
710
  if (options?.modifications) {
635
711
  searchParams.set("modifications", JSON.stringify(options?.modifications));
@@ -637,9 +713,12 @@ var Bannerify = class {
637
713
  if (options?.nocache) {
638
714
  searchParams.set("nocache", "true");
639
715
  }
716
+ if (options?.thumbnail) {
717
+ searchParams.set("thumbnail", "true");
718
+ }
640
719
  searchParams.set("templateId", templateId);
641
720
  searchParams.sort();
642
- searchParams.set("sign", await this.#hashText(searchParams.toString() + searchParams.get("apiKeyHashed")));
721
+ searchParams.set("sign", await this.#signText(searchParams.toString()));
643
722
  return `${this.baseUrl}/templates/signedurl?${searchParams.toString()}`;
644
723
  }
645
724
  };