jwtbutler 1.9.2 → 1.9.3

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
@@ -62,6 +62,25 @@ const api = new jwtbutler({
62
62
  });
63
63
  ```
64
64
 
65
+ if your auth server uses [hCaptcha](https://www.hcaptcha.com), \
66
+ add the public sitekey:
67
+
68
+ ```js
69
+ const api = new jwtbutler({
70
+ captcha: {
71
+ provider: 'hcaptcha',
72
+ sitekey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
73
+ }
74
+ });
75
+ ```
76
+
77
+ if your auth server uses passkeys and you want to show \
78
+ the passkey button in the default login form, enable it:
79
+
80
+ ```js
81
+ passkeys: true;
82
+ ```
83
+
65
84
  ### single sign on
66
85
 
67
86
  if you want to use sso, add all pages to the configuration object:
@@ -5,9 +5,18 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.default = void 0;
8
+ require("core-js/modules/es.array-buffer.constructor.js");
9
+ require("core-js/modules/es.array-buffer.slice.js");
8
10
  require("core-js/modules/es.json.stringify.js");
11
+ require("core-js/modules/es.regexp.exec.js");
12
+ require("core-js/modules/es.string.replace.js");
13
+ require("core-js/modules/es.typed-array.uint8-array.js");
14
+ require("core-js/modules/es.typed-array.fill.js");
15
+ require("core-js/modules/es.typed-array.set.js");
16
+ require("core-js/modules/es.typed-array.sort.js");
9
17
  require("core-js/modules/esnext.iterator.constructor.js");
10
18
  require("core-js/modules/esnext.iterator.for-each.js");
19
+ require("core-js/modules/esnext.typed-array.at.js");
11
20
  require("whatwg-fetch");
12
21
  var _helpers = _interopRequireDefault(require("./_helpers"));
13
22
  // use fetch
@@ -17,6 +26,12 @@ class jwtbutler {
17
26
  if (!('auth_login' in config)) {
18
27
  config.auth_login = 'email';
19
28
  }
29
+ if (!('captcha' in config)) {
30
+ config.captcha = false;
31
+ }
32
+ if (!('passkeys' in config)) {
33
+ config.passkeys = false;
34
+ }
20
35
  this.config = config;
21
36
  }
22
37
  isLoggedIn() {
@@ -62,6 +77,124 @@ class jwtbutler {
62
77
  });
63
78
  });
64
79
  }
80
+ passkeyRegister() {
81
+ return new Promise((resolve, reject) => {
82
+ if (this.isLoggedIn() === false || !('credentials' in navigator)) {
83
+ reject();
84
+ return;
85
+ }
86
+ this.addLoadingState('logging-in');
87
+ fetch(this.config.auth_server + '/passkey-register-options', {
88
+ method: 'POST',
89
+ headers: {
90
+ 'content-type': 'application/json',
91
+ Authorization: 'Bearer ' + _helpers.default.cookieGet('access_token')
92
+ },
93
+ cache: 'no-cache'
94
+ }).then(res => res.json()).catch(error => error).then(response => {
95
+ if (response === undefined || response === null || response.success !== true) {
96
+ this.removeLoadingStates();
97
+ reject(response);
98
+ return;
99
+ }
100
+ let publicKey = this.passkeyPublicKeyFromJson(response.data.publicKey);
101
+ navigator.credentials.create({
102
+ publicKey: publicKey
103
+ }).then(credential => {
104
+ if (credential === null) {
105
+ this.removeLoadingStates();
106
+ reject();
107
+ return;
108
+ }
109
+ fetch(this.config.auth_server + '/passkey-register', {
110
+ method: 'POST',
111
+ body: JSON.stringify({
112
+ credential: this.passkeyCredentialToJson(credential)
113
+ }),
114
+ headers: {
115
+ 'content-type': 'application/json',
116
+ Authorization: 'Bearer ' + _helpers.default.cookieGet('access_token')
117
+ },
118
+ cache: 'no-cache'
119
+ }).then(res => res.json()).catch(error => error).then(response => {
120
+ this.removeLoadingStates();
121
+ if (response !== undefined && response !== null && response.success === true) {
122
+ resolve(response);
123
+ } else {
124
+ reject(response);
125
+ }
126
+ });
127
+ }).catch(error => {
128
+ this.removeLoadingStates();
129
+ reject(error);
130
+ });
131
+ });
132
+ });
133
+ }
134
+ passkeyLogin() {
135
+ let login = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
136
+ return new Promise((resolve, reject) => {
137
+ if (!('credentials' in navigator)) {
138
+ reject();
139
+ return;
140
+ }
141
+ this.addLoadingState('logging-in');
142
+ let body = {};
143
+ if (login !== null && login !== '') {
144
+ body[this.config.auth_login] = login;
145
+ }
146
+ fetch(this.config.auth_server + '/passkey-login-options', {
147
+ method: 'POST',
148
+ body: JSON.stringify(body),
149
+ headers: {
150
+ 'content-type': 'application/json'
151
+ },
152
+ cache: 'no-cache'
153
+ }).then(res => res.json()).catch(error => error).then(response => {
154
+ if (response === undefined || response === null || response.success !== true) {
155
+ this.removeLoadingStates();
156
+ reject(response);
157
+ return;
158
+ }
159
+ let publicKey = this.passkeyPublicKeyFromJson(response.data.publicKey);
160
+ navigator.credentials.get({
161
+ publicKey: publicKey
162
+ }).then(credential => {
163
+ if (credential === null) {
164
+ this.removeLoadingStates();
165
+ reject();
166
+ return;
167
+ }
168
+ fetch(this.config.auth_server + '/passkey-login', {
169
+ method: 'POST',
170
+ body: JSON.stringify({
171
+ credential: this.passkeyCredentialToJson(credential)
172
+ }),
173
+ headers: {
174
+ 'content-type': 'application/json'
175
+ },
176
+ cache: 'no-cache'
177
+ }).then(res => res.json()).catch(error => error).then(response => {
178
+ if (response !== undefined && response !== null && response.success === true) {
179
+ this.setCookies(response.data.access_token).then(() => {
180
+ this.removeLoadingStates();
181
+ resolve(response);
182
+ }).catch(error => {
183
+ this.removeLoadingStates();
184
+ reject(error);
185
+ });
186
+ } else {
187
+ this.removeLoadingStates();
188
+ reject(response);
189
+ }
190
+ });
191
+ }).catch(error => {
192
+ this.removeLoadingStates();
193
+ reject(error);
194
+ });
195
+ });
196
+ });
197
+ }
65
198
  login() {
66
199
  return new Promise((resolve, reject) => {
67
200
  if (_helpers.default.cookieGet('access_token') !== null) {
@@ -265,20 +398,30 @@ class jwtbutler {
265
398
  renderLoginFormWithPromise() {
266
399
  return new Promise((resolve, reject) => {
267
400
  this.buildUpLoginFormHtml();
268
- this.bindLoginFormSubmit().then(() => {
269
- resolve();
401
+ this.captchaRender().then(() => {
402
+ this.bindLoginFormSubmit().then(() => {
403
+ resolve();
404
+ }).catch(error => {
405
+ reject(error);
406
+ });
407
+ this.triggerLoginFormRenderedEvent();
270
408
  }).catch(error => {
271
409
  reject(error);
272
410
  });
273
- this.triggerLoginFormRenderedEvent();
274
411
  });
275
412
  }
276
413
  buildUpLoginFormHtml() {
277
414
  if (!('login_form' in this.config) || this.config.login_form == '') {
278
- this.config.login_form = "<div class=\"login-form\">\n <div class=\"login-form__inner\">\n <form class=\"login-form__form\">\n <ul class=\"login-form__items\">\n <li class=\"login-form__item\">\n <label class=\"login-form__label login-form__label--".concat(this.config.auth_login, "\" for=\"login-form__label--").concat(this.config.auth_login, "\">").concat(this.config.auth_login === 'email' ? 'E-Mail-Adresse' : this.config.auth_login === 'username' ? 'Benutzername' : this.config.auth_login, "</label>\n <input class=\"login-form__input login-form__input--").concat(this.config.auth_login, "\" id=\"login-form__label--").concat(this.config.auth_login, "\" type=\"text\" required=\"required\" autocomplete=\"").concat(this.config.auth_login, "\" name=\"").concat(this.config.auth_login, "\" />\n </li>\n <li class=\"login-form__item\">\n <label class=\"login-form__label login-form__label--password\" for=\"login-form__label--password\">Passwort</label>\n <input class=\"login-form__input login-form__input--password\" id=\"login-form__label--password\" type=\"password\" required=\"required\" autocomplete=\"current-password\" name=\"password\" />\n </li>\n <li class=\"login-form__item\">\n <input class=\"login-form__submit\" type=\"submit\" value=\"Anmelden\" />\n </li>\n </ul>\n </form>\n </div>\n </div>");
415
+ this.config.login_form = "<div class=\"login-form\">\n <div class=\"login-form__inner\">\n <form class=\"login-form__form\">\n <ul class=\"login-form__items\">\n <li class=\"login-form__item\">\n <label class=\"login-form__label login-form__label--".concat(this.config.auth_login, "\" for=\"login-form__label--").concat(this.config.auth_login, "\">").concat(this.config.auth_login === 'email' ? 'E-Mail-Adresse' : this.config.auth_login === 'username' ? 'Benutzername' : this.config.auth_login, "</label>\n <input class=\"login-form__input login-form__input--").concat(this.config.auth_login, "\" id=\"login-form__label--").concat(this.config.auth_login, "\" type=\"text\" required=\"required\" autocomplete=\"").concat(this.config.auth_login, "\" name=\"").concat(this.config.auth_login, "\" />\n </li>\n <li class=\"login-form__item\">\n <label class=\"login-form__label login-form__label--password\" for=\"login-form__label--password\">Passwort</label>\n <input class=\"login-form__input login-form__input--password\" id=\"login-form__label--password\" type=\"password\" required=\"required\" autocomplete=\"current-password\" name=\"password\" />\n </li>\n ").concat(this.captchaEnabled() ? "<li class=\"login-form__item\">\n <div class=\"login-form__captcha\"></div>\n </li>" : '', "\n <li class=\"login-form__item\">\n <input class=\"login-form__submit\" type=\"submit\" value=\"Anmelden\" />\n </li>\n ").concat(this.passkeyEnabled() ? "<li class=\"login-form__item\">\n <button class=\"login-form__passkey\" type=\"button\">Mit Passkey anmelden</button>\n </li>" : '', "\n </ul>\n </form>\n </div>\n </div>");
279
416
  }
280
417
  let dom = new DOMParser().parseFromString(this.config.login_form, 'text/html').body.childNodes[0];
281
418
  this.config.login_form_class = dom.getAttribute('class').split(' ')[0];
419
+ if (this.captchaEnabled() && dom.querySelector('.' + this.config.login_form_class + '__captcha') === null) {
420
+ let submit = dom.querySelector('input[type="submit"]');
421
+ if (submit !== null && submit.closest('li') !== null) {
422
+ submit.closest('li').insertAdjacentHTML('beforebegin', '<li class="' + this.config.login_form_class + '__item"><div class="' + this.config.login_form_class + '__captcha"></div></li>');
423
+ }
424
+ }
282
425
  _helpers.default.remove(document.querySelector('.' + this.config.login_form_class));
283
426
  this.addLoadingState('login-form-visible');
284
427
  let parent = document.body;
@@ -292,6 +435,91 @@ class jwtbutler {
292
435
  this.config.login_form_rendered(document.querySelector('.' + this.config.login_form_class));
293
436
  }
294
437
  }
438
+ captchaEnabled() {
439
+ return this.config.captcha !== false && typeof this.config.captcha === 'object' && (!('provider' in this.config.captcha) || this.config.captcha.provider === 'hcaptcha') && 'sitekey' in this.config.captcha && this.config.captcha.sitekey !== '';
440
+ }
441
+ passkeyEnabled() {
442
+ return this.config.passkeys !== false;
443
+ }
444
+ captchaRender() {
445
+ return new Promise((resolve, reject) => {
446
+ if (!this.captchaEnabled()) {
447
+ resolve();
448
+ return;
449
+ }
450
+ this.captchaLoad().then(() => {
451
+ let captcha = document.querySelector('.' + this.config.login_form_class + '__captcha');
452
+ if (captcha === null) {
453
+ resolve();
454
+ return;
455
+ }
456
+ if (captcha.getAttribute('data-widget-id') !== null) {
457
+ resolve();
458
+ return;
459
+ }
460
+ captcha.setAttribute('data-widget-id', window.hcaptcha.render(captcha, {
461
+ sitekey: this.config.captcha.sitekey
462
+ }));
463
+ resolve();
464
+ }).catch(error => {
465
+ reject(error);
466
+ });
467
+ });
468
+ }
469
+ captchaLoad() {
470
+ return new Promise((resolve, reject) => {
471
+ if (!this.captchaEnabled() || 'hcaptcha' in window) {
472
+ resolve();
473
+ return;
474
+ }
475
+ if (document.querySelector('script[data-jwtbutler-hcaptcha]') !== null) {
476
+ document.querySelector('script[data-jwtbutler-hcaptcha]').addEventListener('load', () => resolve());
477
+ document.querySelector('script[data-jwtbutler-hcaptcha]').addEventListener('error', error => reject(error));
478
+ return;
479
+ }
480
+ let script = document.createElement('script');
481
+ script.setAttribute('src', 'https://js.hcaptcha.com/1/api.js?render=explicit');
482
+ script.setAttribute('async', 'async');
483
+ script.setAttribute('defer', 'defer');
484
+ script.setAttribute('data-jwtbutler-hcaptcha', 'true');
485
+ script.addEventListener('load', () => resolve());
486
+ script.addEventListener('error', error => reject(error));
487
+ document.head.appendChild(script);
488
+ });
489
+ }
490
+ captchaToken(form) {
491
+ return new Promise((resolve, reject) => {
492
+ if (!this.captchaEnabled()) {
493
+ resolve(null);
494
+ return;
495
+ }
496
+ let captcha = form.querySelector('.' + this.config.login_form_class + '__captcha');
497
+ if (captcha === null || !('hcaptcha' in window)) {
498
+ reject({
499
+ public_message: 'Captcha nicht erfolgreich'
500
+ });
501
+ return;
502
+ }
503
+ let token = window.hcaptcha.getResponse(captcha.getAttribute('data-widget-id'));
504
+ if (token === '') {
505
+ reject({
506
+ public_message: 'Captcha nicht erfolgreich'
507
+ });
508
+ return;
509
+ }
510
+ resolve(token);
511
+ });
512
+ }
513
+ captchaReset(form) {
514
+ if (!this.captchaEnabled() || !('hcaptcha' in window)) {
515
+ return;
516
+ }
517
+ let captcha = form.querySelector('.' + this.config.login_form_class + '__captcha');
518
+ if (captcha === null || captcha.getAttribute('data-widget-id') === null) {
519
+ return;
520
+ }
521
+ window.hcaptcha.reset(captcha.getAttribute('data-widget-id'));
522
+ }
295
523
  bindLoginFormSubmit() {
296
524
  return new Promise((resolve, reject) => {
297
525
  let dom = document.querySelector('.' + this.config.login_form_class),
@@ -302,17 +530,23 @@ class jwtbutler {
302
530
  form.querySelector('input[type="submit"]').disabled = true;
303
531
  }
304
532
  _helpers.default.remove(dom.querySelector('.' + this.config.login_form_class + '__error'));
305
- fetch(this.config.auth_server + '/login', {
306
- method: 'POST',
307
- body: JSON.stringify({
308
- [this.config.auth_login]: form.querySelector('input[name="' + this.config.auth_login + '"]').value,
309
- password: form.querySelector('input[name="password"]').value
310
- }),
311
- headers: {
312
- 'content-type': 'application/json'
313
- },
314
- cache: 'no-cache'
315
- }).then(res => res.json()).catch(err => err).then(response => {
533
+ let body = {
534
+ [this.config.auth_login]: form.querySelector('input[name="' + this.config.auth_login + '"]').value,
535
+ password: form.querySelector('input[name="password"]').value
536
+ };
537
+ this.captchaToken(form).then(captchaToken => {
538
+ if (captchaToken !== null) {
539
+ body['h-captcha-response'] = captchaToken;
540
+ }
541
+ return fetch(this.config.auth_server + '/login', {
542
+ method: 'POST',
543
+ body: JSON.stringify(body),
544
+ headers: {
545
+ 'content-type': 'application/json'
546
+ },
547
+ cache: 'no-cache'
548
+ });
549
+ }).then(res => res.json()).catch(error => error).then(response => {
316
550
  if (form.querySelector('input[type="submit"]') !== null) {
317
551
  form.querySelector('input[type="submit"]').disabled = false;
318
552
  }
@@ -325,13 +559,94 @@ class jwtbutler {
325
559
  reject(error);
326
560
  });
327
561
  } else {
328
- form.insertAdjacentHTML('afterbegin', '<div class="' + this.config.login_form_class + '__error">' + response.public_message + '</div>');
562
+ this.removeLoadingStates();
563
+ this.captchaReset(form);
564
+ form.insertAdjacentHTML('afterbegin', '<div class="' + this.config.login_form_class + '__error">' + (response !== undefined && response !== null && 'public_message' in response ? response.public_message : 'Nicht erfolgreich') + '</div>');
329
565
  }
330
566
  });
331
567
  e.preventDefault();
332
568
  }, false);
569
+ if (form.querySelector('.' + this.config.login_form_class + '__passkey') !== null) {
570
+ form.querySelector('.' + this.config.login_form_class + '__passkey').addEventListener('click', e => {
571
+ this.addLoadingState('logging-in');
572
+ _helpers.default.remove(dom.querySelector('.' + this.config.login_form_class + '__error'));
573
+ let login = null;
574
+ if (form.querySelector('input[name="' + this.config.auth_login + '"]') !== null) {
575
+ login = form.querySelector('input[name="' + this.config.auth_login + '"]').value;
576
+ }
577
+ this.passkeyLogin(login).then(() => {
578
+ _helpers.default.remove(document.querySelector('.' + this.config.login_form_class));
579
+ this.removeLoadingStates();
580
+ resolve();
581
+ }).catch(response => {
582
+ this.removeLoadingStates();
583
+ form.insertAdjacentHTML('afterbegin', '<div class="' + this.config.login_form_class + '__error">' + (response !== undefined && response !== null && 'public_message' in response ? response.public_message : 'Nicht erfolgreich') + '</div>');
584
+ });
585
+ e.preventDefault();
586
+ }, false);
587
+ }
333
588
  });
334
589
  }
590
+ passkeyPublicKeyFromJson(publicKey) {
591
+ publicKey.challenge = this.passkeyBase64UrlToBuffer(publicKey.challenge);
592
+ if ('user' in publicKey && 'id' in publicKey.user) {
593
+ publicKey.user.id = this.passkeyBase64UrlToBuffer(publicKey.user.id);
594
+ }
595
+ if ('excludeCredentials' in publicKey) {
596
+ publicKey.excludeCredentials.forEach(credential => {
597
+ credential.id = this.passkeyBase64UrlToBuffer(credential.id);
598
+ });
599
+ }
600
+ if ('allowCredentials' in publicKey) {
601
+ publicKey.allowCredentials.forEach(credential => {
602
+ credential.id = this.passkeyBase64UrlToBuffer(credential.id);
603
+ });
604
+ }
605
+ return publicKey;
606
+ }
607
+ passkeyCredentialToJson(credential) {
608
+ let response = {
609
+ clientDataJSON: this.passkeyBufferToBase64Url(credential.response.clientDataJSON)
610
+ };
611
+ if ('attestationObject' in credential.response) {
612
+ response.attestationObject = this.passkeyBufferToBase64Url(credential.response.attestationObject);
613
+ if (typeof credential.response.getTransports === 'function') {
614
+ response.transports = credential.response.getTransports();
615
+ }
616
+ }
617
+ if ('authenticatorData' in credential.response) {
618
+ response.authenticatorData = this.passkeyBufferToBase64Url(credential.response.authenticatorData);
619
+ response.signature = this.passkeyBufferToBase64Url(credential.response.signature);
620
+ response.userHandle = !('userHandle' in credential.response) || credential.response.userHandle === null ? null : this.passkeyBufferToBase64Url(credential.response.userHandle);
621
+ }
622
+ return {
623
+ id: credential.id,
624
+ rawId: this.passkeyBufferToBase64Url(credential.rawId),
625
+ type: credential.type,
626
+ response: response,
627
+ clientExtensionResults: credential.getClientExtensionResults()
628
+ };
629
+ }
630
+ passkeyBase64UrlToBuffer(value) {
631
+ let base64 = value.replace(/-/g, '+').replace(/_/g, '/');
632
+ while (base64.length % 4 !== 0) {
633
+ base64 += '=';
634
+ }
635
+ let binary = atob(base64),
636
+ bytes = new Uint8Array(binary.length);
637
+ for (let i = 0; i < binary.length; i++) {
638
+ bytes[i] = binary.charCodeAt(i);
639
+ }
640
+ return bytes.buffer;
641
+ }
642
+ passkeyBufferToBase64Url(buffer) {
643
+ let binary = '',
644
+ bytes = new Uint8Array(buffer);
645
+ for (let i = 0; i < bytes.byteLength; i++) {
646
+ binary += String.fromCharCode(bytes[i]);
647
+ }
648
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
649
+ }
335
650
  addLoadingState(state) {
336
651
  document.documentElement.classList.add('jwtbutler-' + state);
337
652
  if (state === 'logging-in' || state === 'logging-out') {
package/package.json CHANGED
@@ -22,19 +22,19 @@
22
22
  "dev": "cross-env NODE_ENV=development npm-run-all --parallel js:watch js:tests:watch copy:watch"
23
23
  },
24
24
  "dependencies": {
25
- "@babel/cli": "^7.28.6",
26
- "@babel/core": "^7.29.0",
25
+ "@babel/cli": "^7.29.7",
26
+ "@babel/core": "^7.29.7",
27
27
  "@babel/plugin-proposal-class-properties": "^7.18.6",
28
28
  "@babel/plugin-proposal-object-rest-spread": "^7.20.7",
29
29
  "@babel/plugin-proposal-optional-chaining": "^7.21.0",
30
30
  "@babel/plugin-proposal-private-methods": "^7.18.6",
31
- "@babel/plugin-transform-runtime": "^7.29.0",
31
+ "@babel/plugin-transform-runtime": "^7.29.7",
32
32
  "@babel/polyfill": "^7.12.1",
33
- "@babel/preset-env": "^7.29.5",
34
- "@babel/preset-react": "^7.28.5",
35
- "@babel/runtime": "^7.29.2",
33
+ "@babel/preset-env": "^7.29.7",
34
+ "@babel/preset-react": "^7.29.7",
35
+ "@babel/runtime": "^7.29.7",
36
36
  "@prettier/plugin-php": "^0.25.0",
37
- "auto-changelog": "^2.5.1",
37
+ "auto-changelog": "^2.6.0",
38
38
  "autoprefixer": "^10.5.0",
39
39
  "babel-jest": "30.4.1",
40
40
  "babel-plugin-array-includes": "^2.0.3",
@@ -49,11 +49,11 @@
49
49
  "dotenv": "^17.4.2",
50
50
  "element-closest": "^3.0.2",
51
51
  "env-cmd": "^11.0.0",
52
- "eslint": "^10.4.0",
52
+ "eslint": "^10.4.1",
53
53
  "exit": "^0.1.2",
54
54
  "from-env": "^1.1.4",
55
55
  "highlight.js": "^11.11.1",
56
- "hlp": "^3.8.1",
56
+ "hlp": "^3.8.4",
57
57
  "ismobilejs": "^1.1.1",
58
58
  "jest": "30.4.2",
59
59
  "jest-cli": "30.4.2",
@@ -61,13 +61,13 @@
61
61
  "mdn-polyfills": "^5.20.0",
62
62
  "move-file-cli": "^3.0.0",
63
63
  "ncp": "^2.0.0",
64
- "npm-check-updates": "^22.2.0",
64
+ "npm-check-updates": "^22.2.1",
65
65
  "npm-run-all": "^4.1.5",
66
66
  "onchange": "^7.1.0",
67
67
  "postcss": "^8.5.15",
68
68
  "postcss-cli": "^11.0.1",
69
69
  "prettier": "^3.8.3",
70
- "puppeteer": "^25.0.4",
70
+ "puppeteer": "^25.1.0",
71
71
  "regenerator-runtime": "^0.14.1",
72
72
  "replace-in-file": "^8.4.0",
73
73
  "run-sequence": "^2.2.1",
@@ -81,7 +81,7 @@
81
81
  "jest-environment-node": "30.4.1"
82
82
  },
83
83
  "name": "jwtbutler",
84
- "version": "1.9.2",
84
+ "version": "1.9.3",
85
85
  "description": "",
86
86
  "main": "_js/_build/script.js",
87
87
  "files": [