@franzzemen/brokenstock-shell 0.10.0 → 0.10.1

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.
@@ -4436,6 +4436,12 @@ class Login {
4436
4436
  this.#activatedRoute = inject(ActivatedRoute);
4437
4437
  /** Whether this browser can sign in with a passkey at all; the button is hidden otherwise (passkey D2). */
4438
4438
  this.passkeys = passkeysSupported();
4439
+ /**
4440
+ * Shown when the passkey prompt closed without a passkey: most often this device has none (a
4441
+ * passkey lives on the device or account that made it). Plain advice, not an error.
4442
+ */
4443
+ this.passkeyHint = signal(false, /* @ts-ignore */
4444
+ ...(ngDevMode ? [{ debugName: "passkeyHint" }] : /* istanbul ignore next */ []));
4439
4445
  }
4440
4446
  /** Safe on the server: the support check is false wherever there is no browser. */
4441
4447
  ngOnInit() {
@@ -4447,6 +4453,7 @@ class Login {
4447
4453
  /** "Sign in with a passkey". The quiet wait must stop first: a browser runs one passkey prompt at a time. */
4448
4454
  onPasskey() {
4449
4455
  this.errors = [];
4456
+ this.passkeyHint.set(false);
4450
4457
  this.#autofill?.abort();
4451
4458
  this.#autofill = undefined;
4452
4459
  this.submitted = true;
@@ -4477,6 +4484,8 @@ class Login {
4477
4484
  this.submitted = false;
4478
4485
  if (err === 'conflict')
4479
4486
  return;
4487
+ if (err === PASSKEY_CANCELLED && !conditional)
4488
+ this.passkeyHint.set(true);
4480
4489
  if (err !== PASSKEY_CANCELLED && !conditional) {
4481
4490
  this.errors.push({ ndx: 0, message: typeof err === 'string' ? err : 'Unable to sign in with a passkey' });
4482
4491
  }
@@ -4659,7 +4668,7 @@ class Login {
4659
4668
  }).catch(console.error);
4660
4669
  }
4661
4670
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: Login, deps: [{ token: AuthenticationService }, { token: i2$1.Router }], target: i0.ɵɵFactoryTarget.Component }); }
4662
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: Login, isStandalone: true, selector: "shell-login", ngImport: i0, template: "@let usernameRegex = USERNAME_REGEX;\n@let passwordRegex = PASSWORD_REGEX;\n<div class=\"login-container\">\n <div class=\"login-header\">\n <h2 class=\"text-text-primary\">{{ appName }}</h2>\n <p class=\"text-secondary-info\">Sign in to continue</p>\n </div>\n @let step = authenticationService.codeStep();\n @if (authenticationService.recoveryCodesToShow(); as codes) {\n <shell-recovery-codes [codes]=\"codes\" [appName]=\"appName\" [username]=\"model.username\" (done)=\"onRecoveryCodesSaved()\" />\n } @else if (step !== 'none') {\n <form (submit)=\"onSubmitCode($event)\" class=\"login-form\" aria-label=\"Authenticator code\">\n @if (step === 'setup' && authenticationService.setupChallenge(); as challenge) {\n <p class=\"text-secondary-info login-code-lead\">\n Set up your authenticator: scan this with Google Authenticator (or Authy, 1Password),\n then enter the 6-digit code it shows.\n </p>\n <img class=\"login-qr\" [src]=\"challenge.qr\" alt=\"QR code for your authenticator app\" width=\"200\" height=\"200\" />\n <p class=\"text-secondary-info login-key\">Can't scan? Enter this key: <code>{{ challenge.key }}</code></p>\n } @else {\n <p class=\"text-secondary-info login-code-lead\">\n @if (useRecovery()) {\n Enter one of your recovery codes.\n } @else {\n Enter the 6-digit code from your authenticator app.\n }\n </p>\n }\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>{{ useRecovery() ? 'Recovery code' : 'Code' }}</mat-label>\n <input\n matInput\n name=\"code\"\n [(ngModel)]=\"codeText\"\n [disabled]=\"submitted\"\n [attr.inputmode]=\"useRecovery() ? 'text' : 'numeric'\"\n [attr.autocomplete]=\"useRecovery() ? 'off' : 'one-time-code'\"\n [attr.maxlength]=\"useRecovery() ? 16 : 7\"\n [placeholder]=\"useRecovery() ? 'xxxxx-xxxxx' : '123456'\"\n autofocus\n />\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n {{ step === 'setup' ? 'Finish setup and sign in' : 'Sign in' }}\n </button>\n <div class=\"login-code-links\">\n @if (step === 'code') {\n <button mat-button type=\"button\" (click)=\"onToggleRecovery()\">\n {{ useRecovery() ? 'Use the 6-digit code' : 'Use a recovery code' }}\n </button>\n }\n <button mat-button type=\"button\" (click)=\"onCancelCode()\">Back</button>\n </div>\n </form>\n } @else {\n <form #loginForm=\"ngForm\" (ngSubmit)=\"onSubmit($event)\" class=\"login-form\">\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Username</mat-label>\n <input\n #usernameInput\n matInput\n placeholder=\"Enter username\"\n [(ngModel)]=\"model.username\"\n name=\"username\"\n autocomplete=\"username webauthn\"\n [disabled]=\"submitted\"\n required\n regexValidator\n [regex]=\"usernameRegex\"\n (cdkAutofill)=\"onAutofill($event, usernameInput)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Password</mat-label>\n <input\n #passwordInput\n matInput\n placeholder=\"Enter password\"\n [type]=\"hide() ? 'password' : 'text'\"\n [(ngModel)]=\"model.password\"\n name=\"password\"\n [disabled]=\"submitted\"\n regexValidator\n [regex]=\"passwordRegex\"\n (cdkAutofill)=\"onAutofill($event, passwordInput)\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n (click)=\"clickEvent($event)\"\n [attr.aria-label]=\"'Hide password'\"\n [attr.aria-pressed]=\"hide()\"\n >\n <mat-icon>{{ hide() ? 'visibility_off' : 'visibility' }}</mat-icon>\n </button>\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n Login\n </button>\n @if (passkeys) {\n <button mat-stroked-button type=\"button\" [disabled]=\"submitted\" class=\"login-button\" (click)=\"onPasskey()\">\n <mat-icon>fingerprint</mat-icon>\n Sign in with a passkey\n </button>\n }\n </form>\n }\n @if (errors.length > 0) {\n <div class=\"login-errors\">\n @for (error of errors; track error.ndx) {\n <div class=\"login-error text-loss\">{{ error.message }}</div>\n }\n </div>\n }\n @if (authenticationService.conflictSessions(); as sessions) {\n <div class=\"login-conflict\">\n <p class=\"text-secondary-info\">\n You are already signed in {{ sessions.length === 1 ? 'somewhere else' : 'on other devices' }}:\n </p>\n @for (session of sessions; track session.sessionId ?? $index) {\n <div class=\"login-conflict-session\">\n @if (canEndIndividually()) {\n <mat-checkbox\n [checked]=\"isSelected(session)\"\n [disabled]=\"!session.sessionId\"\n (change)=\"toggleSelected(session, $event.checked)\"\n [attr.aria-label]=\"'End ' + deviceLabel(session)\"\n ></mat-checkbox>\n }\n <span class=\"text-secondary-info\">\n <strong>{{ deviceLabel(session) }}</strong>\n <!-- signedInAtEpoch is when the password was typed. startEpoch is\n when the row was written, which renewal resets \u2014 so it is shown\n only when there is nothing better, and labelled for what it is. -->\n @if (session.signedInAtEpoch) {\n \u2014 since {{ session.signedInAtEpoch | date:'short' }}\n } @else {\n \u2014 last active {{ session.startEpoch | date:'short' }}\n }\n </span>\n </div>\n }\n <div class=\"login-conflict-actions\">\n <button mat-stroked-button type=\"button\" (click)=\"onCancelConflict()\">Cancel</button>\n @if (selectedCount() > 0) {\n <button mat-stroked-button type=\"button\" (click)=\"onEndSelected()\">\n End {{ selectedCount() === 1 ? 'that one' : selectedCount() + ' of them' }} and sign in\n </button>\n }\n <button mat-stroked-button type=\"button\" (click)=\"onTakeOver()\">\n {{ sessions.length === 1 ? 'End it and sign in' : 'End them all and sign in' }}\n </button>\n @if (authenticationService.additionalSignInAllowed()) {\n <button mat-flat-button type=\"button\" (click)=\"onSignInAsWell()\">Sign in here as well</button>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".login-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center}.login-header{margin-bottom:32px}.login-header h2{font-size:1.5rem;font-weight:300;margin-bottom:8px}.login-form{display:flex;flex-direction:column;align-items:center;gap:8px;width:320px}.login-field{width:100%;text-align:left}.login-takeover{align-self:flex-start;margin-top:4px}.login-button{margin-top:8px;width:100%}.login-errors{margin-top:16px;max-width:320px}.login-conflict{margin-top:16px;max-width:320px;padding:12px 16px;border-radius:4px;background-color:var(--color-surface-light);font-size:.85rem;text-align:left}.login-conflict p{margin:0 0 8px}.login-conflict-session{display:flex;align-items:center;gap:8px;margin:4px 0}.login-conflict-actions{flex-wrap:wrap;display:flex;gap:8px;margin-top:12px;justify-content:flex-end}.login-error{padding:8px 12px;margin-bottom:4px;font-size:.85rem;border-radius:4px;background-color:var(--color-surface-light)}.login-code-lead{margin:0 0 8px;font-size:.9rem}.login-qr{display:block;margin:4px auto 8px;image-rendering:pixelated}.login-key{margin:0 0 8px;font-size:.8rem;word-break:break-all}.login-code-links{display:flex;justify-content:space-between;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i5.CdkAutofill, selector: "[cdkAutofill]", outputs: ["cdkAutofill"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i7.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RegexValidatorDirective, selector: "[regexValidator]", inputs: ["regex"] }, { kind: "ngmodule", type: TextFieldModule }, { kind: "component", type: RecoveryCodes, selector: "shell-recovery-codes", inputs: ["codes", "appName", "username"], outputs: ["done"] }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
4671
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: Login, isStandalone: true, selector: "shell-login", ngImport: i0, template: "@let usernameRegex = USERNAME_REGEX;\n@let passwordRegex = PASSWORD_REGEX;\n<div class=\"login-container\">\n <div class=\"login-header\">\n <h2 class=\"text-text-primary\">{{ appName }}</h2>\n <p class=\"text-secondary-info\">Sign in to continue</p>\n </div>\n @let step = authenticationService.codeStep();\n @if (authenticationService.recoveryCodesToShow(); as codes) {\n <shell-recovery-codes [codes]=\"codes\" [appName]=\"appName\" [username]=\"model.username\" (done)=\"onRecoveryCodesSaved()\" />\n } @else if (step !== 'none') {\n <form (submit)=\"onSubmitCode($event)\" class=\"login-form\" aria-label=\"Authenticator code\">\n @if (step === 'setup' && authenticationService.setupChallenge(); as challenge) {\n <p class=\"text-secondary-info login-code-lead\">\n Set up your authenticator: scan this with Google Authenticator (or Authy, 1Password),\n then enter the 6-digit code it shows.\n </p>\n <img class=\"login-qr\" [src]=\"challenge.qr\" alt=\"QR code for your authenticator app\" width=\"200\" height=\"200\" />\n <p class=\"text-secondary-info login-key\">Can't scan? Enter this key: <code>{{ challenge.key }}</code></p>\n } @else {\n <p class=\"text-secondary-info login-code-lead\">\n @if (useRecovery()) {\n Enter one of your recovery codes.\n } @else {\n Enter the 6-digit code from your authenticator app.\n }\n </p>\n }\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>{{ useRecovery() ? 'Recovery code' : 'Code' }}</mat-label>\n <input\n matInput\n name=\"code\"\n [(ngModel)]=\"codeText\"\n [disabled]=\"submitted\"\n [attr.inputmode]=\"useRecovery() ? 'text' : 'numeric'\"\n [attr.autocomplete]=\"useRecovery() ? 'off' : 'one-time-code'\"\n [attr.maxlength]=\"useRecovery() ? 16 : 7\"\n [placeholder]=\"useRecovery() ? 'xxxxx-xxxxx' : '123456'\"\n autofocus\n />\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n {{ step === 'setup' ? 'Finish setup and sign in' : 'Sign in' }}\n </button>\n <div class=\"login-code-links\">\n @if (step === 'code') {\n <button mat-button type=\"button\" (click)=\"onToggleRecovery()\">\n {{ useRecovery() ? 'Use the 6-digit code' : 'Use a recovery code' }}\n </button>\n }\n <button mat-button type=\"button\" (click)=\"onCancelCode()\">Back</button>\n </div>\n </form>\n } @else {\n <form #loginForm=\"ngForm\" (ngSubmit)=\"onSubmit($event)\" class=\"login-form\">\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Username</mat-label>\n <input\n #usernameInput\n matInput\n placeholder=\"Enter username\"\n [(ngModel)]=\"model.username\"\n name=\"username\"\n autocomplete=\"username webauthn\"\n [disabled]=\"submitted\"\n required\n regexValidator\n [regex]=\"usernameRegex\"\n (cdkAutofill)=\"onAutofill($event, usernameInput)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Password</mat-label>\n <input\n #passwordInput\n matInput\n placeholder=\"Enter password\"\n [type]=\"hide() ? 'password' : 'text'\"\n [(ngModel)]=\"model.password\"\n name=\"password\"\n [disabled]=\"submitted\"\n regexValidator\n [regex]=\"passwordRegex\"\n (cdkAutofill)=\"onAutofill($event, passwordInput)\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n (click)=\"clickEvent($event)\"\n [attr.aria-label]=\"'Hide password'\"\n [attr.aria-pressed]=\"hide()\"\n >\n <mat-icon>{{ hide() ? 'visibility_off' : 'visibility' }}</mat-icon>\n </button>\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n Login\n </button>\n @if (passkeys) {\n <button mat-stroked-button type=\"button\" [disabled]=\"submitted\" class=\"login-button\" (click)=\"onPasskey()\">\n <mat-icon>fingerprint</mat-icon>\n Sign in with a passkey\n </button>\n }\n @if (passkeyHint()) {\n <p class=\"text-secondary-info login-passkey-hint\">\n No passkey on this device? Sign in with your password and code, then add one under Sign-in Code.\n </p>\n }\n </form>\n }\n @if (errors.length > 0) {\n <div class=\"login-errors\">\n @for (error of errors; track error.ndx) {\n <div class=\"login-error text-loss\">{{ error.message }}</div>\n }\n </div>\n }\n @if (authenticationService.conflictSessions(); as sessions) {\n <div class=\"login-conflict\">\n <p class=\"text-secondary-info\">\n You are already signed in {{ sessions.length === 1 ? 'somewhere else' : 'on other devices' }}:\n </p>\n @for (session of sessions; track session.sessionId ?? $index) {\n <div class=\"login-conflict-session\">\n @if (canEndIndividually()) {\n <mat-checkbox\n [checked]=\"isSelected(session)\"\n [disabled]=\"!session.sessionId\"\n (change)=\"toggleSelected(session, $event.checked)\"\n [attr.aria-label]=\"'End ' + deviceLabel(session)\"\n ></mat-checkbox>\n }\n <span class=\"text-secondary-info\">\n <strong>{{ deviceLabel(session) }}</strong>\n <!-- signedInAtEpoch is when the password was typed. startEpoch is\n when the row was written, which renewal resets \u2014 so it is shown\n only when there is nothing better, and labelled for what it is. -->\n @if (session.signedInAtEpoch) {\n \u2014 since {{ session.signedInAtEpoch | date:'short' }}\n } @else {\n \u2014 last active {{ session.startEpoch | date:'short' }}\n }\n </span>\n </div>\n }\n <div class=\"login-conflict-actions\">\n <button mat-stroked-button type=\"button\" (click)=\"onCancelConflict()\">Cancel</button>\n @if (selectedCount() > 0) {\n <button mat-stroked-button type=\"button\" (click)=\"onEndSelected()\">\n End {{ selectedCount() === 1 ? 'that one' : selectedCount() + ' of them' }} and sign in\n </button>\n }\n <button mat-stroked-button type=\"button\" (click)=\"onTakeOver()\">\n {{ sessions.length === 1 ? 'End it and sign in' : 'End them all and sign in' }}\n </button>\n @if (authenticationService.additionalSignInAllowed()) {\n <button mat-flat-button type=\"button\" (click)=\"onSignInAsWell()\">Sign in here as well</button>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".login-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center}.login-header{margin-bottom:32px}.login-header h2{font-size:1.5rem;font-weight:300;margin-bottom:8px}.login-form{display:flex;flex-direction:column;align-items:center;gap:8px;width:320px}.login-field{width:100%;text-align:left}.login-takeover{align-self:flex-start;margin-top:4px}.login-button{margin-top:8px;width:100%}.login-errors{margin-top:16px;max-width:320px}.login-conflict{margin-top:16px;max-width:320px;padding:12px 16px;border-radius:4px;background-color:var(--color-surface-light);font-size:.85rem;text-align:left}.login-conflict p{margin:0 0 8px}.login-conflict-session{display:flex;align-items:center;gap:8px;margin:4px 0}.login-conflict-actions{flex-wrap:wrap;display:flex;gap:8px;margin-top:12px;justify-content:flex-end}.login-error{padding:8px 12px;margin-bottom:4px;font-size:.85rem;border-radius:4px;background-color:var(--color-surface-light)}.login-code-lead{margin:0 0 8px;font-size:.9rem}.login-qr{display:block;margin:4px auto 8px;image-rendering:pixelated}.login-key{margin:0 0 8px;font-size:.8rem;word-break:break-all}.login-code-links{display:flex;justify-content:space-between;width:100%}.login-passkey-hint{margin:4px 0 0;font-size:.85rem}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i5.CdkAutofill, selector: "[cdkAutofill]", outputs: ["cdkAutofill"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i7.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RegexValidatorDirective, selector: "[regexValidator]", inputs: ["regex"] }, { kind: "ngmodule", type: TextFieldModule }, { kind: "component", type: RecoveryCodes, selector: "shell-recovery-codes", inputs: ["codes", "appName", "username"], outputs: ["done"] }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
4663
4672
  }
