@things-factory/auth-ui 10.1.256 → 10.1.258

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.
Files changed (83) hide show
  1. package/dist-client/bootstrap.js +2 -2
  2. package/dist-client/bootstrap.js.map +1 -1
  3. package/dist-client/components/access-decision-popup.d.ts +9 -8
  4. package/dist-client/components/access-decision-popup.js +46 -58
  5. package/dist-client/components/access-decision-popup.js.map +1 -1
  6. package/dist-client/components/access-request-popup.d.ts +13 -8
  7. package/dist-client/components/access-request-popup.js +78 -102
  8. package/dist-client/components/access-request-popup.js.map +1 -1
  9. package/dist-client/components/app-binding-detail.d.ts +7 -5
  10. package/dist-client/components/app-binding-detail.js +62 -82
  11. package/dist-client/components/app-binding-detail.js.map +1 -1
  12. package/dist-client/components/appliance-editor.d.ts +10 -9
  13. package/dist-client/components/appliance-editor.js +87 -138
  14. package/dist-client/components/appliance-editor.js.map +1 -1
  15. package/dist-client/components/application-editor.d.ts +7 -5
  16. package/dist-client/components/application-editor.js +58 -92
  17. package/dist-client/components/application-editor.js.map +1 -1
  18. package/dist-client/components/my-login-history.js +18 -40
  19. package/dist-client/components/my-login-history.js.map +1 -1
  20. package/dist-client/components/my-passkeys.d.ts +4 -3
  21. package/dist-client/components/my-passkeys.js +29 -51
  22. package/dist-client/components/my-passkeys.js.map +1 -1
  23. package/dist-client/components/my-roles.d.ts +3 -4
  24. package/dist-client/components/my-roles.js +36 -55
  25. package/dist-client/components/my-roles.js.map +1 -1
  26. package/dist-client/components/partner-role-editor.js +27 -50
  27. package/dist-client/components/partner-role-editor.js.map +1 -1
  28. package/dist-client/components/profile-component.d.ts +2 -2
  29. package/dist-client/components/profile-component.js +18 -26
  30. package/dist-client/components/profile-component.js.map +1 -1
  31. package/dist-client/components/role-template-picker.d.ts +3 -3
  32. package/dist-client/components/role-template-picker.js +28 -42
  33. package/dist-client/components/role-template-picker.js.map +1 -1
  34. package/dist-client/components/user-detail.d.ts +23 -22
  35. package/dist-client/components/user-detail.js +125 -179
  36. package/dist-client/components/user-detail.js.map +1 -1
  37. package/dist-client/components/user-invite.d.ts +8 -6
  38. package/dist-client/components/user-invite.js +106 -141
  39. package/dist-client/components/user-invite.js.map +1 -1
  40. package/dist-client/components/user-popups-standard.test.js +1 -1
  41. package/dist-client/components/user-popups-standard.test.js.map +1 -1
  42. package/dist-client/components/user-role-editor.js +72 -122
  43. package/dist-client/components/user-role-editor.js.map +1 -1
  44. package/dist-client/entity360/person-360.d.ts +8 -11
  45. package/dist-client/entity360/person-360.js +49 -68
  46. package/dist-client/entity360/person-360.js.map +1 -1
  47. package/dist-client/entries/oauth2/oauth2-decision-page.js +13 -25
  48. package/dist-client/entries/oauth2/oauth2-decision-page.js.map +1 -1
  49. package/dist-client/identity-confirmation/identity-confirmation.d.ts +13 -14
  50. package/dist-client/identity-confirmation/identity-confirmation.js +39 -45
  51. package/dist-client/identity-confirmation/identity-confirmation.js.map +1 -1
  52. package/dist-client/pages/audit/audit-page.d.ts +4 -4
  53. package/dist-client/pages/audit/audit-page.js +28 -39
  54. package/dist-client/pages/audit/audit-page.js.map +1 -1
  55. package/dist-client/pages/domain-link/domain-link-management.js +1 -1
  56. package/dist-client/pages/domain-link/domain-link-management.js.map +1 -1
  57. package/dist-client/pages/partner/partner-management.js +30 -68
  58. package/dist-client/pages/partner/partner-management.js.map +1 -1
  59. package/dist-client/pages/role/role-extra-removal.test.d.mts +1 -0
  60. package/dist-client/pages/role/role-extra-removal.test.mjs +73 -0
  61. package/dist-client/pages/role/role-extra-removal.test.mjs.map +1 -0
  62. package/dist-client/pages/role/role-list-page.js +20 -22
  63. package/dist-client/pages/role/role-list-page.js.map +1 -1
  64. package/dist-client/pages/role/role-privilege-page.d.ts +36 -8
  65. package/dist-client/pages/role/role-privilege-page.js +216 -158
  66. package/dist-client/pages/role/role-privilege-page.js.map +1 -1
  67. package/dist-client/pages/role/role-task-review.test.js +20 -16
  68. package/dist-client/pages/role/role-task-review.test.js.map +1 -1
  69. package/dist-client/pages/role/role-template-gap.test.js +8 -3
  70. package/dist-client/pages/role/role-template-gap.test.js.map +1 -1
  71. package/dist-client/tsconfig.tsbuildinfo +1 -1
  72. package/helps/auth/roles.en.md +13 -49
  73. package/helps/auth/roles.ko.md +13 -32
  74. package/package.json +11 -10
  75. package/tests/page-state.test.ts +2 -1
  76. package/tests/password-rule-shown.test.ts +2 -2
  77. package/tests/person-360-sections.test.ts +9 -5
  78. package/translations/en.json +5 -1
  79. package/translations/es.json +5 -1
  80. package/translations/ja.json +5 -1
  81. package/translations/ko.json +5 -1
  82. package/translations/ms.json +5 -1
  83. package/translations/zh.json +5 -1
@@ -4,7 +4,7 @@ import '../../components/role-selector.js';
4
4
  import gql from 'graphql-tag';
5
5
  import { css, html, LitElement } from 'lit';
6
6
  import { customElement, property, query } from 'lit/decorators.js';
