@things-factory/auth-ui 10.1.17 → 10.1.20

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 (62) hide show
  1. package/dist-client/components/app-binding-detail.d.ts +46 -0
  2. package/dist-client/components/app-binding-detail.js +384 -0
  3. package/dist-client/components/app-binding-detail.js.map +1 -0
  4. package/dist-client/components/appliance-editor.d.ts +37 -0
  5. package/dist-client/components/appliance-editor.js +489 -0
  6. package/dist-client/components/appliance-editor.js.map +1 -0
  7. package/dist-client/components/application-editor.d.ts +31 -0
  8. package/dist-client/components/application-editor.js +491 -0
  9. package/dist-client/components/application-editor.js.map +1 -0
  10. package/dist-client/components/profile-component.js +28 -11
  11. package/dist-client/components/profile-component.js.map +1 -1
  12. package/dist-client/constants/application-type-parity.test.js +62 -0
  13. package/dist-client/constants/application-type-parity.test.js.map +1 -0
  14. package/dist-client/constants/application.d.ts +17 -7
  15. package/dist-client/constants/application.js +18 -8
  16. package/dist-client/constants/application.js.map +1 -1
  17. package/dist-client/pages/app-binding/app-bindings.d.ts +2 -0
  18. package/dist-client/pages/app-binding/app-bindings.js +19 -1
  19. package/dist-client/pages/app-binding/app-bindings.js.map +1 -1
  20. package/dist-client/pages/appliance/home.d.ts +2 -0
  21. package/dist-client/pages/appliance/home.js +21 -3
  22. package/dist-client/pages/appliance/home.js.map +1 -1
  23. package/dist-client/pages/application/applications.d.ts +2 -0
  24. package/dist-client/pages/application/applications.js +21 -3
  25. package/dist-client/pages/application/applications.js.map +1 -1
  26. package/dist-client/pages/secrets-are-not-printed.test.js +21 -3
  27. package/dist-client/pages/secrets-are-not-printed.test.js.map +1 -1
  28. package/dist-client/route.js +0 -15
  29. package/dist-client/route.js.map +1 -1
  30. package/dist-client/tsconfig.tsbuildinfo +1 -1
  31. package/dist-client/utils/form-values.d.ts +29 -0
  32. package/dist-client/utils/form-values.js +45 -0
  33. package/dist-client/utils/form-values.js.map +1 -1
  34. package/dist-client/utils/form-values.test.js +59 -1
  35. package/dist-client/utils/form-values.test.js.map +1 -1
  36. package/dist-client/utils/webauthn-verification.d.ts +56 -0
  37. package/dist-client/utils/webauthn-verification.js +79 -0
  38. package/dist-client/utils/webauthn-verification.js.map +1 -0
  39. package/dist-client/utils/webauthn-verification.test.js +79 -0
  40. package/dist-client/utils/webauthn-verification.test.js.map +1 -0
  41. package/package.json +4 -4
  42. package/things-factory.config.js +0 -5
  43. package/translations/en.json +25 -0
  44. package/translations/ja.json +26 -0
  45. package/translations/ko.json +25 -0
  46. package/translations/ms.json +26 -0
  47. package/translations/zh.json +26 -0
  48. package/dist-client/pages/app-binding/app-binding.d.ts +0 -2
  49. package/dist-client/pages/app-binding/app-binding.js +0 -371
  50. package/dist-client/pages/app-binding/app-binding.js.map +0 -1
  51. package/dist-client/pages/appliance/appliance.d.ts +0 -2
  52. package/dist-client/pages/appliance/appliance.js +0 -390
  53. package/dist-client/pages/appliance/appliance.js.map +0 -1
  54. package/dist-client/pages/appliance/register.js +0 -166
  55. package/dist-client/pages/appliance/register.js.map +0 -1
  56. package/dist-client/pages/application/application.d.ts +0 -2
  57. package/dist-client/pages/application/application.js +0 -384
  58. package/dist-client/pages/application/application.js.map +0 -1
  59. package/dist-client/pages/application/register.js +0 -246
  60. package/dist-client/pages/application/register.js.map +0 -1
  61. /package/dist-client/{pages/appliance/register.d.ts → constants/application-type-parity.test.d.ts} +0 -0
  62. /package/dist-client/{pages/application/register.d.ts → utils/webauthn-verification.test.d.ts} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"applications.js","sourceRoot":"","sources":["../../../client/pages/application/applications.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport '@operato/data-grist/ox-grist.js'\nimport '@operato/context/ox-context-page-toolbar.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { navigate, PageView } from '@operato/shell'\nimport { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { p13n } from '@operato/p13n'\n\nimport { applicationStatusLabel } from '../../utils/application-status.js'\nimport { listOutcome, pageFromResponse } from '../../utils/list-result.js'\n\n/**\n * Registered applications.\n *\n * ── What this screen looked like before ──────────────────────────────────────\n * A hand-written `<table>` with no paging, no sort and no search, and three columns that\n * were not data at all: \"API health\" printed OK, \"Installs\" printed 1, \"status\" printed\n * draft — written into the markup, identical on every row. The previous commit removed the\n * two the server cannot answer and made status real. This one gives the list the same shape\n * as every other list in the product.\n *\n * The server was already ready for it: `applications` takes ListParam and declares\n * `searchables: ['name', 'description']`, so paging, sorting and the header's search box all\n * work by passing the grist's own options through.\n *\n * ── Why the row does not carry health or install counts ──────────────────────\n * Because Application does not declare them. If they arrive one day, they are columns; until\n * then the screen says what it knows. See `utils/application-status.ts` for the matching rule\n * on values the screen has no word for.\n */\n@customElement('applications-page')\nexport class Applications extends p13n(localize(i18next)(PageView)) {\n static styles = [\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n ox-grist {\n overflow-y: auto;\n flex: 1;\n }\n\n [failed] {\n padding: var(--spacing-medium);\n color: var(--md-sys-color-error);\n font: var(--label-font, inherit);\n }\n `\n ]\n\n @property({ type: Boolean }) active: boolean = false\n @property({ type: Object }) config: any\n @property({ type: String }) mode: 'CARD' | 'LIST' | 'GRID' = isMobileDevice() ? 'CARD' : 'GRID'\n\n /** The last fetch did not answer. Distinct from answering with nothing. */\n @property({ type: Boolean }) failed: boolean = false\n\n @query('ox-grist') private grist!: DataGrist\n\n get context() {\n return {\n title: i18next.t('text.applications'),\n help: 'auth/application',\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n actions: [\n {\n title: i18next.t('button.register'),\n action: () => navigate('application-register'),\n icon: 'add'\n }\n ],\n toolbar: false\n }\n }\n\n render() {\n return html`\n <ox-grist\n .mode=${this.mode}\n .config=${this.config}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n >\n <div slot=\"headroom\" class=\"header\">\n <ox-context-page-toolbar class=\"actions\" .context=${this.context}></ox-context-page-toolbar>\n </div>\n </ox-grist>\n\n ${this.failed ? html`<div failed>${i18next.t('text.the list could not be loaded')}</div>` : ''}\n `\n }\n\n async pageInitialized() {\n this.config = {\n rows: { appendable: false, selectable: { multiple: false } },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n {\n type: 'gutter',\n gutterName: 'button',\n icon: 'chevron_right',\n title: i18next.t('button.detail'),\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: any) => {\n if (record?.id) {\n navigate(`application/${record.id}`)\n }\n }\n }\n },\n {\n type: 'string',\n name: 'name',\n header: i18next.t('field.name'),\n record: { editable: false },\n sortable: true,\n width: 200\n },\n {\n type: 'string',\n name: 'description',\n header: i18next.t('field.description'),\n record: { editable: false },\n width: 260\n },\n {\n type: 'string',\n name: 'type',\n header: i18next.t('field.type'),\n record: { align: 'center', editable: false },\n sortable: true,\n width: 120\n },\n {\n type: 'string',\n name: 'statusLabel',\n header: i18next.t('field.status'),\n record: { align: 'center', editable: false },\n width: 120\n },\n {\n type: 'datetime',\n name: 'updatedAt',\n header: i18next.t('field.updated at'),\n sortable: true,\n width: 180\n }\n ]\n }\n }\n\n async pageUpdated(_changes: any, _lifecycle: any) {\n if (this.active) {\n await this.updateComplete\n this.grist.fetch()\n }\n }\n\n async fetchHandler({ page, limit, sortings = [], filters = [] }: FetchOption) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n applications(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items {\n id\n name\n description\n type\n status\n updatedAt\n }\n total\n }\n }\n `,\n variables: { filters, pagination: { page, limit }, sortings },\n context: gqlContext()\n })\n\n const { items, total, failed } = listOutcome(pageFromResponse(response, 'applications'))\n\n this.failed = failed\n\n /*\n * The status is turned into words here rather than in a cell renderer so the rule lives\n * in one tested place — a value this screen has no word for is printed as it arrived.\n */\n return {\n total,\n records: items.map((item: any) => ({\n ...item,\n statusLabel: applicationStatusLabel(item.status, (key: string) => i18next.t(key))\n }))\n }\n }\n}\n"]}
