@marsaude/devtools-shell 0.1.1 → 0.1.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.
@@ -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,40 @@ 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
+ const RECAPTCHA_ONLOAD_CB = '__dcRecaptchaOnload';
1356
+ function ensureRecaptchaScript() {
1357
+ const w = window;
1358
+ if (w.grecaptcha?.render)
1359
+ return Promise.resolve(w.grecaptcha);
1360
+ return new Promise((resolve, reject) => {
1361
+ // The script tag's own `load` event fires before grecaptcha.render is ready.
1362
+ // The library's own `onload=` callback (passed in the URL) only fires once
1363
+ // the API is fully initialized — that's the reliable readiness signal.
1364
+ w[RECAPTCHA_ONLOAD_CB] = () => resolve(w.grecaptcha);
1365
+ const existing = document.getElementById(RECAPTCHA_SCRIPT_ID);
1366
+ if (existing) {
1367
+ if (w.grecaptcha?.render)
1368
+ return resolve(w.grecaptcha);
1369
+ existing.addEventListener('error', () => reject(new Error('reCAPTCHA script failed to load')), { once: true });
1370
+ return; // the pending onload callback above will resolve us
1371
+ }
1372
+ const script = document.createElement('script');
1373
+ script.id = RECAPTCHA_SCRIPT_ID;
1374
+ // `onload=` => library calls us when ready; `render=explicit` => we create
1375
+ // the (invisible) widget ourselves.
1376
+ script.src = `https://www.google.com/recaptcha/api.js?onload=${RECAPTCHA_ONLOAD_CB}&render=explicit`;
1377
+ script.async = true;
1378
+ script.defer = true;
1379
+ script.onerror = () => reject(new Error('reCAPTCHA script failed to load'));
1380
+ document.head.appendChild(script);
1381
+ });
1382
+ }
1315
1383
  function onlyDigits(v) {
1316
1384
  return (v ?? '').replace(/\D/g, '');
1317
1385
  }