7
- import { client } from '@operato/graphql';
7
+ import { QueryController } from '@operato/graphql';
8
8
  import { i18next } from '@operato/i18n';
9
9
  import { OxPrompt } from '@operato/popup/ox-prompt.js';
10
10
  import { ButtonContainerStyles } from '@operato/styles';
@@ -19,7 +19,15 @@ let OAuth2DecisionPage = class OAuth2DecisionPage extends LitElement {
19
19
  this.req = {};
20
20
  this.redirectURI = '';
21
21
  this.transactionID = '';
22
- this.roles = [];
22
+ this.myRoles = new QueryController(this, gql `
23
+ query myRoles {
24
+ myRoles {
25
+ id
26
+ name
27
+ description
28
+ }
29
+ }
30
+ `);
23
31
  }
24
32
  static { this.styles = [
25
33
  EntryControlSizeStyles,
@@ -75,6 +83,9 @@ let OAuth2DecisionPage = class OAuth2DecisionPage extends LitElement {
75
83
  }
76
84
  `
77
85
  ]; }
86
+ get roles() {
87
+ return this.myRoles.data?.myRoles ?? [];
88
+ }
78
89
  render() {
79
90
  return html `
80
91
  <h2>Application Binding</h2>
@@ -116,9 +127,6 @@ let OAuth2DecisionPage = class OAuth2DecisionPage extends LitElement {
116
127
  </div>
117
128
  `;
118
129
  }
119
- async firstUpdated() {
120
- this.fetchMyRoles();
121
- }
122
130
  updated(changedProps) {
123
131
  if (changedProps.has('data')) {
124
132
  this.user = this.data?.oauth2?.user || this.user;
@@ -129,22 +137,6 @@ let OAuth2DecisionPage = class OAuth2DecisionPage extends LitElement {
129
137
  this.transactionID = this.data?.oauth2?.transactionID || this.transactionID;
130
138
  }
131
139
  }
132
- async fetchMyRoles() {
133
- const response = await client.query({
134
- query: gql `
135
- query myRoles {
136
- myRoles {
137
- id
138
- name
139
- description
140
- }
141
- }
142
- `
143
- });
144
- if (!response.errors?.length) {
145
- this.roles = response.data.myRoles;
146
- }
147
- }
148
140
  async decision(allowOrCancel) {
149
141
  const selectedRoles = this.selectedRoles();
150
142
  if (allowOrCancel && !selectedRoles?.length) {
@@ -212,10 +204,6 @@ __decorate([
212
204
  property({ type: String }),
213
205
  __metadata("design:type", String)
214
206
  ], OAuth2DecisionPage.prototype, "transactionID", void 0);
215
- __decorate([
216
- property({ type: Array }),
217
- __metadata("design:type", Array)
218
- ], OAuth2DecisionPage.prototype, "roles", void 0);
219
207
  __decorate([
220
208
  query('role-selector'),
221
209
  __metadata("design:type", Object)
@@ -1 +1 @@
1
- {"version":3,"file":"oauth2-decision-page.js","sourceRoot":"","sources":["../../../client/entries/oauth2/oauth2-decision-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/elevated-button.js'\nimport '../../components/role-selector.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { ButtonContainerStyles } from '@operato/styles'\nimport { EntryControlSizeStyles } from '../../entry-control-size-styles.js'\n\n@customElement('oauth2-decision')\nclass OAuth2DecisionPage extends LitElement {\n static styles = [\n EntryControlSizeStyles,\n ButtonContainerStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n\n border: 1px solid var(--md-sys-color-primary);\n margin: var(--spacing-medium);\n padding: var(--spacing-medium);\n font: normal 15px var(--theme-font);\n color: var(--md-sys-color-secondary);\n }\n\n [field-2column] {\n border-radius: var(--border-radius);\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 5px 15px;\n clear: both;\n max-width: var(--input-container-max-width);\n }\n\n [field] {\n display: flex;\n flex-direction: column;\n padding-bottom: var(--spacing-medium);\n }\n\n input {\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n min-width: 250px;\n font: var(--input-font);\n }\n\n label {\n display: flex;\n flex-direction: column;\n font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n text-transform: var(--label-text-transform);\n }\n\n md-elevated-button {\n margin-right: var(--spacing-small);\n }\n `\n ]\n\n @property({ type: Object }) data: any = {}\n @property({ type: Object }) user: any = {}\n @property({ type: Object }) client: any = {}\n @property({ type: Object }) domain: any = {}\n @property({ type: Object }) req: any = {}\n @property({ type: String }) redirectURI: string = ''\n @property({ type: String }) transactionID: string = ''\n @property({ type: Array }) roles: any[] = []\n\n @query('role-selector') roleSelector!: HTMLElement & { selectedRoles: () => any[] }\n\n render() {\n return html`\n <h2>Application Binding</h2>\n <p>Hi ${this.user.name}!</p>\n <p><b> ${this.client.name} </b> is requesting access to your account.</p>\n <p>Do you approve?</p>\n\n <form field-2column>\n <div field>\n <label\n >${i18next.t('label.company')}\n <input readonly value=\"${this.domain.subdomain}\" />\n </label>\n\n <label\n >${i18next.t('label.client-id')}\n <input readonly value=\"${this.req.clientID}\" />\n </label>\n </div>\n\n <div field>\n <label\n >${i18next.t('label.redirect_uri')}\n <input readonly value=\"${this.redirectURI}\" />\n </label>\n\n <label\n >${i18next.t('label.state')}\n <input readonly value=\"${this.req.state}\" />\n </label>\n </div>\n </form>\n\n <role-selector .roleCategory=\"${this.domain.name}\" .roles=\"${this.roles}\"></role-selector>\n\n <div class=\"button-container\">\n <md-elevated-button @click=\"${() => this.decision(true)}\" raised label=\"Allow\"></md-elevated-button>\n <md-elevated-button @click=\"${() => this.decision(false)}\" raised danger label=\"Deny\"></md-elevated-button>\n </div>\n `\n }\n\n async firstUpdated() {\n this.fetchMyRoles()\n }\n\n updated(changedProps) {\n if (changedProps.has('data')) {\n this.user = this.data?.oauth2?.user || this.user\n this.client = this.data?.oauth2?.client || this.client\n this.domain = this.data?.domain || this.domain\n this.req = this.data?.oauth2?.req || this.req\n this.redirectURI = this.data?.oauth2?.redirectURI || this.redirectURI\n this.transactionID = this.data?.oauth2?.transactionID || this.transactionID\n }\n }\n\n async fetchMyRoles() {\n const response = await client.query({\n query: gql`\n query myRoles {\n myRoles {\n id\n name\n description\n }\n }\n `\n })\n\n if (!response.errors?.length) {\n this.roles = response.data.myRoles\n }\n }\n\n async decision(allowOrCancel) {\n const selectedRoles = this.selectedRoles()\n if (allowOrCancel && !selectedRoles?.length) {\n await OxPrompt.open({\n title: i18next.t('title.nothing selected'),\n text: i18next.t('text.should select at least one of x', { x: i18next.t('label.role') }),\n confirmButton: { text: i18next.t('button.confirm') }\n })\n\n return\n }\n\n let data: any = {\n transaction_id: this.transactionID,\n subdomain: this.domain.subdomain,\n appKey: this.req.clientID,\n state: this.req.state,\n email: this.user.email,\n scopes: selectedRoles\n }\n if (!allowOrCancel) {\n data.cancel = 'Deny'\n }\n\n const response = await fetch('/oauth/decision', {\n method: 'POST',\n mode: 'cors',\n credentials: 'include',\n headers: {\n 'Content-Type': 'application/json'\n },\n redirect: 'follow',\n body: JSON.stringify(data)\n })\n\n if (response.redirected) {\n location.href = response.url\n }\n }\n\n selectedRoles() {\n return this.roleSelector.selectedRoles()\n }\n}\n"]}
1
+ {"version":3,"file":"oauth2-decision-page.js","sourceRoot":"","sources":["../../../client/entries/oauth2/oauth2-decision-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/elevated-button.js'\nimport '../../components/role-selector.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport { QueryController } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { ButtonContainerStyles } from '@operato/styles'\nimport { EntryControlSizeStyles } from '../../entry-control-size-styles.js'\n\n@customElement('oauth2-decision')\nclass OAuth2DecisionPage extends LitElement {\n static styles = [\n EntryControlSizeStyles,\n ButtonContainerStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n\n border: 1px solid var(--md-sys-color-primary);\n margin: var(--spacing-medium);\n padding: var(--spacing-medium);\n font: normal 15px var(--theme-font);\n color: var(--md-sys-color-secondary);\n }\n\n [field-2column] {\n border-radius: var(--border-radius);\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 5px 15px;\n clear: both;\n max-width: var(--input-container-max-width);\n }\n\n [field] {\n display: flex;\n flex-direction: column;\n padding-bottom: var(--spacing-medium);\n }\n\n input {\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n min-width: 250px;\n font: var(--input-font);\n }\n\n label {\n display: flex;\n flex-direction: column;\n font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n text-transform: var(--label-text-transform);\n }\n\n md-elevated-button {\n margin-right: var(--spacing-small);\n }\n `\n ]\n\n @property({ type: Object }) data: any = {}\n @property({ type: Object }) user: any = {}\n @property({ type: Object }) client: any = {}\n @property({ type: Object }) domain: any = {}\n @property({ type: Object }) req: any = {}\n @property({ type: String }) redirectURI: string = ''\n @property({ type: String }) transactionID: string = ''\n\n private myRoles = new QueryController(\n this,\n gql`\n query myRoles {\n myRoles {\n id\n name\n description\n }\n }\n `\n )\n\n get roles(): any[] {\n return this.myRoles.data?.myRoles ?? []\n }\n\n @query('role-selector') roleSelector!: HTMLElement & { selectedRoles: () => any[] }\n\n render() {\n return html`\n <h2>Application Binding</h2>\n <p>Hi ${this.user.name}!</p>\n <p><b> ${this.client.name} </b> is requesting access to your account.</p>\n <p>Do you approve?</p>\n\n <form field-2column>\n <div field>\n <label\n >${i18next.t('label.company')}\n <input readonly value=\"${this.domain.subdomain}\" />\n </label>\n\n <label\n >${i18next.t('label.client-id')}\n <input readonly value=\"${this.req.clientID}\" />\n </label>\n </div>\n\n <div field>\n <label\n >${i18next.t('label.redirect_uri')}\n <input readonly value=\"${this.redirectURI}\" />\n </label>\n\n <label\n >${i18next.t('label.state')}\n <input readonly value=\"${this.req.state}\" />\n </label>\n </div>\n </form>\n\n <role-selector .roleCategory=\"${this.domain.name}\" .roles=\"${this.roles}\"></role-selector>\n\n <div class=\"button-container\">\n <md-elevated-button @click=\"${() => this.decision(true)}\" raised label=\"Allow\"></md-elevated-button>\n <md-elevated-button @click=\"${() => this.decision(false)}\" raised danger label=\"Deny\"></md-elevated-button>\n </div>\n `\n }\n\n updated(changedProps) {\n if (changedProps.has('data')) {\n this.user = this.data?.oauth2?.user || this.user\n this.client = this.data?.oauth2?.client || this.client\n this.domain = this.data?.domain || this.domain\n this.req = this.data?.oauth2?.req || this.req\n this.redirectURI = this.data?.oauth2?.redirectURI || this.redirectURI\n this.transactionID = this.data?.oauth2?.transactionID || this.transactionID\n }\n }\n\n async decision(allowOrCancel) {\n const selectedRoles = this.selectedRoles()\n if (allowOrCancel && !selectedRoles?.length) {\n await OxPrompt.open({\n title: i18next.t('title.nothing selected'),\n text: i18next.t('text.should select at least one of x', { x: i18next.t('label.role') }),\n confirmButton: { text: i18next.t('button.confirm') }\n })\n\n return\n }\n\n let data: any = {\n transaction_id: this.transactionID,\n subdomain: this.domain.subdomain,\n appKey: this.req.clientID,\n state: this.req.state,\n email: this.user.email,\n scopes: selectedRoles\n }\n if (!allowOrCancel) {\n data.cancel = 'Deny'\n }\n\n const response = await fetch('/oauth/decision', {\n method: 'POST',\n mode: 'cors',\n credentials: 'include',\n headers: {\n 'Content-Type': 'application/json'\n },\n redirect: 'follow',\n body: JSON.stringify(data)\n })\n\n if (response.redirected) {\n location.href = response.url\n }\n }\n\n selectedRoles() {\n return this.roleSelector.selectedRoles()\n }\n}\n"]}
@@ -8,15 +8,8 @@ export declare const IDENTITY_CONFIRMATION_REFUSAL = "identity-confirmation-requ
8
8
  export declare const CONFIRMED_IDENTITY_CONTEXT: {
9
9
  handledRefusals: string[];
10
10
  };
11
- /** Whether `error` - an Apollo error, or a response with `errors` - is that refusal. */
12
- export declare function needsIdentityConfirmation(error: any): boolean;
13
11
  /** Ask the person to confirm it is them - true once the server took it. Two screens asking at once share one popup. */
14
12
  export declare function confirmIdentity(): Promise<boolean>;
15
- /**
16
- * Run `read`; when the door refuses for want of a recent confirmation, ask for it and read once more. Undefined when
17
- * the person did not confirm - the screen then says so (`identity-confirmation-notice`) rather than an error.
18
- */
19
- export declare function withConfirmedIdentity<T>(read: () => Promise<T>): Promise<T | undefined>;
20
13
  /** Until when the confirmation in this browser holds - null when there is none (ADR-0168 decision 3). */
21
14
  export declare function confirmedUntil(): Promise<Date | null>;
22
15
  declare const IdentityConfirmationNotice_base: (new (...args: any[]) => LitElement) & typeof LitElement;
@@ -32,21 +25,27 @@ export declare class IdentityConfirmationNotice extends IdentityConfirmationNoti
32
25
  private ask;
33
26
  }