1
+ {"version":3,"file":"applications.js","sourceRoot":"","sources":["../../../client/pages/application/applications.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AAEnC,OAAO,iCAAiC,CAAA;AACxC,OAAO,6CAA6C,CAAA;AAEpD,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAGlE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AAEpC,OAAO,wCAAwC,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAA;AAC1E,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAE1E;;;;;;;;;;;;;;;;;;GAkBG;AAEI,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC;IAA5D;;QAyBwB,WAAM,GAAY,KAAK,CAAA;QAExB,SAAI,GAA6B,cAAc,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;QAE/F,2EAA2E;QAC9C,WAAM,GAAY,KAAK,CAAA;IAqKtD,CAAC;aAlMQ,WAAM,GAAG;QACd,iBAAiB;QACjB,kBAAkB;QAClB,eAAe;QACf,GAAG,CAAA;;;;;;;;;;;;;;;;;KAiBF;KACF,AAtBY,CAsBZ;IAWD,IAAI,OAAO;QACT,OAAO;YACL,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC;YACrC,IAAI,EAAE,kBAAkB;YACxB,MAAM,EAAE;gBACN,OAAO,EAAE,CAAC,MAAc,EAAE,EAAE;oBAC1B,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,MAAM,CAAA;gBAChC,CAAC;gBACD,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,UAAU,IAAI,EAAE;aACpC;YACD,OAAO,EAAE;gBACP;oBACE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC;oBACnC,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;oBAC/B,IAAI,EAAE,KAAK;iBACZ;aACF;YACD,OAAO,EAAE,KAAK;SACf,CAAA;IACH,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAA;;gBAEC,IAAI,CAAC,IAAI;kBACP,IAAI,CAAC,MAAM;wBACL,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;kCAClB,IAAI,CAAC,yBAAyB,CAAC,UAAU,CAAE;;;8DAGf,IAAI,CAAC,OAAO;;;;QAIlE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA,eAAe,OAAO,CAAC,CAAC,CAAC,mCAAmC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;KAC/F,CAAA;IACH,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,IAAI,CAAC,MAAM,GAAG;YACZ,IAAI,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;YAC5D,OAAO,EAAE;gBACP,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE;gBAC1C;oBACE,IAAI,EAAE,QAAQ;oBACd,UAAU,EAAE,QAAQ;oBACpB,IAAI,EAAE,eAAe;oBACrB,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;oBACjC,QAAQ,EAAE;wBACR,KAAK,EAAE,CAAC,QAAa,EAAE,KAAU,EAAE,OAAY,EAAE,MAAW,EAAE,EAAE;4BAC9D,IAAI,MAAM,EAAE,EAAE,EAAE,CAAC;gCACf,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;4BAC5B,CAAC;wBACH,CAAC;qBACF;iBACF;gBACD;oBACE,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,MAAM;oBACZ,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;oBAC/B,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;oBAC3B,QAAQ,EAAE,IAAI;oBACd,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,aAAa;oBACnB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC;oBACtC,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;oBAC3B,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,MAAM;oBACZ,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;oBAC/B,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE;oBAC5C,QAAQ,EAAE,IAAI;oBACd,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,aAAa;oBACnB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC;oBACjC,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE;oBAC5C,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE,IAAI,EAAE,UAAU;oBAChB,IAAI,EAAE,WAAW;oBACjB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC;oBACrC,QAAQ,EAAE,IAAI;oBACd,KAAK,EAAE,GAAG;iBACX;aACF;SACF,CAAA;IACH,CAAC;IAED;;;;OAIG;IACK,UAAU,CAAC,aAAsB;QACvC,MAAM,KAAK,GAAG,SAAS,CACrB,IAAI,CAAA;yBACe,aAAa;+BACP,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;iBACtC,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE;6BACP,EACvB;YACE,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;SAClG,CACF,CAAA;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,QAAa,EAAE,UAAe;QAC9C,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,CAAC,cAAc,CAAA;YACzB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QACpB,CAAC;IACH,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,EAAe;QAC1E,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;OAcT;YACD,SAAS,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE;YAC7D,OAAO,EAAE,UAAU,EAAE;SACtB,CAAC,CAAA;QAEF,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,WAAW,CAAC,gBAAgB,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAA;QAExF,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QAEpB;;;WAGG;QACH,OAAO;YACL,KAAK;YACL,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,CAAC;gBACjC,GAAG,IAAI;gBACP,WAAW,EAAE,sBAAsB,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;aAClF,CAAC,CAAC;SACJ,CAAA;IACH,CAAC;;AAzK4B;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;;4CAAwB;AACxB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;4CAAY;AACX;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;0CAAoE;AAGlE;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;;4CAAwB;AAEzB;IAA1B,KAAK,CAAC,UAAU,CAAC;;2CAA0B;AAhCjC,YAAY;IADxB,aAAa,CAAC,mBAAmB,CAAC;GACtB,YAAY,CAmMxB","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport '@operato/data-grist/ox-grist.js'\nimport '@operato/context/ox-context-page-toolbar.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { openPopup } from '@operato/layout'\nimport { PageView } from '@operato/shell'\nimport { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { p13n } from '@operato/p13n'\n\nimport '../../components/application-editor.js'\nimport { applicationStatusLabel } from '../../utils/application-status.js'\nimport { listOutcome, pageFromResponse } from '../../utils/list-result.js'\n\n/**\n * Registered applications.\n *\n * ── What this screen looked like before ──────────────────────────────────────\n * A hand-written `<table>` with no paging, no sort and no search, and three columns that\n * were not data at all: \"API health\" printed OK, \"Installs\" printed 1, \"status\" printed\n * draft — written into the markup, identical on every row. The previous commit removed the\n * two the server cannot answer and made status real. This one gives the list the same shape\n * as every other list in the product.\n *\n * The server was already ready for it: `applications` takes ListParam and declares\n * `searchables: ['name', 'description']`, so paging, sorting and the header's search box all\n * work by passing the grist's own options through.\n *\n * ── Why the row does not carry health or install counts ──────────────────────\n * Because Application does not declare them. If they arrive one day, they are columns; until\n * then the screen says what it knows. See `utils/application-status.ts` for the matching rule\n * on values the screen has no word for.\n */\n@customElement('applications-page')\nexport class Applications extends p13n(localize(i18next)(PageView)) {\n static styles = [\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n ox-grist {\n overflow-y: auto;\n flex: 1;\n }\n\n [failed] {\n padding: var(--spacing-medium);\n color: var(--md-sys-color-error);\n font: var(--label-font, inherit);\n }\n `\n ]\n\n @property({ type: Boolean }) active: boolean = false\n @property({ type: Object }) config: any\n @property({ type: String }) mode: 'CARD' | 'LIST' | 'GRID' = isMobileDevice() ? 'CARD' : 'GRID'\n\n /** The last fetch did not answer. Distinct from answering with nothing. */\n @property({ type: Boolean }) failed: boolean = false\n\n @query('ox-grist') private grist!: DataGrist\n\n get context() {\n return {\n title: i18next.t('text.applications'),\n help: 'auth/application',\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n actions: [\n {\n title: i18next.t('button.register'),\n action: () => this.openEditor(),\n icon: 'add'\n }\n ],\n toolbar: false\n }\n }\n\n render() {\n return html`\n <ox-grist\n .mode=${this.mode}\n .config=${this.config}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n >\n <div slot=\"headroom\" class=\"header\">\n <ox-context-page-toolbar class=\"actions\" .context=${this.context}></ox-context-page-toolbar>\n </div>\n </ox-grist>\n\n ${this.failed ? html`<div failed>${i18next.t('text.the list could not be loaded')}</div>` : ''}\n `\n }\n\n async pageInitialized() {\n this.config = {\n rows: { appendable: false, selectable: { multiple: false } },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n {\n type: 'gutter',\n gutterName: 'button',\n icon: 'chevron_right',\n title: i18next.t('button.detail'),\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: any) => {\n if (record?.id) {\n this.openEditor(record.id)\n }\n }\n }\n },\n {\n type: 'string',\n name: 'name',\n header: i18next.t('field.name'),\n record: { editable: false },\n sortable: true,\n width: 200\n },\n {\n type: 'string',\n name: 'description',\n header: i18next.t('field.description'),\n record: { editable: false },\n width: 260\n },\n {\n type: 'string',\n name: 'type',\n header: i18next.t('field.type'),\n record: { align: 'center', editable: false },\n sortable: true,\n width: 120\n },\n {\n type: 'string',\n name: 'statusLabel',\n header: i18next.t('field.status'),\n record: { align: 'center', editable: false },\n width: 120\n },\n {\n type: 'datetime',\n name: 'updatedAt',\n header: i18next.t('field.updated at'),\n sortable: true,\n width: 180\n }\n ]\n }\n }\n\n /*\n * Registering and editing both happen over the list, the way every other resource in this\n * package works. `application-register` and `application/{id}` were routes; nothing outside\n * auth-ui linked to either.\n */\n private openEditor(applicationId?: string) {\n const popup = openPopup(\n html`<application-editor\n .applicationId=${applicationId}\n @application-changed=${() => this.grist.fetch()}\n @close=${() => popup.close()}\n ></application-editor>`,\n {\n backdrop: true,\n size: 'medium',\n title: applicationId ? i18next.t('text.application') : i18next.t('text.register new application')\n }\n )\n }\n\n async pageUpdated(_changes: any, _lifecycle: any) {\n if (this.active) {\n await this.updateComplete\n this.grist.fetch()\n }\n }\n\n async fetchHandler({ page, limit, sortings = [], filters = [] }: FetchOption) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n applications(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items {\n id\n name\n description\n type\n status\n updatedAt\n }\n total\n }\n }\n `,\n variables: { filters, pagination: { page, limit }, sortings },\n context: gqlContext()\n })\n\n const { items, total, failed } = listOutcome(pageFromResponse(response, 'applications'))\n\n this.failed = failed\n\n /*\n * The status is turned into words here rather than in a cell renderer so the rule lives\n * in one tested place — a value this screen has no word for is printed as it arrived.\n */\n return {\n total,\n records: items.map((item: any) => ({\n ...item,\n statusLabel: applicationStatusLabel(item.status, (key: string) => i18next.t(key))\n }))\n }\n }\n}\n"]}
@@ -14,9 +14,16 @@ import { join } from 'path';
14
14
  *
