valtech-components 4.0.178 → 4.0.179

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.
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
56
56
  * Current version of valtech-components.
57
57
  * This is automatically updated during the publish process.
58
58
  */
59
- const VERSION = '4.0.178';
59
+ const VERSION = '4.0.179';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -6587,21 +6587,145 @@ class OAuthService {
6587
6587
  this.clearLocalStorageFallback();
6588
6588
  const nonce = this.generateNonce();
6589
6589
  this.expectedNonce = nonce;
6590
- // Flujo REDIRECT (sin popup). El popup quedaba COLGADO bajo COOP: Google
6591
- // setea Cross-Origin-Opener-Policy window.opener queda severado (el
6592
- // postMessage del popup no llega), BroadcastChannel/localStorage no siempre
6593
- // entregan, y popup.closed queda bloqueado ("COOP policy would block the
6594
- // window.closed call") → ni siquiera dispara el cancel: la ventana principal
6595
- // espera para siempre sin redirigir. Navegamos la VENTANA COMPLETA al /start;
6596
- // al volver, OAuthCallbackComponent corre como ventana principal (redirect_uri
6597
- // SIN popup=true) y completa la sesión vía completeInMainWindow → navega al
6598
- // home. Sin popup = sin COOP = sin cuelgue. (El link-flow sigue por popup.)
6599
- const redirectUri = `${window.location.origin}/auth/oauth/callback`;
6590
+ // popup=true sobrevive el bounce cross-origin (Google/MS borran window.name
6591
+ // por privacidad). El backend lo reenvía en el redirect final vía appendParams.
6592
+ const redirectUri = `${window.location.origin}/auth/oauth/callback?popup=true`;
6600
6593
  const startUrl = `${this.config?.apiUrl}/v2/auth/oauth/${provider}/start?redirect_uri=${encodeURIComponent(redirectUri)}&client_nonce=${encodeURIComponent(nonce)}`;
6601
- window.location.assign(startUrl);
6602
- // La página se descarga acá; el observable no emite (la finalización ocurre
6603
- // en el callback, ya como ventana principal). Sin listeners ni polling.
6604
- return () => { };
6594
+ // Abrir popup centrado
6595
+ const width = 500;
6596
+ const height = 600;
6597
+ const left = window.screenX + (window.outerWidth - width) / 2;
6598
+ const top = window.screenY + (window.outerHeight - height) / 2;
6599
+ const features = `width=${width},height=${height},left=${left},top=${top},popup=yes`;
6600
+ this.popup = window.open(startUrl, 'oauth', features);
6601
+ if (!this.popup) {
6602
+ observer.error({
6603
+ code: 'POPUP_BLOCKED',
6604
+ message: 'El navegador bloqueó la ventana emergente. Por favor, permite popups para este sitio.',
6605
+ });
6606
+ return () => { };
6607
+ }
6608
+ // Escuchar mensajes del popup
6609
+ this.messageHandler = (event) => {
6610
+ // Validar origen
6611
+ if (event.origin !== window.location.origin) {
6612
+ return;
6613
+ }
6614
+ // Validar tipo de mensaje
6615
+ const data = event.data;
6616
+ if (data?.type !== 'oauth-callback') {
6617
+ return;
6618
+ }
6619
+ // Verificar el nonce del flujo (M-07): si no coincide, no es nuestro
6620
+ // callback → ignorar (no consumir el observable).
6621
+ if (!this.isNonceValid(data)) {
6622
+ return;
6623
+ }
6624
+ // Limpiar
6625
+ this.cleanup();
6626
+ // Emitir resultado dentro de NgZone para trigger change detection
6627
+ this.ngZone.run(() => {
6628
+ if (data.error) {
6629
+ observer.error(data.error);
6630
+ }
6631
+ else if (data.tokens) {
6632
+ observer.next(data.tokens);
6633
+ observer.complete();
6634
+ }
6635
+ else {
6636
+ observer.error({
6637
+ code: 'INVALID_RESPONSE',
6638
+ message: 'Respuesta inválida del servidor de autenticación',
6639
+ });
6640
+ }
6641
+ });
6642
+ };
6643
+ window.addEventListener('message', this.messageHandler);
6644
+ // BroadcastChannel: canal adicional immune a COOP (funciona entre misma
6645
+ // origin sin importar el browsing context group). Es el canal más fiable
6646
+ // cuando COOP sever el link opener→popup al pasar por Google/MS.
6647
+ try {
6648
+ this.broadcastChannel = new BroadcastChannel('valtech-oauth-callback');
6649
+ this.broadcastChannel.onmessage = (event) => {
6650
+ const data = event.data;
6651
+ if (data?.type !== 'oauth-callback')
6652
+ return;
6653
+ if (!this.isNonceValid(data))
6654
+ return;
6655
+ this.clearLocalStorageFallback();
6656
+ this.cleanup();
6657
+ this.ngZone.run(() => {
6658
+ if (data.error) {
6659
+ observer.error(data.error);
6660
+ }
6661
+ else if (data.tokens) {
6662
+ observer.next(data.tokens);
6663
+ observer.complete();
6664
+ }
6665
+ else {
6666
+ observer.error({
6667
+ code: 'INVALID_RESPONSE',
6668
+ message: 'Respuesta inválida del servidor de autenticación',
6669
+ });
6670
+ }
6671
+ });
6672
+ };
6673
+ }
6674
+ catch {
6675
+ // BroadcastChannel no disponible (contextos muy restringidos) — ignorar
6676
+ }
6677
+ // Polling de localStorage (COOP workaround - no podemos detectar popup.closed)
6678
+ // También verifica si el popup se cerró manualmente
6679
+ this.checkClosedInterval = setInterval(() => {
6680
+ // Primero verificar localStorage (funciona aunque COOP bloquee todo)
6681
+ const storedData = this.checkLocalStorageFallback();
6682
+ if (storedData) {
6683
+ this.cleanup();
6684
+ this.ngZone.run(() => {
6685
+ if (storedData.error) {
6686
+ observer.error(storedData.error);
6687
+ }
6688
+ else if (storedData.tokens) {
6689
+ // Gate de diagnóstico: la metadata de tokens (keys, presencia y
6690
+ // longitud del firebaseToken) no debe filtrarse a la consola en
6691
+ // producción (L4 del audit). Solo en dev.
6692
+ if (isDevMode()) {
6693
+ console.log('[OAuthService] Retrieved tokens from localStorage fallback', {
6694
+ keys: Object.keys(storedData.tokens),
6695
+ hasFirebaseToken: !!storedData.tokens.firebaseToken,
6696
+ firebaseTokenLength: storedData.tokens.firebaseToken?.length ?? 0,
6697
+ });
6698
+ }
6699
+ observer.next(storedData.tokens);
6700
+ observer.complete();
6701
+ }
6702
+ else {
6703
+ observer.error({
6704
+ code: 'INVALID_RESPONSE',
6705
+ message: 'Respuesta inválida del servidor de autenticación',
6706
+ });
6707
+ }
6708
+ });
6709
+ return;
6710
+ }
6711
+ // Intentar verificar si popup se cerró (puede fallar por COOP)
6712
+ try {
6713
+ if (this.popup?.closed) {
6714
+ this.cleanup();
6715
+ this.ngZone.run(() => {
6716
+ observer.error({
6717
+ code: 'POPUP_CLOSED',
6718
+ message: 'Se cerró la ventana de autenticación',
6719
+ });
6720
+ });
6721
+ }
6722
+ }
6723
+ catch {
6724
+ // COOP bloquea acceso a popup.closed - ignorar y seguir con polling
6725
+ }
6726
+ }, 300);
6727
+ // Cleanup cuando el observable se destruye
6728
+ return () => this.cleanup();
6605
6729
  });
