@ofidj/entry 3.10.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 fidj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @ofidj/entry
2
+
3
+ The Fidj entry, as a package: the sign-in and account screens, the service
4
+ agreement, the provider window and the design system. One implementation, shared
5
+ by Fidj's own console, every generated app and mleweb.
6
+
7
+ Read the [workspace README](../README.md) for the product target and
8
+ [AGENTS.md](../AGENTS.md) for the working rules. The flow these screens
9
+ implement is [Entry: one flow, the same everywhere](../README.md#entry-one-flow-the-same-everywhere).
10
+
11
+ ## Why it exists
12
+
13
+ These files used to live only in `generator-fidj`'s template, which meant the
14
+ only way to ship them was to generate an app. Fidj's own console was therefore
15
+ generated too — `fidj-app` built as a module, wrapped by the generator, published
16
+ from `.gen/fidj/www` — and you could not tell from `fidj-app/src` what
17
+ `fidj.ovh` actually draws. The alternative, keeping a second Angular copy in
18
+ step by hand, is what produced four unguarded copies of the service agreement.
19
+
20
+ A package is the third answer: the console imports the same code a generated app
21
+ renders, so there is one implementation and no hand-porting.
22
+
23
+ ## Use it
24
+
25
+ ```sh
26
+ npm install @ofidj/entry
27
+ ```
28
+
29
+ ```ts
30
+ import { providerEntry, bindAgreement, acceptedAgreement } from "@ofidj/entry";
31
+ import "@ofidj/entry/tokens.css";
32
+ import "@ofidj/entry/style.css";
33
+ ```
34
+
35
+ `tokens.css` defines every colour, family and radius the product paints with;
36
+ nothing else may introduce a literal. `fonts.css` is shipped separately because
37
+ each consumer serves its own font files and the `url()`s differ by one path
38
+ prefix.
39
+
40
+ ## What is here, and what is not
41
+
42
+ Moved, and covered by characterization tests in `test/`:
43
+
44
+ | Module | What it holds |
45
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
46
+ | `service-agreement.ts` | the agreement block and its loading, `providerEntry`'s three shapes, the remembered address, sign-in error wording |
47
+ | `provider-window.ts` | the Fidj window and the answer it relays back |
48
+ | `version.ts` | the `fidj@<version>` badge |
49
+ | `tokens.css`, `fonts.css`, `style.css` | the design system |
50
+
51
+ Still in `generator-fidj`'s template, and next to move: the screens inside
52
+ `content.ts` — the sign-in entry, `#/forgot`, `#/reset`, `#/verify`, `#/account`
53
+ and the Fidj-hosted OIDC interaction page. They import `app.config.json`
54
+ directly, so they move behind a config argument rather than verbatim.
55
+
56
+ **The generator has not switched over yet.** Its template still carries its own
57
+ copies and builds from them, so a change made here reaches nobody until that
58
+ lands. `test/no-drift-from-the-generator.test.mjs` fails while the two disagree,
59
+ and is deleted in the same change that deletes the generator's copies.
60
+
61
+ ## Release
62
+
63
+ This package joins [the one release series](../README.md#one-release-series-for-every-ofidj-package)
64
+ and publishes from `./dist` when its `package` branch is pushed. It sits after
65
+ `@ofidj/contracts` and `@ofidj/node` and before `@ofidj/generator-fidj` in the
66
+ dependency chain, because the generator and the console both consume it.
package/fonts.css ADDED
@@ -0,0 +1,92 @@
1
+ /* The design system's typefaces, self-hosted.
2
+ *
3
+ * A generated site has to stay statically hostable, and an app that sells
4
+ * privacy should not hand a third party a request log on every sign-in. Latin
5
+ * and latin-ext only; other scripts fall back to the stack in tokens.css.
6
+ *
7
+ * The url()s are runtime paths relative to the built main.css, not bundle
8
+ * inputs — the files ride along in public/ and esbuild leaves them external.
9
+ * A consumer that serves the same tokens from somewhere else (fidj-app mounts
10
+ * its console under /module/) writes its own @font-face block instead.
11
+ */
12
+
13
+ @font-face {
14
+ font-family: "IBM Plex Sans";
15
+ font-style: normal;
16
+ font-weight: 400 600;
17
+ font-display: swap;
18
+ src: url("fonts/IBMPlexSans-400_600-latin.woff2") format("woff2");
19
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
20
+ U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
21
+ U+2212, U+2215, U+FEFF, U+FFFD;
22
+ }
23
+ @font-face {
24
+ font-family: "IBM Plex Sans";
25
+ font-style: normal;
26
+ font-weight: 400 600;
27
+ font-display: swap;
28
+ src: url("fonts/IBMPlexSans-400_600-latin-ext.woff2") format("woff2");
29
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
30
+ U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
31
+ U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
32
+ }
33
+ @font-face {
34
+ font-family: "IBM Plex Mono";
35
+ font-style: normal;
36
+ font-weight: 400;
37
+ font-display: swap;
38
+ src: url("fonts/IBMPlexMono-400-latin.woff2") format("woff2");
39
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
40
+ U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
41
+ U+2212, U+2215, U+FEFF, U+FFFD;
42
+ }
43
+ @font-face {
44
+ font-family: "IBM Plex Mono";
45
+ font-style: normal;
46
+ font-weight: 400;
47
+ font-display: swap;
48
+ src: url("fonts/IBMPlexMono-400-latin-ext.woff2") format("woff2");
49
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
50
+ U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
51
+ U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
52
+ }
53
+ @font-face {
54
+ font-family: "IBM Plex Mono";
55
+ font-style: normal;
56
+ font-weight: 500;
57
+ font-display: swap;
58
+ src: url("fonts/IBMPlexMono-500-latin.woff2") format("woff2");
59
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
60
+ U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
61
+ U+2212, U+2215, U+FEFF, U+FFFD;
62
+ }
63
+ @font-face {
64
+ font-family: "IBM Plex Mono";
65
+ font-style: normal;
66
+ font-weight: 500;
67
+ font-display: swap;
68
+ src: url("fonts/IBMPlexMono-500-latin-ext.woff2") format("woff2");
69
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
70
+ U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
71
+ U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
72
+ }
73
+ @font-face {
74
+ font-family: "Instrument Serif";
75
+ font-style: normal;
76
+ font-weight: 400;
77
+ font-display: swap;
78
+ src: url("fonts/InstrumentSerif-400-latin.woff2") format("woff2");
79
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
80
+ U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
81
+ U+2212, U+2215, U+FEFF, U+FFFD;
82
+ }
83
+ @font-face {
84
+ font-family: "Instrument Serif";
85
+ font-style: normal;
86
+ font-weight: 400;
87
+ font-display: swap;
88
+ src: url("fonts/InstrumentSerif-400-latin-ext.woff2") format("woff2");
89
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
90
+ U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
91
+ U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
92
+ }
package/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { agreementMarkup, acceptedAgreement, signInErrorMessage, bindAgreement, providerEntry, signInHint, rememberSignIn, forgetSignIn, showEmailEntry, type SigninShape, } from "./service-agreement.js";
2
+ export { openProviderWindow, relayProviderAnswer, type ProviderWindow, } from "./provider-window.js";
3
+ export { showVersionBadge } from "./version.js";
package/index.js ADDED
@@ -0,0 +1,11 @@
1
+ // The Fidj entry, in one place.
2
+ //
3
+ // Everything here was lifted verbatim out of generator-fidj's template, where
4
+ // it could only ship by being generated. It is a package now so that the
5
+ // console can import the same screens a generated app renders, instead of the
6
+ // two being kept in step by hand — which is how the service agreement ended up
7
+ // existing in four copies that no test could diff.
8
+ export { agreementMarkup, acceptedAgreement, signInErrorMessage, bindAgreement, providerEntry, signInHint, rememberSignIn, forgetSignIn, showEmailEntry, } from "./service-agreement.js";
9
+ export { openProviderWindow, relayProviderAnswer, } from "./provider-window.js";
10
+ export { showVersionBadge } from "./version.js";
11
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsZ0NBQWdDO0FBQ2hDLEVBQUU7QUFDRiw4RUFBOEU7QUFDOUUseUVBQXlFO0FBQ3pFLDhFQUE4RTtBQUM5RSwrRUFBK0U7QUFDL0UsbURBQW1EO0FBQ25ELE9BQU8sRUFDTCxlQUFlLEVBQ2YsaUJBQWlCLEVBQ2pCLGtCQUFrQixFQUNsQixhQUFhLEVBQ2IsYUFBYSxFQUNiLFVBQVUsRUFDVixjQUFjLEVBQ2QsWUFBWSxFQUNaLGNBQWMsR0FFZixNQUFNLHdCQUF3QixDQUFDO0FBQ2hDLE9BQU8sRUFDTCxrQkFBa0IsRUFDbEIsbUJBQW1CLEdBRXBCLE1BQU0sc0JBQXNCLENBQUM7QUFDOUIsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sY0FBYyxDQUFDIn0=
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@ofidj/entry",
3
+ "version": "3.10.0",
4
+ "description": "The Fidj entry: sign-in, account recovery, the service agreement and the design system, shared verbatim by Fidj, the generated apps and the console.",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "main": "./index.js",
11
+ "types": "./index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./index.d.ts",
15
+ "default": "./index.js"
16
+ },
17
+ "./tokens.css": "./tokens.css",
18
+ "./fonts.css": "./fonts.css",
19
+ "./style.css": "./style.css"
20
+ },
21
+ "scripts": {
22
+ "build": "rm -rf dist && tsc --project tsconfig.json && cp src/*.css dist/ && cp *.md LICENSE dist/ && cp package.json dist/",
23
+ "test": "npm run build && node --test test/*.test.mjs",
24
+ "bp:style:fix": "prettier --write \"src/**/*.ts\" \"test/**/*.mjs\" --no-error-on-unmatched-pattern"
25
+ },
26
+ "devDependencies": {
27
+ "jsdom": "^25.0.1",
28
+ "prettier": "^3.3.3",
29
+ "typescript": "^5.6.3"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/ofidj/fidj-entry.git"
34
+ }
35
+ }
@@ -0,0 +1,9 @@
1
+ export type ProviderWindow = {
2
+ show(url: string): void;
3
+ isOpen(): boolean;
4
+ focus(): void;
5
+ giveUp(): void;
6
+ answer(): Promise<URL | null>;
7
+ };
8
+ export declare function openProviderWindow(): ProviderWindow | null;
9
+ export declare function relayProviderAnswer(): boolean;
@@ -0,0 +1,165 @@
1
+ // Signing in happens in a window of its own.
2
+ //
3
+ // Fidj's screens are served from another origin — Fidj's front end, or the API
4
+ // itself, depending on the deployment — and that origin refuses to be framed,
5
+ // so no dialog drawn inside this page can hold them. A window can, and it keeps
6
+ // the page the person was already on: they come back to what they were reading,
7
+ // not to a shell that reloaded underneath them and forgot where they were.
8
+ //
9
+ // The window is opened empty and synchronously, before anything is awaited. A
10
+ // window opened after an await is a window the browser no longer connects to
11
+ // the click that asked for it, and every popup blocker refuses it — so the
12
+ // authorization URL, which takes a discovery round-trip to build, is fetched
13
+ // afterwards and navigated into a window that already exists.
14
+ // One window per page, not one per browser.
15
+ //
16
+ // Naming it is what lets a second press reuse the window instead of stacking
17
+ // another one on top, and that much is wanted. Sharing the name across tabs is
18
+ // not: the answer comes back to whichever page opened the window, so a second
19
+ // tab reusing the first tab's window would hand that tab's sign-in to a page
20
+ // that never asked, and leave the one that did waiting forever. Stable for this
21
+ // document, unique to it.
22
+ const WINDOW_NAME = "fidj-signin-" + Math.random().toString(36).slice(2, 10);
23
+ const CALLBACK = "fidj:oidc-callback";
24
+ // Comfortable rather than minimal: the provider asks for a password, states who
25
+ // is asking and where the person is going back to, and a cramped window turns
26
+ // that into scrolling. Clamped to the screen so a small display still gets a
27
+ // window it can show whole, and centred on the display the opener is on.
28
+ function features() {
29
+ const width = Math.min(560, Math.max(320, window.screen.availWidth - 80));
30
+ const height = Math.min(760, Math.max(480, window.screen.availHeight - 80));
31
+ const left = Math.round((window.screenX || 0) + Math.max(0, (window.outerWidth - width) / 2));
32
+ const top = Math.round((window.screenY || 0) + Math.max(0, (window.outerHeight - height) / 3));
33
+ return `popup=yes,width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
34
+ }
35
+ export function openProviderWindow() {
36
+ let opened = null;
37
+ try {
38
+ opened = window.open("", WINDOW_NAME, features());
39
+ }
40
+ catch {
41
+ opened = null;
42
+ }
43
+ // Blocked, or a browser that refuses named windows. The caller falls back to
44
+ // leaving this page, which is the flow this one replaced.
45
+ if (!opened)
46
+ return null;
47
+ try {
48
+ opened.document.write('<!doctype html><meta charset="utf-8"><title>Signing in with Fidj</title><body style="margin:0;font:15px/1.5 system-ui;color:#4a4540;display:grid;place-items:center;height:100vh">Opening Fidj…</body>');
49
+ opened.document.close();
50
+ }
51
+ catch { }
52
+ return {
53
+ show(url) {
54
+ // `replace`, so the blank placeholder is not a history entry the person
55
+ // can press Back into.
56
+ try {
57
+ opened.location.replace(url);
58
+ }
59
+ catch {
60
+ opened.location.href = url;
61
+ }
62
+ },
63
+ isOpen() {
64
+ try {
65
+ return !opened.closed;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ },
71
+ focus() {
72
+ try {
73
+ opened.focus();
74
+ }
75
+ catch { }
76
+ },
77
+ giveUp() {
78
+ try {
79
+ opened.close();
80
+ }
81
+ catch { }
82
+ },
83
+ answer() {
84
+ return new Promise((resolve) => {
85
+ let done = false;
86
+ const finish = (result) => {
87
+ if (done)
88
+ return;
89
+ done = true;
90
+ window.removeEventListener("message", onMessage);
91
+ window.clearInterval(watch);
92
+ try {
93
+ opened.close();
94
+ }
95
+ catch { }
96
+ resolve(result);
97
+ };
98
+ const onMessage = (event) => {
99
+ // Only this origin, and only the window this entry opened: the
100
+ // callback carries a single-use authorization code, and anything else
101
+ // posting into this page is not part of this conversation.
102
+ if (event.origin !== window.location.origin)
103
+ return;
104
+ if (event.source !== opened)
105
+ return;
106
+ const data = event.data;
107
+ if (!data || data.fidj !== CALLBACK || typeof data.href !== "string")
108
+ return;
109
+ let callback;
110
+ try {
111
+ callback = new URL(data.href);
112
+ }
113
+ catch {
114
+ return;
115
+ }
116
+ if (callback.origin !== window.location.origin)
117
+ return;
118
+ finish(callback);
119
+ };
120
+ window.addEventListener("message", onMessage);
121
+ // A window closed by hand answers nothing, and nothing is what this
122
+ // page would wait for forever otherwise. `closed` is the only signal a
123
+ // cross-origin window gives, so it is polled.
124
+ const watch = window.setInterval(() => {
125
+ if (opened.closed)
126
+ finish(null);
127
+ }, 400);
128
+ });
129
+ },
130
+ };
131
+ }
132
+ // The other half, running in that window: the provider has just answered into
133
+ // it, and its only remaining job is to hand the answer back and get out of the
134
+ // way. It never completes the sign-in itself — the transaction that has to
135
+ // match the answer was written by the page that opened it, and session storage
136
+ // is copied into a new window, not shared with it.
137
+ //
138
+ // Answering `true` means the hand-off was made and this window asked to close;
139
+ // the caller shows one line and, if the browser refuses to close a window it
140
+ // did not see scripted open, finishes the sign-in here after all.
141
+ export function relayProviderAnswer() {
142
+ let opener = null;
143
+ try {
144
+ opener = window.opener;
145
+ }
146
+ catch {
147
+ return false;
148
+ }
149
+ if (!opener || opener === window || opener.closed)
150
+ return false;
151
+ if (!new URL(window.location.href).searchParams.has("state"))
152
+ return false;
153
+ try {
154
+ opener.postMessage({ fidj: CALLBACK, href: window.location.href }, window.location.origin);
155
+ }
156
+ catch {
157
+ return false;
158
+ }
159
+ try {
160
+ window.close();
161
+ }
162
+ catch { }
163
+ return true;
164
+ }
165
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJvdmlkZXItd2luZG93LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL3Byb3ZpZGVyLXdpbmRvdy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSw2Q0FBNkM7QUFDN0MsRUFBRTtBQUNGLCtFQUErRTtBQUMvRSw4RUFBOEU7QUFDOUUsZ0ZBQWdGO0FBQ2hGLGdGQUFnRjtBQUNoRiwyRUFBMkU7QUFDM0UsRUFBRTtBQUNGLDhFQUE4RTtBQUM5RSw2RUFBNkU7QUFDN0UsMkVBQTJFO0FBQzNFLDZFQUE2RTtBQUM3RSw4REFBOEQ7QUFFOUQsNENBQTRDO0FBQzVDLEVBQUU7QUFDRiw2RUFBNkU7QUFDN0UsK0VBQStFO0FBQy9FLDhFQUE4RTtBQUM5RSw2RUFBNkU7QUFDN0UsZ0ZBQWdGO0FBQ2hGLDBCQUEwQjtBQUMxQixNQUFNLFdBQVcsR0FBRyxjQUFjLEdBQUcsSUFBSSxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBQzdFLE1BQU0sUUFBUSxHQUFHLG9CQUFvQixDQUFDO0FBRXRDLGdGQUFnRjtBQUNoRiw4RUFBOEU7QUFDOUUsNkVBQTZFO0FBQzdFLHlFQUF5RTtBQUN6RSxTQUFTLFFBQVE7SUFDZixNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxNQUFNLENBQUMsTUFBTSxDQUFDLFVBQVUsR0FBRyxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBQzFFLE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUMsV0FBVyxHQUFHLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDNUUsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FDckIsQ0FBQyxNQUFNLENBQUMsT0FBTyxJQUFJLENBQUMsQ0FBQyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLFVBQVUsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FDckUsQ0FBQztJQUNGLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQ3BCLENBQUMsTUFBTSxDQUFDLE9BQU8sSUFBSSxDQUFDLENBQUMsR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxDQUFDLE1BQU0sQ0FBQyxXQUFXLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQ3ZFLENBQUM7SUFDRixPQUFPLG1CQUFtQixLQUFLLFdBQVcsTUFBTSxTQUFTLElBQUksUUFBUSxHQUFHLCtCQUErQixDQUFDO0FBQzFHLENBQUM7QUFxQkQsTUFBTSxVQUFVLGtCQUFrQjtJQUNoQyxJQUFJLE1BQU0sR0FBa0IsSUFBSSxDQUFDO0lBQ2pDLElBQUksQ0FBQztRQUNILE1BQU0sR0FBRyxNQUFNLENBQUMsSUFBSSxDQUFDLEVBQUUsRUFBRSxXQUFXLEVBQUUsUUFBUSxFQUFFLENBQUMsQ0FBQztJQUNwRCxDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1AsTUFBTSxHQUFHLElBQUksQ0FBQztJQUNoQixDQUFDO0lBQ0QsNkVBQTZFO0lBQzdFLDBEQUEwRDtJQUMxRCxJQUFJLENBQUMsTUFBTTtRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ3pCLElBQUksQ0FBQztRQUNILE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUNuQix3TUFBd00sQ0FDek0sQ0FBQztRQUNGLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxFQUFFLENBQUM7SUFDMUIsQ0FBQztJQUFDLE1BQU0sQ0FBQyxDQUFBLENBQUM7SUFDVixPQUFPO1FBQ0wsSUFBSSxDQUFDLEdBQVc7WUFDZCx3RUFBd0U7WUFDeEUsdUJBQXVCO1lBQ3ZCLElBQUksQ0FBQztnQkFDSCxNQUFPLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUNoQyxDQUFDO1lBQUMsTUFBTSxDQUFDO2dCQUNQLE1BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSSxHQUFHLEdBQUcsQ0FBQztZQUM5QixDQUFDO1FBQ0gsQ0FBQztRQUNELE1BQU07WUFDSixJQUFJLENBQUM7Z0JBQ0gsT0FBTyxDQUFDLE1BQU8sQ0FBQyxNQUFNLENBQUM7WUFDekIsQ0FBQztZQUFDLE1BQU0sQ0FBQztnQkFDUCxPQUFPLEtBQUssQ0FBQztZQUNmLENBQUM7UUFDSCxDQUFDO1FBQ0QsS0FBSztZQUNILElBQUksQ0FBQztnQkFDSCxNQUFPLENBQUMsS0FBSyxFQUFFLENBQUM7WUFDbEIsQ0FBQztZQUFDLE1BQU0sQ0FBQyxDQUFBLENBQUM7UUFDWixDQUFDO1FBQ0QsTUFBTTtZQUNKLElBQUksQ0FBQztnQkFDSCxNQUFPLENBQUMsS0FBSyxFQUFFLENBQUM7WUFDbEIsQ0FBQztZQUFDLE1BQU0sQ0FBQyxDQUFBLENBQUM7UUFDWixDQUFDO1FBQ0QsTUFBTTtZQUNKLE9BQU8sSUFBSSxPQUFPLENBQWEsQ0FBQyxPQUFPLEVBQUUsRUFBRTtnQkFDekMsSUFBSSxJQUFJLEdBQUcsS0FBSyxDQUFDO2dCQUNqQixNQUFNLE1BQU0sR0FBRyxDQUFDLE1BQWtCLEVBQUUsRUFBRTtvQkFDcEMsSUFBSSxJQUFJO3dCQUFFLE9BQU87b0JBQ2pCLElBQUksR0FBRyxJQUFJLENBQUM7b0JBQ1osTUFBTSxDQUFDLG1CQUFtQixDQUFDLFNBQVMsRUFBRSxTQUFTLENBQUMsQ0FBQztvQkFDakQsTUFBTSxDQUFDLGFBQWEsQ0FBQyxLQUFLLENBQUMsQ0FBQztvQkFDNUIsSUFBSSxDQUFDO3dCQUNILE1BQU8sQ0FBQyxLQUFLLEVBQUUsQ0FBQztvQkFDbEIsQ0FBQztvQkFBQyxNQUFNLENBQUMsQ0FBQSxDQUFDO29CQUNWLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQztnQkFDbEIsQ0FBQyxDQUFDO2dCQUNGLE1BQU0sU0FBUyxHQUFHLENBQUMsS0FBbUIsRUFBRSxFQUFFO29CQUN4QywrREFBK0Q7b0JBQy9ELHNFQUFzRTtvQkFDdEUsMkRBQTJEO29CQUMzRCxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssTUFBTSxDQUFDLFFBQVEsQ0FBQyxNQUFNO3dCQUFFLE9BQU87b0JBQ3BELElBQUksS0FBSyxDQUFDLE1BQU0sS0FBSyxNQUFNO3dCQUFFLE9BQU87b0JBQ3BDLE1BQU0sSUFBSSxHQUFHLEtBQUssQ0FBQyxJQUErQyxDQUFDO29CQUNuRSxJQUFJLENBQUMsSUFBSSxJQUFJLElBQUksQ0FBQyxJQUFJLEtBQUssUUFBUSxJQUFJLE9BQU8sSUFBSSxDQUFDLElBQUksS0FBSyxRQUFRO3dCQUNsRSxPQUFPO29CQUNULElBQUksUUFBYSxDQUFDO29CQUNsQixJQUFJLENBQUM7d0JBQ0gsUUFBUSxHQUFHLElBQUksR0FBRyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztvQkFDaEMsQ0FBQztvQkFBQyxNQUFNLENBQUM7d0JBQ1AsT0FBTztvQkFDVCxDQUFDO29CQUNELElBQUksUUFBUSxDQUFDLE1BQU0sS0FBSyxNQUFNLENBQUMsUUFBUSxDQUFDLE1BQU07d0JBQUUsT0FBTztvQkFDdkQsTUFBTSxDQUFDLFFBQVEsQ0FBQyxDQUFDO2dCQUNuQixDQUFDLENBQUM7Z0JBQ0YsTUFBTSxDQUFDLGdCQUFnQixDQUFDLFNBQVMsRUFBRSxTQUFTLENBQUMsQ0FBQztnQkFDOUMsb0VBQW9FO2dCQUNwRSx1RUFBdUU7Z0JBQ3ZFLDhDQUE4QztnQkFDOUMsTUFBTSxLQUFLLEdBQUcsTUFBTSxDQUFDLFdBQVcsQ0FBQyxHQUFHLEVBQUU7b0JBQ3BDLElBQUksTUFBTyxDQUFDLE1BQU07d0JBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDO2dCQUNuQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUM7WUFDVixDQUFDLENBQUMsQ0FBQztRQUNMLENBQUM7S0FDRixDQUFDO0FBQ0osQ0FBQztBQUVELDhFQUE4RTtBQUM5RSwrRUFBK0U7QUFDL0UsMkVBQTJFO0FBQzNFLCtFQUErRTtBQUMvRSxtREFBbUQ7QUFDbkQsRUFBRTtBQUNGLCtFQUErRTtBQUMvRSw2RUFBNkU7QUFDN0Usa0VBQWtFO0FBQ2xFLE1BQU0sVUFBVSxtQkFBbUI7SUFDakMsSUFBSSxNQUFNLEdBQWtCLElBQUksQ0FBQztJQUNqQyxJQUFJLENBQUM7UUFDSCxNQUFNLEdBQUcsTUFBTSxDQUFDLE1BQXVCLENBQUM7SUFDMUMsQ0FBQztJQUFDLE1BQU0sQ0FBQztRQUNQLE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUNELElBQUksQ0FBQyxNQUFNLElBQUksTUFBTSxLQUFLLE1BQU0sSUFBSSxNQUFNLENBQUMsTUFBTTtRQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ2hFLElBQUksQ0FBQyxJQUFJLEdBQUcsQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDLFlBQVksQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDO1FBQUUsT0FBTyxLQUFLLENBQUM7SUFDM0UsSUFBSSxDQUFDO1FBQ0gsTUFBTSxDQUFDLFdBQVcsQ0FDaEIsRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxNQUFNLENBQUMsUUFBUSxDQUFDLElBQUksRUFBRSxFQUM5QyxNQUFNLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FDdkIsQ0FBQztJQUNKLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUM7SUFDRCxJQUFJLENBQUM7UUFDSCxNQUFNLENBQUMsS0FBSyxFQUFFLENBQUM7SUFDakIsQ0FBQztJQUFDLE1BQU0sQ0FBQyxDQUFBLENBQUM7SUFDVixPQUFPLElBQUksQ0FBQztBQUNkLENBQUMifQ==
@@ -0,0 +1,13 @@
1
+ export declare function agreementMarkup(): string;
2
+ export declare function acceptedAgreement(form: HTMLFormElement): {
3
+ termsAccepted: boolean;
4
+ termsVersion: string;
5
+ } | null;
6
+ export declare function signInErrorMessage(error: unknown): "Too many attempts. Please wait before trying again." | "We could not sign in to this account. Check the email and password." | "An account already uses this email. Check the password or sign in instead." | "We cannot reach Fidj right now. Please try again." | "We could not sign in to this account. Please try again.";
7
+ export declare function bindAgreement(form: HTMLFormElement | null, title: string, endpoint: string, appId: string, checked?: boolean): Promise<void>;
8
+ export declare function signInHint(appId: string): string;
9
+ export declare function rememberSignIn(appId: string, label: string): void;
10
+ export declare function forgetSignIn(appId: string): void;
11
+ export type SigninShape = "button" | "inline" | "both";
12
+ export declare function providerEntry(title: string, appId: string, credentials: string, isFidjItself?: boolean, shape?: SigninShape): string;
13
+ export declare function showEmailEntry(open: boolean, focus?: boolean): void;
@@ -0,0 +1,184 @@
1
+ export function agreementMarkup() {
2
+ return `<div class="signin-agreement"><label class="agreement-choice"><input id="service-agreement" type="checkbox" aria-required="true" disabled><span id="agreement-label">I accept the service agreement for this app.</span></label><button type="button" id="read-agreement" disabled>Read service agreement</button><p id="agreement-status" class="fineprint" role="status">Loading service agreement…</p><button type="button" id="retry-agreement" hidden>Retry</button></div><dialog id="agreement-dialog" aria-labelledby="agreement-heading"><h2 id="agreement-heading">Service agreement</h2><p id="agreement-version"></p><p id="agreement-text"></p><button type="button" id="close-agreement">Close agreement</button></dialog>`;
3
+ }
4
+ export function acceptedAgreement(form) {
5
+ const checkbox = form.querySelector("#service-agreement");
6
+ if (!checkbox?.checked || checkbox.disabled || !checkbox.dataset.version)
7
+ return null;
8
+ return { termsAccepted: true, termsVersion: checkbox.dataset.version };
9
+ }
10
+ export function signInErrorMessage(error) {
11
+ const detail = error;
12
+ const reason = typeof detail?.reason === "string" ? detail.reason : typeof detail?.message === "string" ? detail.message : "";
13
+ if (detail?.code === 429)
14
+ return "Too many attempts. Please wait before trying again.";
15
+ if (reason === "unknown-user")
16
+ return "We could not sign in to this account. Check the email and password.";
17
+ if (reason === "already exists - inconsistent request")
18
+ return "An account already uses this email. Check the password or sign in instead.";
19
+ if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET|EHOSTUNREACH|network/i.test(reason))
20
+ return "We cannot reach Fidj right now. Please try again.";
21
+ return "We could not sign in to this account. Please try again.";
22
+ }
23
+ export async function bindAgreement(form, title, endpoint, appId, checked = false) {
24
+ if (!form)
25
+ return;
26
+ // An app whose only door is Fidj carries no agreement block: Fidj asks for
27
+ // this app's agreement on the screen that names it, and records the version.
28
+ const checkbox = form.querySelector("#service-agreement");
29
+ if (!checkbox)
30
+ return;
31
+ const read = form.querySelector("#read-agreement");
32
+ const dialog = form.querySelector("#agreement-dialog");
33
+ const status = form.querySelector("#agreement-status");
34
+ const retry = form.querySelector("#retry-agreement");
35
+ // What this agreement gates is the app's own door, and only that one. The
36
+ // Fidj door is about to be asked the same question on the screen that names
37
+ // the app, where the answer is recorded with its version — so an agreement
38
+ // that is still loading, or that could not be loaded at all, must not be what
39
+ // stands between a person and the door that does not need it.
40
+ const gated = form.querySelector("#email-entry") || form;
41
+ const submitButtons = gated.querySelectorAll('button[type="submit"]');
42
+ const update = () => submitButtons.forEach(button => { button.disabled = checkbox.disabled; });
43
+ form.querySelector("#agreement-label").textContent = `I accept the service agreement for ${title}.`;
44
+ update();
45
+ checkbox.addEventListener("change", update);
46
+ read.addEventListener("click", () => dialog.showModal());
47
+ form.querySelector("#close-agreement").addEventListener("click", () => dialog.close());
48
+ const load = async () => {
49
+ retry.hidden = true;
50
+ status.textContent = "Loading service agreement…";
51
+ try {
52
+ const response = await fetch(`${endpoint}/apps/${encodeURIComponent(appId)}`, { signal: AbortSignal.timeout(10000) });
53
+ if (!response.ok)
54
+ throw new Error(response.status === 404 ? "missing" : "unreachable");
55
+ const agreement = (await response.json()).app?.agreement;
56
+ if (!agreement || typeof agreement.version !== "string" || !agreement.version || typeof agreement.text !== "string" || !agreement.text)
57
+ throw new Error("missing");
58
+ if (!form.isConnected)
59
+ return;
60
+ checkbox.dataset.version = agreement.version;
61
+ checkbox.disabled = false;
62
+ checkbox.checked = checked;
63
+ read.disabled = false;
64
+ dialog.querySelector("#agreement-version").textContent = `Version ${agreement.version}`;
65
+ dialog.querySelector("#agreement-text").textContent = agreement.text;
66
+ status.textContent = "Required to sign in. Optional data choices stay separate.";
67
+ update();
68
+ }
69
+ catch (error) {
70
+ if (!form.isConnected)
71
+ return;
72
+ status.textContent = error instanceof Error && error.message === "missing"
73
+ ? "This app has no service agreement available."
74
+ : "We cannot reach Fidj right now. Try again.";
75
+ retry.hidden = false;
76
+ }
77
+ };
78
+ retry.addEventListener("click", load);
79
+ await load();
80
+ }
81
+ // The entry every app that delegates to the provider renders, in one place
82
+ // because both app shapes render it and they had drifted apart.
83
+ //
84
+ // "Continue with Fidj" alone borrows the grammar of an optional social login —
85
+ // that button always sits next to an email and a password — so on an app whose
86
+ // accounts *are* Fidj accounts, the missing form reads as something broken. The
87
+ // explanation therefore comes before the button, not as reassurance after it,
88
+ // and nothing presupposes an account the person may not have yet.
89
+ const hintKey = (appId) => "fidj.entry." + appId;
90
+ export function signInHint(appId) {
91
+ try {
92
+ return localStorage.getItem(hintKey(appId)) || "";
93
+ }
94
+ catch {
95
+ return "";
96
+ }
97
+ }
98
+ // Remembered on this app's own origin, about this app's own member: no
99
+ // cross-site question is asked, and none is answered. Signing out forgets, so a
100
+ // shared browser does not show the next person an address.
101
+ export function rememberSignIn(appId, label) {
102
+ try {
103
+ if (label)
104
+ localStorage.setItem(hintKey(appId), label);
105
+ }
106
+ catch { }
107
+ }
108
+ export function forgetSignIn(appId) {
109
+ try {
110
+ localStorage.removeItem(hintKey(appId));
111
+ }
112
+ catch { }
113
+ }
114
+ export function providerEntry(title, appId, credentials, isFidjItself = false, shape = "button") {
115
+ const escapeText = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
116
+ const hint = signInHint(appId);
117
+ const both = shape === "both" && Boolean(credentials);
118
+ // The app's own form and nothing else. Nothing is folded away because there is
119
+ // no second way in to fold it under, and the trade-off is stated plainly:
120
+ // on this path it is this site that holds the password.
121
+ if (shape === "inline")
122
+ return (`<p class="signin-lead">${isFidjItself
123
+ ? "One account across every app that uses Fidj, and a separate set of choices for each one."
124
+ : `${escapeText(title)} accounts are Fidj accounts. Sign in below — ${escapeText(title)} handles your password itself on this page.`}</p>` +
125
+ agreementMarkup() +
126
+ credentials);
127
+ const lead = hint
128
+ ? `<p class="signin-lead">You signed in here with Fidj before. ${escapeText(title)} accounts are Fidj accounts — continue as yourself, or use another.</p>`
129
+ : both
130
+ ? `<p class="signin-lead">${escapeText(title)} accounts are Fidj accounts. Fidj asks in a window of its own, so this site never sees your password.</p>`
131
+ : isFidjItself
132
+ ? `<p class="signin-lead">One account across every app that uses Fidj, and a separate set of choices for each one. Fidj asks in a window of its own; this page stays where it is.</p>`
133
+ : `<p class="signin-lead">${escapeText(title)} accounts are Fidj accounts. You will sign in — or create yours — in a Fidj window, so this site never sees your password.</p>`;
134
+ // The accent is the Fidj mark, and this is the one control on the screen that
135
+ // is Fidj's rather than the app's. It is also the door the app would rather
136
+ // people took, and those two happen to want the same thing.
137
+ const fidj = hint
138
+ ? `<button class="primary fidj-entry" type="submit" name="entry" value="fidj">Continue as ${escapeText(hint)}</button><button type="button" id="forget-hint" class="quiet">Use a different account</button>`
139
+ : `<button class="primary fidj-entry" type="submit" name="entry" value="fidj">${isFidjItself ? "Sign in" : "Sign in with Fidj"}</button>`;
140
+ // Fidj is the only door: it collects the agreement itself, a moment later, on
141
+ // the screen that names the app — and records it with its version. Collecting
142
+ // it here first recorded nothing and asked the same question twice.
143
+ if (shape !== "both" || !both)
144
+ return lead + fidj;
145
+ // Both doors. The app's own one is a second thought for the person who wants
146
+ // it, which is what a disclosure is for: it costs one click and takes nothing
147
+ // away, where a second button of equal weight cost everybody a decision.
148
+ return (lead +
149
+ fidj +
150
+ `<div class="signin-alternate"><button type="button" id="use-email" class="signin-toggle" aria-expanded="false" aria-controls="email-entry"><span class="signin-toggle-label">Inline form<span class="caret" aria-hidden="true"></span></span></button>
151
+ <div id="email-entry" hidden>${agreementMarkup()}${credentials}</div></div>`);
152
+ }
153
+ // Opening the app's own form takes the space the Fidj door was using.
154
+ //
155
+ // The two are alternatives, not a list, and watching one fold away as the other
156
+ // arrives is what says so — where a form appearing underneath a button that is
157
+ // still there reads as "and also". The toggle stays put through both states, so
158
+ // it ends up labelling whichever one is on screen: a caret pointing down at a
159
+ // form that is not here yet, and up at the one it would put away.
160
+ //
161
+ // Shared, because both app shapes render the same entry.
162
+ export function showEmailEntry(open, focus = false) {
163
+ const fields = document.getElementById("email-entry");
164
+ const toggle = document.getElementById("use-email");
165
+ if (!fields || !toggle)
166
+ return;
167
+ toggle.setAttribute("aria-expanded", String(open));
168
+ fields.hidden = !open;
169
+ const door = document.querySelector(".fidj-entry");
170
+ if (door) {
171
+ door.classList.toggle("is-folded", open);
172
+ // Out of the tab order and out of the accessibility tree the moment it
173
+ // starts leaving: something mid-fold is not something to press, and an
174
+ // animation is not what decides that.
175
+ if (open)
176
+ door.setAttribute("aria-hidden", "true");
177
+ else
178
+ door.removeAttribute("aria-hidden");
179
+ door.tabIndex = open ? -1 : 0;
180
+ }
181
+ if (open && focus)
182
+ document.getElementById("email")?.focus();
183
+ }
184
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS1hZ3JlZW1lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvc2VydmljZS1hZ3JlZW1lbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsTUFBTSxVQUFVLGVBQWU7SUFDN0IsT0FBTyx5c0JBQXlzQixDQUFDO0FBQ250QixDQUFDO0FBRUQsTUFBTSxVQUFVLGlCQUFpQixDQUFDLElBQXFCO0lBQ3JELE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxhQUFhLENBQW1CLG9CQUFvQixDQUFDLENBQUM7SUFDNUUsSUFBSSxDQUFDLFFBQVEsRUFBRSxPQUFPLElBQUksUUFBUSxDQUFDLFFBQVEsSUFBSSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsT0FBTztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ3RGLE9BQU8sRUFBRSxhQUFhLEVBQUUsSUFBSSxFQUFFLFlBQVksRUFBRSxRQUFRLENBQUMsT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO0FBQ3pFLENBQUM7QUFFRCxNQUFNLFVBQVUsa0JBQWtCLENBQUMsS0FBYztJQUMvQyxNQUFNLE1BQU0sR0FBRyxLQUE2RCxDQUFDO0lBQzdFLE1BQU0sTUFBTSxHQUFHLE9BQU8sTUFBTSxFQUFFLE1BQU0sS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLE9BQU8sTUFBTSxFQUFFLE9BQU8sS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUM5SCxJQUFJLE1BQU0sRUFBRSxJQUFJLEtBQUssR0FBRztRQUFFLE9BQU8scURBQXFELENBQUM7SUFDdkYsSUFBSSxNQUFNLEtBQUssY0FBYztRQUFFLE9BQU8scUVBQXFFLENBQUM7SUFDNUcsSUFBSSxNQUFNLEtBQUssdUNBQXVDO1FBQUUsT0FBTyw0RUFBNEUsQ0FBQztJQUM1SSxJQUFJLG1FQUFtRSxDQUFDLElBQUks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