@marsaude/devtools-shell 0.1.1 → 0.1.2

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.
@@ -1138,6 +1138,11 @@ class DevtoolsApiService {
1138
1138
  this.config = inject(DEVTOOLS_CONFIG);
1139
1139
  this.utils = inject(DevtoolsUtils);
1140
1140
  this.typeform = inject(DevtoolsTypeform);
1141
+ // ---- reCAPTCHA (v2 Invisible) -----------------------------------------
1142
+ /** Cached invisible-widget id: rendered once, re-executed per request. */
1143
+ this.recaptchaWidgetId = null;
1144
+ /** Resolver for the in-flight execute() — the widget callback fires this. */
1145
+ this.recaptchaResolver = null;
1141
1146
  }
1142
1147
  get api() {
1143
1148
  return this.config.apiBaseUrl;
@@ -1296,14 +1301,43 @@ class DevtoolsApiService {
1296
1301
  return of([]);
1297
1302
  return forkJoin(valid.map((id) => this.deleteAuthStatement(id)));
1298
1303
  }
1299
- // ---- reCAPTCHA --------------------------------------------------------
1300
1304
  recaptcha() {
1301
1305
  const key = this.config.recaptchaSiteKey;
1302
- const grecaptcha = window.grecaptcha;
1303
- if (!key || !grecaptcha?.execute)
1306
+ if (!key) {
1307
+ console.warn('[devtools] recaptchaSiteKey not configured — request will be sent without a reCAPTCHA token');
1304
1308
  return of('');
1305
- return from(new Promise((resolve) => {
1306
- grecaptcha.ready(() => grecaptcha.execute(key, { action: 'login' }).then(resolve).catch(() => resolve('')));
1309
+ }
1310
+ return from(ensureRecaptchaScript()
1311
+ .then((grecaptcha) => new Promise((resolve) => {
1312
+ // v2 Invisible delivers the token via callback, not a promise.
1313
+ // Point the shared resolver at THIS request before executing.
1314
+ this.recaptchaResolver = (token) => {
1315
+ this.recaptchaResolver = null;
1316
+ try {
1317
+ grecaptcha.reset(this.recaptchaWidgetId);
1318
+ }
1319
+ catch {
1320
+ /* ignore */
1321
+ }
1322
+ resolve(token || '');
1323
+ };
1324
+ if (this.recaptchaWidgetId == null) {
1325
+ const host = document.createElement('div');
1326
+ host.style.display = 'none';
1327
+ document.body.appendChild(host);
1328
+ this.recaptchaWidgetId = grecaptcha.render(host, {
1329
+ sitekey: key,
1330
+ size: 'invisible',
1331
+ callback: (token) => this.recaptchaResolver?.(token),
1332
+ 'error-callback': () => this.recaptchaResolver?.(''),
1333
+ 'expired-callback': () => this.recaptchaResolver?.(''),
1334
+ });
1335
+ }
1336
+ grecaptcha.execute(this.recaptchaWidgetId);
1337
+ }))
1338
+ .catch((e) => {
1339
+ console.error('[devtools] failed to load reCAPTCHA', e);
1340
+ return '';
1307
1341
  }));
1308
1342
  }
1309
1343
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
@@ -1312,6 +1346,42 @@ class DevtoolsApiService {
1312
1346
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsApiService, decorators: [{
1313
1347
  type: Injectable
1314
1348
  }] });
1349
+ const RECAPTCHA_SCRIPT_ID = 'dc-recaptcha-v2-invisible';
1350
+ /**
1351
+ * Lazy-load the reCAPTCHA v2 (explicit render) script (once) and resolve with
1352
+ * the `grecaptcha` API. The host only passes `recaptchaSiteKey`; the package
1353
+ * injects the script itself so nothing depends on the host having it loaded.
1354
+ */
1355
+ function ensureRecaptchaScript() {
1356
+ const w = window;
1357
+ if (w.grecaptcha?.render)
1358
+ return Promise.resolve(w.grecaptcha);
1359
+ return new Promise((resolve, reject) => {
1360
+ const onReady = () => {
1361
+ if (w.grecaptcha?.render)
1362
+ resolve(w.grecaptcha);
1363
+ else
1364
+ reject(new Error('grecaptcha unavailable after script load'));
1365
+ };
1366
+ const existing = document.getElementById(RECAPTCHA_SCRIPT_ID);
1367
+ if (existing) {
1368
+ if (w.grecaptcha?.render)
1369
+ return resolve(w.grecaptcha);
1370
+ existing.addEventListener('load', onReady, { once: true });
1371
+ existing.addEventListener('error', () => reject(new Error('reCAPTCHA script failed to load')), { once: true });
1372
+ return;
1373
+ }
1374
+ const script = document.createElement('script');
1375
+ script.id = RECAPTCHA_SCRIPT_ID;
1376
+ // `render=explicit` so we control the (invisible) widget creation ourselves.
1377
+ script.src = 'https://www.google.com/recaptcha/api.js?render=explicit';
1378
+ script.async = true;
1379
+ script.defer = true;
1380
+ script.onload = onReady;
1381
+ script.onerror = () => reject(new Error('reCAPTCHA script failed to load'));
1382
+ document.head.appendChild(script);
1383
+ });
1384
+ }
1315
1385
  function onlyDigits(v) {
1316
1386
  return (v ?? '').replace(/\D/g, '');
1317
1387
  }