34
27
  /**
35
- * A screen's hold on what a confirmation opened: it reads through `read`, which asks when the door does, and puts
36
- * what it read away (`onPutAway` drops the screen's data) the moment the confirmation ends - the screen does not
37
- * keep showing it past ten minutes. `unconfirmed` says why nothing is shown - declined, or ran out - and `notice`
38
- * draws that, with the way to ask again. A screen that goes away drops its data by calling `putAway()` itself.
28
+ * A screen's hold on what a confirmation opened. `ask()` finds whether a confirmation holds and, when none does, asks
29
+ * for it; `confirmed` is true while one holds and the screen reads only then (its query's variables are undefined
30
+ * otherwise). The moment the confirmation ends, `confirmed` turns false and `onPutAway` runs - the screen does not keep
31
+ * showing what it read past ten minutes. `unconfirmed` says why nothing is shown - declined, or ran out - and `notice`
32
+ * draws that, with the way to ask again. A screen that goes away drops what it shows by calling `putAway()` itself.
39
33
  */
40
34
  export declare class ConfirmedIdentityController implements ReactiveController {
41
35
  private host;
42
36
  private onPutAway;
37
+ confirmed: boolean;
43
38
  unconfirmed: 'declined' | 'expired' | null;
44
39
  private timer?;
45
- constructor(host: ReactiveControllerHost, onPutAway: () => void);
46
- read<T>(run: () => Promise<T>): Promise<T | undefined>;
40
+ constructor(host: ReactiveControllerHost, onPutAway?: () => void);
41
+ /** Confirmed when a confirmation holds; otherwise the person is asked, and declining is said in its place. */
42
+ ask(): Promise<void>;
47
43
  /** Drop what the screen shows; `reason` is said in its place, none when the screen only went away. */
48
44
  putAway(reason?: 'declined' | 'expired' | null): void;
49
- notice(again: () => void): TemplateResult;
45
+ notice(): TemplateResult;
50
46
  hostDisconnected(): void;
47
+ private confirmedByNotice;
48
+ /** Confirmed until the confirmation in this browser ends - false when none holds. */
49
+ private hold;
51
50
  }