15
15
  * ⚠ Comments are stripped first — the prose here and in those screens names every field this
16
16
  * searches for, explaining why they are absent.
17
+ *
18
+ * ⚠ **The scan covers `components/` as well as `pages/`, and that is the whole point.** The
19
+ * three screens named above moved into popups under `components/` when the register and detail
20
+ * routes were removed. A scan rooted at `pages/` went on passing afterwards — not because the
21
+ * rule held, but because everything it was written to watch had walked out of its range. The
22
+ * search has to follow the credentials, not the folder they used to live in.
17
23
  */
18
24
  const SECRETS = ['appSecret', 'refreshToken', 'clientSecret', 'privateKey', 'accessToken'];
19
- const PAGES = join(__dirname);
25
+ /** Everywhere an auth screen can live: the routed pages and the popups they open. */
26
+ const SCREENS = [join(__dirname), join(__dirname, '..', 'components')];
20
27
  function tsFilesUnder(dir) {
21
28
  return readdirSync(dir).flatMap(entry => {
22
29
  const path = join(dir, entry);
@@ -35,7 +42,7 @@ function sourceOf(path) {
35
42
  describe('auth screens', () => {
36
43
  it('never bind a credential into a plain input', () => {
37
44
  const offenders = [];
38
- for (const path of tsFilesUnder(PAGES)) {
45
+ for (const path of SCREENS.flatMap(tsFilesUnder)) {
39
46
  const source = sourceOf(path);
40
47
  for (const secret of SECRETS) {
41
48
  /*
@@ -45,7 +52,7 @@ describe('auth screens', () => {
45
52
  */
46
53
  const bound = new RegExp(`<input[^>]*\\.value=\\$\\{[^}]*\\b${secret}\\b(?!Url)[^}]*\\}`);
47
54
  if (bound.test(source)) {
48
- offenders.push(`${path.split('/pages/')[1]} — ${secret}`);
55
+ offenders.push(`${path.split('/client/')[1]} — ${secret}`);
49
56
  }
50
57
  }
51
58
  }
@@ -61,6 +68,17 @@ describe('auth screens', () => {
61
68
  const bound = new RegExp(`<input[^>]*\\.value=\\$\\{[^}]*\\bappSecret\\b(?!Url)[^}]*\\}`);
62
69
  expect(bound.test(sample)).toBe(true);
63
70
  });
71
+ it('looks at the popups, not only the routed pages', () => {
72
+ /*
73
+ * The case that would have caught the silent green. `appliance-editor`, `application-editor`
74
+ * and `app-binding-detail` are where the credentials are shown now; a scan that cannot see
75
+ * them proves nothing about them.
76
+ */
77
+ const scanned = SCREENS.flatMap(tsFilesUnder);
78
+ for (const screen of ['appliance-editor.ts', 'application-editor.ts', 'app-binding-detail.ts']) {
79
+ expect(scanned.some(path => path.endsWith(screen))).toBe(true);
80
+ }
81
+ });
64
82
  it('does not mistake an endpoint for a credential', () => {
65
83
  const sample = '<input id="x" type="text" .value=${app.accessTokenUrl} readonly />';
66
84
  const bound = new RegExp(`<input[^>]*\\.value=\\$\\{[^}]*\\baccessToken\\b(?!Url)[^}]*\\}`);
@@ -1 +1 @@
1
- {"version":3,"file":"secrets-are-not-printed.test.js","sourceRoot":"","sources":["../../client/pages/secrets-are-not-printed.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAA;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B;;;;;;;;;;;;;;GAcG;AAEH,MAAM,OAAO,GAAG,CAAC,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,CAAC,CAAA;AAE1F,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAA;AAE7B,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAE7B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YACjC,OAAO,YAAY,CAAC,IAAI,CAAC,CAAA;QAC3B,CAAC;QAED,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC3E,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC;SAC/B,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC;SAChC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;SAC/B,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAA;AACpC,CAAC;AAED,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,SAAS,GAAa,EAAE,CAAA;QAE9B,KAAK,MAAM,IAAI,IAAI,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;YAE7B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B;;;;mBAIG;gBACH,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,qCAAqC,MAAM,oBAAoB,CAAC,CAAA;gBAEzF,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;oBACvB,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,CAAC,CAAA;gBAC3D,CAAC;YACH,CAAC;QACH,CAAC;QAED,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC/B,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C;;;;WAIG;QACH,MAAM,MAAM,GAAG,+DAA+D,CAAA;QAC9E,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,+DAA+D,CAAC,CAAA;QAEzF,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,MAAM,GAAG,oEAAoE,CAAA;QACnF,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,iEAAiE,CAAC,CAAA;QAE3F,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACxC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { readdirSync, readFileSync, statSync } from 'fs'\nimport { join } from 'path'\n\n/**\n * No auth screen prints a credential into a plain input.\n *\n * Three of them did, from the moment the page loaded: the binding detail showed an access\n * token and a refresh token, the appliance detail showed an access token, the application\n * detail showed its client secret. `secret-field` replaced all four — masked, copyable\n * without revealing, revealed only when the reader asks.\n *\n * This is worth a test rather than a habit because the wrong version is the natural one to\n * write. A readonly input beside a copy button is what a credential looks like in every\n * other admin console anyone has seen, it reviews cleanly, and nothing in the build objects.\n *\n * ⚠ Comments are stripped first — the prose here and in those screens names every field this\n * searches for, explaining why they are absent.\n */\n\nconst SECRETS = ['appSecret', 'refreshToken', 'clientSecret', 'privateKey', 'accessToken']\n\nconst PAGES = join(__dirname)\n\nfunction tsFilesUnder(dir: string): string[] {\n return readdirSync(dir).flatMap(entry => {\n const path = join(dir, entry)\n\n if (statSync(path).isDirectory()) {\n return tsFilesUnder(path)\n }\n\n return entry.endsWith('.ts') && !entry.endsWith('.test.ts') ? [path] : []\n })\n}\n\nfunction sourceOf(path: string): string {\n return readFileSync(path, 'utf-8')\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/^[ \\t]*\\/\\/.*$/gm, '')\n .replace(/<!--[\\s\\S]*?-->/g, '')\n}\n\ndescribe('auth screens', () => {\n it('never bind a credential into a plain input', () => {\n const offenders: string[] = []\n\n for (const path of tsFilesUnder(PAGES)) {\n const source = sourceOf(path)\n\n for (const secret of SECRETS) {\n /*\n * An `<input>` whose value expression ends at the secret's name. The trailing\n * boundary matters: `accessTokenUrl` is an endpoint, not a credential, and an\n * earlier version of this search flagged it.\n */\n const bound = new RegExp(`<input[^>]*\\\\.value=\\\\$\\\\{[^}]*\\\\b${secret}\\\\b(?!Url)[^}]*\\\\}`)\n\n if (bound.test(source)) {\n offenders.push(`${path.split('/pages/')[1]} — ${secret}`)\n }\n }\n }\n\n expect(offenders).toEqual([])\n })\n\n it('still knows what it is looking for', () => {\n /*\n * A search that matches nothing passes whether the screens are clean or the pattern is\n * broken. This proves the pattern finds the shape it is meant to find, so a green run\n * above means the screens are clean rather than the regex is dead.\n */\n const sample = '<input id=\"x\" type=\"text\" .value=${app.appSecret} readonly />'\n const bound = new RegExp(`<input[^>]*\\\\.value=\\\\$\\\\{[^}]*\\\\bappSecret\\\\b(?!Url)[^}]*\\\\}`)\n\n expect(bound.test(sample)).toBe(true)\n })\n\n it('does not mistake an endpoint for a credential', () => {\n const sample = '<input id=\"x\" type=\"text\" .value=${app.accessTokenUrl} readonly />'\n const bound = new RegExp(`<input[^>]*\\\\.value=\\\\$\\\\{[^}]*\\\\baccessToken\\\\b(?!Url)[^}]*\\\\}`)\n\n expect(bound.test(sample)).toBe(false)\n })\n})\n"]}
1
+ {"version":3,"file":"secrets-are-not-printed.test.js","sourceRoot":"","sources":["../../client/pages/secrets-are-not-printed.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync, readFileSync, statSync } from 'fs'\nimport { join } from 'path'\n\n/**\n * No auth screen prints a credential into a plain input.\n *\n * Three of them did, from the moment the page loaded: the binding detail showed an access\n * token and a refresh token, the appliance detail showed an access token, the application\n * detail showed its client secret. `secret-field` replaced all four — masked, copyable\n * without revealing, revealed only when the reader asks.\n *\n * This is worth a test rather than a habit because the wrong version is the natural one to\n * write. A readonly input beside a copy button is what a credential looks like in every\n * other admin console anyone has seen, it reviews cleanly, and nothing in the build objects.\n *\n * ⚠ Comments are stripped first — the prose here and in those screens names every field this\n * searches for, explaining why they are absent.\n *\n * ⚠ **The scan covers `components/` as well as `pages/`, and that is the whole point.** The\n * three screens named above moved into popups under `components/` when the register and detail\n * routes were removed. A scan rooted at `pages/` went on passing afterwards — not because the\n * rule held, but because everything it was written to watch had walked out of its range. The\n * search has to follow the credentials, not the folder they used to live in.\n */\n\nconst SECRETS = ['appSecret', 'refreshToken', 'clientSecret', 'privateKey', 'accessToken']\n\n/** Everywhere an auth screen can live: the routed pages and the popups they open. */\nconst SCREENS = [join(__dirname), join(__dirname, '..', 'components')]\n\nfunction tsFilesUnder(dir: string): string[] {\n return readdirSync(dir).flatMap(entry => {\n const path = join(dir, entry)\n\n if (statSync(path).isDirectory()) {\n return tsFilesUnder(path)\n }\n\n return entry.endsWith('.ts') && !entry.endsWith('.test.ts') ? [path] : []\n })\n}\n\nfunction sourceOf(path: string): string {\n return readFileSync(path, 'utf-8')\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/^[ \\t]*\\/\\/.*$/gm, '')\n .replace(/<!--[\\s\\S]*?-->/g, '')\n}\n\ndescribe('auth screens', () => {\n it('never bind a credential into a plain input', () => {\n const offenders: string[] = []\n\n for (const path of SCREENS.flatMap(tsFilesUnder)) {\n const source = sourceOf(path)\n\n for (const secret of SECRETS) {\n /*\n * An `<input>` whose value expression ends at the secret's name. The trailing\n * boundary matters: `accessTokenUrl` is an endpoint, not a credential, and an\n * earlier version of this search flagged it.\n */\n const bound = new RegExp(`<input[^>]*\\\\.value=\\\\$\\\\{[^}]*\\\\b${secret}\\\\b(?!Url)[^}]*\\\\}`)\n\n if (bound.test(source)) {\n offenders.push(`${path.split('/client/')[1]} — ${secret}`)\n }\n }\n }\n\n expect(offenders).toEqual([])\n })\n\n it('still knows what it is looking for', () => {\n /*\n * A search that matches nothing passes whether the screens are clean or the pattern is\n * broken. This proves the pattern finds the shape it is meant to find, so a green run\n * above means the screens are clean rather than the regex is dead.\n */\n const sample = '<input id=\"x\" type=\"text\" .value=${app.appSecret} readonly />'\n const bound = new RegExp(`<input[^>]*\\\\.value=\\\\$\\\\{[^}]*\\\\bappSecret\\\\b(?!Url)[^}]*\\\\}`)\n\n expect(bound.test(sample)).toBe(true)\n })\n\n it('looks at the popups, not only the routed pages', () => {\n /*\n * The case that would have caught the silent green. `appliance-editor`, `application-editor`\n * and `app-binding-detail` are where the credentials are shown now; a scan that cannot see\n * them proves nothing about them.\n */\n const scanned = SCREENS.flatMap(tsFilesUnder)\n\n for (const screen of ['appliance-editor.ts', 'application-editor.ts', 'app-binding-detail.ts']) {\n expect(scanned.some(path => path.endsWith(screen))).toBe(true)\n }\n })\n\n it('does not mistake an endpoint for a credential', () => {\n const sample = '<input id=\"x\" type=\"text\" .value=${app.accessTokenUrl} readonly />'\n const bound = new RegExp(`<input[^>]*\\\\.value=\\\\$\\\\{[^}]*\\\\baccessToken\\\\b(?!Url)[^}]*\\\\}`)\n\n expect(bound.test(sample)).toBe(false)\n })\n})\n"]}
@@ -6,30 +6,15 @@ export default function route(page) {
6
6
  /* webpackChunkName: "profile" */
7
7
  './pages/profile.js');
8
8
  return page;
9
- case 'appliance-register':
10
- import('./pages/appliance/register');
11
- return page;
12
- case 'application':
13
- import('./pages/application/application');
14
- return page;
15
9
  case 'applications':
16
10
  import('./pages/application/applications');
17
11
  return page;
18
- case 'application-register':
19
- import('./pages/application/register');
20
- return page;
21
12
  case 'app-bindings':
22
13
  import('./pages/app-binding/app-bindings');
23
14
  return page;
24
- case 'app-binding':
25
- import('./pages/app-binding/app-binding');
26
- return page;
27
15
  case 'appliance-home':
28
16
  import('./pages/appliance/home');
29
17
  return page;
30
- case 'appliance':
31
- import('./pages/appliance/appliance');
32
- return page;
33
18
  case 'roles':
34
19
  import('./pages/role/role-list-page');
35
20
  return page;
@@ -1 +1 @@
1
- {"version":3,"file":"route.js","sourceRoot":"","sources":["../client/route.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,IAAI;IAChC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,SAAS;YACZ,MAAM;YACJ,2BAA2B;YAC3B,iCAAiC;YACjC,oBAAoB,CACrB,CAAA;YACD,OAAO,IAAI,CAAA;QAEb,KAAK,oBAAoB;YACvB,MAAM,CAAC,4BAA4B,CAAC,CAAA;YACpC,OAAO,IAAI,CAAA;QAEb,KAAK,aAAa;YAChB,MAAM,CAAC,iCAAiC,CAAC,CAAA;YACzC,OAAO,IAAI,CAAA;QAEb,KAAK,cAAc;YACjB,MAAM,CAAC,kCAAkC,CAAC,CAAA;YAC1C,OAAO,IAAI,CAAA;QAEb,KAAK,sBAAsB;YACzB,MAAM,CAAC,8BAA8B,CAAC,CAAA;YACtC,OAAO,IAAI,CAAA;QAEb,KAAK,cAAc;YACjB,MAAM,CAAC,kCAAkC,CAAC,CAAA;YAC1C,OAAO,IAAI,CAAA;QAEb,KAAK,aAAa;YAChB,MAAM,CAAC,iCAAiC,CAAC,CAAA;YACzC,OAAO,IAAI,CAAA;QAEb,KAAK,gBAAgB;YACnB,MAAM,CAAC,wBAAwB,CAAC,CAAA;YAChC,OAAO,IAAI,CAAA;QAEb,KAAK,WAAW;YACd,MAAM,CAAC,6BAA6B,CAAC,CAAA;YACrC,OAAO,IAAI,CAAA;QAEb,KAAK,OAAO;YACV,MAAM,CAAC,6BAA6B,CAAC,CAAA;YACrC,OAAO,IAAI,CAAA;QAEb,KAAK,iBAAiB;YACpB,MAAM,CAAC,kCAAkC,CAAC,CAAA;YAC1C,OAAO,IAAI,CAAA;QAEb,KAAK,OAAO;YACV,MAAM,CAAC,6BAA6B,CAAC,CAAA;YACrC,OAAO,IAAI,CAAA;QAEb,KAAK,UAAU;YACb,MAAM,CAAC,oCAAoC,CAAC,CAAA;YAC5C,OAAO,IAAI,CAAA;QAEb,KAAK,SAAS;YACZ,MAAM,CAAC,kCAAkC,CAAC,CAAA;YAC1C,OAAO,IAAI,CAAA;QAEb,KAAK,eAAe;YAClB,MAAM,CAAC,8CAA8C,CAAC,CAAA;YACtD,OAAO,IAAI,CAAA;QAEb,KAAK,UAAU;YACb,MAAM,CAAC,mCAAmC,CAAC,CAAA;YAC3C,OAAO,IAAI,CAAA;QAEb,KAAK,YAAY;YACf,MAAM,CAAC,+CAA+C,CAAC,CAAA;YACvD,OAAO,IAAI,CAAA;QAEb,KAAK,gBAAgB;YACnB,MAAM,CAAC,mDAAmD,CAAC,CAAA;YAC3D,OAAO,IAAI,CAAA;QAEb,KAAK,cAAc;YACjB,MAAM,CAAC,+CAA+C,CAAC,CAAA;YACvD,OAAO,IAAI,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["export default function route(page) {\n switch (page) {\n case 'profile':\n import(\n /* webpackPrefetch: true */\n /* webpackChunkName: \"profile\" */\n './pages/profile.js'\n )\n return page\n\n case 'appliance-register':\n import('./pages/appliance/register')\n return page\n\n case 'application':\n import('./pages/application/application')\n return page\n\n case 'applications':\n import('./pages/application/applications')\n return page\n\n case 'application-register':\n import('./pages/application/register')\n return page\n\n case 'app-bindings':\n import('./pages/app-binding/app-bindings')\n return page\n\n case 'app-binding':\n import('./pages/app-binding/app-binding')\n return page\n\n case 'appliance-home':\n import('./pages/appliance/home')\n return page\n\n case 'appliance':\n import('./pages/appliance/appliance')\n return page\n\n case 'roles':\n import('./pages/role/role-list-page')\n return page\n\n case 'role-privileges':\n import('./pages/role/role-privilege-page')\n return page\n\n case 'users':\n import('./pages/user/user-list-page')\n return page\n\n case 'partners':\n import('./pages/partner/partner-management')\n return page\n\n case 'domains':\n import('./pages/domain/domain-management')\n return page\n\n case 'domain-owners':\n import('./pages/domain-owner/domain-owner-management')\n return page\n\n case 'env-vars':\n import('./pages/env-var/env-var-list-page')\n return page\n\n case 'attributes':\n import('./pages/attribute/attribute-set-management.js')\n return page\n\n case 'auth-providers':\n import('./pages/auth-provider/auth-provider-management.js')\n return page\n\n case 'domain-links':\n import('./pages/domain-link/domain-link-management.js')\n return page\n }\n}\n"]}
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../client/route.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,IAAI;IAChC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,SAAS;YACZ,MAAM;YACJ,2BAA2B;YAC3B,iCAAiC;YACjC,oBAAoB,CACrB,CAAA;YACD,OAAO,IAAI,CAAA;QAEb,KAAK,cAAc;YACjB,MAAM,CAAC,kCAAkC,CAAC,CAAA;YAC1C,OAAO,IAAI,CAAA;QAEb,KAAK,cAAc;YACjB,MAAM,CAAC,kCAAkC,CAAC,CAAA;YAC1C,OAAO,IAAI,CAAA;QAEb,KAAK,gBAAgB;YACnB,MAAM,CAAC,wBAAwB,CAAC,CAAA;YAChC,OAAO,IAAI,CAAA;QAEb,KAAK,OAAO;YACV,MAAM,CAAC,6BAA6B,CAAC,CAAA;YACrC,OAAO,IAAI,CAAA;QAEb,KAAK,iBAAiB;YACpB,MAAM,CAAC,kCAAkC,CAAC,CAAA;YAC1C,OAAO,IAAI,CAAA;QAEb,KAAK,OAAO;YACV,MAAM,CAAC,6BAA6B,CAAC,CAAA;YACrC,OAAO,IAAI,CAAA;QAEb,KAAK,UAAU;YACb,MAAM,CAAC,oCAAoC,CAAC,CAAA;YAC5C,OAAO,IAAI,CAAA;QAEb,KAAK,SAAS;YACZ,MAAM,CAAC,kCAAkC,CAAC,CAAA;YAC1C,OAAO,IAAI,CAAA;QAEb,KAAK,eAAe;YAClB,MAAM,CAAC,8CAA8C,CAAC,CAAA;YACtD,OAAO,IAAI,CAAA;QAEb,KAAK,UAAU;YACb,MAAM,CAAC,mCAAmC,CAAC,CAAA;YAC3C,OAAO,IAAI,CAAA;QAEb,KAAK,YAAY;YACf,MAAM,CAAC,+CAA+C,CAAC,CAAA;YACvD,OAAO,IAAI,CAAA;QAEb,KAAK,gBAAgB;YACnB,MAAM,CAAC,mDAAmD,CAAC,CAAA;YAC3D,OAAO,IAAI,CAAA;QAEb,KAAK,cAAc;YACjB,MAAM,CAAC,+CAA+C,CAAC,CAAA;YACvD,OAAO,IAAI,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["export default function route(page) {\n switch (page) {\n case 'profile':\n import(\n /* webpackPrefetch: true */\n /* webpackChunkName: \"profile\" */\n './pages/profile.js'\n )\n return page\n\n case 'applications':\n import('./pages/application/applications')\n return page\n\n case 'app-bindings':\n import('./pages/app-binding/app-bindings')\n return page\n\n case 'appliance-home':\n import('./pages/appliance/home')\n return page\n\n case 'roles':\n import('./pages/role/role-list-page')\n return page\n\n case 'role-privileges':\n import('./pages/role/role-privilege-page')\n return page\n\n case 'users':\n import('./pages/user/user-list-page')\n return page\n\n case 'partners':\n import('./pages/partner/partner-management')\n return page\n\n case 'domains':\n import('./pages/domain/domain-management')\n return page\n\n case 'domain-owners':\n import('./pages/domain-owner/domain-owner-management')\n return page\n\n case 'env-vars':\n import('./pages/env-var/env-var-list-page')\n return page\n\n case 'attributes':\n import('./pages/attribute/attribute-set-management.js')\n return page\n\n case 'auth-providers':\n import('./pages/auth-provider/auth-provider-management.js')\n return page\n\n case 'domain-links':\n import('./pages/domain-link/domain-link-management.js')\n return page\n }\n}\n"]}