4664
4673
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: Login, decorators: [{
4665
4674
  type: Component,
@@ -4676,7 +4685,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
4676
4685
  TextFieldModule,
4677
4686
  DatePipe,
4678
4687
  RecoveryCodes,
4679
- ], template: "@let usernameRegex = USERNAME_REGEX;\n@let passwordRegex = PASSWORD_REGEX;\n<div class=\"login-container\">\n <div class=\"login-header\">\n <h2 class=\"text-text-primary\">{{ appName }}</h2>\n <p class=\"text-secondary-info\">Sign in to continue</p>\n </div>\n @let step = authenticationService.codeStep();\n @if (authenticationService.recoveryCodesToShow(); as codes) {\n <shell-recovery-codes [codes]=\"codes\" [appName]=\"appName\" [username]=\"model.username\" (done)=\"onRecoveryCodesSaved()\" />\n } @else if (step !== 'none') {\n <form (submit)=\"onSubmitCode($event)\" class=\"login-form\" aria-label=\"Authenticator code\">\n @if (step === 'setup' && authenticationService.setupChallenge(); as challenge) {\n <p class=\"text-secondary-info login-code-lead\">\n Set up your authenticator: scan this with Google Authenticator (or Authy, 1Password),\n then enter the 6-digit code it shows.\n </p>\n <img class=\"login-qr\" [src]=\"challenge.qr\" alt=\"QR code for your authenticator app\" width=\"200\" height=\"200\" />\n <p class=\"text-secondary-info login-key\">Can't scan? Enter this key: <code>{{ challenge.key }}</code></p>\n } @else {\n <p class=\"text-secondary-info login-code-lead\">\n @if (useRecovery()) {\n Enter one of your recovery codes.\n } @else {\n Enter the 6-digit code from your authenticator app.\n }\n </p>\n }\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>{{ useRecovery() ? 'Recovery code' : 'Code' }}</mat-label>\n <input\n matInput\n name=\"code\"\n [(ngModel)]=\"codeText\"\n [disabled]=\"submitted\"\n [attr.inputmode]=\"useRecovery() ? 'text' : 'numeric'\"\n [attr.autocomplete]=\"useRecovery() ? 'off' : 'one-time-code'\"\n [attr.maxlength]=\"useRecovery() ? 16 : 7\"\n [placeholder]=\"useRecovery() ? 'xxxxx-xxxxx' : '123456'\"\n autofocus\n />\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n {{ step === 'setup' ? 'Finish setup and sign in' : 'Sign in' }}\n </button>\n <div class=\"login-code-links\">\n @if (step === 'code') {\n <button mat-button type=\"button\" (click)=\"onToggleRecovery()\">\n {{ useRecovery() ? 'Use the 6-digit code' : 'Use a recovery code' }}\n </button>\n }\n <button mat-button type=\"button\" (click)=\"onCancelCode()\">Back</button>\n </div>\n </form>\n } @else {\n <form #loginForm=\"ngForm\" (ngSubmit)=\"onSubmit($event)\" class=\"login-form\">\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Username</mat-label>\n <input\n #usernameInput\n matInput\n placeholder=\"Enter username\"\n [(ngModel)]=\"model.username\"\n name=\"username\"\n autocomplete=\"username webauthn\"\n [disabled]=\"submitted\"\n required\n regexValidator\n [regex]=\"usernameRegex\"\n (cdkAutofill)=\"onAutofill($event, usernameInput)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Password</mat-label>\n <input\n #passwordInput\n matInput\n placeholder=\"Enter password\"\n [type]=\"hide() ? 'password' : 'text'\"\n [(ngModel)]=\"model.password\"\n name=\"password\"\n [disabled]=\"submitted\"\n regexValidator\n [regex]=\"passwordRegex\"\n (cdkAutofill)=\"onAutofill($event, passwordInput)\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n (click)=\"clickEvent($event)\"\n [attr.aria-label]=\"'Hide password'\"\n [attr.aria-pressed]=\"hide()\"\n >\n <mat-icon>{{ hide() ? 'visibility_off' : 'visibility' }}</mat-icon>\n </button>\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n Login\n </button>\n @if (passkeys) {\n <button mat-stroked-button type=\"button\" [disabled]=\"submitted\" class=\"login-button\" (click)=\"onPasskey()\">\n <mat-icon>fingerprint</mat-icon>\n Sign in with a passkey\n </button>\n }\n </form>\n }\n @if (errors.length > 0) {\n <div class=\"login-errors\">\n @for (error of errors; track error.ndx) {\n <div class=\"login-error text-loss\">{{ error.message }}</div>\n }\n </div>\n }\n @if (authenticationService.conflictSessions(); as sessions) {\n <div class=\"login-conflict\">\n <p class=\"text-secondary-info\">\n You are already signed in {{ sessions.length === 1 ? 'somewhere else' : 'on other devices' }}:\n </p>\n @for (session of sessions; track session.sessionId ?? $index) {\n <div class=\"login-conflict-session\">\n @if (canEndIndividually()) {\n <mat-checkbox\n [checked]=\"isSelected(session)\"\n [disabled]=\"!session.sessionId\"\n (change)=\"toggleSelected(session, $event.checked)\"\n [attr.aria-label]=\"'End ' + deviceLabel(session)\"\n ></mat-checkbox>\n }\n <span class=\"text-secondary-info\">\n <strong>{{ deviceLabel(session) }}</strong>\n <!-- signedInAtEpoch is when the password was typed. startEpoch is\n when the row was written, which renewal resets \u2014 so it is shown\n only when there is nothing better, and labelled for what it is. -->\n @if (session.signedInAtEpoch) {\n \u2014 since {{ session.signedInAtEpoch | date:'short' }}\n } @else {\n \u2014 last active {{ session.startEpoch | date:'short' }}\n }\n </span>\n </div>\n }\n <div class=\"login-conflict-actions\">\n <button mat-stroked-button type=\"button\" (click)=\"onCancelConflict()\">Cancel</button>\n @if (selectedCount() > 0) {\n <button mat-stroked-button type=\"button\" (click)=\"onEndSelected()\">\n End {{ selectedCount() === 1 ? 'that one' : selectedCount() + ' of them' }} and sign in\n </button>\n }\n <button mat-stroked-button type=\"button\" (click)=\"onTakeOver()\">\n {{ sessions.length === 1 ? 'End it and sign in' : 'End them all and sign in' }}\n </button>\n @if (authenticationService.additionalSignInAllowed()) {\n <button mat-flat-button type=\"button\" (click)=\"onSignInAsWell()\">Sign in here as well</button>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".login-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center}.login-header{margin-bottom:32px}.login-header h2{font-size:1.5rem;font-weight:300;margin-bottom:8px}.login-form{display:flex;flex-direction:column;align-items:center;gap:8px;width:320px}.login-field{width:100%;text-align:left}.login-takeover{align-self:flex-start;margin-top:4px}.login-button{margin-top:8px;width:100%}.login-errors{margin-top:16px;max-width:320px}.login-conflict{margin-top:16px;max-width:320px;padding:12px 16px;border-radius:4px;background-color:var(--color-surface-light);font-size:.85rem;text-align:left}.login-conflict p{margin:0 0 8px}.login-conflict-session{display:flex;align-items:center;gap:8px;margin:4px 0}.login-conflict-actions{flex-wrap:wrap;display:flex;gap:8px;margin-top:12px;justify-content:flex-end}.login-error{padding:8px 12px;margin-bottom:4px;font-size:.85rem;border-radius:4px;background-color:var(--color-surface-light)}.login-code-lead{margin:0 0 8px;font-size:.9rem}.login-qr{display:block;margin:4px auto 8px;image-rendering:pixelated}.login-key{margin:0 0 8px;font-size:.8rem;word-break:break-all}.login-code-links{display:flex;justify-content:space-between;width:100%}\n"] }]
4688
+ ], template: "@let usernameRegex = USERNAME_REGEX;\n@let passwordRegex = PASSWORD_REGEX;\n<div class=\"login-container\">\n <div class=\"login-header\">\n <h2 class=\"text-text-primary\">{{ appName }}</h2>\n <p class=\"text-secondary-info\">Sign in to continue</p>\n </div>\n @let step = authenticationService.codeStep();\n @if (authenticationService.recoveryCodesToShow(); as codes) {\n <shell-recovery-codes [codes]=\"codes\" [appName]=\"appName\" [username]=\"model.username\" (done)=\"onRecoveryCodesSaved()\" />\n } @else if (step !== 'none') {\n <form (submit)=\"onSubmitCode($event)\" class=\"login-form\" aria-label=\"Authenticator code\">\n @if (step === 'setup' && authenticationService.setupChallenge(); as challenge) {\n <p class=\"text-secondary-info login-code-lead\">\n Set up your authenticator: scan this with Google Authenticator (or Authy, 1Password),\n then enter the 6-digit code it shows.\n </p>\n <img class=\"login-qr\" [src]=\"challenge.qr\" alt=\"QR code for your authenticator app\" width=\"200\" height=\"200\" />\n <p class=\"text-secondary-info login-key\">Can't scan? Enter this key: <code>{{ challenge.key }}</code></p>\n } @else {\n <p class=\"text-secondary-info login-code-lead\">\n @if (useRecovery()) {\n Enter one of your recovery codes.\n } @else {\n Enter the 6-digit code from your authenticator app.\n }\n </p>\n }\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>{{ useRecovery() ? 'Recovery code' : 'Code' }}</mat-label>\n <input\n matInput\n name=\"code\"\n [(ngModel)]=\"codeText\"\n [disabled]=\"submitted\"\n [attr.inputmode]=\"useRecovery() ? 'text' : 'numeric'\"\n [attr.autocomplete]=\"useRecovery() ? 'off' : 'one-time-code'\"\n [attr.maxlength]=\"useRecovery() ? 16 : 7\"\n [placeholder]=\"useRecovery() ? 'xxxxx-xxxxx' : '123456'\"\n autofocus\n />\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n {{ step === 'setup' ? 'Finish setup and sign in' : 'Sign in' }}\n </button>\n <div class=\"login-code-links\">\n @if (step === 'code') {\n <button mat-button type=\"button\" (click)=\"onToggleRecovery()\">\n {{ useRecovery() ? 'Use the 6-digit code' : 'Use a recovery code' }}\n </button>\n }\n <button mat-button type=\"button\" (click)=\"onCancelCode()\">Back</button>\n </div>\n </form>\n } @else {\n <form #loginForm=\"ngForm\" (ngSubmit)=\"onSubmit($event)\" class=\"login-form\">\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Username</mat-label>\n <input\n #usernameInput\n matInput\n placeholder=\"Enter username\"\n [(ngModel)]=\"model.username\"\n name=\"username\"\n autocomplete=\"username webauthn\"\n [disabled]=\"submitted\"\n required\n regexValidator\n [regex]=\"usernameRegex\"\n (cdkAutofill)=\"onAutofill($event, usernameInput)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Password</mat-label>\n <input\n #passwordInput\n matInput\n placeholder=\"Enter password\"\n [type]=\"hide() ? 'password' : 'text'\"\n [(ngModel)]=\"model.password\"\n name=\"password\"\n [disabled]=\"submitted\"\n regexValidator\n [regex]=\"passwordRegex\"\n (cdkAutofill)=\"onAutofill($event, passwordInput)\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n (click)=\"clickEvent($event)\"\n [attr.aria-label]=\"'Hide password'\"\n [attr.aria-pressed]=\"hide()\"\n >\n <mat-icon>{{ hide() ? 'visibility_off' : 'visibility' }}</mat-icon>\n </button>\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n Login\n </button>\n @if (passkeys) {\n <button mat-stroked-button type=\"button\" [disabled]=\"submitted\" class=\"login-button\" (click)=\"onPasskey()\">\n <mat-icon>fingerprint</mat-icon>\n Sign in with a passkey\n </button>\n }\n @if (passkeyHint()) {\n <p class=\"text-secondary-info login-passkey-hint\">\n No passkey on this device? Sign in with your password and code, then add one under Sign-in Code.\n </p>\n }\n </form>\n }\n @if (errors.length > 0) {\n <div class=\"login-errors\">\n @for (error of errors; track error.ndx) {\n <div class=\"login-error text-loss\">{{ error.message }}</div>\n }\n </div>\n }\n @if (authenticationService.conflictSessions(); as sessions) {\n <div class=\"login-conflict\">\n <p class=\"text-secondary-info\">\n You are already signed in {{ sessions.length === 1 ? 'somewhere else' : 'on other devices' }}:\n </p>\n @for (session of sessions; track session.sessionId ?? $index) {\n <div class=\"login-conflict-session\">\n @if (canEndIndividually()) {\n <mat-checkbox\n [checked]=\"isSelected(session)\"\n [disabled]=\"!session.sessionId\"\n (change)=\"toggleSelected(session, $event.checked)\"\n [attr.aria-label]=\"'End ' + deviceLabel(session)\"\n ></mat-checkbox>\n }\n <span class=\"text-secondary-info\">\n <strong>{{ deviceLabel(session) }}</strong>\n <!-- signedInAtEpoch is when the password was typed. startEpoch is\n when the row was written, which renewal resets \u2014 so it is shown\n only when there is nothing better, and labelled for what it is. -->\n @if (session.signedInAtEpoch) {\n \u2014 since {{ session.signedInAtEpoch | date:'short' }}\n } @else {\n \u2014 last active {{ session.startEpoch | date:'short' }}\n }\n </span>\n </div>\n }\n <div class=\"login-conflict-actions\">\n <button mat-stroked-button type=\"button\" (click)=\"onCancelConflict()\">Cancel</button>\n @if (selectedCount() > 0) {\n <button mat-stroked-button type=\"button\" (click)=\"onEndSelected()\">\n End {{ selectedCount() === 1 ? 'that one' : selectedCount() + ' of them' }} and sign in\n </button>\n }\n <button mat-stroked-button type=\"button\" (click)=\"onTakeOver()\">\n {{ sessions.length === 1 ? 'End it and sign in' : 'End them all and sign in' }}\n </button>\n @if (authenticationService.additionalSignInAllowed()) {\n <button mat-flat-button type=\"button\" (click)=\"onSignInAsWell()\">Sign in here as well</button>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".login-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center}.login-header{margin-bottom:32px}.login-header h2{font-size:1.5rem;font-weight:300;margin-bottom:8px}.login-form{display:flex;flex-direction:column;align-items:center;gap:8px;width:320px}.login-field{width:100%;text-align:left}.login-takeover{align-self:flex-start;margin-top:4px}.login-button{margin-top:8px;width:100%}.login-errors{margin-top:16px;max-width:320px}.login-conflict{margin-top:16px;max-width:320px;padding:12px 16px;border-radius:4px;background-color:var(--color-surface-light);font-size:.85rem;text-align:left}.login-conflict p{margin:0 0 8px}.login-conflict-session{display:flex;align-items:center;gap:8px;margin:4px 0}.login-conflict-actions{flex-wrap:wrap;display:flex;gap:8px;margin-top:12px;justify-content:flex-end}.login-error{padding:8px 12px;margin-bottom:4px;font-size:.85rem;border-radius:4px;background-color:var(--color-surface-light)}.login-code-lead{margin:0 0 8px;font-size:.9rem}.login-qr{display:block;margin:4px auto 8px;image-rendering:pixelated}.login-key{margin:0 0 8px;font-size:.8rem;word-break:break-all}.login-code-links{display:flex;justify-content:space-between;width:100%}.login-passkey-hint{margin:4px 0 0;font-size:.85rem}\n"] }]
4680
4689
  }], ctorParameters: () => [{ type: AuthenticationService }, { type: i2$1.Router }] });
4681
4690
 
4682
4691
  /*