52
51
  export {};
@@ -8,19 +8,14 @@ import { openPopup } from '@operato/layout';
8
8
  /*
9
9
  * ── Screens of a door that asks for a recent confirmation (ADR-0168) ─────────────────────
10
10
  * The server refuses such a door until the person confirmed it is them in this browser, within the last ten minutes.
11
- * A screen reads through `ConfirmedIdentityController`, which asks once and reads again, and puts away what it shows
12
- * the moment the confirmation ends - a screen kept open past ten minutes asks again (the user, 2026-10-28). One way
13
- * for every app - none draws its own password box.
11
+ * A screen reads only while its `ConfirmedIdentityController` says `confirmed` - the controller asks for the
12
+ * confirmation when none holds - and what it shows is put away the moment the confirmation ends: a screen kept open
13
+ * past ten minutes asks again (the user, 2026-10-28). One way for every app - none draws its own password box.
14
14
  */
15
15
  /** The server's refusal code for a door that asks for a recent confirmation. */
16
16
  export const IDENTITY_CONFIRMATION_REFUSAL = 'identity-confirmation-required';
17
17
  /** The query context for such a door: the screen asks for the confirmation itself, so no error snackbar. */
18
18
  export const CONFIRMED_IDENTITY_CONTEXT = { handledRefusals: [IDENTITY_CONFIRMATION_REFUSAL] };
19
- /** Whether `error` - an Apollo error, or a response with `errors` - is that refusal. */
20
- export function needsIdentityConfirmation(error) {
21
- const errors = error?.graphQLErrors?.length ? error.graphQLErrors : (error?.errors ?? []);
22
- return errors.some((one) => one?.extensions?.refusal?.code === IDENTITY_CONFIRMATION_REFUSAL);
23
- }
24
19
  let asking = null;