6606
6730
  }
6607
6731
  /**
@@ -54106,7 +54230,8 @@ const ORGANIZATION_VIEW_I18N = {
54106
54230
  apiKeysFeaturePermsDesc: 'Cada key lleva solo los permisos que tú le asignes — concede el mínimo.',
54107
54231
  apiKeysFeatureSecretTitle: 'Secreto único',
54108
54232
  apiKeysFeatureSecretDesc: 'El secreto se muestra una sola vez. Guárdalo en un gestor de secretos.',
54109
- switchOrgCta: 'Cambiar organización',
54233
+ activeOrgTitle: 'Organización activa',
54234
+ switchOrgCta: 'Cambiar',
54110
54235
  orgsMoreInfo: 'Aprender más sobre organizaciones',
54111
54236
  orgsInfoTitle: '¿Qué son las organizaciones?',
54112
54237
  orgsInfoSubtitle: 'Todo lo que necesitas saber',
@@ -54205,7 +54330,8 @@ const ORGANIZATION_VIEW_I18N = {
54205
54330
  apiKeysFeaturePermsDesc: 'Each key carries only the permissions you assign — grant the minimum.',
54206
54331
  apiKeysFeatureSecretTitle: 'One-time secret',
54207
54332
  apiKeysFeatureSecretDesc: 'The secret is shown only once. Store it in a secret manager.',
54208
- switchOrgCta: 'Switch organization',
54333
+ activeOrgTitle: 'Active organization',
54334
+ switchOrgCta: 'Switch',
54209
54335
  orgsMoreInfo: 'Learn more about organizations',
54210
54336
  orgsInfoTitle: 'What are organizations?',
54211
54337
  orgsInfoSubtitle: 'Everything you need to know',
@@ -54397,14 +54523,18 @@ class OrganizationViewComponent {
54397
54523
  return this.tt('planEnterprise');
54398
54524
  return this.tt('planFree');
54399
54525
  });
54400
- this.switchOrgButtonProps = computed(() => ({
54401
- text: this.tt('switchOrgCta'),
54402
- color: 'dark',
54403
- fill: 'outline',
54404
- size: 'default',
54405
- shape: 'round',
54406
- type: 'button',
54407
- token: 'org-switch',
54526
+ this.switchOrgSectionProps = computed(() => ({
54527
+ title: this.tt('activeOrgTitle'),
54528
+ action: {
54529
+ text: this.tt('switchOrgCta'),
54530
+ color: 'dark',
54531
+ fill: 'outline',
54532
+ size: 'small',
54533
+ shape: 'round',
54534
+ type: 'button',
54535
+ token: 'org-switch',
54536
+ state: ComponentStates.ENABLED,
54537
+ },
54408
54538
  }));
54409
54539
  this.editButtonProps = computed(() => ({
54410
54540
  text: this.tt('editCta'),
@@ -54923,15 +55053,8 @@ class OrganizationViewComponent {
54923
55053
  content: pageDescription(),
54924
55054
  }"
54925
55055
  />
54926
- <!-- "Aprender más sobre organizaciones" bajo el título+subtítulo
54927
- principal de la vista (abre el explainer org-info-sheet). -->
54928
- <button class="org-more-info-link" type="button" (click)="onOrgInfo()">
54929
- {{ tt('orgsMoreInfo') }}
54930
- </button>
54931
- <!-- Cambiar de organización activa. -->
54932
- <div class="org-switch-row">
54933
- <val-button [props]="switchOrgButtonProps()" (onClick)="onSwitchOrg()" />
54934
- </div>
55056
+ <!-- Sección "Organización activa" con botón Cambiar a la derecha. -->
55057
+ <val-section-header [props]="switchOrgSectionProps()" (actionClick)="onSwitchOrg()" />
54935
55058
  </header>
54936
55059
 
54937
55060
  @if (resolvedConfig().showInfo) {
@@ -55233,7 +55356,7 @@ class OrganizationViewComponent {
55233
55356
  (created)="onOrgCreated($event)"
55234
55357
  />
55235
55358
  </div>
55236
- `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;flex-direction:column;gap:16px}.invite-cta__text{display:flex;flex-direction:column;gap:4px}.invite-cta__actions{display:flex;gap:12px;flex-wrap:wrap}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}.org-info-logo{display:flex;justify-content:center;padding:8px 0 16px}.org-logo-img{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--ion-color-light)}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field:last-child{border-bottom:none}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}.section-title-danger{display:flex;align-items:center;gap:8px;margin-bottom:12px}.section-title-danger val-title{display:block;margin:0}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"], dependencies: [{ kind: "component", type: CreateOrgModalComponent, selector: "val-create-org-modal", inputs: ["i18nNamespace", "isOpen"], outputs: ["dismissed", "created"] }, { kind: "component", type: CtaCardComponent, selector: "val-cta-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "directive", type: HasPermissionDirective, selector: "[valHasPermission]", inputs: ["valHasPermission"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: MemberCardComponent, selector: "val-member-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: PermissionsModalComponent, selector: "val-permissions-modal", inputs: ["isOpen", "config"], outputs: ["dismissed"] }] }); }
55359
+ `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;flex-direction:column;gap:16px}.invite-cta__text{display:flex;flex-direction:column;gap:4px}.invite-cta__actions{display:flex;gap:12px;flex-wrap:wrap}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}.org-info-logo{display:flex;justify-content:center;padding:8px 0 16px}.org-logo-img{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--ion-color-light)}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field:last-child{border-bottom:none}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}.section-title-danger{display:flex;align-items:center;gap:8px;margin-bottom:12px}.section-title-danger val-title{display:block;margin:0}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"], dependencies: [{ kind: "component", type: CreateOrgModalComponent, selector: "val-create-org-modal", inputs: ["i18nNamespace", "isOpen"], outputs: ["dismissed", "created"] }, { kind: "component", type: CtaCardComponent, selector: "val-cta-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "directive", type: HasPermissionDirective, selector: "[valHasPermission]", inputs: ["valHasPermission"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: MemberCardComponent, selector: "val-member-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: PermissionsModalComponent, selector: "val-permissions-modal", inputs: ["isOpen", "config"], outputs: ["dismissed"] }, { kind: "component", type: SectionHeaderComponent, selector: "val-section-header", inputs: ["props"], outputs: ["actionClick"] }] }); }
55237
55360
  }
55238
55361
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrganizationViewComponent, decorators: [{
55239
55362
  type: Component,
@@ -55253,6 +55376,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
55253
55376
  TransferOwnershipModalComponent,
55254
55377
  MemberImportModalComponent,
55255
55378
  OrgInfoSheetComponent,
55379
+ SectionHeaderComponent,
55256
55380
  SwitchOrgModalComponent,
55257
55381
  ], template: `
55258
55382
  <div class="page">
@@ -55266,15 +55390,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
55266
55390
  content: pageDescription(),
55267
55391
  }"
55268
55392
  />
55269
- <!-- "Aprender más sobre organizaciones" bajo el título+subtítulo
55270
- principal de la vista (abre el explainer org-info-sheet). -->
55271
- <button class="org-more-info-link" type="button" (click)="onOrgInfo()">
55272
- {{ tt('orgsMoreInfo') }}
55273
- </button>
55274
- <!-- Cambiar de organización activa. -->
55275
- <div class="org-switch-row">
55276
- <val-button [props]="switchOrgButtonProps()" (onClick)="onSwitchOrg()" />
55277
- </div>
55393
+ <!-- Sección "Organización activa" con botón Cambiar a la derecha. -->
55394
+ <val-section-header [props]="switchOrgSectionProps()" (actionClick)="onSwitchOrg()" />
55278
55395
  </header>
55279
55396
 
55280
55397
  @if (resolvedConfig().showInfo) {