@lenne.tech/ng-base 18.1.2 → 22.0.0
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.
- package/CLAUDE.md +55 -0
- package/LICENSE +2 -2
- package/README.md +237 -44
- package/fesm2022/lenne.tech-ng-base-ai.mjs +767 -0
- package/fesm2022/lenne.tech-ng-base-ai.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-auth-provide.mjs +939 -0
- package/fesm2022/lenne.tech-ng-base-auth-provide.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-auth.mjs +454 -0
- package/fesm2022/lenne.tech-ng-base-auth.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-bug-bug-report-dialog-BRzVmKJr.mjs +825 -0
- package/fesm2022/lenne.tech-ng-base-bug-bug-report-dialog-BRzVmKJr.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-bug-bug-report-service-DWoTb_iw.mjs +351 -0
- package/fesm2022/lenne.tech-ng-base-bug-bug-report-service-DWoTb_iw.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-bug-capture-engine-TkjMDdqj.mjs +1046 -0
- package/fesm2022/lenne.tech-ng-base-bug-capture-engine-TkjMDdqj.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-bug-constants-Brm2KGHi.mjs +4 -0
- package/fesm2022/lenne.tech-ng-base-bug-constants-Brm2KGHi.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-bug-messages-BsP4fRQU.mjs +179 -0
- package/fesm2022/lenne.tech-ng-base-bug-messages-BsP4fRQU.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-bug-provide.mjs +26 -0
- package/fesm2022/lenne.tech-ng-base-bug-provide.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-bug-report-Tzlhe1Cm.mjs +62 -0
- package/fesm2022/lenne.tech-ng-base-bug-report-Tzlhe1Cm.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-bug-server.mjs +818 -0
- package/fesm2022/lenne.tech-ng-base-bug-server.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-bug.mjs +196 -0
- package/fesm2022/lenne.tech-ng-base-bug.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-files-ui.mjs +445 -0
- package/fesm2022/lenne.tech-ng-base-files-ui.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-files.mjs +474 -0
- package/fesm2022/lenne.tech-ng-base-files.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-server.mjs +408 -0
- package/fesm2022/lenne.tech-ng-base-server.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-share.mjs +76 -0
- package/fesm2022/lenne.tech-ng-base-share.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-sync.mjs +1096 -0
- package/fesm2022/lenne.tech-ng-base-sync.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-testing.mjs +171 -0
- package/fesm2022/lenne.tech-ng-base-testing.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-ui-provide.mjs +124 -0
- package/fesm2022/lenne.tech-ng-base-ui-provide.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-ui.mjs +34611 -0
- package/fesm2022/lenne.tech-ng-base-ui.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base-utils.mjs +287 -0
- package/fesm2022/lenne.tech-ng-base-utils.mjs.map +1 -0
- package/fesm2022/lenne.tech-ng-base.mjs +615 -902
- package/fesm2022/lenne.tech-ng-base.mjs.map +1 -1
- package/i18n/auth/de.json +17 -0
- package/i18n/auth/en.json +17 -0
- package/i18n/bug/de.json +85 -0
- package/i18n/bug/en.json +85 -0
- package/i18n/de.json +8 -0
- package/i18n/en.json +8 -0
- package/i18n/files/de.json +27 -0
- package/i18n/files/en.json +27 -0
- package/i18n/share/de.json +8 -0
- package/i18n/share/en.json +8 -0
- package/i18n/ui/de.json +38 -0
- package/i18n/ui/en.json +38 -0
- package/package.json +127 -58
- package/types/lenne.tech-ng-base-ai.d.ts +830 -0
- package/types/lenne.tech-ng-base-auth-provide.d.ts +603 -0
- package/types/lenne.tech-ng-base-auth.d.ts +320 -0
- package/types/lenne.tech-ng-base-bug-provide.d.ts +272 -0
- package/types/lenne.tech-ng-base-bug-rolldown-runtime-C25Le4-w.d.ts +1 -0
- package/types/lenne.tech-ng-base-bug-server.d.ts +316 -0
- package/types/lenne.tech-ng-base-bug.d.ts +202 -0
- package/types/lenne.tech-ng-base-files-ui.d.ts +116 -0
- package/types/lenne.tech-ng-base-files.d.ts +288 -0
- package/types/lenne.tech-ng-base-server.d.ts +262 -0
- package/types/lenne.tech-ng-base-share.d.ts +21 -0
- package/types/lenne.tech-ng-base-sync.d.ts +380 -0
- package/types/lenne.tech-ng-base-testing.d.ts +327 -0
- package/types/lenne.tech-ng-base-ui-provide.d.ts +65 -0
- package/types/lenne.tech-ng-base-ui.d.ts +7732 -0
- package/types/lenne.tech-ng-base-utils.d.ts +150 -0
- package/types/lenne.tech-ng-base.d.ts +503 -0
- package/ui/styles/components/accordion.css +3 -0
- package/ui/styles/components/alert-dialog.css +4 -0
- package/ui/styles/components/alert.css +3 -0
- package/ui/styles/components/aspect-ratio.css +3 -0
- package/ui/styles/components/attachment.css +4 -0
- package/ui/styles/components/autocomplete.css +4 -0
- package/ui/styles/components/avatar.css +3 -0
- package/ui/styles/components/badge.css +3 -0
- package/ui/styles/components/breadcrumb.css +3 -0
- package/ui/styles/components/bubble.css +3 -0
- package/ui/styles/components/button-group.css +3 -0
- package/ui/styles/components/button.css +3 -0
- package/ui/styles/components/calendar.css +5 -0
- package/ui/styles/components/card.css +3 -0
- package/ui/styles/components/checkbox.css +3 -0
- package/ui/styles/components/code-block.css +4 -0
- package/ui/styles/components/collapsible.css +3 -0
- package/ui/styles/components/combobox.css +5 -0
- package/ui/styles/components/command.css +5 -0
- package/ui/styles/components/context-menu.css +3 -0
- package/ui/styles/components/date-picker.css +7 -0
- package/ui/styles/components/dialog.css +4 -0
- package/ui/styles/components/drawer.css +3 -0
- package/ui/styles/components/dropdown-menu.css +3 -0
- package/ui/styles/components/empty.css +3 -0
- package/ui/styles/components/field.css +5 -0
- package/ui/styles/components/hover-card.css +3 -0
- package/ui/styles/components/index.css +91 -0
- package/ui/styles/components/input-group.css +6 -0
- package/ui/styles/components/input-otp.css +3 -0
- package/ui/styles/components/input.css +3 -0
- package/ui/styles/components/item.css +4 -0
- package/ui/styles/components/kbd.css +3 -0
- package/ui/styles/components/label.css +3 -0
- package/ui/styles/components/lt-breadcrumb.css +3 -0
- package/ui/styles/components/lt-bug-attachments.css +5 -0
- package/ui/styles/components/lt-bug-journey-timeline.css +5 -0
- package/ui/styles/components/lt-bug-report-dialog.css +15 -0
- package/ui/styles/components/lt-bug-report.css +4 -0
- package/ui/styles/components/lt-color-mode-button.css +3 -0
- package/ui/styles/components/lt-confirm-dialog.css +4 -0
- package/ui/styles/components/lt-data-view.css +5 -0
- package/ui/styles/components/lt-date-picker.css +3 -0
- package/ui/styles/components/lt-date-time-picker.css +5 -0
- package/ui/styles/components/lt-enter-action.css +2 -0
- package/ui/styles/components/lt-footer.css +3 -0
- package/ui/styles/components/lt-form-field.css +4 -0
- package/ui/styles/components/lt-header.css +5 -0
- package/ui/styles/components/lt-imports.css +21 -0
- package/ui/styles/components/lt-navigation-menu.css +3 -0
- package/ui/styles/components/lt-notice.css +3 -0
- package/ui/styles/components/lt-overlay-defaults.css +3 -0
- package/ui/styles/components/lt-page-card.css +3 -0
- package/ui/styles/components/lt-page-title.css +2 -0
- package/ui/styles/components/lt-password-input.css +4 -0
- package/ui/styles/components/lt-readonly-field.css +2 -0
- package/ui/styles/components/lt-select.css +4 -0
- package/ui/styles/components/lt-text-field.css +5 -0
- package/ui/styles/components/lt-toolbar-item.css +2 -0
- package/ui/styles/components/lt-toolbar.css +3 -0
- package/ui/styles/components/lt-tus-upload-component.css +5 -0
- package/ui/styles/components/marker.css +3 -0
- package/ui/styles/components/menubar.css +3 -0
- package/ui/styles/components/message-scroller.css +4 -0
- package/ui/styles/components/message.css +3 -0
- package/ui/styles/components/native-select.css +3 -0
- package/ui/styles/components/navigation-menu.css +3 -0
- package/ui/styles/components/pagination.css +4 -0
- package/ui/styles/components/popover.css +3 -0
- package/ui/styles/components/progress.css +3 -0
- package/ui/styles/components/questionnaire.css +4 -0
- package/ui/styles/components/radio-group.css +3 -0
- package/ui/styles/components/resizable.css +3 -0
- package/ui/styles/components/select.css +3 -0
- package/ui/styles/components/separator.css +3 -0
- package/ui/styles/components/sheet.css +4 -0
- package/ui/styles/components/sidebar.css +9 -0
- package/ui/styles/components/skeleton.css +3 -0
- package/ui/styles/components/slider.css +3 -0
- package/ui/styles/components/sonner.css +4 -0
- package/ui/styles/components/spinner.css +3 -0
- package/ui/styles/components/switch.css +3 -0
- package/ui/styles/components/table.css +3 -0
- package/ui/styles/components/tabs.css +4 -0
- package/ui/styles/components/textarea.css +3 -0
- package/ui/styles/components/toggle-group.css +4 -0
- package/ui/styles/components/toggle.css +3 -0
- package/ui/styles/components/tooltip.css +3 -0
- package/ui/styles/components/trace.css +3 -0
- package/ui/styles/components/typography.css +3 -0
- package/ui/styles/components/utils.css +2 -0
- package/ui/styles/core.css +137 -0
- package/ui/styles/palette.css +94 -0
- package/ui/styles/theme.css +14 -0
- package/ui/styles/transitions.css +131 -0
- package/base-cms/index.d.ts +0 -5
- package/base-cms/interfaces/base-cms-module-config.interface.d.ts +0 -16
- package/base-cms/pages/base-cms/base-cms.component.d.ts +0 -92
- package/base-cms/public-api.d.ts +0 -5
- package/base-components/components/breadcrumb/breadcrumb.component.d.ts +0 -10
- package/base-components/components/change-password/change-password.component.d.ts +0 -24
- package/base-components/components/checkbox/checkbox.component.d.ts +0 -12
- package/base-components/components/delete-account/delete-account.component.d.ts +0 -15
- package/base-components/components/fab-button/fab-button.component.d.ts +0 -16
- package/base-components/components/input/input.component.d.ts +0 -27
- package/base-components/components/loading-bar/loading-bar.component.d.ts +0 -17
- package/base-components/components/model-form/model-form-base.component.d.ts +0 -64
- package/base-components/components/model-form/model-form.component.d.ts +0 -139
- package/base-components/components/model-form-sub/model-form-sub.component.d.ts +0 -56
- package/base-components/components/model-table/model-table.component.d.ts +0 -115
- package/base-components/components/refence-input/refence-input.component.d.ts +0 -84
- package/base-components/components/select/select.component.d.ts +0 -25
- package/base-components/components/skeleton-loader/skeleton-loader.component.d.ts +0 -10
- package/base-components/components/tags/tags.component.d.ts +0 -60
- package/base-components/components/textarea/textarea.component.d.ts +0 -21
- package/base-components/components/toast/toast.component.d.ts +0 -19
- package/base-components/components/upload-image/upload-image.component.d.ts +0 -111
- package/base-components/directives/back-button.directive.d.ts +0 -9
- package/base-components/directives/context-menu.directive.d.ts +0 -22
- package/base-components/directives/sort.directive.d.ts +0 -15
- package/base-components/directives/stop-propagation.directive.d.ts +0 -10
- package/base-components/index.d.ts +0 -5
- package/base-components/pipes/ellipses.pipe.d.ts +0 -10
- package/base-components/public-api.d.ts +0 -26
- package/base-prototype/index.d.ts +0 -5
- package/base-prototype/pages/email-verification/email-verification.component.d.ts +0 -18
- package/base-prototype/pages/forgot-password/forgot-password.component.d.ts +0 -26
- package/base-prototype/pages/login/login.component.d.ts +0 -33
- package/base-prototype/pages/profile/profile.component.d.ts +0 -24
- package/base-prototype/pages/register/register.component.d.ts +0 -31
- package/base-prototype/pages/set-password/set-password.component.d.ts +0 -29
- package/base-prototype/public-api.d.ts +0 -9
- package/esm2022/base-cms/interfaces/base-cms-module-config.interface.mjs +0 -3
- package/esm2022/base-cms/lenne.tech-ng-base-base-cms.mjs +0 -5
- package/esm2022/base-cms/pages/base-cms/base-cms.component.mjs +0 -238
- package/esm2022/base-cms/public-api.mjs +0 -8
- package/esm2022/base-components/components/breadcrumb/breadcrumb.component.mjs +0 -23
- package/esm2022/base-components/components/change-password/change-password.component.mjs +0 -97
- package/esm2022/base-components/components/checkbox/checkbox.component.mjs +0 -38
- package/esm2022/base-components/components/delete-account/delete-account.component.mjs +0 -59
- package/esm2022/base-components/components/fab-button/fab-button.component.mjs +0 -24
- package/esm2022/base-components/components/input/input.component.mjs +0 -75
- package/esm2022/base-components/components/loading-bar/loading-bar.component.mjs +0 -37
- package/esm2022/base-components/components/model-form/model-form-base.component.mjs +0 -145
- package/esm2022/base-components/components/model-form/model-form.component.mjs +0 -591
- package/esm2022/base-components/components/model-form-sub/model-form-sub.component.mjs +0 -117
- package/esm2022/base-components/components/model-table/model-table.component.mjs +0 -478
- package/esm2022/base-components/components/refence-input/refence-input.component.mjs +0 -278
- package/esm2022/base-components/components/select/select.component.mjs +0 -62
- package/esm2022/base-components/components/skeleton-loader/skeleton-loader.component.mjs +0 -33
- package/esm2022/base-components/components/tags/tags.component.mjs +0 -227
- package/esm2022/base-components/components/textarea/textarea.component.mjs +0 -79
- package/esm2022/base-components/components/toast/toast.component.mjs +0 -69
- package/esm2022/base-components/components/upload-image/upload-image.component.mjs +0 -346
- package/esm2022/base-components/directives/back-button.directive.mjs +0 -25
- package/esm2022/base-components/directives/context-menu.directive.mjs +0 -112
- package/esm2022/base-components/directives/sort.directive.mjs +0 -61
- package/esm2022/base-components/directives/stop-propagation.directive.mjs +0 -24
- package/esm2022/base-components/lenne.tech-ng-base-base-components.mjs +0 -5
- package/esm2022/base-components/pipes/ellipses.pipe.mjs +0 -23
- package/esm2022/base-components/public-api.mjs +0 -30
- package/esm2022/base-prototype/lenne.tech-ng-base-base-prototype.mjs +0 -5
- package/esm2022/base-prototype/pages/email-verification/email-verification.component.mjs +0 -60
- package/esm2022/base-prototype/pages/forgot-password/forgot-password.component.mjs +0 -78
- package/esm2022/base-prototype/pages/login/login.component.mjs +0 -114
- package/esm2022/base-prototype/pages/profile/profile.component.mjs +0 -87
- package/esm2022/base-prototype/pages/register/register.component.mjs +0 -100
- package/esm2022/base-prototype/pages/set-password/set-password.component.mjs +0 -91
- package/esm2022/base-prototype/public-api.mjs +0 -11
- package/esm2022/lenne.tech-ng-base.mjs +0 -5
- package/esm2022/public-api.mjs +0 -25
- package/esm2022/shared/classes/auth.class.mjs +0 -14
- package/esm2022/shared/classes/basic-user.class.mjs +0 -49
- package/esm2022/shared/classes/graphql-enum.class.mjs +0 -10
- package/esm2022/shared/classes/graphql-meta.class.mjs +0 -345
- package/esm2022/shared/classes/graphql-type.class.mjs +0 -14
- package/esm2022/shared/classes/helper.class.mjs +0 -307
- package/esm2022/shared/classes/sort.class.mjs +0 -38
- package/esm2022/shared/classes/standard.class.mjs +0 -40
- package/esm2022/shared/decorators/debounce.decorator.mjs +0 -12
- package/esm2022/shared/enums/comparison-operator.enum.mjs +0 -16
- package/esm2022/shared/enums/graphql-request-type.enum.mjs +0 -10
- package/esm2022/shared/enums/logical-operator.enum.mjs +0 -10
- package/esm2022/shared/enums/skeleton-type.enum.mjs +0 -7
- package/esm2022/shared/enums/sort-order.enum.mjs +0 -6
- package/esm2022/shared/enums/toast-type.enum.mjs +0 -8
- package/esm2022/shared/functions/ws-client.function.mjs +0 -39
- package/esm2022/shared/interfaces/base-module-config.interface.mjs +0 -3
- package/esm2022/shared/interfaces/breadcrumb.interface.mjs +0 -2
- package/esm2022/shared/interfaces/can-component-deactivate.interface.mjs +0 -2
- package/esm2022/shared/interfaces/cms-enum-values.interface.mjs +0 -2
- package/esm2022/shared/interfaces/cms-field-config.interface.mjs +0 -2
- package/esm2022/shared/interfaces/cms-model-config.interface.mjs +0 -2
- package/esm2022/shared/interfaces/cms-table-field.interface.mjs +0 -2
- package/esm2022/shared/interfaces/combined-filter-input.interface.mjs +0 -2
- package/esm2022/shared/interfaces/compress-options.interface.mjs +0 -2
- package/esm2022/shared/interfaces/cropping-options.interface.mjs +0 -2
- package/esm2022/shared/interfaces/field-object.interface.mjs +0 -2
- package/esm2022/shared/interfaces/field.interface.mjs +0 -2
- package/esm2022/shared/interfaces/filter-input.interface.mjs +0 -2
- package/esm2022/shared/interfaces/find-args.interface.mjs +0 -2
- package/esm2022/shared/interfaces/graphql-crud-type.interface.mjs +0 -2
- package/esm2022/shared/interfaces/graphql-fields-object.interface.mjs +0 -2
- package/esm2022/shared/interfaces/graphql-options.interface.mjs +0 -2
- package/esm2022/shared/interfaces/graphql-plus-options.interfacen.mjs +0 -2
- package/esm2022/shared/interfaces/graphql-type-collection.interface.mjs +0 -2
- package/esm2022/shared/interfaces/login-config.interface.mjs +0 -2
- package/esm2022/shared/interfaces/register-config.interface.mjs +0 -2
- package/esm2022/shared/interfaces/single-filter-input.interface.mjs +0 -2
- package/esm2022/shared/interfaces/sort-input.interface.mjs +0 -2
- package/esm2022/shared/interfaces/time-components.interface.mjs +0 -2
- package/esm2022/shared/interfaces/toast.interface.mjs +0 -2
- package/esm2022/shared/lenne.tech-ng-base-shared.mjs +0 -5
- package/esm2022/shared/public-api.mjs +0 -74
- package/esm2022/shared/services/auth.service.mjs +0 -152
- package/esm2022/shared/services/breadcrumb.service.mjs +0 -69
- package/esm2022/shared/services/cms.service.mjs +0 -82
- package/esm2022/shared/services/file.service.mjs +0 -86
- package/esm2022/shared/services/forms.service.mjs +0 -61
- package/esm2022/shared/services/graphql-meta.service.mjs +0 -132
- package/esm2022/shared/services/graphql-plus.service.mjs +0 -93
- package/esm2022/shared/services/graphql.service.mjs +0 -510
- package/esm2022/shared/services/image.service.mjs +0 -102
- package/esm2022/shared/services/loader.service.mjs +0 -78
- package/esm2022/shared/services/navigation.service.mjs +0 -38
- package/esm2022/shared/services/scroll.service.mjs +0 -207
- package/esm2022/shared/services/storage.service.mjs +0 -149
- package/esm2022/shared/services/toast.service.mjs +0 -39
- package/esm2022/shared/services/user.service.mjs +0 -160
- package/esm2022/shared/services/ws.service.mjs +0 -17
- package/esm2022/shared/types/graphql-fields.type.mjs +0 -2
- package/esm2022/shared/types/storage.type.mjs +0 -2
- package/esm2022/shared/validators/email.validator.mjs +0 -7
- package/esm2022/shared/validators/match.validator.mjs +0 -22
- package/esm2022/shared/validators/secure-password.validator.mjs +0 -7
- package/esm2022/src/base.module.mjs +0 -64
- package/esm2022/src/decorators/auto-unsubscribe.decorator.mjs +0 -23
- package/esm2022/src/directives/lazy-load.directive.mjs +0 -48
- package/esm2022/src/directives/match-height.directive.mjs +0 -92
- package/esm2022/src/directives/resizable.directive.mjs +0 -92
- package/esm2022/src/factories/apollo-options.factory.mjs +0 -108
- package/esm2022/src/functions/extract-files.function.mjs +0 -87
- package/esm2022/src/guards/auth.guard.mjs +0 -91
- package/esm2022/src/guards/can-deactivate.guard.mjs +0 -16
- package/esm2022/src/guards/logout.guard.mjs +0 -34
- package/esm2022/src/pipes/date-ago.pipe.mjs +0 -64
- package/esm2022/src/pipes/safe-html.pipe.mjs +0 -22
- package/esm2022/src/services/fullscreen.service.mjs +0 -51
- package/esm2022/src/services/seo.service.mjs +0 -109
- package/esm2022/src/services/theme.service.mjs +0 -44
- package/fesm2022/lenne.tech-ng-base-base-cms.mjs +0 -251
- package/fesm2022/lenne.tech-ng-base-base-cms.mjs.map +0 -1
- package/fesm2022/lenne.tech-ng-base-base-components.mjs +0 -2917
- package/fesm2022/lenne.tech-ng-base-base-components.mjs.map +0 -1
- package/fesm2022/lenne.tech-ng-base-base-prototype.mjs +0 -505
- package/fesm2022/lenne.tech-ng-base-base-prototype.mjs.map +0 -1
- package/fesm2022/lenne.tech-ng-base-shared.mjs +0 -2863
- package/fesm2022/lenne.tech-ng-base-shared.mjs.map +0 -1
- package/index.d.ts +0 -5
- package/public-api.d.ts +0 -17
- package/shared/classes/auth.class.d.ts +0 -10
- package/shared/classes/basic-user.class.d.ts +0 -27
- package/shared/classes/graphql-enum.class.d.ts +0 -7
- package/shared/classes/graphql-meta.class.d.ts +0 -66
- package/shared/classes/graphql-type.class.d.ts +0 -13
- package/shared/classes/helper.class.d.ts +0 -99
- package/shared/classes/sort.class.d.ts +0 -12
- package/shared/classes/standard.class.d.ts +0 -32
- package/shared/decorators/debounce.decorator.d.ts +0 -1
- package/shared/enums/comparison-operator.enum.d.ts +0 -14
- package/shared/enums/graphql-request-type.enum.d.ts +0 -8
- package/shared/enums/logical-operator.enum.d.ts +0 -8
- package/shared/enums/skeleton-type.enum.d.ts +0 -5
- package/shared/enums/sort-order.enum.d.ts +0 -4
- package/shared/enums/toast-type.enum.d.ts +0 -6
- package/shared/functions/ws-client.function.d.ts +0 -5
- package/shared/index.d.ts +0 -5
- package/shared/interfaces/base-module-config.interface.d.ts +0 -61
- package/shared/interfaces/breadcrumb.interface.d.ts +0 -4
- package/shared/interfaces/can-component-deactivate.interface.d.ts +0 -7
- package/shared/interfaces/cms-enum-values.interface.d.ts +0 -4
- package/shared/interfaces/cms-field-config.interface.d.ts +0 -45
- package/shared/interfaces/cms-model-config.interface.d.ts +0 -14
- package/shared/interfaces/cms-table-field.interface.d.ts +0 -10
- package/shared/interfaces/combined-filter-input.interface.d.ts +0 -9
- package/shared/interfaces/compress-options.interface.d.ts +0 -15
- package/shared/interfaces/cropping-options.interface.d.ts +0 -12
- package/shared/interfaces/field-object.interface.d.ts +0 -6
- package/shared/interfaces/field.interface.d.ts +0 -5
- package/shared/interfaces/filter-input.interface.d.ts +0 -9
- package/shared/interfaces/find-args.interface.d.ts +0 -11
- package/shared/interfaces/graphql-crud-type.interface.d.ts +0 -7
- package/shared/interfaces/graphql-fields-object.interface.d.ts +0 -6
- package/shared/interfaces/graphql-options.interface.d.ts +0 -11
- package/shared/interfaces/graphql-plus-options.interfacen.d.ts +0 -9
- package/shared/interfaces/graphql-type-collection.interface.d.ts +0 -7
- package/shared/interfaces/login-config.interface.d.ts +0 -6
- package/shared/interfaces/register-config.interface.d.ts +0 -4
- package/shared/interfaces/single-filter-input.interface.d.ts +0 -11
- package/shared/interfaces/sort-input.interface.d.ts +0 -5
- package/shared/interfaces/time-components.interface.d.ts +0 -6
- package/shared/interfaces/toast.interface.d.ts +0 -8
- package/shared/public-api.d.ts +0 -65
- package/shared/services/auth.service.d.ts +0 -44
- package/shared/services/breadcrumb.service.d.ts +0 -32
- package/shared/services/cms.service.d.ts +0 -27
- package/shared/services/file.service.d.ts +0 -22
- package/shared/services/forms.service.d.ts +0 -14
- package/shared/services/graphql-meta.service.d.ts +0 -45
- package/shared/services/graphql-plus.service.d.ts +0 -27
- package/shared/services/graphql.service.d.ts +0 -71
- package/shared/services/image.service.d.ts +0 -27
- package/shared/services/loader.service.d.ts +0 -32
- package/shared/services/navigation.service.d.ts +0 -15
- package/shared/services/scroll.service.d.ts +0 -85
- package/shared/services/storage.service.d.ts +0 -49
- package/shared/services/toast.service.d.ts +0 -15
- package/shared/services/user.service.d.ts +0 -77
- package/shared/services/ws.service.d.ts +0 -8
- package/shared/types/graphql-fields.type.d.ts +0 -5
- package/shared/types/storage.type.d.ts +0 -4
- package/shared/validators/email.validator.d.ts +0 -1
- package/shared/validators/match.validator.d.ts +0 -4
- package/shared/validators/secure-password.validator.d.ts +0 -1
- package/src/base.module.d.ts +0 -16
- package/src/decorators/auto-unsubscribe.decorator.d.ts +0 -5
- package/src/directives/lazy-load.directive.d.ts +0 -17
- package/src/directives/match-height.directive.d.ts +0 -20
- package/src/directives/resizable.directive.d.ts +0 -26
- package/src/factories/apollo-options.factory.d.ts +0 -16
- package/src/functions/extract-files.function.d.ts +0 -15
- package/src/guards/auth.guard.d.ts +0 -27
- package/src/guards/can-deactivate.guard.d.ts +0 -10
- package/src/guards/logout.guard.d.ts +0 -16
- package/src/pipes/date-ago.pipe.d.ts +0 -34
- package/src/pipes/safe-html.pipe.d.ts +0 -13
- package/src/services/fullscreen.service.d.ts +0 -18
- package/src/services/seo.service.d.ts +0 -50
- package/src/services/theme.service.d.ts +0 -17
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lenne.tech-ng-base-sync.mjs","names":["emptyResult"],"sources":["../../../projects/ng-base/sync/src/lib/sync-store.ts","../../../projects/ng-base/sync/src/lib/sync-engine.ts","../../../projects/ng-base/sync/src/lib/sync-hints.ts","../../../projects/ng-base/sync/src/lib/offline-sync.ts","../../../projects/ng-base/sync/src/lib/provide-lt-sync.ts"],"sourcesContent":["/**\n * IndexedDB storage of the offline sync (`injectLtOfflineSync`).\n *\n * ONE database per origin holds every synchronized model: records, the outbox of unsent changes,\n * and meta values (cursors, the owning user). One database is what makes sign-out safe — the\n * nest-server's `SECURITY.md` asks a client to delete every record, cursor and unsent change, and\n * deleting the whole database is the one operation that cannot miss a model.\n *\n * Every tab of the origin shares the database; IndexedDB serializes their read-write transactions.\n * A transaction stays open only while `fn` awaits requests of that same transaction, so nothing\n * inside `run()` may await anything else (a fetch, a timer, an app hook).\n */\n\nimport type { LtSyncRecord } from './sync-types';\n\n/** Name of the database every model shares */\nexport const LT_SYNC_DB_NAME = 'lt-offline-sync';\n\n/** BroadcastChannel the tabs of an origin use to tell each other about changes */\nexport const LT_SYNC_CHANNEL_NAME = 'lt-offline-sync';\n\nconst STORES = ['meta', 'outbox', 'records'];\n\n/** An outbox entry: a protocol push item (§6.2) plus local bookkeeping */\nexport interface LtSyncOutboxItem {\n /** Set once a push carried the item; it is never changed again, a repeat must carry the same change (§6.5) */\n attempted?: boolean;\n baseVersion?: number;\n data?: Record<string, unknown>;\n id: string;\n mutationId: string;\n op: 'delete' | 'upsert';\n seq?: number;\n}\n\n/** What `run()` hands its callback: the transaction's operations, scoped to one model */\nexport interface LtSyncStoreTx {\n addOutbox: (item: LtSyncOutboxItem) => Promise<unknown>;\n /** Delete every local record of the model except `keepIds` */\n clearRecords: (keepIds: Set<string>) => Promise<void>;\n deleteMeta: (key: string) => Promise<unknown>;\n deleteRecord: (id: string) => Promise<unknown>;\n getMeta: <V = unknown>(key: string) => Promise<undefined | V>;\n getRecord: (id: string) => Promise<LtSyncRecord | undefined>;\n /** The model's unsent changes in the order they were made */\n listOutbox: () => Promise<LtSyncOutboxItem[]>;\n listRecords: () => Promise<LtSyncRecord[]>;\n putOutbox: (item: LtSyncOutboxItem) => Promise<unknown>;\n putRecord: (record: LtSyncRecord) => Promise<unknown>;\n removeOutbox: (mutationId: string) => Promise<void>;\n setMeta: (key: string, value: unknown) => Promise<unknown>;\n}\n\nconst done = <R>(request: IDBRequest<R>): Promise<R> =>\n new Promise((resolve, reject) => {\n request.addEventListener('success', () => resolve(request.result));\n request.addEventListener('error', () => reject(request.error));\n });\n\nlet connection: IDBDatabase | undefined;\nlet opening: Promise<IDBDatabase> | undefined;\n\nfunction openDatabase(): Promise<IDBDatabase> {\n if (connection) return Promise.resolve(connection);\n opening ??= (async () => {\n const request = indexedDB.open(LT_SYNC_DB_NAME, 1);\n request.addEventListener('upgradeneeded', () => {\n const db = request.result;\n db.createObjectStore('records', { keyPath: ['model', 'id'] }).createIndex('model', 'model');\n const outbox = db.createObjectStore('outbox', { autoIncrement: true, keyPath: 'seq' });\n outbox.createIndex('mutationId', 'mutationId', { unique: true });\n outbox.createIndex('model', 'model');\n db.createObjectStore('meta', { keyPath: 'key' });\n });\n try {\n const db = await done(request);\n // Another tab deleting the database (sign-out, account switch) must not wait for this one\n db.addEventListener('versionchange', () => {\n db.close();\n if (connection === db) connection = undefined;\n });\n connection = db;\n return db;\n } finally {\n opening = undefined;\n }\n })();\n return opening;\n}\n\n/**\n * The data no longer belongs to the user a sync run started for: signed out (database deleted) or\n * another account claimed it. The run's write was refused, nothing of it was stored.\n */\nexport class LtSyncOwnerChangedError extends Error {\n constructor() {\n super('the local sync data changed owner');\n this.name = 'LtSyncOwnerChangedError';\n }\n}\n\n/**\n * Run `fn` in one read-write transaction over the scope's records, outbox and meta values. `scope`\n * is a model, or a model and tenant (`note#<tenantId>`).\n *\n * With `owner`, the transaction first checks that the data still belongs to that user and refuses\n * otherwise (`LtSyncOwnerChangedError`). A sync answer can arrive after sign-out deleted the\n * database, or after another account claimed it — in this tab or another one. Unfenced, it would\n * reopen the database and write the previous user's records for the next user to see.\n */\nexport async function runLtSyncTx<R>(model: string, fn: (tx: LtSyncStoreTx) => Promise<R>, options: { owner?: string } = {}): Promise<R> {\n const db = await openDatabase();\n const transaction = db.transaction(STORES, 'readwrite');\n const finished = new Promise<void>((resolve, reject) => {\n transaction.addEventListener('complete', () => resolve());\n transaction.addEventListener('abort', () => reject(transaction.error ?? new Error('transaction aborted')));\n transaction.addEventListener('error', () => reject(transaction.error));\n });\n const store = (name: string) => transaction.objectStore(name);\n const strip = (stored: (LtSyncRecord & { model?: string }) | undefined): LtSyncRecord | undefined => {\n if (!stored) return undefined;\n const { model: _model, ...record } = stored;\n return record;\n };\n const tx: LtSyncStoreTx = {\n addOutbox: (item) => {\n const { seq: _seq, ...rest } = item;\n return done(store('outbox').add({ ...rest, model }));\n },\n clearRecords: async (keepIds) => {\n const keys = (await done(store('records').index('model').getAllKeys(model))) as [string, string][];\n for (const key of keys) {\n if (!keepIds.has(key[1])) await done(store('records').delete(key));\n }\n },\n deleteMeta: (key) => done(store('meta').delete(key)),\n deleteRecord: (id) => done(store('records').delete([model, id])),\n getMeta: async <V>(key: string) => ((await done(store('meta').get(key))) as undefined | { value: V })?.value,\n getRecord: async (id) => strip(await done(store('records').get([model, id]))),\n listOutbox: async () => {\n // An index lists equal keys in primary-key order, which is `seq`: the order the changes were made\n const items = (await done(store('outbox').index('model').getAll(model))) as (LtSyncOutboxItem & { model?: string })[];\n return items.map(({ model: _model, ...item }) => item);\n },\n listRecords: async () => ((await done(store('records').index('model').getAll(model))) as LtSyncRecord[]).map((record) => strip(record)!),\n putOutbox: (item) => done(store('outbox').put({ ...item, model })),\n putRecord: (record) => done(store('records').put({ ...record, model })),\n removeOutbox: async (mutationId) => {\n const key = await done(store('outbox').index('mutationId').getKey(mutationId));\n if (key !== undefined) await done(store('outbox').delete(key));\n },\n setMeta: (key, value) => done(store('meta').put({ key, value })),\n };\n let result: R;\n try {\n if (options.owner !== undefined && (await tx.getMeta<string>('owner')) !== options.owner) throw new LtSyncOwnerChangedError();\n result = await fn(tx);\n } catch (error) {\n transaction.abort();\n await finished.catch(() => undefined);\n throw error;\n }\n await finished;\n return result;\n}\n\n/** The scopes (model, or model and tenant) that hold unsent changes, in no particular order */\nexport async function listLtSyncPendingModels(): Promise<string[]> {\n const db = await openDatabase();\n const transaction = db.transaction(['outbox'], 'readonly');\n const items = (await done(transaction.objectStore('outbox').getAll())) as { model: string }[];\n return [...new Set(items.map((item) => item.model))];\n}\n\n/** The user the local data belongs to, if any */\nexport function getLtSyncOwner(): Promise<string | undefined> {\n return runLtSyncTx('', (tx) => tx.getMeta<string>('owner'));\n}\n\n/**\n * Bind the local data to the signed-in user, in ONE transaction. The same user keeps it. Anybody\n * else — another user, or nobody at all while data is present — means it is not this user's: every\n * model is wiped before anything is shown or pulled (nest-server `SECURITY.md`). Data without an\n * owner can only be a write that outlived a sign-out, which is exactly what must not be inherited.\n * Answers whether it wiped.\n */\nexport async function claimLtSyncOwner(userId: string): Promise<boolean> {\n const db = await openDatabase();\n const transaction = db.transaction(STORES, 'readwrite');\n const finished = new Promise<void>((resolve, reject) => {\n transaction.addEventListener('complete', () => resolve());\n transaction.addEventListener('abort', () => reject(transaction.error ?? new Error('transaction aborted')));\n transaction.addEventListener('error', () => reject(transaction.error));\n });\n const meta = transaction.objectStore('meta');\n const owner = ((await done(meta.get('owner'))) as undefined | { value: string })?.value;\n let wiped = false;\n if (owner !== userId) {\n const held = (await done(transaction.objectStore('records').count())) + (await done(transaction.objectStore('outbox').count())) + (await done(meta.count()));\n if (held > 0) {\n for (const name of STORES) await done(transaction.objectStore(name).clear());\n wiped = true;\n }\n await done(meta.put({ key: 'owner', value: userId }));\n }\n await finished;\n if (wiped) postLtSyncMessage({ type: 'cleared' });\n return wiped;\n}\n\n/**\n * Delete every record, cursor and unsent change of every model — on sign-out. Other tabs close\n * their connection on `versionchange` and are told via the channel. A sync answer arriving later\n * finds no owner and is refused (`runLtSyncTx` with `owner`).\n */\nexport async function deleteLtSyncDatabase(): Promise<void> {\n if (typeof indexedDB === 'undefined') return;\n await opening?.catch(() => undefined);\n connection?.close();\n connection = undefined;\n await new Promise<void>((resolve, reject) => {\n const request = indexedDB.deleteDatabase(LT_SYNC_DB_NAME);\n request.addEventListener('success', () => resolve());\n request.addEventListener('error', () => reject(request.error));\n // A tab that ignores `versionchange` would block forever; the delete completes once it closes\n request.addEventListener('blocked', () => undefined);\n });\n postLtSyncMessage({ type: 'cleared' });\n}\n\n/** Messages the tabs exchange */\nexport type LtSyncMessage = { scope: string; type: 'changed' } | { type: 'cleared' };\n\n/**\n * Tell every listener of this origin, this tab's included (a fresh channel is not the receiver's\n * own); a no-op where BroadcastChannel is missing\n */\nfunction postLtSyncMessage(message: LtSyncMessage): void {\n if (typeof BroadcastChannel === 'undefined') return;\n const channel = new BroadcastChannel(LT_SYNC_CHANNEL_NAME);\n try {\n channel.postMessage(message);\n } finally {\n channel.close();\n }\n}\n","/**\n * Offline-sync client of the nest-server sync module (>= 12.2.0).\n *\n * The protocol is `docs/offline-sync-protocol.md` in @lenne.tech/nest-server; section numbers (§n)\n * below point into it. This file is the protocol engine without Angular: local changes into the outbox,\n * push, pull, conflicts, resync. `injectLtOfflineSync()` adds signals, triggers and tab\n * coordination on top. Ported from the nest-server's reference test client\n * (`tests/clients/sync-client.mjs`), which passes the protocol's browser and Node scenarios.\n *\n * A local record is `{ id, version, updatedAt, deletedAt, data }`; `version` 0 means \"created\n * here, not on the server yet\". The outbox holds the unsent changes in the order they were made.\n * An item a push has carried (`attempted`) is never changed again: the server may have recorded it\n * under its mutation id, and a repeat must carry the same change (§6.2, §6.5). A later edit\n * becomes an item of its own.\n */\n\nimport type { LtSyncConflict, LtSyncConflictResolution, LtSyncIdFormat, LtSyncPushResult, LtSyncRecord, LtSyncResult } from './sync-types';\nimport { getLtSyncOwner, listLtSyncPendingModels, LtSyncOwnerChangedError, type LtSyncOutboxItem, type LtSyncStoreTx, runLtSyncTx } from './sync-store';\n\n/** Push at most this many items per request: the server's default `maxPushItems` (§4) */\nconst PUSH_CHUNK = 100;\n\n/** A push answering `conflict` on every round (a racing writer, a hook that keeps resending) stops here */\nconst MAX_PUSH_ROUNDS = 50;\n\n/** The request could not reach the server */\nexport class LtSyncOfflineError extends Error {\n constructor(cause: unknown) {\n super(`offline: ${cause instanceof Error ? cause.message : String(cause)}`);\n this.name = 'LtSyncOfflineError';\n }\n}\n\n/** The server answered 401: the session ended. Nothing was applied, nothing is discarded (§4) */\nexport class LtSyncUnauthorizedError extends Error {\n constructor() {\n super('unauthorized');\n this.name = 'LtSyncUnauthorizedError';\n }\n}\n\n/** The server asked to slow down: 429 with `Retry-After`, or 503 (§10) */\nexport class LtSyncBackoffError extends Error {\n constructor(\n readonly status: number,\n readonly retryAfterMs: number | undefined,\n ) {\n super(`server answered ${status}`);\n this.name = 'LtSyncBackoffError';\n }\n}\n\n// --- ids ------------------------------------------------------------------------------------------\n\nconst hex = (bytes: Uint8Array): string => Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');\n\n/**\n * An ObjectId-hex id with the random tail §3 requires: 4 timestamp bytes plus 8 bytes from a secure\n * source, fresh for every id. A driver's default ObjectId would make the next id predictable.\n */\nexport function ltSyncObjectId(): string {\n const random = new Uint8Array(8);\n globalThis.crypto.getRandomValues(random);\n return (\n Math.floor(Date.now() / 1000)\n .toString(16)\n .padStart(8, '0') + hex(random)\n );\n}\n\n/** A uuidv7: 48 bits of milliseconds, version 7, the variant, the rest random (`crypto.randomUUID()` is v4) */\nexport function ltSyncUuidV7(): string {\n const bytes = new Uint8Array(16);\n globalThis.crypto.getRandomValues(bytes);\n let time = Date.now();\n for (let index = 5; index >= 0; index--) {\n bytes[index] = time % 256;\n time = Math.floor(time / 256);\n }\n bytes[6] = 0x70 | (bytes[6]! & 0x0f);\n bytes[8] = 0x80 | (bytes[8]! & 0x3f);\n const value = hex(bytes);\n return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;\n}\n\n// --- urls -----------------------------------------------------------------------------------------\n\n/**\n * Environment of the engine, set by `provideLtSync()` from DI (API URL builder, sync base path, the auth-aware\n * `LT_FETCH`). The engine itself stays framework-free; tests call {@link configureLtSync} directly.\n */\nexport interface LtSyncEnvironment {\n /** Sync base path (default `/sync`). */\n basePath: string;\n /** Build a full API URL for a path (`LtApiUrlService.buildUrl`). */\n buildApiUrl: (path: string) => string;\n /** `fetch` for every request (the auth-aware `LT_FETCH`: cookie or JWT like every other request). */\n fetch: (input: string, init: RequestInit) => Promise<Response>;\n}\n\nlet environment: LtSyncEnvironment = {\n basePath: '/sync',\n buildApiUrl: (path) => path,\n fetch: (input, init) => fetch(input, { credentials: 'include', ...init }),\n};\n\n/** Set the engine environment (done by `provideLtSync()`). */\nexport function configureLtSync(env: Partial<LtSyncEnvironment>): void {\n environment = { ...environment, ...env };\n}\n\n/** The sync base path (default `/sync`) */\nexport function getLtSyncBasePath(): string {\n return environment.basePath || '/sync';\n}\n\n/** A full URL for a sync path, e.g. `/note?limit=200` or `/hints?models=note` */\nexport function buildLtSyncUrl(path: string): string {\n return environment.buildApiUrl(`${getLtSyncBasePath()}${path}`);\n}\n\n/** The configured fetch (auth-aware once `provideLtSync()` ran). */\nexport function ltSyncFetch(input: string, init: RequestInit): Promise<Response> {\n return environment.fetch(input, init);\n}\n\n/** Milliseconds a `Retry-After` header asks for: seconds or an HTTP date */\nfunction retryAfterMs(value: null | string): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n const date = Date.parse(value);\n return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());\n}\n\n// --- engine ---------------------------------------------------------------------------------------\n\n/** Options of the protocol engine */\nexport interface LtSyncEngineOptions<T> {\n /** `fetch` for the requests (default: the auth-aware `LT_FETCH` set by `provideLtSync()`) */\n fetch?: (input: string, init: RequestInit) => Promise<Response>;\n idFormat?: LtSyncIdFormat;\n /** The model's name under the sync base path */\n model: string;\n onConflict?: (conflict: LtSyncConflict<T>) => LtSyncConflictResolution<T> | Promise<LtSyncConflictResolution<T>>;\n pageLimit?: number;\n tenantId?: string;\n}\n\n/** The protocol engine of one model */\nexport interface LtSyncEngine<T> {\n create: (data: T) => Promise<LtSyncRecord<T>>;\n get: (id: string) => Promise<LtSyncRecord<T> | undefined>;\n /** The live local records, sorted by id */\n list: () => Promise<LtSyncRecord<T>[]>;\n readonly model: string;\n /** Where the engine keeps its data: the model, or the model and the tenant */\n readonly scope: string;\n pendingCount: () => Promise<number>;\n /** Push the outbox only — the sign-out flush. Never throws */\n pushPending: () => Promise<void>;\n remove: (id: string) => Promise<void>;\n /** Push the outbox, then pull until `hasMore` is false. Never throws for offline, 401 or a backoff */\n sync: () => Promise<LtSyncResult>;\n update: (id: string, data: Partial<T>) => Promise<LtSyncRecord<T>>;\n}\n\n/** The engines of this tab by scope, so the sign-out flush pushes with the options the app gave */\nconst engines = new Map<string, LtSyncEngine<any>>();\n\n/**\n * Where a model's local data lives: per model, and per tenant when one is given. A change made\n * under one tenant must never go out with another tenant's `X-Tenant-Id` — the server stamps a\n * create into the tenant of the header — nor show in that tenant's list.\n */\nexport function ltSyncScope(model: string, tenantId?: string): string {\n return tenantId ? `${model}#${tenantId}` : model;\n}\n\n/** The model and tenant of a scope (`ltSyncScope` in reverse) */\nfunction parseLtSyncScope(scope: string): { model: string; tenantId?: string } {\n const at = scope.indexOf('#');\n return at === -1 ? { model: scope } : { model: scope.slice(0, at), tenantId: scope.slice(at + 1) };\n}\n\n/** A pulled item or a server record as a local record */\nfunction localOf(item: NonNullable<LtSyncPushResult['record']>): LtSyncRecord {\n return {\n data: item.deletedAt ? undefined : item.data,\n deletedAt: item.deletedAt ?? null,\n id: item.id,\n updatedAt: item.updatedAt ?? null,\n version: item.version,\n };\n}\n\n/** The last queued change of a record */\nfunction lastPending(outbox: LtSyncOutboxItem[], id: string): LtSyncOutboxItem | undefined {\n return outbox.filter((item) => item.id === id).at(-1);\n}\n\n/**\n * The `baseVersion` of a new change: the version the change before it produces (§6.2) — the\n * stored version, or one more than a change already on its way.\n */\nfunction nextBase(record: LtSyncRecord, pending: LtSyncOutboxItem | undefined): number {\n return pending ? (pending.baseVersion ?? 0) + 1 : record.version;\n}\n\n/** The item as the protocol takes it, without the local bookkeeping */\nfunction pushItem(item: LtSyncOutboxItem): Record<string, unknown> {\n const { baseVersion, data, id, mutationId, op } = item;\n return { ...(baseVersion === undefined ? {} : { baseVersion }), ...(data === undefined ? {} : { data }), id, mutationId, op };\n}\n\nfunction emptyResult(): LtSyncResult {\n return { applied: 0, conflicts: [], forbidden: false, needsLogin: false, offline: false, rejected: [], resynced: false, retried: 0 };\n}\n\n/** Create the protocol engine of one model and register it for the sign-out flush */\nexport function createLtSyncEngine<T extends Record<string, unknown> = Record<string, unknown>>(options: LtSyncEngineOptions<T>): LtSyncEngine<T> {\n const { idFormat = 'objectId', model, onConflict, pageLimit = 200, tenantId } = options;\n const fetchImpl = options.fetch ?? ltSyncFetch;\n const scope = ltSyncScope(model, tenantId);\n const cursorKey = `cursor:${scope}`;\n const run = <R>(fn: (tx: LtSyncStoreTx) => Promise<R>) => runLtSyncTx(scope, fn);\n /**\n * A write of a sync run, refused once the data no longer belongs to the user the run started for.\n * Its answers can arrive after sign-out deleted the database or another account claimed it.\n */\n const runAs = <R>(owner: string | undefined, fn: (tx: LtSyncStoreTx) => Promise<R>) => runLtSyncTx(scope, fn, owner === undefined ? {} : { owner });\n const newId = () => (idFormat === 'uuidv7' ? ltSyncUuidV7() : ltSyncObjectId());\n\n /** One request; answers `{ status, body }`, throws offline, 401 and backoff */\n async function request(method: 'GET' | 'POST', path: string, payload?: unknown): Promise<{ body: any; status: number }> {\n const headers: Record<string, string> = { accept: 'application/json' };\n // Required of every push (§4): it forces the CORS preflight that guards cookie writes\n if (payload !== undefined) headers['content-type'] = 'application/json';\n if (tenantId) headers['x-tenant-id'] = tenantId;\n let response: Response;\n try {\n response = await fetchImpl(buildLtSyncUrl(path), { body: payload === undefined ? undefined : JSON.stringify(payload), headers, method });\n } catch (error) {\n throw new LtSyncOfflineError(error);\n }\n if (response.status === 401) throw new LtSyncUnauthorizedError();\n if (response.status === 429 || response.status === 503) throw new LtSyncBackoffError(response.status, retryAfterMs(response.headers.get('retry-after')));\n const text = await response.text();\n let body: any;\n try {\n body = text ? JSON.parse(text) : undefined;\n } catch {\n body = undefined;\n }\n if (response.status === 410 || (response.status === 403 && method === 'GET')) return { body, status: response.status };\n if (!response.ok) {\n const error = new Error(`${method} ${getLtSyncBasePath()}${path} answered ${response.status}: ${text.slice(0, 300)}`) as Error & { status: number };\n error.status = response.status;\n throw error;\n }\n return { body, status: response.status };\n }\n\n /** Act on one item's result (§7) */\n async function settle(\n tx: LtSyncStoreTx,\n item: LtSyncOutboxItem,\n outcome: LtSyncPushResult | undefined,\n result: LtSyncResult,\n resolution: LtSyncConflictResolution<T>,\n ): Promise<void> {\n const status = outcome?.status;\n if (status === 'retry' || !status) {\n // The same change is still in progress elsewhere (a second tab), or the server had a transient\n // problem: it stays queued, unchanged, and goes out again with the next sync\n result.retried++;\n return;\n }\n await tx.removeOutbox(item.mutationId);\n const record = await tx.getRecord(item.id);\n if (status === 'applied') {\n result.applied++;\n if (item.op === 'delete') {\n await tx.deleteRecord(item.id);\n } else if (record && (record.version ?? 0) < (outcome.version ?? 0)) {\n await tx.putRecord({ ...record, version: outcome.version! });\n }\n return;\n }\n const later = (await tx.listOutbox()).filter((entry) => entry.id === item.id);\n if (status === 'merged' && outcome.record) {\n result.applied++;\n // The merged record is the server's; edits still waiting behind this change stay visible\n const pendingData = later.filter((entry) => entry.op === 'upsert').reduce((data, entry) => ({ ...data, ...entry.data }), {});\n const merged = localOf(outcome.record);\n await tx.putRecord(merged.deletedAt ? merged : { ...merged, data: { ...merged.data, ...pendingData } });\n return;\n }\n if (status === 'conflict') {\n result.conflicts.push(outcome);\n // Later changes of the record were based on the refused one, so they go too\n for (const entry of later) await tx.removeOutbox(entry.mutationId);\n if (!outcome.record || outcome.record.deletedAt) {\n await tx.deleteRecord(item.id);\n return;\n }\n const server = localOf(outcome.record);\n if (resolution === 'server') {\n await tx.putRecord(server);\n return;\n }\n // The app's answer goes out on top of the server's state (§7: push again with baseVersion = record.version)\n await tx.putRecord({ ...server, data: { ...server.data, ...resolution.data } });\n await tx.addOutbox({ baseVersion: server.version, data: resolution.data as Record<string, unknown>, id: item.id, mutationId: ltSyncUuidV7(), op: 'upsert' });\n return;\n }\n // Not stored, and never will be: the changes queued behind it carry a `baseVersion` that assumed\n // it was applied. Sent on their own they could meet another writer's version with that very\n // number and be applied on top of it (protocol §11 since nest-server 12.3.0), so they go too\n for (const entry of later) await tx.removeOutbox(entry.mutationId);\n if (status === 'not_found') {\n await tx.deleteRecord(item.id);\n return;\n }\n // rejected: retrying the same change will not help; a record the server never had goes with it\n result.rejected.push(outcome);\n if (record && !record.version) {\n await tx.deleteRecord(item.id);\n } else if (record) {\n // The device shows the refused change. The server's record did not change, so no delta would\n // ever bring it back: drop the cursor, and the pull right after this push restores it\n await tx.deleteMeta(cursorKey);\n }\n }\n\n /** The app's conflict answer, asked outside any transaction (an IndexedDB transaction does not survive an awaited hook) */\n async function resolveConflict(item: LtSyncOutboxItem, outcome: LtSyncPushResult | undefined): Promise<LtSyncConflictResolution<T>> {\n if (outcome?.status !== 'conflict' || !onConflict || !outcome.record || outcome.record.deletedAt) return 'server';\n const local = (await run((tx) => tx.getRecord(item.id))) as LtSyncRecord<T> | undefined;\n const answer = await onConflict({ change: { data: item.data as Partial<T> | undefined, op: item.op }, local, server: localOf(outcome.record) as LtSyncRecord<T> });\n return answer === 'server' || !answer?.data ? 'server' : answer;\n }\n\n async function push(result: LtSyncResult, owner: string | undefined): Promise<void> {\n const carried = new Set<string>();\n for (let round = 0; round < MAX_PUSH_ROUNDS; round++) {\n // Read and mark in ONE transaction, and send what that transaction read: an edit between a\n // separate read and mark would fold into an item already on its way and be lost.\n // At most ONE change per record per request: the next one's `baseVersion` assumes this one\n // is applied. Sent together, a change based on a refused one would meet another writer's\n // version with the same number and be applied on top of it; sent after, it is dropped first.\n const chunk = await runAs(owner, async (tx) => {\n const records = new Set<string>();\n const items: LtSyncOutboxItem[] = [];\n for (const item of await tx.listOutbox()) {\n if (items.length === PUSH_CHUNK) break;\n if (records.has(item.id)) continue;\n records.add(item.id);\n if (!carried.has(item.mutationId)) items.push(item);\n }\n for (const item of items) await tx.putOutbox({ ...item, attempted: true });\n return items;\n });\n if (!chunk.length) return;\n for (const item of chunk) carried.add(item.mutationId);\n const response = await request('POST', `/${encodeURIComponent(model)}`, { items: chunk.map(pushItem) });\n const results: LtSyncPushResult[] = response.body?.results ?? [];\n for (let index = 0; index < chunk.length; index++) {\n const item = chunk[index]!;\n const resolution = await resolveConflict(item, results[index]);\n await runAs(owner, (tx) => settle(tx, item, results[index], result, resolution));\n }\n }\n }\n\n /** Pull every page after the stored cursor; answers whether a resync dropped the local records */\n async function pull(result: LtSyncResult, owner: string | undefined): Promise<boolean> {\n let cursor = await run((tx) => tx.getMeta<string>(cursorKey));\n for (let page = 0; page < 10_000; page++) {\n const query = new URLSearchParams({ limit: String(pageLimit) });\n if (cursor) query.set('cursor', cursor);\n const response = await request('GET', `/${encodeURIComponent(model)}?${query}`);\n if (response.status === 403) {\n // The caller may no longer pull this model (pull roles withdrawn, tenant left): the copies go,\n // like on `410 access`. Unsent changes stay, but cannot be pushed under this account (§10)\n result.forbidden = true;\n await runAs(owner, async (tx) => {\n await tx.clearRecords(new Set((await tx.listOutbox()).map((item) => item.id)));\n await tx.deleteMeta(cursorKey);\n });\n return false;\n }\n if (response.status === 410) {\n if (result.resynced) throw new Error(`resync loop: ${JSON.stringify(response.body)}`);\n // Keep the unsent changes, delete every local record, pull without a cursor (§8)\n result.resynced = true;\n await runAs(owner, async (tx) => {\n await tx.clearRecords(new Set((await tx.listOutbox()).map((item) => item.id)));\n await tx.deleteMeta(cursorKey);\n });\n cursor = undefined;\n continue;\n }\n const body = response.body as { cursor: string; hasMore: boolean; items: NonNullable<LtSyncPushResult['record']>[] };\n // A page's items and its cursor in one local transaction: a crash after it resumes with the next page (§5.2)\n await runAs(owner, async (tx) => {\n const pending = new Set((await tx.listOutbox()).map((item) => item.id));\n for (const item of body.items ?? []) {\n // An unsent local change wins locally until its push settles; the push meets the newer version then\n if (pending.has(item.id)) continue;\n // A page may repeat a record or deliver an older one; the higher version wins (§5.2)\n const local = await tx.getRecord(item.id);\n if (local && local.version > item.version) continue;\n if (item.deletedAt) await tx.deleteRecord(item.id);\n else await tx.putRecord(localOf(item));\n }\n await tx.setMeta(cursorKey, body.cursor);\n });\n cursor = body.cursor;\n if (!body.hasMore) return result.resynced;\n }\n throw new Error('pull did not end');\n }\n\n async function syncOnce(): Promise<LtSyncResult> {\n const result = emptyResult();\n try {\n // Every write of this run is bound to the user the data belongs to now (see `runAs`)\n const owner = await getLtSyncOwner();\n await push(result, owner);\n // A resync drops the local records but keeps the unsent changes; they meet the new state now (§8)\n if (await pull(result, owner)) await push(result, owner);\n } catch (error) {\n // Signed out or another account mid-run: what this run would have written is not theirs to keep\n if (error instanceof LtSyncOwnerChangedError) return result;\n if (error instanceof LtSyncUnauthorizedError) result.needsLogin = true;\n else if (error instanceof LtSyncOfflineError) result.offline = true;\n else if (error instanceof LtSyncBackoffError) result.retryAfterMs = error.retryAfterMs ?? 0;\n else throw error;\n }\n return result;\n }\n\n /** One sync per model across the tabs of the origin (Web Locks); correct without them, only busier (§6.5) */\n async function syncLocked(): Promise<LtSyncResult> {\n const locks = typeof navigator === 'undefined' ? undefined : navigator.locks;\n if (!locks) return syncOnce();\n return await locks.request(`lt-offline-sync:${scope}`, syncOnce);\n }\n\n // Calls during a running sync collapse into ONE follow-up run, so a burst of triggers costs two runs at most\n let current: null | Promise<LtSyncResult> = null;\n let next: null | Promise<LtSyncResult> = null;\n function sync(): Promise<LtSyncResult> {\n if (!current) {\n current = syncLocked().finally(() => {\n current = null;\n });\n return current;\n }\n next ??= current\n .catch(() => undefined)\n .then(() => {\n next = null;\n return sync();\n });\n return next;\n }\n\n const engine: LtSyncEngine<T> = {\n async create(data) {\n const record: LtSyncRecord<T> = { data, deletedAt: null, id: newId(), updatedAt: null, version: 0 };\n await run(async (tx) => {\n await tx.putRecord(record as LtSyncRecord);\n await tx.addOutbox({ data: data as Record<string, unknown>, id: record.id, mutationId: ltSyncUuidV7(), op: 'upsert' });\n });\n return record;\n },\n get: (id) => run((tx) => tx.getRecord(id)) as Promise<LtSyncRecord<T> | undefined>,\n async list() {\n const records = (await run((tx) => tx.listRecords())) as LtSyncRecord<T>[];\n return records.filter((record) => !record.deletedAt).sort((a, b) => (a.id < b.id ? -1 : 1));\n },\n model,\n pendingCount: () => run(async (tx) => (await tx.listOutbox()).length),\n async pushPending() {\n try {\n // Bound to the owner like a sync: an answer arriving after the sign-out deleted the data writes nothing\n await push(emptyResult(), await getLtSyncOwner());\n } catch {\n // Best effort: whatever did not go out is deleted with the rest on sign-out\n }\n },\n scope,\n async remove(id) {\n await run(async (tx) => {\n const record = await tx.getRecord(id);\n if (!record || record.deletedAt) return;\n const pending = lastPending(await tx.listOutbox(), id);\n if (pending && !pending.attempted) {\n await tx.removeOutbox(pending.mutationId);\n if (pending.baseVersion === undefined) {\n // Created and deleted before it ever reached the server: nothing to tell the server\n await tx.deleteRecord(id);\n return;\n }\n }\n const baseVersion = pending && !pending.attempted ? pending.baseVersion : nextBase(record, pending);\n await tx.addOutbox({ baseVersion, id, mutationId: ltSyncUuidV7(), op: 'delete' });\n await tx.putRecord({ ...record, data: undefined, deletedAt: new Date().toISOString() });\n });\n },\n sync,\n async update(id, data) {\n return run(async (tx) => {\n const record = await tx.getRecord(id);\n if (!record || record.deletedAt) throw new Error(`no live local record ${id}`);\n const pending = lastPending(await tx.listOutbox(), id);\n if (pending && pending.op === 'upsert' && !pending.attempted) {\n // Folded into a change no push has carried yet (§6.2 allows one item per record)\n await tx.putOutbox({ ...pending, data: { ...pending.data, ...(data as Record<string, unknown>) } });\n } else {\n await tx.addOutbox({ baseVersion: nextBase(record, pending), data: data as Record<string, unknown>, id, mutationId: ltSyncUuidV7(), op: 'upsert' });\n }\n const changed = { ...record, data: { ...record.data, ...(data as Record<string, unknown>) } };\n await tx.putRecord(changed);\n return changed as LtSyncRecord<T>;\n });\n },\n };\n engines.set(scope, engine);\n return engine;\n}\n\n/**\n * Before sign-out: send what is still unsent, for at most `timeoutMs`, never failing. Whatever\n * did not go out is deleted with the rest afterwards — after signing out it could not be pushed\n * under this account anyway (nest-server `SECURITY.md`).\n *\n * Only when the local data belongs to `userId`, the user signing out. After an expired session\n * the outbox of the PREVIOUS user is still on the device; sent now, it would be created and\n * changed under the current user's session and account. It is deleted unsent instead.\n */\nexport async function flushLtSyncBeforeSignOut(userId: string | undefined, timeoutMs = 5000): Promise<void> {\n if (typeof indexedDB === 'undefined' || !userId) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, timeoutMs);\n });\n const flush = (async () => {\n if ((await getLtSyncOwner().catch(() => undefined)) !== userId) return;\n const scopes = await listLtSyncPendingModels().catch(() => [] as string[]);\n // Each scope with its own tenant header: the engine the app created, or one rebuilt from the scope\n await Promise.all(scopes.map((scope) => (engines.get(scope) ?? createLtSyncEngine(parseLtSyncScope(scope))).pushPending()));\n })();\n try {\n await Promise.race([flush, timeout]);\n } finally {\n clearTimeout(timer);\n }\n}\n\n/** Forget the registered engines (tests) */\nexport function resetLtSyncEngines(): void {\n engines.clear();\n}\n","/**\n * The hint stream of the offline sync: `GET <basePath>/hints` as server-sent events (protocol §9).\n *\n * Read with `fetch` and a `ReadableStream`, not `EventSource`: an EventSource cannot send an\n * `Authorization` header, so in JWT mode (the fallback the auth-aware fetch switches to when the cookie\n * is refused) it would never be signed in (decision E2 of DEV-3917).\n *\n * The server ends a stream on purpose before its first hint after the re-check interval (30 s by\n * default) and does not replay that hint. So every (re)connect calls `onConnect`, and the caller\n * pulls once — without it a change would wait for the next focus or start whenever hints are\n * further apart than the interval.\n */\n\nimport { buildLtSyncUrl, ltSyncFetch } from './sync-engine';\n\n/** Hard cap on one SSE line (1 MiB): bails out on a stream that never sends `\\n` */\nconst LINE_LIMIT = 1024 * 1024;\n\n/** Reconnect delay until the server sends `retry:` (it sends 3000) */\nconst DEFAULT_RETRY_MS = 3000;\n\n/** Upper bound of the backoff after failed connects */\nconst MAX_BACKOFF_MS = 60_000;\n\nexport interface LtSyncHintsOptions {\n /** `fetch` for the stream (default: the auth-aware `LT_FETCH`) */\n fetch?: (input: string, init: RequestInit) => Promise<Response>;\n /** The models to hear about; a model the caller may not pull simply never sends a hint */\n models: string[];\n /** Every (re)connect. Hints sent while disconnected are not replayed: pull once (§9) */\n onConnect: () => void;\n /** A change of `model` was committed: pulling is worthwhile */\n onHint: (model: string) => void;\n /** The stream stopped for good: the server switched SSE off (`404`) or the session ended (`401`) */\n onStop?: (reason: 'disabled' | 'unauthorized') => void;\n /** Restrict to one tenant (`X-Tenant-Id`) */\n tenantId?: string;\n}\n\ninterface SseEvent {\n data: string;\n event: string;\n retry?: number;\n}\n\n/** Read server-sent events off a body until it ends or the signal aborts */\nasync function readEvents(body: ReadableStream<Uint8Array>, onEvent: (event: SseEvent) => void, signal: AbortSignal): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let event: SseEvent = { data: '', event: 'message' };\n const dispatchLine = (line: string) => {\n if (line === '') {\n if (event.data || event.retry !== undefined) onEvent(event);\n event = { data: '', event: 'message' };\n return;\n }\n // A comment line: the keep-alive the server sends every 25 s\n if (line.startsWith(':')) return;\n const colon = line.indexOf(':');\n const field = colon === -1 ? line : line.slice(0, colon);\n const value = colon === -1 ? '' : line.slice(colon + 1).replace(/^ /, '');\n if (field === 'event') event.event = value;\n else if (field === 'data') event.data = event.data ? `${event.data}\\n${value}` : value;\n else if (field === 'retry' && /^\\d+$/.test(value)) event.retry = Number(value);\n };\n const onAbort = () => void reader.cancel().catch(() => undefined);\n signal.addEventListener('abort', onAbort);\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) return;\n buffer += decoder.decode(value, { stream: true });\n let newline = buffer.search(/\\r?\\n|\\r/);\n while (newline !== -1) {\n const line = buffer.slice(0, newline);\n const skip = buffer[newline] === '\\r' && buffer[newline + 1] === '\\n' ? 2 : 1;\n buffer = buffer.slice(newline + skip);\n dispatchLine(line);\n newline = buffer.search(/\\r?\\n|\\r/);\n }\n if (buffer.length > LINE_LIMIT) throw new Error('sync hint stream: line exceeds 1 MiB');\n }\n } finally {\n signal.removeEventListener('abort', onAbort);\n reader.releaseLock();\n }\n}\n\n/** One instance listening for the hints of its model */\nexport interface LtSyncHintSubscriber {\n model: string;\n onConnect: () => void;\n onHint: () => void;\n onStop?: (reason: 'disabled' | 'unauthorized') => void;\n tenantId?: string;\n}\n\ninterface HintHub {\n close?: () => void;\n models: string;\n subscribers: Set<LtSyncHintSubscriber>;\n}\n\n/** The ONE stream this tab keeps per tenant, by tenant ('' without one) */\nconst hubs = new Map<string, HintHub>();\n\n/**\n * Listen for the hints of a model on the ONE stream this tab keeps per tenant, for every model in\n * use (`?models=` takes a list, §9). A stream per `useLtOfflineSync()` call would hold a connection\n * each; over HTTP/1.1 a browser allows six per host, so six instances on one page would stall every\n * other request to the API, sign-in and sign-out included. Answers the unsubscribe.\n */\nexport function subscribeLtSyncHints(subscriber: LtSyncHintSubscriber): () => void {\n const key = subscriber.tenantId ?? '';\n let hub = hubs.get(key);\n if (!hub) {\n hub = { models: '', subscribers: new Set() };\n hubs.set(key, hub);\n }\n const joined = hub;\n joined.subscribers.add(subscriber);\n connectHub(key, joined);\n return () => {\n if (!joined.subscribers.delete(subscriber)) return;\n if (joined.subscribers.size === 0) {\n joined.close?.();\n if (hubs.get(key) === joined) hubs.delete(key);\n return;\n }\n connectHub(key, joined);\n };\n}\n\n/** (Re)open the hub's stream for the models its subscribers use; a no-op while the set is unchanged */\nfunction connectHub(key: string, hub: HintHub): void {\n const models = [...new Set([...hub.subscribers].map((subscriber) => subscriber.model))].sort().join(',');\n if (hub.close && hub.models === models) return;\n hub.close?.();\n hub.models = models;\n // Each callback walks a copy: a subscriber may join or leave inside one, and a Set visits members\n // added during the walk, so one that joins in `onStop` would get the stop of a stream it never had.\n // `Array.from` and not `[...set]`, which unicorn(no-useless-spread) reports in every vendored core.\n hub.close = openLtSyncHints({\n models: models.split(','),\n onConnect: () => {\n for (const subscriber of Array.from(hub.subscribers)) subscriber.onConnect();\n },\n onHint: (model) => {\n for (const subscriber of Array.from(hub.subscribers)) if (subscriber.model === model) subscriber.onHint();\n },\n onStop: (reason) => {\n hub.close = undefined;\n hub.models = '';\n for (const subscriber of Array.from(hub.subscribers)) subscriber.onStop?.(reason);\n },\n tenantId: key || undefined,\n });\n}\n\n/**\n * Open the hint stream and keep it open — reconnecting after the server ends it, backing off after\n * failures — until the returned `close()` is called or the stream stops for good (`onStop`).\n */\nexport function openLtSyncHints(options: LtSyncHintsOptions): () => void {\n const { models, onConnect, onHint, onStop, tenantId } = options;\n const fetchImpl = options.fetch ?? ltSyncFetch;\n let closed = false;\n let controller: AbortController | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let wake: (() => void) | undefined;\n let retryMs = DEFAULT_RETRY_MS;\n let failures = 0;\n\n const wait = (ms: number) =>\n new Promise<void>((resolve) => {\n wake = resolve;\n timer = setTimeout(resolve, ms);\n });\n\n const loop = async () => {\n while (!closed) {\n controller = new AbortController();\n try {\n const query = new URLSearchParams({ models: models.join(',') });\n const headers: Record<string, string> = { accept: 'text/event-stream' };\n if (tenantId) headers['x-tenant-id'] = tenantId;\n const response = await fetchImpl(buildLtSyncUrl(`/hints?${query}`), { headers, method: 'GET', signal: controller.signal });\n if (response.status === 404 || response.status === 401) {\n await response.body?.cancel().catch(() => undefined);\n if (!closed) onStop?.(response.status === 404 ? 'disabled' : 'unauthorized');\n return;\n }\n if (!response.ok || !response.body) throw new Error(`sync hint stream answered ${response.status}`);\n failures = 0;\n onConnect();\n await readEvents(\n response.body,\n (event) => {\n if (event.retry !== undefined) retryMs = event.retry;\n if (event.event !== 'change') return;\n try {\n const model = (JSON.parse(event.data) as { model?: unknown }).model;\n if (typeof model === 'string') onHint(model);\n } catch {\n // A malformed hint carries nothing to act on; the next pull catches up anyway\n }\n },\n controller.signal,\n );\n // The server ended the stream on purpose: reconnect after its `retry`\n if (!closed) await wait(retryMs);\n } catch {\n if (closed) return;\n failures++;\n await wait(Math.min(retryMs * 2 ** Math.min(failures - 1, 5), MAX_BACKOFF_MS));\n }\n }\n };\n void loop();\n\n return () => {\n closed = true;\n controller?.abort();\n clearTimeout(timer);\n wake?.();\n };\n}\n","import { DOCUMENT, isPlatformBrowser } from '@angular/common';\nimport { assertInInjectionContext, DestroyRef, effect, inject, PLATFORM_ID, signal, untracked } from '@angular/core';\nimport { LtAuthStore } from '@lenne.tech/ng-base/auth/provide';\n\nimport { createLtSyncEngine } from './sync-engine';\nimport { subscribeLtSyncHints } from './sync-hints';\nimport { claimLtSyncOwner, LT_SYNC_CHANNEL_NAME, type LtSyncMessage } from './sync-store';\nimport type { LtOfflineSyncOptions, LtOfflineSyncState, LtSyncPushResult, LtSyncRecord, LtSyncResult } from './sync-types';\n\n/** Offline-synchronised records of one model, created by {@link injectLtOfflineSync}. */\nexport type LtOfflineSync<T = Record<string, unknown>> = LtOfflineSyncState<T>;\n\n/** First wait after a failed sync (offline, 429, 503, `retry`); doubles up to the maximum (§11) */\nconst BACKOFF_START_MS = 1000;\nconst BACKOFF_MAX_MS = 60_000;\n\n/** A local change syncs this long after the last one, so typing does not push every keystroke */\nconst LOCAL_CHANGE_DELAY_MS = 300;\n\nconst emptyResult = (overrides: Partial<LtSyncResult> = {}): LtSyncResult => ({\n applied: 0,\n conflicts: [],\n forbidden: false,\n needsLogin: false,\n offline: false,\n rejected: [],\n resynced: false,\n retried: 0,\n ...overrides,\n});\n\n/**\n * Offline-first records of one model (counterpart of `useLtOfflineSync()`), bound to the current injection context.\n *\n * Records live in IndexedDB; changes made offline wait in an outbox and go out with the next sync.\n * Triggers: start, `online`, window focus, `visibilitychange` (a hidden tab closes the hint stream), a server hint\n * and every reconnect of the hint stream (protocol §9), 300 ms after a local change; backoff 1 s → 60 s. Tabs\n * coordinate: one syncs at a time (Web Locks), the others pick up the result (BroadcastChannel); without either API\n * it stays correct, only busier.\n *\n * The local data belongs to the signed-in user (`LtAuthStore.user()`, the same signal as `LtAuthService.user()`). A user change re-claims ownership of the\n * local database: another user's data is wiped before anything is shown or pulled. `signOut()` (`LtAuthService` / `LtAuthStore`) sends\n * what is unsent and then deletes it (hook of `provideLtSync()`). An expired session deletes nothing: after signing\n * in again the outbox goes out without duplicates (protocol §4).\n *\n * During SSR or without IndexedDB it is an inert stub (empty records, mutations reject). Without a signed-in user\n * `records` is empty and mutations throw. Everything stops when the injection context is destroyed.\n * Requires `provideLtSync()` and `provideLtAuth()`.\n *\n * @example\n * ```ts\n * export class NotesPage {\n * protected readonly notes = injectLtOfflineSync<{ title: string }>('note');\n *\n * add(title: string) {\n * return this.notes.create({ title });\n * }\n * }\n * // template: @for (note of notes.records(); track note.id) { … } · notes.pendingCount() · notes.offline()\n * ```\n */\nexport function injectLtOfflineSync<T extends Record<string, unknown> = Record<string, unknown>>(model: string, options: LtOfflineSyncOptions<T> = {}): LtOfflineSync<T> {\n assertInInjectionContext(injectLtOfflineSync);\n\n const records = signal<readonly LtSyncRecord<T>[]>([]);\n const pendingCount = signal(0);\n const offline = signal(false);\n const syncing = signal(false);\n const needsLogin = signal(false);\n const forbidden = signal(false);\n const conflicts = signal<LtSyncPushResult[]>([]);\n const rejected = signal<LtSyncPushResult[]>([]);\n const error = signal<Error | null>(null);\n const lastSyncedAt = signal<Date | null>(null);\n\n const state = {\n conflicts: conflicts.asReadonly(),\n error: error.asReadonly(),\n forbidden: forbidden.asReadonly(),\n lastSyncedAt: lastSyncedAt.asReadonly(),\n needsLogin: needsLogin.asReadonly(),\n offline: offline.asReadonly(),\n pendingCount: pendingCount.asReadonly(),\n records: records.asReadonly(),\n rejected: rejected.asReadonly(),\n syncing: syncing.asReadonly(),\n };\n\n // IndexedDB exists only in the browser: on the server the records render as an empty list\n if (!isPlatformBrowser(inject(PLATFORM_ID)) || typeof indexedDB === 'undefined') {\n const browserOnly = () => Promise.reject(new Error('injectLtOfflineSync: offline sync runs in the browser only'));\n return { ...state, create: browserOnly, get: async () => undefined, remove: browserOnly, sync: async () => emptyResult(), update: browserOnly };\n }\n\n const auth = inject(LtAuthStore);\n const destroyRef = inject(DestroyRef);\n const doc = inject(DOCUMENT);\n const win = doc.defaultView;\n const auto = options.auto !== false;\n const engine = createLtSyncEngine<T>({ idFormat: options.idFormat, model, onConflict: options.onConflict, pageLimit: options.pageLimit, tenantId: options.tenantId });\n const channel = typeof BroadcastChannel === 'undefined' ? undefined : new BroadcastChannel(LT_SYNC_CHANNEL_NAME);\n\n // --- owner: the local data belongs to the signed-in user ----------------------------------------\n\n let ownerId: null | string = null;\n let claiming: null | { id: string; done: Promise<boolean> } = null;\n const currentUserId = () => {\n const id = auth.user()?.id;\n return id ? String(id) : null;\n };\n\n /** Whether a signed-in user owns the local data; claims it first, wiping another user's (account switch) */\n async function ensureOwner(): Promise<boolean> {\n const id = currentUserId();\n if (!id) {\n ownerId = null;\n return false;\n }\n if (id === ownerId) {\n return true;\n }\n // A claim is per user: one still running for the previous user must not answer for this one\n if (claiming?.id !== id) {\n const pending = { done: claimLtSyncOwner(id), id };\n claiming = pending;\n void pending.done.finally(() => {\n if (claiming === pending) {\n claiming = null;\n }\n });\n }\n await claiming.done;\n // The user changed while the claim ran: claim for the one signed in now, never show the data to them as the other's\n if (currentUserId() !== id) {\n return ensureOwner();\n }\n ownerId = id;\n return true;\n }\n\n async function refresh(): Promise<void> {\n if (!(await ensureOwner())) {\n records.set([]);\n pendingCount.set(0);\n return;\n }\n const [list, pending] = await Promise.all([engine.list(), engine.pendingCount()]);\n records.set(list);\n pendingCount.set(pending);\n }\n\n async function owned<R>(fn: () => Promise<R>): Promise<R> {\n if (!(await ensureOwner())) {\n throw new Error('injectLtOfflineSync: no signed-in user owns the local data');\n }\n return fn();\n }\n\n // --- sync ---------------------------------------------------------------------------------------\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let backoffMs = 0;\n let disposed = false;\n let closeHints: (() => void) | undefined;\n /** Why the hint stream stopped: `disabled` for good (404), `unauthorized` until a sync gets through */\n let hintsStopped: 'disabled' | 'unauthorized' | null = null;\n\n /**\n * Tell the other tabs and instances that this scope changed. A destroyed instance has closed its channel (posting on\n * it would throw), so a sync still finishing after the destroy announces through a short-lived one.\n */\n function announce(): void {\n const message = { scope: engine.scope, type: 'changed' } satisfies LtSyncMessage;\n if (!disposed) {\n channel?.postMessage(message);\n return;\n }\n if (typeof BroadcastChannel === 'undefined') {\n return;\n }\n const once = new BroadcastChannel(LT_SYNC_CHANNEL_NAME);\n try {\n once.postMessage(message);\n } finally {\n once.close();\n }\n }\n\n function schedule(ms: number): void {\n if (disposed) {\n return;\n }\n clearTimeout(timer);\n timer = setTimeout(() => void sync().catch(() => undefined), ms);\n }\n\n /** Wait longer after every failure, as long as the server asks or cannot be reached (§11) */\n function backOff(retryAfterMs = 0): void {\n backoffMs = Math.min(backoffMs ? backoffMs * 2 : BACKOFF_START_MS, BACKOFF_MAX_MS);\n // While the browser knows it is offline, its `online` event is the trigger, not a timer\n if (auto && win?.navigator.onLine !== false) {\n schedule(Math.max(retryAfterMs, backoffMs));\n }\n }\n\n async function sync(): Promise<LtSyncResult> {\n if (!(await ensureOwner())) {\n needsLogin.set(true);\n return emptyResult({ needsLogin: true });\n }\n syncing.set(true);\n try {\n const result = await engine.sync();\n error.set(null);\n offline.set(result.offline);\n needsLogin.set(result.needsLogin);\n forbidden.set(result.forbidden);\n conflicts.set(result.conflicts);\n rejected.set(result.rejected);\n if (result.offline || result.retryAfterMs !== undefined || result.retried) {\n backOff(result.retryAfterMs);\n } else if (!result.needsLogin) {\n backoffMs = 0;\n lastSyncedAt.set(new Date());\n // The stream stopped on a 401; a sync that got through means the user is back\n if (hintsStopped === 'unauthorized') {\n hintsStopped = null;\n startHints();\n }\n }\n return result;\n } catch (caught) {\n error.set(caught instanceof Error ? caught : new Error(String(caught)));\n throw caught;\n } finally {\n syncing.set(false);\n await refresh();\n announce();\n }\n }\n\n const autoSync = () => {\n if (auto) {\n void sync().catch(() => undefined);\n }\n };\n\n async function afterLocalChange(): Promise<void> {\n announce();\n await refresh();\n if (auto) {\n schedule(LOCAL_CHANGE_DELAY_MS);\n }\n }\n\n // --- hints (§9) ---------------------------------------------------------------------------------\n\n function startHints(): void {\n if (!auto || options.hints === false || closeHints || hintsStopped || disposed) {\n return;\n }\n if (doc.visibilityState === 'hidden' || !auth.user()?.id) {\n return;\n }\n // One stream per tab and tenant, shared by every instance (see `subscribeLtSyncHints`)\n closeHints = subscribeLtSyncHints({\n model,\n // Hints sent while the stream was down are not replayed: every (re)connect pulls once\n onConnect: autoSync,\n onHint: autoSync,\n onStop: (reason) => {\n stopHints();\n hintsStopped = reason;\n },\n tenantId: options.tenantId,\n });\n }\n\n function stopHints(): void {\n closeHints?.();\n closeHints = undefined;\n }\n\n // --- triggers -----------------------------------------------------------------------------------\n\n const onOnline = () => {\n offline.set(false);\n backoffMs = 0;\n autoSync();\n };\n const onOffline = () => {\n offline.set(true);\n };\n const onVisibility = () => {\n // A hidden tab keeps no stream open; coming back, the reconnect pulls\n if (doc.visibilityState === 'hidden') {\n stopHints();\n return;\n }\n startHints();\n autoSync();\n };\n const onMessage = (event: MessageEvent<LtSyncMessage>) => {\n if (event.data?.type === 'cleared') {\n // Signed out or another account in some tab: the database is gone, claim it anew\n ownerId = null;\n void refresh();\n } else if (event.data?.type === 'changed' && event.data.scope === engine.scope) {\n void refresh();\n }\n };\n\n win?.addEventListener('online', onOnline);\n win?.addEventListener('offline', onOffline);\n win?.addEventListener('focus', autoSync);\n doc.addEventListener('visibilitychange', onVisibility);\n if (win?.navigator.onLine === false) {\n offline.set(true);\n }\n channel?.addEventListener('message', onMessage);\n\n // A user change (sign-in, sign-out, account switch) re-claims the local data before anything is shown or pulled\n let watchedUserId = auth.user()?.id ?? null;\n const userWatch = effect(() => {\n const id = auth.user()?.id ?? null;\n untracked(() => {\n if (id === watchedUserId) {\n return;\n }\n watchedUserId = id;\n stopHints();\n hintsStopped = hintsStopped === 'disabled' ? 'disabled' : null;\n void refresh().then(() => {\n if (!id) {\n return;\n }\n startHints();\n autoSync();\n });\n });\n });\n\n destroyRef.onDestroy(() => {\n disposed = true;\n clearTimeout(timer);\n stopHints();\n userWatch.destroy();\n channel?.removeEventListener('message', onMessage);\n channel?.close();\n win?.removeEventListener('online', onOnline);\n win?.removeEventListener('offline', onOffline);\n win?.removeEventListener('focus', autoSync);\n doc.removeEventListener('visibilitychange', onVisibility);\n });\n\n // Start: show what is local, then sync and listen for hints\n void refresh().then(() => {\n startHints();\n autoSync();\n });\n\n return {\n ...state,\n async create(data) {\n const record = await owned(() => engine.create(data));\n await afterLocalChange();\n return record;\n },\n async get(id) {\n return (await ensureOwner()) ? engine.get(id) : undefined;\n },\n async remove(id) {\n await owned(() => engine.remove(id));\n await afterLocalChange();\n },\n sync,\n async update(id, data) {\n const record = await owned(() => engine.update(id, data));\n await afterLocalChange();\n return record;\n },\n };\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport { inject, InjectionToken, makeEnvironmentProviders, PLATFORM_ID, provideAppInitializer, type EnvironmentProviders } from '@angular/core';\nimport { LT_FETCH, LT_RUNTIME_CONFIG, LtApiUrlService, mergeLtFeatureOptions } from '@lenne.tech/ng-base';\nimport { LT_AUTH_SIGN_OUT_HOOKS, type LtAuthSignOutHook } from '@lenne.tech/ng-base/auth/provide';\n\nimport { configureLtSync, flushLtSyncBeforeSignOut } from './sync-engine';\nimport { deleteLtSyncDatabase } from './sync-store';\nimport type { LtSyncOptions } from './sync-types';\n\nconst LT_SYNC_INPUT_OPTIONS = new InjectionToken<LtSyncOptions>('LT_SYNC_INPUT_OPTIONS', { factory: () => ({}) });\n\n/** Resolved sync options (defaults ← `provideLtSync(options)` ← runtime `NG_PUBLIC_LT_SYNC_*`). */\nexport const LT_SYNC_OPTIONS = new InjectionToken<Required<LtSyncOptions>>('LT_SYNC_OPTIONS', {\n factory: () => mergeLtFeatureOptions<Required<LtSyncOptions>>({ basePath: '/sync' }, inject(LT_SYNC_INPUT_OPTIONS), inject(LT_RUNTIME_CONFIG, { optional: true })?.lt.sync),\n});\n\n/**\n * Offline sync with the nest-server sync module (≥ 12.2.0), opt-in.\n *\n * Configures the framework-free engine (API URL, base path, the auth-aware `LT_FETCH`) and registers a sign-out hook:\n * before `POST /iam/sign-out` the outbox of the signed-in user is flushed (≤ 5 s), afterwards the IndexedDB is deleted,\n * so no record, cursor or unsent change survives a sign-out. Requires `provideLtAuth()`.\n */\nexport function provideLtSync(options: LtSyncOptions = {}): EnvironmentProviders {\n return makeEnvironmentProviders([\n { provide: LT_SYNC_INPUT_OPTIONS, useValue: options },\n provideAppInitializer(() => {\n if (!isPlatformBrowser(inject(PLATFORM_ID))) {\n return;\n }\n const api = inject(LtApiUrlService);\n const fetch = inject(LT_FETCH);\n configureLtSync({ basePath: inject(LT_SYNC_OPTIONS).basePath, buildApiUrl: (path) => api.buildUrl(path), fetch: (input, init) => fetch(input, init) });\n }),\n {\n multi: true,\n provide: LT_AUTH_SIGN_OUT_HOOKS,\n useFactory: (): LtAuthSignOutHook => {\n const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n return {\n afterSignOut: () => (isBrowser ? deleteLtSyncDatabase() : undefined),\n beforeSignOut: (userId) => (isBrowser ? flushLtSyncBeforeSignOut(userId ?? undefined, 5000) : undefined),\n };\n },\n },\n ]);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
const DEFAULT_HYDRATION_TIMEOUT = 15e3;
|
|
4
|
+
async function waitForHydration(page, options = {}) {
|
|
5
|
+
const timeout = options.timeout ?? 15e3;
|
|
6
|
+
await page.waitForFunction(() => window["__ltNgBaseHydrated"] === true, { timeout });
|
|
7
|
+
}
|
|
8
|
+
async function gotoAndWaitForHydration(page, url, options = {}) {
|
|
9
|
+
await gotoWithAbortRetry(page, url);
|
|
10
|
+
await waitForHydration(page, options);
|
|
11
|
+
}
|
|
12
|
+
async function gotoWithAbortRetry(page, url, maxAttempts = 3) {
|
|
13
|
+
for (let attempt = 1;; attempt++) try {
|
|
14
|
+
await page.goto(url);
|
|
15
|
+
return;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
+
if (attempt >= maxAttempts || !message.includes("net::ERR_ABORTED")) throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function waitForURLAndHydration(page, url, options = {}) {
|
|
22
|
+
const { timeout = 3e4, hydrationTimeout } = options;
|
|
23
|
+
await page.waitForURL(url, { timeout });
|
|
24
|
+
await waitForHydration(page, { timeout: hydrationTimeout });
|
|
25
|
+
}
|
|
26
|
+
async function fillInput(page, selector, value, options = {}) {
|
|
27
|
+
const { delay = 5, clear = true } = options;
|
|
28
|
+
await page.locator(selector).click();
|
|
29
|
+
if (clear) {
|
|
30
|
+
await page.keyboard.press("ControlOrMeta+a");
|
|
31
|
+
await page.keyboard.press("Backspace");
|
|
32
|
+
}
|
|
33
|
+
await page.keyboard.type(value, { delay });
|
|
34
|
+
}
|
|
35
|
+
async function fillInputs(page, fields, options = {}) {
|
|
36
|
+
for (const [selector, value] of Object.entries(fields)) await fillInput(page, selector, value, options);
|
|
37
|
+
}
|
|
38
|
+
function generateTestUser(prefix = "e2e") {
|
|
39
|
+
const testId = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
40
|
+
return {
|
|
41
|
+
email: `${prefix}-${testId}@test.com`,
|
|
42
|
+
password: `TestPass${testId}!`,
|
|
43
|
+
name: `E2E Test User ${prefix}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function generateRandomString(length = 8, charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") {
|
|
47
|
+
let result = "";
|
|
48
|
+
for (let i = 0; i < length; i++) result += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function generateTOTP(secret, options = {}) {
|
|
52
|
+
const { digits = 6, period = 30, algorithm = "sha1" } = options;
|
|
53
|
+
const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
54
|
+
let bits = "";
|
|
55
|
+
for (const char of secret.toUpperCase().replace(/=/g, "")) {
|
|
56
|
+
const val = base32Chars.indexOf(char);
|
|
57
|
+
if (val === -1) continue;
|
|
58
|
+
bits += val.toString(2).padStart(5, "0");
|
|
59
|
+
}
|
|
60
|
+
const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));
|
|
61
|
+
for (let i = 0; i < keyBytes.length; i++) keyBytes[i] = parseInt(bits.slice(i * 8, (i + 1) * 8), 2);
|
|
62
|
+
const counter = Math.floor(Date.now() / 1e3 / period);
|
|
63
|
+
const counterBuffer = Buffer.alloc(8);
|
|
64
|
+
counterBuffer.writeBigUInt64BE(BigInt(counter));
|
|
65
|
+
const hmac = crypto.createHmac(algorithm, keyBytes);
|
|
66
|
+
hmac.update(counterBuffer);
|
|
67
|
+
const hash = hmac.digest();
|
|
68
|
+
const offset = (hash[hash.length - 1] ?? 0) & 15;
|
|
69
|
+
return ((((hash[offset] ?? 0) & 127) << 24 | ((hash[offset + 1] ?? 0) & 255) << 16 | ((hash[offset + 2] ?? 0) & 255) << 8 | (hash[offset + 3] ?? 0) & 255) % Math.pow(10, digits)).toString().padStart(digits, "0");
|
|
70
|
+
}
|
|
71
|
+
function extractTOTPSecret(otpauthUrl) {
|
|
72
|
+
return decodeURIComponent(otpauthUrl).match(/secret=([A-Z2-7]+)/i)?.[1]?.toUpperCase() ?? null;
|
|
73
|
+
}
|
|
74
|
+
function parseTOTPUrl(otpauthUrl) {
|
|
75
|
+
const decoded = decodeURIComponent(otpauthUrl);
|
|
76
|
+
const pathMatch = decoded.match(/otpauth:\/\/totp\/(?:([^:]+):)?([^?]+)/);
|
|
77
|
+
const getParam = (name) => {
|
|
78
|
+
return decoded.match(new RegExp(`${name}=([^&]+)`, "i"))?.[1] ?? null;
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
issuer: getParam("issuer") ?? pathMatch?.[1] ?? null,
|
|
82
|
+
account: pathMatch?.[2] ?? null,
|
|
83
|
+
secret: getParam("secret")?.toUpperCase() ?? null,
|
|
84
|
+
algorithm: getParam("algorithm") || "SHA1",
|
|
85
|
+
digits: parseInt(getParam("digits") || "6", 10),
|
|
86
|
+
period: parseInt(getParam("period") || "30", 10)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function waitForElement(page, selector, options = {}) {
|
|
90
|
+
const { timeout = 1e4, stable = true } = options;
|
|
91
|
+
const locator = page.locator(selector);
|
|
92
|
+
await locator.waitFor({
|
|
93
|
+
state: "visible",
|
|
94
|
+
timeout
|
|
95
|
+
});
|
|
96
|
+
if (stable) await locator.evaluate((el) => {
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
if (!el.getAnimations || el.getAnimations().length === 0) {
|
|
99
|
+
resolve();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
Promise.allSettled(el.getAnimations().map((a) => a.finished)).then(() => resolve());
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function waitForNetworkIdle(page, options = {}) {
|
|
107
|
+
const { timeout = 3e4 } = options;
|
|
108
|
+
await page.waitForLoadState("networkidle", { timeout });
|
|
109
|
+
}
|
|
110
|
+
async function hasText(page, selector, text) {
|
|
111
|
+
return (await page.locator(selector).textContent())?.includes(text) ?? false;
|
|
112
|
+
}
|
|
113
|
+
async function getAllText(page, selector) {
|
|
114
|
+
const locators = page.locator(selector);
|
|
115
|
+
const count = await locators.count();
|
|
116
|
+
const texts = [];
|
|
117
|
+
for (let i = 0; i < count; i++) {
|
|
118
|
+
const text = await locators.nth(i).textContent();
|
|
119
|
+
if (text) texts.push(text.trim());
|
|
120
|
+
}
|
|
121
|
+
return texts;
|
|
122
|
+
}
|
|
123
|
+
function hashTestPassword(password) {
|
|
124
|
+
return createHash("sha256").update(password).digest("hex");
|
|
125
|
+
}
|
|
126
|
+
async function createTestUser(request, options = {}) {
|
|
127
|
+
const user = options.user ?? generateTestUser(options.prefix);
|
|
128
|
+
const response = await request.post(`${resolveApiUrl(options.apiUrl)}${options.basePath ?? "/iam"}/sign-up/email`, { data: {
|
|
129
|
+
email: user.email,
|
|
130
|
+
name: user.name,
|
|
131
|
+
password: hashTestPassword(user.password),
|
|
132
|
+
...options.extra
|
|
133
|
+
} });
|
|
134
|
+
if (!response.ok()) throw new Error(`createTestUser: sign-up failed with ${response.status()}: ${await response.text()}`);
|
|
135
|
+
let body = null;
|
|
136
|
+
try {
|
|
137
|
+
body = await response.json();
|
|
138
|
+
} catch {
|
|
139
|
+
body = null;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
body,
|
|
143
|
+
response,
|
|
144
|
+
user
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function loginTestUser(page, user, options = {}) {
|
|
148
|
+
await gotoAndWaitForHydration(page, options.loginPath ?? "/auth/login");
|
|
149
|
+
const email = options.emailSelector ?? "input[autocomplete=\"email\"], input[autocomplete=\"username\"], input[type=\"email\"], input[name=\"email\"]";
|
|
150
|
+
const password = options.passwordSelector ?? "input[autocomplete=\"current-password\"], input[type=\"password\"]";
|
|
151
|
+
await fillInput(page, `:is(${email}) >> nth=0`, user.email);
|
|
152
|
+
await fillInput(page, `:is(${password}) >> nth=0`, user.password);
|
|
153
|
+
await page.click(options.submitSelector ?? "button[type=\"submit\"]");
|
|
154
|
+
const expectUrl = options.expectUrl === void 0 ? /\/app(\/|$|\?)/ : options.expectUrl;
|
|
155
|
+
if (expectUrl !== null) await waitForURLAndHydration(page, expectUrl);
|
|
156
|
+
}
|
|
157
|
+
async function loginTestUserViaApi(request, user, options = {}) {
|
|
158
|
+
const response = await request.post(`${resolveApiUrl(options.apiUrl)}${options.basePath ?? "/iam"}/sign-in/email`, { data: {
|
|
159
|
+
email: user.email,
|
|
160
|
+
password: hashTestPassword(user.password)
|
|
161
|
+
} });
|
|
162
|
+
if (!response.ok()) throw new Error(`loginTestUserViaApi: sign-in failed with ${response.status()}: ${await response.text()}`);
|
|
163
|
+
return await response.json();
|
|
164
|
+
}
|
|
165
|
+
function resolveApiUrl(explicit) {
|
|
166
|
+
const env = globalThis.process?.env ?? {};
|
|
167
|
+
return (explicit || env["NG_API_URL"] || env["NG_PUBLIC_API_URL"] || "http://localhost:3000").replace(/\/+$/, "");
|
|
168
|
+
}
|
|
169
|
+
export { DEFAULT_HYDRATION_TIMEOUT, createTestUser, extractTOTPSecret, fillInput, fillInputs, generateRandomString, generateTOTP, generateTestUser, getAllText, gotoAndWaitForHydration, hasText, hashTestPassword, loginTestUser, loginTestUserViaApi, parseTOTPUrl, waitForElement, waitForHydration, waitForNetworkIdle, waitForURLAndHydration };
|
|
170
|
+
|
|
171
|
+
//# sourceMappingURL=lenne.tech-ng-base-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lenne.tech-ng-base-testing.mjs","names":[],"sources":["../../../projects/ng-base/testing/src/lib/playwright-helpers.ts","../../../projects/ng-base/testing/src/lib/auth-helpers.ts"],"sourcesContent":["/// <reference types=\"node\" />\n/**\n * Playwright Test Helpers for Angular Applications\n *\n * This module provides reusable test utilities for E2E testing\n * with Playwright in Angular (SSR) projects using @lenne.tech/ng-base.\n *\n * @example\n * ```typescript\n * import {\n * waitForHydration,\n * gotoAndWaitForHydration,\n * fillInput,\n * generateTestUser,\n * generateTOTP,\n * } from '@lenne.tech/ng-base/testing';\n *\n * test('my test', async ({ page }) => {\n * await gotoAndWaitForHydration(page, '/auth/login');\n * await fillInput(page, 'input[name=\"email\"]', 'test@example.com');\n * });\n * ```\n */\n\nimport type { Page } from '@playwright/test';\nimport * as crypto from 'node:crypto';\n\n// =============================================================================\n// Angular Hydration Helpers\n// =============================================================================\n\n/**\n * Default timeout for hydration waiting (in milliseconds)\n */\nexport const DEFAULT_HYDRATION_TIMEOUT = 15000;\n\n/**\n * Wait for Angular hydration to complete\n *\n * `provideLtNgBase()` sets `window.__ltNgBaseHydrated = true` once the app is stable in the browser\n * (`ApplicationRef.whenStable()`), which is after hydration finished. Event replay (`withEventReplay()`) keeps\n * input made before that moment, but assertions on component state are only reliable afterwards.\n *\n * @param page - Playwright page object\n * @param options - Configuration options\n * @param options.timeout - Maximum time to wait (default: 15000ms)\n *\n * @example\n * ```typescript\n * await waitForHydration(page);\n * await page.click('button'); // Now safe to interact\n * ```\n */\nexport async function waitForHydration(page: Page, options: { timeout?: number } = {}): Promise<void> {\n const timeout = options.timeout ?? DEFAULT_HYDRATION_TIMEOUT;\n\n await page.waitForFunction(() => (window as unknown as Record<string, unknown>)['__ltNgBaseHydrated'] === true, {\n timeout,\n });\n}\n\n/**\n * Navigate to URL and wait for Angular hydration to complete\n *\n * Combines page.goto() with hydration waiting. Use this instead of\n * page.goto() when you need to interact with Vue components immediately.\n *\n * @param page - Playwright page object\n * @param url - URL to navigate to (relative or absolute)\n * @param options - Configuration options\n * @param options.timeout - Maximum time to wait for hydration (default: 15000ms)\n *\n * @example\n * ```typescript\n * await gotoAndWaitForHydration(page, '/auth/login');\n * // Page is now hydrated and ready for interaction\n * ```\n */\nexport async function gotoAndWaitForHydration(page: Page, url: string, options: { timeout?: number } = {}): Promise<void> {\n await gotoWithAbortRetry(page, url);\n await waitForHydration(page, options);\n}\n\n/**\n * `page.goto` that survives the page navigating away on its own while the goto is\n * still pending.\n *\n * Chromium lets a hard navigation the page starts itself (`location.reload()`,\n * `location.href`, `navigateTo(..., { external: true })`) cancel a pending\n * `page.goto` while the page holds user activation, which it does for a few\n * seconds after any Playwright `click()`. A test that clicks a button whose\n * handler reloads the app and then navigates on fails with\n * `page.goto: net::ERR_ABORTED at <url>`. A client-side `navigateTo` (a history\n * push) cancels nothing; without a preceding click, neither does a hard one.\n *\n * Re-issuing the goto is enough: the new navigation in turn cancels the page's\n * still-pending one, so the test lands where it asked to. There is nothing to\n * wait for in between — the competing navigation has not committed yet, so\n * `waitForLoadState('load')` would return at once on the old document.\n *\n * Only `net::ERR_ABORTED` is retried, at most `maxAttempts` times in total; every\n * other error propagates on the first attempt. A 204 response aborts the same way\n * and still fails, only after the retries. Measured in Chromium, the only engine\n * the lt starters test; other engines may word the abort differently.\n */\nasync function gotoWithAbortRetry(page: Page, url: string, maxAttempts = 3): Promise<void> {\n for (let attempt = 1; ; attempt++) {\n try {\n await page.goto(url);\n return;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (attempt >= maxAttempts || !message.includes('net::ERR_ABORTED')) {\n throw error;\n }\n }\n }\n}\n\n/**\n * Wait for URL navigation and then Angular hydration\n *\n * Use after form submissions or actions that trigger navigation.\n * Ensures both the navigation completes AND the new page is hydrated.\n *\n * @param page - Playwright page object\n * @param url - URL pattern to wait for (string or RegExp)\n * @param options - Configuration options\n * @param options.timeout - Maximum time to wait for URL (default: 30000ms)\n * @param options.hydrationTimeout - Maximum time to wait for hydration (default: 15000ms)\n *\n * @example\n * ```typescript\n * await page.click('button[type=\"submit\"]');\n * await waitForURLAndHydration(page, /\\/dashboard/);\n * ```\n */\nexport async function waitForURLAndHydration(page: Page, url: string | RegExp, options: { timeout?: number; hydrationTimeout?: number } = {}): Promise<void> {\n const { timeout = 30000, hydrationTimeout } = options;\n\n await page.waitForURL(url, { timeout });\n await waitForHydration(page, { timeout: hydrationTimeout });\n}\n\n// =============================================================================\n// Vue Input Helpers\n// =============================================================================\n\n/**\n * Fill an input field with proper Vue 3 reactivity support\n *\n * Uses keyboard typing instead of Playwright's fill() to properly\n * trigger Vue's v-model updates. This is necessary because fill()\n * sets the value directly without firing input events in some cases.\n *\n * @param page - Playwright page object\n * @param selector - CSS selector for the input element\n * @param value - Value to type into the input\n * @param options - Configuration options\n * @param options.delay - Delay between keystrokes in ms (default: 5)\n * @param options.clear - Whether to clear existing content (default: true)\n *\n * @example\n * ```typescript\n * await fillInput(page, 'input[name=\"email\"]', 'test@example.com');\n * await fillInput(page, '#password', 'secret', { delay: 10 });\n * ```\n */\nexport async function fillInput(page: Page, selector: string, value: string, options: { delay?: number; clear?: boolean } = {}): Promise<void> {\n const { delay = 5, clear = true } = options;\n const locator = page.locator(selector);\n\n // Click to focus the input\n await locator.click();\n\n // Clear existing content if requested\n if (clear) {\n // ControlOrMeta: Cmd+A on macOS, Ctrl+A elsewhere (Control+A only moves the caret on macOS).\n await page.keyboard.press('ControlOrMeta+a');\n await page.keyboard.press('Backspace');\n }\n\n // Type the value - real keyboard events, so Angular bindings and Signal Forms see every keystroke\n await page.keyboard.type(value, { delay });\n}\n\n/**\n * Fill multiple input fields at once\n *\n * Convenience function for filling multiple form fields.\n *\n * @param page - Playwright page object\n * @param fields - Object mapping selectors to values\n * @param options - Configuration options passed to fillInput\n *\n * @example\n * ```typescript\n * await fillInputs(page, {\n * 'input[name=\"email\"]': 'test@example.com',\n * 'input[name=\"password\"]': 'secret123',\n * });\n * ```\n */\nexport async function fillInputs(page: Page, fields: Record<string, string>, options: { delay?: number; clear?: boolean } = {}): Promise<void> {\n for (const [selector, value] of Object.entries(fields)) {\n await fillInput(page, selector, value, options);\n }\n}\n\n// =============================================================================\n// Test Data Generators\n// =============================================================================\n\n/**\n * Test user credentials interface\n */\nexport interface TestUser {\n email: string;\n password: string;\n name: string;\n}\n\n/**\n * Generate unique test user credentials\n *\n * Creates unique email, password, and name for test isolation.\n * Each call generates different credentials to avoid test conflicts.\n *\n * @param prefix - Prefix for the email address (default: 'e2e')\n * @returns Object with email, password, and name\n *\n * @example\n * ```typescript\n * const user = generateTestUser('auth-test');\n * // { email: 'auth-test-abc123@test.com', password: 'TestPassabc123!', name: 'E2E Test User auth-test' }\n * ```\n */\nexport function generateTestUser(prefix: string = 'e2e'): TestUser {\n const testId = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);\n\n return {\n email: `${prefix}-${testId}@test.com`,\n password: `TestPass${testId}!`,\n name: `E2E Test User ${prefix}`,\n };\n}\n\n/**\n * Generate a random string for test data\n *\n * @param length - Length of the string (default: 8)\n * @param charset - Characters to use (default: alphanumeric)\n *\n * @example\n * ```typescript\n * const id = generateRandomString(12);\n * const code = generateRandomString(6, '0123456789');\n * ```\n */\nexport function generateRandomString(length: number = 8, charset: string = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): string {\n let result = '';\n for (let i = 0; i < length; i++) {\n result += charset.charAt(Math.floor(Math.random() * charset.length));\n }\n return result;\n}\n\n// =============================================================================\n// TOTP / 2FA Helpers\n// =============================================================================\n\n/**\n * Generate a TOTP code from a secret (RFC 6238)\n *\n * Implements the Time-based One-Time Password algorithm for testing\n * 2FA functionality. Compatible with Google Authenticator and similar apps.\n *\n * @param secret - Base32-encoded TOTP secret\n * @param options - Configuration options\n * @param options.digits - Number of digits in the code (default: 6)\n * @param options.period - Time period in seconds (default: 30)\n * @param options.algorithm - Hash algorithm (default: 'sha1')\n * @returns 6-digit TOTP code as string\n *\n * @example\n * ```typescript\n * const secret = 'JBSWY3DPEHPK3PXP';\n * const code = generateTOTP(secret);\n * await page.fill('input[name=\"code\"]', code);\n * ```\n */\nexport function generateTOTP(secret: string, options: { digits?: number; period?: number; algorithm?: 'sha1' | 'sha256' | 'sha512' } = {}): string {\n const { digits = 6, period = 30, algorithm = 'sha1' } = options;\n\n // Decode Base32 secret\n const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '';\n\n for (const char of secret.toUpperCase().replace(/=/g, '')) {\n const val = base32Chars.indexOf(char);\n if (val === -1) continue;\n bits += val.toString(2).padStart(5, '0');\n }\n\n const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));\n for (let i = 0; i < keyBytes.length; i++) {\n keyBytes[i] = parseInt(bits.slice(i * 8, (i + 1) * 8), 2);\n }\n\n // Calculate counter based on current time\n const counter = Math.floor(Date.now() / 1000 / period);\n const counterBuffer = Buffer.alloc(8);\n counterBuffer.writeBigUInt64BE(BigInt(counter));\n\n // Generate HMAC\n const hmac = crypto.createHmac(algorithm, keyBytes);\n hmac.update(counterBuffer);\n const hash = hmac.digest();\n\n // Dynamic truncation\n const offset = (hash[hash.length - 1] ?? 0) & 0x0f;\n const code = (((hash[offset] ?? 0) & 0x7f) << 24) | (((hash[offset + 1] ?? 0) & 0xff) << 16) | (((hash[offset + 2] ?? 0) & 0xff) << 8) | ((hash[offset + 3] ?? 0) & 0xff);\n\n // Generate code with correct number of digits\n const mod = Math.pow(10, digits);\n return (code % mod).toString().padStart(digits, '0');\n}\n\n/**\n * Extract TOTP secret from otpauth:// URL\n *\n * Parses the secret parameter from a TOTP QR code URL.\n *\n * @param otpauthUrl - The otpauth:// URL (or data URL containing it)\n * @returns The extracted secret, or null if not found\n *\n * @example\n * ```typescript\n * const qrSrc = await page.locator('img[alt=\"QR Code\"]').getAttribute('src');\n * const secret = extractTOTPSecret(qrSrc);\n * const code = generateTOTP(secret);\n * ```\n */\nexport function extractTOTPSecret(otpauthUrl: string): string | null {\n // Handle URL-encoded strings (e.g., from QR code image src)\n const decoded = decodeURIComponent(otpauthUrl);\n\n // Extract secret parameter\n const match = decoded.match(/secret=([A-Z2-7]+)/i);\n return match?.[1]?.toUpperCase() ?? null;\n}\n\n/**\n * Extract all parameters from otpauth:// URL\n *\n * @param otpauthUrl - The otpauth:// URL\n * @returns Object with issuer, account, secret, algorithm, digits, period\n *\n * @example\n * ```typescript\n * const params = parseTOTPUrl(otpauthUrl);\n * console.log(params.issuer, params.secret);\n * ```\n */\nexport function parseTOTPUrl(otpauthUrl: string): {\n issuer: string | null;\n account: string | null;\n secret: string | null;\n algorithm: string;\n digits: number;\n period: number;\n} {\n const decoded = decodeURIComponent(otpauthUrl);\n\n // Extract account from path: otpauth://totp/Issuer:account@example.com\n const pathMatch = decoded.match(/otpauth:\\/\\/totp\\/(?:([^:]+):)?([^?]+)/);\n\n // Extract query parameters\n const getParam = (name: string): string | null => {\n const match = decoded.match(new RegExp(`${name}=([^&]+)`, 'i'));\n return match?.[1] ?? null;\n };\n\n return {\n issuer: getParam('issuer') ?? pathMatch?.[1] ?? null,\n account: pathMatch?.[2] ?? null,\n secret: getParam('secret')?.toUpperCase() ?? null,\n algorithm: getParam('algorithm') || 'SHA1',\n digits: parseInt(getParam('digits') || '6', 10),\n period: parseInt(getParam('period') || '30', 10),\n };\n}\n\n// =============================================================================\n// Wait Helpers\n// =============================================================================\n\n/**\n * Wait for an element to be visible and stable\n *\n * Useful for waiting for animations to complete or elements to settle.\n *\n * @param page - Playwright page object\n * @param selector - CSS selector for the element\n * @param options - Configuration options\n * @param options.timeout - Maximum time to wait (default: 10000ms)\n * @param options.stable - Wait for element to be stable (default: true)\n *\n * @example\n * ```typescript\n * await waitForElement(page, '.modal-content');\n * ```\n */\nexport async function waitForElement(page: Page, selector: string, options: { timeout?: number; stable?: boolean } = {}): Promise<void> {\n const { timeout = 10000, stable = true } = options;\n const locator = page.locator(selector);\n\n await locator.waitFor({ state: 'visible', timeout });\n\n if (stable) {\n // Wait for element to be stable (no longer animating)\n await locator.evaluate((el) => {\n return new Promise<void>((resolve) => {\n if (!el.getAnimations || el.getAnimations().length === 0) {\n resolve();\n return;\n }\n // allSettled, not all: `finished` REJECTS (AbortError) when an animation is cancelled\n // instead of finishing, and with `all` this promise then never resolved, so the test\n // hung until Playwright's timeout. A cancelled animation is not running either.\n void Promise.allSettled(el.getAnimations().map((a) => a.finished)).then(() => resolve());\n });\n });\n }\n}\n\n/**\n * Wait for network to be idle\n *\n * Useful for waiting for all API calls to complete.\n *\n * @param page - Playwright page object\n * @param options - Configuration options\n * @param options.timeout - Maximum time to wait (default: 30000ms)\n *\n * @example\n * ```typescript\n * await page.click('button[type=\"submit\"]');\n * await waitForNetworkIdle(page);\n * ```\n */\nexport async function waitForNetworkIdle(page: Page, options: { timeout?: number } = {}): Promise<void> {\n const { timeout = 30000 } = options;\n await page.waitForLoadState('networkidle', { timeout });\n}\n\n// =============================================================================\n// Assertion Helpers\n// =============================================================================\n\n/**\n * Check if an element contains specific text\n *\n * @param page - Playwright page object\n * @param selector - CSS selector for the element\n * @param text - Text to search for\n * @returns true if text is found\n *\n * @example\n * ```typescript\n * const hasError = await hasText(page, '.error', 'Invalid email');\n * ```\n */\nexport async function hasText(page: Page, selector: string, text: string): Promise<boolean> {\n const locator = page.locator(selector);\n const content = await locator.textContent();\n return content?.includes(text) ?? false;\n}\n\n/**\n * Get all text content from matching elements\n *\n * @param page - Playwright page object\n * @param selector - CSS selector for the elements\n * @returns Array of text content\n *\n * @example\n * ```typescript\n * const errors = await getAllText(page, '.error-message');\n * ```\n */\nexport async function getAllText(page: Page, selector: string): Promise<string[]> {\n const locators = page.locator(selector);\n const count = await locators.count();\n const texts: string[] = [];\n\n for (let i = 0; i < count; i++) {\n const text = await locators.nth(i).textContent();\n if (text) texts.push(text.trim());\n }\n\n return texts;\n}\n","/// <reference types=\"node\" />\nimport type { APIRequestContext, APIResponse, Page } from '@playwright/test';\nimport { createHash } from 'node:crypto';\n\nimport { fillInput, generateTestUser, gotoAndWaitForHydration, type TestUser, waitForURLAndHydration } from './playwright-helpers';\n\n/** SHA-256 hex of a password, as the browser sends it (nest-server expects hashed passwords). */\nexport function hashTestPassword(password: string): string {\n return createHash('sha256').update(password).digest('hex');\n}\n\n/** Options of {@link createTestUser}. */\nexport interface CreateTestUserOptions {\n /** API base URL, e.g. `http://localhost:3000` (default `NG_API_URL` / `NG_PUBLIC_API_URL`, else `http://localhost:3000`). */\n apiUrl?: string;\n /** Better Auth base path (default `/iam`). */\n basePath?: string;\n /** Extra sign-up fields (e.g. `termsAndPrivacyAccepted: true` when the backend's `signUpChecks` require it). */\n extra?: Record<string, unknown>;\n /** Prefix for the generated email (default `e2e`). */\n prefix?: string;\n /** Use this user instead of a generated one. */\n user?: TestUser;\n}\n\n/** Result of {@link createTestUser}. */\nexport interface CreatedTestUser {\n response: APIResponse;\n /** Response body (`{ token?, user }`). */\n body: Record<string, unknown> | null;\n user: TestUser;\n}\n\n/**\n * Create a user through the API (`POST <api>/iam/sign-up/email`, password hashed like the browser does). Fails the\n * test with the response text when sign-up does not answer 2xx.\n *\n * @example\n * const { user } = await createTestUser(request, { extra: { termsAndPrivacyAccepted: true } });\n * await loginTestUser(page, user);\n */\nexport async function createTestUser(request: APIRequestContext, options: CreateTestUserOptions = {}): Promise<CreatedTestUser> {\n const user = options.user ?? generateTestUser(options.prefix);\n const response = await request.post(`${resolveApiUrl(options.apiUrl)}${options.basePath ?? '/iam'}/sign-up/email`, {\n data: { email: user.email, name: user.name, password: hashTestPassword(user.password), ...options.extra },\n });\n if (!response.ok()) {\n throw new Error(`createTestUser: sign-up failed with ${response.status()}: ${await response.text()}`);\n }\n let body: Record<string, unknown> | null = null;\n try {\n body = (await response.json()) as Record<string, unknown>;\n } catch {\n body = null;\n }\n return { body, response, user };\n}\n\n/** Options of {@link loginTestUser}. */\nexport interface LoginTestUserOptions {\n /** Selector of the email input (default: an input with `autocomplete=\"email\"`/`\"username\"`, `type=\"email\"` or `name=\"email\"`). */\n emailSelector?: string;\n /** URL (or pattern) expected after the login (default `/app`). Pass `null` to skip the wait. */\n expectUrl?: RegExp | string | null;\n /** Login page (default `/auth/login`). */\n loginPath?: string;\n /** Selector of the password input (default: `autocomplete=\"current-password\"`, else the first `type=\"password\"`). */\n passwordSelector?: string;\n /** Selector of the submit button (default `button[type=\"submit\"]`). */\n submitSelector?: string;\n}\n\n/** Sign in through the login form (waits for hydration first) and wait for the target page. */\nexport async function loginTestUser(page: Page, user: Pick<TestUser, 'email' | 'password'>, options: LoginTestUserOptions = {}): Promise<void> {\n await gotoAndWaitForHydration(page, options.loginPath ?? '/auth/login');\n // Signal Forms name fields by their path (`login.email`), so the defaults match on autocomplete/type instead of name.\n const email = options.emailSelector ?? 'input[autocomplete=\"email\"], input[autocomplete=\"username\"], input[type=\"email\"], input[name=\"email\"]';\n const password = options.passwordSelector ?? 'input[autocomplete=\"current-password\"], input[type=\"password\"]';\n await fillInput(page, `:is(${email}) >> nth=0`, user.email);\n await fillInput(page, `:is(${password}) >> nth=0`, user.password);\n await page.click(options.submitSelector ?? 'button[type=\"submit\"]');\n const expectUrl = options.expectUrl === undefined ? /\\/app(\\/|$|\\?)/ : options.expectUrl;\n if (expectUrl !== null) {\n await waitForURLAndHydration(page, expectUrl);\n }\n}\n\n/**\n * Sign in through the API (`POST <api>/iam/sign-in/email`); the request context keeps the session cookie for later\n * API calls. Returns the response body (`{ token?, user }`).\n */\nexport async function loginTestUserViaApi(\n request: APIRequestContext,\n user: Pick<TestUser, 'email' | 'password'>,\n options: Pick<CreateTestUserOptions, 'apiUrl' | 'basePath'> = {},\n): Promise<Record<string, unknown>> {\n const response = await request.post(`${resolveApiUrl(options.apiUrl)}${options.basePath ?? '/iam'}/sign-in/email`, {\n data: { email: user.email, password: hashTestPassword(user.password) },\n });\n if (!response.ok()) {\n throw new Error(`loginTestUserViaApi: sign-in failed with ${response.status()}: ${await response.text()}`);\n }\n return (await response.json()) as Record<string, unknown>;\n}\n\nfunction resolveApiUrl(explicit?: string): string {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {};\n return (explicit || env['NG_API_URL'] || env['NG_PUBLIC_API_URL'] || 'http://localhost:3000').replace(/\\/+$/, '');\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { DestroyRef, PLATFORM_ID, REQUEST, Service, computed, inject, makeEnvironmentProviders, provideAppInitializer, signal } from "@angular/core";
|
|
3
|
+
import { LT_TOAST_ADAPTER, LtCookieService } from "@lenne.tech/ng-base";
|
|
4
|
+
import { DOCUMENT, isPlatformBrowser } from "@angular/common";
|
|
5
|
+
const LT_COLOR_MODE_COOKIE = "lt-color-mode";
|
|
6
|
+
const LT_COLOR_MODE_SCRIPT = `(function(){try{var m=(document.cookie.match(/(?:^|; )${LT_COLOR_MODE_COOKIE}=([^;]*)/)||[])[1]||'system';var d=m==='dark'||(m==='system'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',d);document.documentElement.style.colorScheme=d?'dark':'light';}catch(e){}})();`;
|
|
7
|
+
const COOKIE_MAX_AGE = 31536e3;
|
|
8
|
+
const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
9
|
+
var LtColorModeService = class LtColorModeService {
|
|
10
|
+
document = inject(DOCUMENT);
|
|
11
|
+
cookies = inject(LtCookieService);
|
|
12
|
+
request = inject(REQUEST, { optional: true });
|
|
13
|
+
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
14
|
+
modeState = signal(parseLtColorMode(this.cookies.get(LT_COLOR_MODE_COOKIE)), ...ngDevMode ? [{ debugName: "modeState" }] : /* istanbul ignore next */ []);
|
|
15
|
+
systemDark = signal(false, ...ngDevMode ? [{ debugName: "systemDark" }] : /* istanbul ignore next */ []);
|
|
16
|
+
mode = this.modeState.asReadonly();
|
|
17
|
+
resolved = computed(() => {
|
|
18
|
+
const mode = this.modeState();
|
|
19
|
+
if (mode === "system") return this.systemDark() ? "dark" : "light";
|
|
20
|
+
return mode;
|
|
21
|
+
}, ...ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []);
|
|
22
|
+
constructor() {
|
|
23
|
+
if (this.isBrowser) this.listenToSystemScheme();
|
|
24
|
+
else this.systemDark.set(this.request?.headers.get("sec-ch-prefers-color-scheme")?.toLowerCase() === "dark");
|
|
25
|
+
this.apply();
|
|
26
|
+
}
|
|
27
|
+
setMode(mode) {
|
|
28
|
+
this.modeState.set(mode);
|
|
29
|
+
this.cookies.set(LT_COLOR_MODE_COOKIE, mode, {
|
|
30
|
+
maxAge: COOKIE_MAX_AGE,
|
|
31
|
+
path: "/",
|
|
32
|
+
sameSite: "lax"
|
|
33
|
+
});
|
|
34
|
+
this.apply();
|
|
35
|
+
}
|
|
36
|
+
toggle() {
|
|
37
|
+
this.setMode(this.resolved() === "dark" ? "light" : "dark");
|
|
38
|
+
}
|
|
39
|
+
listenToSystemScheme() {
|
|
40
|
+
const view = this.document.defaultView;
|
|
41
|
+
if (!view || typeof view.matchMedia !== "function") return;
|
|
42
|
+
const query = view.matchMedia(DARK_QUERY);
|
|
43
|
+
this.systemDark.set(query.matches);
|
|
44
|
+
const listener = (event) => {
|
|
45
|
+
this.systemDark.set(event.matches);
|
|
46
|
+
this.apply();
|
|
47
|
+
};
|
|
48
|
+
query.addEventListener?.("change", listener);
|
|
49
|
+
inject(DestroyRef).onDestroy(() => query.removeEventListener?.("change", listener));
|
|
50
|
+
}
|
|
51
|
+
apply() {
|
|
52
|
+
const root = this.document.documentElement;
|
|
53
|
+
if (!root) return;
|
|
54
|
+
const dark = this.resolved() === "dark";
|
|
55
|
+
root.classList.toggle("dark", dark);
|
|
56
|
+
try {
|
|
57
|
+
root.style.colorScheme = dark ? "dark" : "light";
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
61
|
+
minVersion: "12.0.0",
|
|
62
|
+
version: "22.2.4",
|
|
63
|
+
ngImport: i0,
|
|
64
|
+
type: LtColorModeService,
|
|
65
|
+
deps: [],
|
|
66
|
+
target: i0.ɵɵFactoryTarget.Service
|
|
67
|
+
});
|
|
68
|
+
static ɵprov = i0.ɵɵngDeclareService({
|
|
69
|
+
minVersion: "22.0.0",
|
|
70
|
+
version: "22.2.4",
|
|
71
|
+
ngImport: i0,
|
|
72
|
+
type: LtColorModeService
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
76
|
+
minVersion: "12.0.0",
|
|
77
|
+
version: "22.2.4",
|
|
78
|
+
ngImport: i0,
|
|
79
|
+
type: LtColorModeService,
|
|
80
|
+
decorators: [{ type: Service }],
|
|
81
|
+
ctorParameters: () => []
|
|
82
|
+
});
|
|
83
|
+
function parseLtColorMode(value) {
|
|
84
|
+
return value === "dark" || value === "light" || value === "system" ? value : "system";
|
|
85
|
+
}
|
|
86
|
+
let sonner;
|
|
87
|
+
function loadToast() {
|
|
88
|
+
sonner ??= import("@spartan-ng/brain/sonner").then((module) => module.toast);
|
|
89
|
+
return sonner;
|
|
90
|
+
}
|
|
91
|
+
const ltSonnerToastAdapter = { show(options) {
|
|
92
|
+
const message = options.title ?? options.description ?? "";
|
|
93
|
+
const data = {
|
|
94
|
+
description: options.title ? options.description : void 0,
|
|
95
|
+
duration: options.duration
|
|
96
|
+
};
|
|
97
|
+
loadToast().then((toast) => {
|
|
98
|
+
switch (options.color) {
|
|
99
|
+
case "error":
|
|
100
|
+
toast.error(message, data);
|
|
101
|
+
break;
|
|
102
|
+
case "success":
|
|
103
|
+
toast.success(message, data);
|
|
104
|
+
break;
|
|
105
|
+
case "warning":
|
|
106
|
+
toast.warning(message, data);
|
|
107
|
+
break;
|
|
108
|
+
case "info":
|
|
109
|
+
toast.info(message, data);
|
|
110
|
+
break;
|
|
111
|
+
default: toast(message, data);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return true;
|
|
115
|
+
} };
|
|
116
|
+
function provideLtUi(options = {}) {
|
|
117
|
+
return makeEnvironmentProviders([...options.sonnerToasts === false ? [] : [{
|
|
118
|
+
provide: LT_TOAST_ADAPTER,
|
|
119
|
+
useValue: ltSonnerToastAdapter
|
|
120
|
+
}], ...options.colorMode === false ? [] : [provideAppInitializer(() => void inject(LtColorModeService))]]);
|
|
121
|
+
}
|
|
122
|
+
export { LT_COLOR_MODE_COOKIE, LT_COLOR_MODE_SCRIPT, LtColorModeService, ltSonnerToastAdapter, provideLtUi };
|
|
123
|
+
|
|
124
|
+
//# sourceMappingURL=lenne.tech-ng-base-ui-provide.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lenne.tech-ng-base-ui-provide.mjs","names":[],"sources":["../../../projects/ng-base/ui/provide/src/lib/color-mode.ts","../../../projects/ng-base/ui/provide/src/lib/provide-lt-ui.ts"],"sourcesContent":["import { DOCUMENT, isPlatformBrowser } from '@angular/common';\nimport { computed, DestroyRef, inject, PLATFORM_ID, REQUEST, Service, signal } from '@angular/core';\nimport { LtCookieService } from '@lenne.tech/ng-base';\n\n/** Color mode preference. */\nexport type LtColorMode = 'dark' | 'light' | 'system';\n\n/** Cookie holding the preference (read during SSR so the first paint has the right theme). */\nexport const LT_COLOR_MODE_COOKIE = 'lt-color-mode';\n\n/**\n * Inline script for `<head>` (before any stylesheet) that sets `.dark` on `<html>` from the cookie or\n * `prefers-color-scheme` before the first paint, so dark mode never flashes light.\n */\nexport const LT_COLOR_MODE_SCRIPT = `(function(){try{var m=(document.cookie.match(/(?:^|; )${LT_COLOR_MODE_COOKIE}=([^;]*)/)||[])[1]||'system';var d=m==='dark'||(m==='system'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',d);document.documentElement.style.colorScheme=d?'dark':'light';}catch(e){}})();`;\n\n/** Lifetime of the preference cookie (one year, in seconds). */\nconst COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\nconst DARK_QUERY = '(prefers-color-scheme: dark)';\n\n/**\n * Light / dark / system color mode: toggles `.dark` on `<html>` (Tailwind `dark:` variant and the theme tokens),\n * persists the preference in a cookie (SSR reads it), follows `prefers-color-scheme` in `system` mode.\n *\n * - Server: reads the preference from the request cookie and sets or clears `.dark` on the rendered document, so the\n * HTML arrives with the right theme. In `system` mode the server only knows the scheme when the browser sends the\n * `Sec-CH-Prefers-Color-Scheme` client hint (the app opts in with `Accept-CH`); otherwise `LT_COLOR_MODE_SCRIPT`\n * fixes it before the first paint.\n * - Browser: toggles the class and `color-scheme` on `<html>` and listens to `prefers-color-scheme` changes.\n *\n * The class is applied as soon as the service exists (an `lt-color-mode-button` creates it). When the app renders no\n * button, create it at startup: `provideAppInitializer(() => void inject(LtColorModeService))`.\n */\n@Service()\nexport class LtColorModeService {\n private readonly document = inject(DOCUMENT);\n private readonly cookies = inject(LtCookieService);\n private readonly request = inject(REQUEST, { optional: true });\n private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n private readonly modeState = signal<LtColorMode>(parseLtColorMode(this.cookies.get(LT_COLOR_MODE_COOKIE)));\n private readonly systemDark = signal(false);\n\n /** The preference. */\n readonly mode = this.modeState.asReadonly();\n /** The effective mode. */\n readonly resolved = computed<'dark' | 'light'>(() => {\n const mode = this.modeState();\n if (mode === 'system') {\n return this.systemDark() ? 'dark' : 'light';\n }\n return mode;\n });\n\n constructor() {\n if (this.isBrowser) {\n this.listenToSystemScheme();\n } else {\n this.systemDark.set(this.request?.headers.get('sec-ch-prefers-color-scheme')?.toLowerCase() === 'dark');\n }\n this.apply();\n }\n\n /** Set the preference (persisted for one year in the `lt-color-mode` cookie). */\n setMode(mode: LtColorMode): void {\n this.modeState.set(mode);\n this.cookies.set(LT_COLOR_MODE_COOKIE, mode, { maxAge: COOKIE_MAX_AGE, path: '/', sameSite: 'lax' });\n this.apply();\n }\n\n /** Switch between light and dark (leaves `system`). */\n toggle(): void {\n this.setMode(this.resolved() === 'dark' ? 'light' : 'dark');\n }\n\n private listenToSystemScheme(): void {\n const view = this.document.defaultView;\n if (!view || typeof view.matchMedia !== 'function') {\n return;\n }\n const query = view.matchMedia(DARK_QUERY);\n this.systemDark.set(query.matches);\n const listener = (event: MediaQueryListEvent) => {\n this.systemDark.set(event.matches);\n this.apply();\n };\n query.addEventListener?.('change', listener);\n inject(DestroyRef).onDestroy(() => query.removeEventListener?.('change', listener));\n }\n\n /** Write the resolved mode to `<html>` (server: the SSR document; browser: the live document). */\n private apply(): void {\n const root = this.document.documentElement;\n if (!root) {\n return;\n }\n const dark = this.resolved() === 'dark';\n root.classList.toggle('dark', dark);\n try {\n root.style.colorScheme = dark ? 'dark' : 'light';\n } catch {\n // The server DOM may not implement `style.colorScheme`; the class is what the theme relies on.\n }\n }\n}\n\n/** Parse a stored preference; unknown values fall back to `system`. */\nfunction parseLtColorMode(value: string | null): LtColorMode {\n return value === 'dark' || value === 'light' || value === 'system' ? value : 'system';\n}\n","import { inject, makeEnvironmentProviders, provideAppInitializer, type EnvironmentProviders } from '@angular/core';\nimport { LT_TOAST_ADAPTER, type LtToastAdapter, type LtToastOptions } from '@lenne.tech/ng-base';\n\nimport { LtColorModeService } from './color-mode';\n\n/** Options of {@link provideLtUi}. */\nexport interface LtUiOptions {\n /** Start `LtColorModeService` at bootstrap, so the `.dark` class is right from the SSR render on (default `true`). */\n colorMode?: boolean;\n /** Bind `LtToastService` / error toasts to Spartan Sonner (`<hlm-toaster />` in the app shell). Default `true`. */\n sonnerToasts?: boolean;\n}\n\ntype LtSonnerToast = typeof import('@spartan-ng/brain/sonner').toast;\nlet sonner: Promise<LtSonnerToast> | undefined;\n\n/** Load Sonner's `toast()` on first use (keeps it out of the initial bundle). */\nfunction loadToast(): Promise<LtSonnerToast> {\n sonner ??= import('@spartan-ng/brain/sonner').then((module) => module.toast);\n return sonner;\n}\n\n/** Sonner toast adapter: maps the lt colors onto Sonner's variants; loads Sonner lazily on the first toast. */\nexport const ltSonnerToastAdapter: LtToastAdapter = {\n show(options: LtToastOptions): boolean {\n const message = options.title ?? options.description ?? '';\n const data = { description: options.title ? options.description : undefined, duration: options.duration };\n void loadToast().then((toast) => {\n switch (options.color) {\n case 'error':\n toast.error(message, data);\n break;\n case 'success':\n toast.success(message, data);\n break;\n case 'warning':\n toast.warning(message, data);\n break;\n case 'info':\n toast.info(message, data);\n break;\n default:\n toast(message, data);\n }\n });\n return true;\n },\n};\n\n/**\n * UI providers for `app.config.ts`: the color mode (started at bootstrap) and the toast bridge to Spartan Sonner (loaded\n * on the first toast). Put `<hlm-toaster />` (from `@lenne.tech/ng-base/ui`, ideally in a `@defer` block) into the\n * app shell. Lives in its own entry point and imports neither the component layer nor the CDK, so it adds almost\n * nothing to the initial bundle. Overlay defaults (toasts above dialogs): `provideLtUiOverlayDefaults()` from\n * `@lenne.tech/ng-base/ui` in the providers of the (lazy) routes that open overlays.\n */\nexport function provideLtUi(options: LtUiOptions = {}): EnvironmentProviders {\n return makeEnvironmentProviders([\n ...(options.sonnerToasts === false ? [] : [{ provide: LT_TOAST_ADAPTER, useValue: ltSonnerToastAdapter }]),\n ...(options.colorMode === false ? [] : [provideAppInitializer(() => void inject(LtColorModeService))]),\n ]);\n}\n"],"mappings":"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"}
|