25
20
  /** Ask the person to confirm it is them - true once the server took it. Two screens asking at once share one popup. */
26
21
  export function confirmIdentity() {
@@ -35,22 +30,6 @@ export function confirmIdentity() {
35
30
  }
36
31
  return asking;
37
32
  }
38
- /**
39
- * Run `read`; when the door refuses for want of a recent confirmation, ask for it and read once more. Undefined when
40
- * the person did not confirm - the screen then says so (`identity-confirmation-notice`) rather than an error.
41
- */
42
- export async function withConfirmedIdentity(read) {
43
- try {
44
- return await read();
45
- }
46
- catch (error) {
47
- if (!needsIdentityConfirmation(error))
48
- throw error;
49
- if (!(await confirmIdentity()))
50
- return undefined;
51
- return await read();
52
- }
53
- }
54
33
  /** Until when the confirmation in this browser holds - null when there is none (ADR-0168 decision 3). */
55
34
  export async function confirmedUntil() {
56
35
  try {
@@ -102,47 +81,62 @@ IdentityConfirmationNotice = __decorate([
102
81
  ], IdentityConfirmationNotice);
103
82
  export { IdentityConfirmationNotice };
104
83
  /**
105
- * A screen's hold on what a confirmation opened: it reads through `read`, which asks when the door does, and puts
106
- * what it read away (`onPutAway` drops the screen's data) the moment the confirmation ends - the screen does not
107
- * keep showing it past ten minutes. `unconfirmed` says why nothing is shown - declined, or ran out - and `notice`
108
- * draws that, with the way to ask again. A screen that goes away drops its data by calling `putAway()` itself.
84
+ * A screen's hold on what a confirmation opened. `ask()` finds whether a confirmation holds and, when none does, asks
85
+ * for it; `confirmed` is true while one holds and the screen reads only then (its query's variables are undefined
86
+ * otherwise). The moment the confirmation ends, `confirmed` turns false and `onPutAway` runs - the screen does not keep
87
+ * showing what it read past ten minutes. `unconfirmed` says why nothing is shown - declined, or ran out - and `notice`
88
+ * draws that, with the way to ask again. A screen that goes away drops what it shows by calling `putAway()` itself.
109
89
  */
110
90
  export class ConfirmedIdentityController {
111
- constructor(host, onPutAway) {
91
+ constructor(host, onPutAway = () => { }) {
112
92
  this.host = host;
113
93
  this.onPutAway = onPutAway;
94
+ this.confirmed = false;
114
95
  this.unconfirmed = null;
115
96
  host.addController(this);
116
97
  }
117
- async read(run) {
118
- const answer = await withConfirmedIdentity(run);
119
- if (answer === undefined) {
120
- this.putAway('declined');
121
- return undefined;
122
- }
123
- this.unconfirmed = null;
124
- clearTimeout(this.timer);
125
- const until = await confirmedUntil();
126
- /* The read just passed the door, so a confirmation is there; not knowing until when, the screen does not keep it. */
127
- const left = until ? until.getTime() - Date.now() : 0;
128
- this.timer = setTimeout(() => this.putAway('expired'), Math.max(left, 0));
129
- this.host.requestUpdate();
130
- return answer;
98
+ /** Confirmed when a confirmation holds; otherwise the person is asked, and declining is said in its place. */
99
+ async ask() {
100
+ if (await this.hold())
101
+ return;
102
+ if (!(await confirmIdentity()))
103
+ return this.putAway('declined');
104
+ /* Just confirmed; not knowing until when, the screen does not keep it. */
105
+ if (!(await this.hold()))
106
+ this.putAway('expired');
131
107
  }
132
108
  /** Drop what the screen shows; `reason` is said in its place, none when the screen only went away. */
133
109
  putAway(reason = null) {
134
110
  clearTimeout(this.timer);
135
111
  this.timer = undefined;
112
+ this.confirmed = false;
136
113
  this.unconfirmed = reason;
137
114
  this.onPutAway();
138
115
  this.host.requestUpdate();
139
116
  }
140
- notice(again) {
141
- return html `<identity-confirmation-notice ?expired=${this.unconfirmed === 'expired'} @confirmed=${again}></identity-confirmation-notice>`;
117
+ notice() {
118
+ return html `<identity-confirmation-notice ?expired=${this.unconfirmed === 'expired'} @confirmed=${() => this.confirmedByNotice()}></identity-confirmation-notice>`;
142
119
  }
143
120
  hostDisconnected() {
144
121
  clearTimeout(this.timer);
145
122
  this.timer = undefined;
146
123
  }
124
+ async confirmedByNotice() {
125
+ if (!(await this.hold()))
126
+ this.putAway('expired');
127
+ }
128
+ /** Confirmed until the confirmation in this browser ends - false when none holds. */
129
+ async hold() {
130
+ const until = await confirmedUntil();
131
+ const left = until ? until.getTime() - Date.now() : 0;
132
+ if (left <= 0)
133
+ return false;
134
+ clearTimeout(this.timer);
135
+ this.timer = setTimeout(() => this.putAway('expired'), left);
136
+ this.confirmed = true;
137
+ this.unconfirmed = null;
138
+ this.host.requestUpdate();
139
+ return true;
140
+ }
147
141
  }
148
142
  //# sourceMappingURL=identity-confirmation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"identity-confirmation.js","sourceRoot":"","sources":["../../client/identity-confirmation/identity-confirmation.ts"],"names":[],"mappings":"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","sourcesContent":["import './identity-confirm-popup.js'\nimport '@material/web/button/outlined-button.js'\n\nimport { css, html, LitElement } from 'lit'\nimport type { ReactiveController, ReactiveControllerHost, TemplateResult } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { openPopup } from '@operato/layout'\n\n/*\n * ── Screens of a door that asks for a recent confirmation (ADR-0168) ─────────────────────\n * The server refuses such a door until the person confirmed it is them in this browser, within the last ten minutes.\n * A screen reads through `ConfirmedIdentityController`, which asks once and reads again, and puts away what it shows\n * the moment the confirmation ends - a screen kept open past ten minutes asks again (the user, 2026-10-28). One way\n * for every app - none draws its own password box.\n */\n\n/** The server's refusal code for a door that asks for a recent confirmation. */\nexport const IDENTITY_CONFIRMATION_REFUSAL = 'identity-confirmation-required'\n\n/** The query context for such a door: the screen asks for the confirmation itself, so no error snackbar. */\nexport const CONFIRMED_IDENTITY_CONTEXT = { handledRefusals: [IDENTITY_CONFIRMATION_REFUSAL] }\n\n/** Whether `error` - an Apollo error, or a response with `errors` - is that refusal. */\nexport function needsIdentityConfirmation(error: any): boolean {\n const errors = error?.graphQLErrors?.length ? error.graphQLErrors : (error?.errors ?? [])\n return errors.some((one: any) => one?.extensions?.refusal?.code === IDENTITY_CONFIRMATION_REFUSAL)\n}\n\nlet asking: Promise<boolean> | null = null\n\n/** Ask the person to confirm it is them - true once the server took it. Two screens asking at once share one popup. */\nexport function confirmIdentity(): Promise<boolean> {\n if (!asking) {\n const popup = openPopup<boolean>(html`<identity-confirm-popup></identity-confirm-popup>`, {\n backdrop: true,\n /* A form of one field stands at its own height - a fraction of the screen clipped it and left half the card empty. */\n size: 'fit',\n title: i18next.t('title.confirm it is you')\n })\n asking = popup.result.then(result => result === true).finally(() => (asking = null))\n }\n return asking\n}\n\n/**\n * Run `read`; when the door refuses for want of a recent confirmation, ask for it and read once more. Undefined when\n * the person did not confirm - the screen then says so (`identity-confirmation-notice`) rather than an error.\n */\nexport async function withConfirmedIdentity<T>(read: () => Promise<T>): Promise<T | undefined> {\n try {\n return await read()\n } catch (error) {\n if (!needsIdentityConfirmation(error)) throw error\n if (!(await confirmIdentity())) return undefined\n return await read()\n }\n}\n\n/** Until when the confirmation in this browser holds - null when there is none (ADR-0168 decision 3). */\nexport async function confirmedUntil(): Promise<Date | null> {\n try {\n const response = await fetch('/auth/confirm-identity', { headers: { Accept: 'application/json' } })\n const { confirmedUntil } = response.ok ? await response.json() : { confirmedUntil: null }\n return confirmedUntil ? new Date(confirmedUntil) : null\n } catch {\n return null\n }\n}\n\n/**\n * What a screen shows in place of what it would not read: the confirmation was declined, or it ran out and what the\n * screen showed was put away. Its button asks again and fires `confirmed` once the person did.\n */\n@customElement('identity-confirmation-notice')\nexport class IdentityConfirmationNotice extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px) 0;\n color: var(--ui-muted);\n }\n `\n\n /** The confirmation ran out and what the screen showed was put away - rather than a confirmation not yet given. */\n @property({ type: Boolean }) expired = false\n\n render() {\n return html`\n <span>${i18next.t(this.expired ? 'text.identity confirmation expired' : 'text.identity confirmation needed')}</span>\n <md-outlined-button @click=${() => this.ask()}>${i18next.t('button.confirm it is you')}</md-outlined-button>\n `\n }\n\n private async ask() {\n if (await confirmIdentity()) this.dispatchEvent(new CustomEvent('confirmed', { bubbles: true, composed: true }))\n }\n}\n\n/**\n * A screen's hold on what a confirmation opened: it reads through `read`, which asks when the door does, and puts\n * what it read away (`onPutAway` drops the screen's data) the moment the confirmation ends - the screen does not\n * keep showing it past ten minutes. `unconfirmed` says why nothing is shown - declined, or ran out - and `notice`\n * draws that, with the way to ask again. A screen that goes away drops its data by calling `putAway()` itself.\n */\nexport class ConfirmedIdentityController implements ReactiveController {\n unconfirmed: 'declined' | 'expired' | null = null\n\n private timer?: ReturnType<typeof setTimeout>\n\n constructor(\n private host: ReactiveControllerHost,\n private onPutAway: () => void\n ) {\n host.addController(this)\n }\n\n async read<T>(run: () => Promise<T>): Promise<T | undefined> {\n const answer = await withConfirmedIdentity(run)\n if (answer === undefined) {\n this.putAway('declined')\n return undefined\n }\n\n this.unconfirmed = null\n clearTimeout(this.timer)\n const until = await confirmedUntil()\n /* The read just passed the door, so a confirmation is there; not knowing until when, the screen does not keep it. */\n const left = until ? until.getTime() - Date.now() : 0\n this.timer = setTimeout(() => this.putAway('expired'), Math.max(left, 0))\n this.host.requestUpdate()\n return answer\n }\n\n /** Drop what the screen shows; `reason` is said in its place, none when the screen only went away. */\n putAway(reason: 'declined' | 'expired' | null = null) {\n clearTimeout(this.timer)\n this.timer = undefined\n this.unconfirmed = reason\n this.onPutAway()\n this.host.requestUpdate()\n }\n\n notice(again: () => void): TemplateResult {\n return html`<identity-confirmation-notice ?expired=${this.unconfirmed === 'expired'} @confirmed=${again}></identity-confirmation-notice>`\n }\n\n hostDisconnected() {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n}\n"]}
1
+ {"version":3,"file":"identity-confirmation.js","sourceRoot":"","sources":["../../client/identity-confirmation/identity-confirmation.ts"],"names":[],"mappings":"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","sourcesContent":["import './identity-confirm-popup.js'\nimport '@material/web/button/outlined-button.js'\n\nimport { css, html, LitElement } from 'lit'\nimport type { ReactiveController, ReactiveControllerHost, TemplateResult } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { openPopup } from '@operato/layout'\n\n/*\n * ── Screens of a door that asks for a recent confirmation (ADR-0168) ─────────────────────\n * The server refuses such a door until the person confirmed it is them in this browser, within the last ten minutes.\n * A screen reads only while its `ConfirmedIdentityController` says `confirmed` - the controller asks for the\n * confirmation when none holds - and what it shows is put away the moment the confirmation ends: a screen kept open\n * past ten minutes asks again (the user, 2026-10-28). One way for every app - none draws its own password box.\n */\n\n/** The server's refusal code for a door that asks for a recent confirmation. */\nexport const IDENTITY_CONFIRMATION_REFUSAL = 'identity-confirmation-required'\n\n/** The query context for such a door: the screen asks for the confirmation itself, so no error snackbar. */\nexport const CONFIRMED_IDENTITY_CONTEXT = { handledRefusals: [IDENTITY_CONFIRMATION_REFUSAL] }\n\nlet asking: Promise<boolean> | null = null\n\n/** Ask the person to confirm it is them - true once the server took it. Two screens asking at once share one popup. */\nexport function confirmIdentity(): Promise<boolean> {\n if (!asking) {\n const popup = openPopup<boolean>(html`<identity-confirm-popup></identity-confirm-popup>`, {\n backdrop: true,\n /* A form of one field stands at its own height - a fraction of the screen clipped it and left half the card empty. */\n size: 'fit',\n title: i18next.t('title.confirm it is you')\n })\n asking = popup.result.then(result => result === true).finally(() => (asking = null))\n }\n return asking\n}\n\n/** Until when the confirmation in this browser holds - null when there is none (ADR-0168 decision 3). */\nexport async function confirmedUntil(): Promise<Date | null> {\n try {\n const response = await fetch('/auth/confirm-identity', { headers: { Accept: 'application/json' } })\n const { confirmedUntil } = response.ok ? await response.json() : { confirmedUntil: null }\n return confirmedUntil ? new Date(confirmedUntil) : null\n } catch {\n return null\n }\n}\n\n/**\n * What a screen shows in place of what it would not read: the confirmation was declined, or it ran out and what the\n * screen showed was put away. Its button asks again and fires `confirmed` once the person did.\n */\n@customElement('identity-confirmation-notice')\nexport class IdentityConfirmationNotice extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px) 0;\n color: var(--ui-muted);\n }\n `\n\n /** The confirmation ran out and what the screen showed was put away - rather than a confirmation not yet given. */\n @property({ type: Boolean }) expired = false\n\n render() {\n return html`\n <span>${i18next.t(this.expired ? 'text.identity confirmation expired' : 'text.identity confirmation needed')}</span>\n <md-outlined-button @click=${() => this.ask()}>${i18next.t('button.confirm it is you')}</md-outlined-button>\n `\n }\n\n private async ask() {\n if (await confirmIdentity()) this.dispatchEvent(new CustomEvent('confirmed', { bubbles: true, composed: true }))\n }\n}\n\n/**\n * A screen's hold on what a confirmation opened. `ask()` finds whether a confirmation holds and, when none does, asks\n * for it; `confirmed` is true while one holds and the screen reads only then (its query's variables are undefined\n * otherwise). The moment the confirmation ends, `confirmed` turns false and `onPutAway` runs - the screen does not keep\n * showing what it read past ten minutes. `unconfirmed` says why nothing is shown - declined, or ran out - and `notice`\n * draws that, with the way to ask again. A screen that goes away drops what it shows by calling `putAway()` itself.\n */\nexport class ConfirmedIdentityController implements ReactiveController {\n confirmed = false\n unconfirmed: 'declined' | 'expired' | null = null\n\n private timer?: ReturnType<typeof setTimeout>\n\n constructor(\n private host: ReactiveControllerHost,\n private onPutAway: () => void = () => {}\n ) {\n host.addController(this)\n }\n\n /** Confirmed when a confirmation holds; otherwise the person is asked, and declining is said in its place. */\n async ask(): Promise<void> {\n if (await this.hold()) return\n if (!(await confirmIdentity())) return this.putAway('declined')\n /* Just confirmed; not knowing until when, the screen does not keep it. */\n if (!(await this.hold())) this.putAway('expired')\n }\n\n /** Drop what the screen shows; `reason` is said in its place, none when the screen only went away. */\n putAway(reason: 'declined' | 'expired' | null = null) {\n clearTimeout(this.timer)\n this.timer = undefined\n this.confirmed = false\n this.unconfirmed = reason\n this.onPutAway()\n this.host.requestUpdate()\n }\n\n notice(): TemplateResult {\n return html`<identity-confirmation-notice ?expired=${this.unconfirmed === 'expired'} @confirmed=${() => this.confirmedByNotice()}></identity-confirmation-notice>`\n }\n\n hostDisconnected() {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n\n private async confirmedByNotice() {\n if (!(await this.hold())) this.putAway('expired')\n }\n\n /** Confirmed until the confirmation in this browser ends - false when none holds. */\n private async hold(): Promise<boolean> {\n const until = await confirmedUntil()\n const left = until ? until.getTime() - Date.now() : 0\n if (left <= 0) return false\n clearTimeout(this.timer)\n this.timer = setTimeout(() => this.putAway('expired'), left)\n this.confirmed = true\n this.unconfirmed = null\n this.host.requestUpdate()\n return true\n }\n}\n"]}
@@ -21,8 +21,10 @@ export declare class AuditPage extends AuditPageBase {
21
21
  private door?;
22
22
  private outcome;
23
23
  /** How many records each outcome chip would show under the other conditions, counted by the records' own door. */
24
- private outcomeCounts;
25
- private health?;
24
+ private outcomes;
25
+ private auditHealth;
26
+ private get outcomeCounts();
27
+ private get health();
26
28
  private grist;
27
29
  get context(): {
28
30
  title: string;
@@ -37,10 +39,8 @@ export declare class AuditPage extends AuditPageBase {
37
39
  private show;
38
40
  private lastDays;
39
41
  private pick;
40
- private countOutcomes;
41
42
  private narrow;
42
43
  pageUpdated(_changes: any, _lifecycle: any): Promise<void>;
43
- private readHealth;
44
44
  private configOf;
45
45
  fetchHandler({ page, limit }: FetchOption): Promise<{
46
46
  total: any;
@@ -6,7 +6,7 @@ import gql from 'graphql-tag';
6
6
  import { css, html, nothing } from 'lit';
7
7
  import { customElement, property, query, state } from 'lit/decorators.js';
8
8
  import { keyed } from 'lit/directives/keyed.js';
9
- import { client } from '@operato/graphql';
9
+ import { client, QueryController } from '@operato/graphql';
10
10
  import { i18next, localize } from '@operato/i18n';
11
11
  import { formatWhen } from '@operato/utils/when.js';
12
12
  import { p13n } from '@operato/p13n';
@@ -33,7 +33,24 @@ let AuditPage = class AuditPage extends AuditPageBase {
33
33
  this.to = new Date();
34
34
  this.outcome = '';
35
35
  /** How many records each outcome chip would show under the other conditions, counted by the records' own door. */
36
- this.outcomeCounts = new Map();
36
+ /*
37
+ * The outcome chips' numbers - the records' own door asked for its total once per outcome, under the period, the
38
+ * person and the privilege narrowed to, in one request. The chips keep their numbers while new ones are read.
39
+ */
40
+ this.outcomes = new QueryController(this, gql `
41
+ query (${OUTCOMES.map((_, at) => `$f${at}: AuditRecordFilter!`).join(', ')}) {
42
+ ${OUTCOMES.map((_, at) => `c${at}: auditRecords(filter: $f${at}) { total }`).join('\n')}
43
+ }
44
+ `, {
45
+ variables: () => {
46
+ if (this.view !== 'records')
47
+ return undefined;
48
+ const filter = { from: this.from.toISOString(), to: this.to.toISOString(), actorId: this.actor?.id, door: this.door };
49
+ return Object.fromEntries(OUTCOMES.map((outcome, at) => [`f${at}`, { ...filter, outcome: outcome || undefined, page: 1, limit: 1 }]));
50
+ },
51
+ keepPrevious: true
52
+ });
53
+ this.auditHealth = new QueryController(this, gql `{ auditHealth { failures since } }`);
37
54
  }
38
55
  static { this.styles = [
39
56
  WorkSurfacePageStyles,
@@ -79,6 +96,15 @@ let AuditPage = class AuditPage extends AuditPageBase {
79
96
  }
80
97
  `
81
98
  ]; }
99
+ get outcomeCounts() {
100
+ const data = this.outcomes.data;
101
+ return new Map(OUTCOMES.map((outcome, at) => [outcome || 'all', data?.[`c${at}`]?.total ?? 0]));
102
+ }
103
+ get health() {
104
+ if (this.auditHealth.data?.auditHealth)
105
+ return this.auditHealth.data.auditHealth;
106
+ return this.auditHealth.error || this.auditHealth.data ? 'unknown' : undefined;
107
+ }
82
108
  get context() {
83
109
  return {
84
110
  title: i18next.t('audit.title'),
@@ -170,24 +196,6 @@ let AuditPage = class AuditPage extends AuditPageBase {
170
196
  this.days = null;
171
197
  this.grist?.fetch();
172
198
  }
173
- /*
174
- * The outcome chips' numbers - the records' own door asked for its total once per outcome, under the period, the
175
- * person and the privilege narrowed to, in one request.
176
- */
177
- async countOutcomes(filter) {
178
- const response = await client.query({
179
- query: gql `
180
- query (${OUTCOMES.map((_, at) => `$f${at}: AuditRecordFilter!`).join(', ')}) {
181
- ${OUTCOMES.map((_, at) => `c${at}: auditRecords(filter: $f${at}) { total }`).join('\n')}
182
- }
183
- `,
184
- variables: Object.fromEntries(OUTCOMES.map((outcome, at) => [`f${at}`, { ...filter, outcome: outcome || undefined, page: 1, limit: 1 }])),
185
- fetchPolicy: 'no-cache'
186
- });
187
- if (response.errors?.length)
188
- return console.error(response.errors);
189
- this.outcomeCounts = new Map(OUTCOMES.map((outcome, at) => [outcome || 'all', response.data?.[`c${at}`]?.total ?? 0]));
190
- }
191
199
  narrow(change) {
192
200
  Object.assign(this, change);
193
201
  if (this.view !== 'records')
@@ -199,16 +207,6 @@ let AuditPage = class AuditPage extends AuditPageBase {
199
207
  if (this.active) {
200
208
  await this.updateComplete;
201
209
  this.grist?.fetch();
202
- this.readHealth();
203
- }
204
- }
205
- async readHealth() {
206
- try {
207
- const response = await client.query({ query: gql `{ auditHealth { failures since } }`, fetchPolicy: 'no-cache' });
208
- this.health = response.errors?.length || !response.data?.auditHealth ? 'unknown' : response.data.auditHealth;
209
- }
210
- catch {
211
- this.health = 'unknown';
212
210
  }
213
211
  }
214
212
  configOf(view) {
@@ -342,7 +340,6 @@ let AuditPage = class AuditPage extends AuditPageBase {
342
340
  .sort((a, b) => used(b) - used(a));
343
341
  return { total: records.length, records };
344
342
  }
345
- this.countOutcomes({ ...period, actorId: this.actor?.id, door: this.door });
346
343
  const response = await client.query({
347
344
  query: gql `
348
345
  query ($filter: AuditRecordFilter!) {
@@ -438,14 +435,6 @@ __decorate([
438
435
  state(),
439
436
  __metadata("design:type", String)
440
437
  ], AuditPage.prototype, "outcome", void 0);
441
- __decorate([
442
- state(),
443
- __metadata("design:type", Object)
444
- ], AuditPage.prototype, "outcomeCounts", void 0);
445
- __decorate([
446
- state(),
447
- __metadata("design:type", Object)
448
- ], AuditPage.prototype, "health", void 0);
449
438
  __decorate([
450
439
  query('ox-grist'),
451
440
  __metadata("design:type", Function)