@flytedesk/app-kit 3.2.3 → 4.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/README.md +130 -10
- package/dist/auth/client/httpClient.d.ts +79 -1
- package/dist/auth/client/httpClient.js +185 -16
- package/dist/auth/client/httpClient.js.map +1 -1
- package/dist/auth/client/index.d.ts +4 -4
- package/dist/auth/client/index.js +3 -3
- package/dist/auth/client/index.js.map +1 -1
- package/dist/auth/client/useAuthSession.d.ts +61 -13
- package/dist/auth/client/useAuthSession.js +185 -42
- package/dist/auth/client/useAuthSession.js.map +1 -1
- package/dist/auth/errors.d.ts +30 -7
- package/dist/auth/errors.js +38 -8
- package/dist/auth/errors.js.map +1 -1
- package/dist/auth/guards.d.ts +12 -0
- package/dist/auth/guards.js +24 -6
- package/dist/auth/guards.js.map +1 -1
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/index.js +2 -2
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/oidc-client.js +12 -2
- package/dist/auth/oidc-client.js.map +1 -1
- package/dist/auth/plugin.d.ts +6 -0
- package/dist/auth/plugin.js +532 -304
- package/dist/auth/plugin.js.map +1 -1
- package/dist/auth/shared-types.d.ts +28 -1
- package/dist/auth/shared-types.js +35 -0
- package/dist/auth/shared-types.js.map +1 -1
- package/dist/auth/silentAuthPage.d.ts +20 -0
- package/dist/auth/silentAuthPage.js +75 -0
- package/dist/auth/silentAuthPage.js.map +1 -0
- package/dist/auth/testing/fake-idp.d.ts +35 -0
- package/dist/auth/testing/fake-idp.js +86 -2
- package/dist/auth/testing/fake-idp.js.map +1 -1
- package/dist/auth/testing/index.d.ts +3 -1
- package/dist/auth/testing/index.js +3 -1
- package/dist/auth/testing/index.js.map +1 -1
- package/dist/auth/testing/memory-store.d.ts +44 -0
- package/dist/auth/testing/memory-store.js +120 -0
- package/dist/auth/testing/memory-store.js.map +1 -0
- package/dist/auth/tokens.d.ts +24 -0
- package/dist/auth/tokens.js +31 -1
- package/dist/auth/tokens.js.map +1 -1
- package/dist/auth/types.d.ts +231 -96
- package/dist/auth/unavailable.d.ts +10 -0
- package/dist/auth/unavailable.js +11 -0
- package/dist/auth/unavailable.js.map +1 -0
- package/dist/bigquery/errors.d.ts +14 -3
- package/dist/bigquery/errors.js +54 -13
- package/dist/bigquery/errors.js.map +1 -1
- package/dist/bigquery/index.d.ts +5 -3
- package/dist/bigquery/index.js +4 -2
- package/dist/bigquery/index.js.map +1 -1
- package/dist/bigquery/query.d.ts +9 -2
- package/dist/bigquery/query.js +38 -16
- package/dist/bigquery/query.js.map +1 -1
- package/dist/bigquery/types.d.ts +45 -1
- package/dist/flags/plugin.js +4 -20
- package/dist/flags/plugin.js.map +1 -1
- package/dist/postgres/advisoryLock.d.ts +58 -0
- package/dist/postgres/advisoryLock.js +71 -0
- package/dist/postgres/advisoryLock.js.map +1 -0
- package/dist/postgres/index.d.ts +6 -0
- package/dist/postgres/index.js +7 -0
- package/dist/postgres/index.js.map +1 -0
- package/dist/profile/plugin.js +3 -20
- package/dist/profile/plugin.js.map +1 -1
- package/package.json +7 -2
- package/scripts/pending-release-count.mjs +0 -37
- package/scripts/release.sh +0 -126
- package/scripts/release.test.ts +0 -205
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
import { AuthUnavailableError } from "./httpClient.js";
|
|
1
2
|
import { type AppSwitcherEntry } from "../shared-types.js";
|
|
2
|
-
export type AuthStatus = "loading" | "authenticated" | "unauthenticated" | "error";
|
|
3
|
+
export type AuthStatus = "loading" | "authenticated" | "unauthenticated" | "unavailable" | "error";
|
|
4
|
+
/** First delay before re-verifying a session the server could not verify (AK-23). */
|
|
5
|
+
export declare const UNAVAILABLE_RETRY_BASE_MS = 1000;
|
|
6
|
+
/** The backoff's ceiling: an outage is retried every this-many ms at most. */
|
|
7
|
+
export declare const UNAVAILABLE_RETRY_MAX_MS = 30000;
|
|
8
|
+
/**
|
|
9
|
+
* Jittered exponential backoff for the `"unavailable"` state's automatic retries: the
|
|
10
|
+
* ceiling doubles per attempt (1s, 2s, 4s, … capped at UNAVAILABLE_RETRY_MAX_MS) and the
|
|
11
|
+
* delay is drawn uniformly from its upper half ("equal jitter", AK-23 L-c) — so every
|
|
12
|
+
* tab that saw the same outage does not retry in the same instant when it ends, while a
|
|
13
|
+
* retry never comes sooner than half the ceiling. `attempt` counts from 0; `random` is
|
|
14
|
+
* `Math.random` (a seam for tests).
|
|
15
|
+
*/
|
|
16
|
+
export declare function unavailableRetryDelayMs(attempt: number, random?: () => number): number;
|
|
3
17
|
interface AuthSessionActions {
|
|
4
18
|
/** Surfaces a failed sign-out as a rejection (fail loudly, AK-16) — local state is
|
|
5
19
|
* cleared once the api confirms it, OR once a `logout_store_failed` response proves
|
|
@@ -8,40 +22,74 @@ interface AuthSessionActions {
|
|
|
8
22
|
* promise (as a design system's AccountMenu typically does) gets an honest failure
|
|
9
23
|
* instead of silence. */
|
|
10
24
|
signOut: () => Promise<void>;
|
|
11
|
-
/** Starts a new bootstrap load from scratch — e.g. a Retry action on the error state.
|
|
25
|
+
/** Starts a new bootstrap load from scratch — e.g. a Retry action on the error state.
|
|
26
|
+
* It supersedes any load (or pending retry) already in flight: that load's answer is
|
|
27
|
+
* ignored, and the one `POST /refresh` it may be waiting on is joined, not repeated. */
|
|
12
28
|
refetch: () => void;
|
|
13
29
|
}
|
|
14
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The "All flytedesk apps" switcher feed is decoration, not the session (AK-23, N2): it
|
|
32
|
+
* never decides the status. When `GET {routePrefix}/apps` fails, `apps` is `[]` and
|
|
33
|
+
* `appsError` carries the failure (for an optional "couldn't load apps" hint); the session
|
|
34
|
+
* still becomes "authenticated". The next successful load clears it.
|
|
35
|
+
*/
|
|
36
|
+
interface AppsFeed {
|
|
37
|
+
apps: AppSwitcherEntry[];
|
|
38
|
+
appsError: Error | null;
|
|
39
|
+
}
|
|
40
|
+
export interface LoadingSessionState extends AuthSessionActions, AppsFeed {
|
|
15
41
|
status: "loading";
|
|
16
42
|
user: null;
|
|
17
|
-
|
|
43
|
+
appsError: null;
|
|
18
44
|
error: null;
|
|
19
45
|
}
|
|
20
46
|
/** "authenticated" always carries a user — there is no way to construct this variant
|
|
21
47
|
* without one, unlike the pre-AK-18 `{ status: AuthStatus; user: TUser | null }` shape
|
|
22
48
|
* that forced every consumer to handle an impossible `authenticated && !user` state by
|
|
23
49
|
* hand (sms-app's App.tsx, before this card). */
|
|
24
|
-
export interface AuthenticatedSessionState<TUser> extends AuthSessionActions {
|
|
50
|
+
export interface AuthenticatedSessionState<TUser> extends AuthSessionActions, AppsFeed {
|
|
25
51
|
status: "authenticated";
|
|
26
52
|
user: TUser;
|
|
27
|
-
apps: AppSwitcherEntry[];
|
|
28
53
|
error: null;
|
|
29
54
|
}
|
|
30
|
-
export interface UnauthenticatedSessionState extends AuthSessionActions {
|
|
55
|
+
export interface UnauthenticatedSessionState extends AuthSessionActions, AppsFeed {
|
|
31
56
|
status: "unauthenticated";
|
|
32
57
|
user: null;
|
|
33
|
-
|
|
58
|
+
appsError: null;
|
|
34
59
|
error: null;
|
|
35
60
|
}
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
|
|
61
|
+
/**
|
|
62
|
+
* AK-23: the server could not verify the session right now — flytedesk-id or the app's
|
|
63
|
+
* AuthStore is down (an `AuthUnavailableError`, a 503). NOT signed out: the session is
|
|
64
|
+
* kept (`user` is the last verified user, or null if none was verified yet) and the hook
|
|
65
|
+
* re-verifies it by itself, with exponential backoff (`retryInMs` until the next try,
|
|
66
|
+
* capped at UNAVAILABLE_RETRY_MAX_MS), until the server answers — then it moves to
|
|
67
|
+
* "authenticated", or to "unauthenticated" only if the server actually says so. Entered
|
|
68
|
+
* from the bootstrap, or from "authenticated" when any request made through this
|
|
69
|
+
* client hits the outage — including the automatic refresh reaching no decision at all
|
|
70
|
+
* (no answer, no answer in time, or an infrastructure 502/503/504: `AUTH_UNREACHABLE_CODE`).
|
|
71
|
+
* Render it as "reconnecting", never as a sign-in screen.
|
|
72
|
+
*
|
|
73
|
+
* `user` is DISPLAY-ONLY (AK-23, L-c): it is who was signed in when the outage began, kept
|
|
74
|
+
* so the page can keep showing them while it reconnects. It is not a verified identity
|
|
75
|
+
* right now — no authorization decision (showing an admin-only control, enabling a
|
|
76
|
+
* destructive action) may be based on it; the server re-checks every request anyway.
|
|
77
|
+
*/
|
|
78
|
+
export interface UnavailableSessionState<TUser> extends AuthSessionActions, AppsFeed {
|
|
79
|
+
status: "unavailable";
|
|
80
|
+
user: TUser | null;
|
|
81
|
+
error: AuthUnavailableError;
|
|
82
|
+
retryInMs: number;
|
|
83
|
+
}
|
|
84
|
+
/** Set only when loading the session failed for a reason OTHER than being signed out or
|
|
85
|
+
* an auth outage — the api errored, or a request it depends on missed its deadline. */
|
|
86
|
+
export interface ErrorSessionState extends AuthSessionActions, AppsFeed {
|
|
39
87
|
status: "error";
|
|
40
88
|
user: null;
|
|
41
|
-
|
|
89
|
+
appsError: null;
|
|
42
90
|
error: Error;
|
|
43
91
|
}
|
|
44
|
-
export type AuthSessionState<TUser> = LoadingSessionState | AuthenticatedSessionState<TUser> | UnauthenticatedSessionState | ErrorSessionState;
|
|
92
|
+
export type AuthSessionState<TUser> = LoadingSessionState | AuthenticatedSessionState<TUser> | UnauthenticatedSessionState | UnavailableSessionState<TUser> | ErrorSessionState;
|
|
45
93
|
/**
|
|
46
94
|
* `TUser` defaults to `unknown` deliberately: a consuming app almost always has its
|
|
47
95
|
* own richer local type (or the exact shape @flytedesk/app-kit/auth's AuthedUser
|
|
@@ -17,9 +17,24 @@
|
|
|
17
17
|
* keeps SessionKit, just pointed at this module's apiRequest instead of its local copy.
|
|
18
18
|
*/
|
|
19
19
|
import * as React from "react";
|
|
20
|
-
import { apiRequest, ApiRequestError, setAccessToken } from "./httpClient.js";
|
|
20
|
+
import { apiRequest, ApiRequestError, AuthUnavailableError, onAuthUnavailable, refreshAccessToken, setAccessToken, } from "./httpClient.js";
|
|
21
21
|
import { AUTH_ROUTE_PREFIX, authRoutePath } from "../shared-types.js";
|
|
22
|
-
|
|
22
|
+
/** First delay before re-verifying a session the server could not verify (AK-23). */
|
|
23
|
+
export const UNAVAILABLE_RETRY_BASE_MS = 1_000;
|
|
24
|
+
/** The backoff's ceiling: an outage is retried every this-many ms at most. */
|
|
25
|
+
export const UNAVAILABLE_RETRY_MAX_MS = 30_000;
|
|
26
|
+
/**
|
|
27
|
+
* Jittered exponential backoff for the `"unavailable"` state's automatic retries: the
|
|
28
|
+
* ceiling doubles per attempt (1s, 2s, 4s, … capped at UNAVAILABLE_RETRY_MAX_MS) and the
|
|
29
|
+
* delay is drawn uniformly from its upper half ("equal jitter", AK-23 L-c) — so every
|
|
30
|
+
* tab that saw the same outage does not retry in the same instant when it ends, while a
|
|
31
|
+
* retry never comes sooner than half the ceiling. `attempt` counts from 0; `random` is
|
|
32
|
+
* `Math.random` (a seam for tests).
|
|
33
|
+
*/
|
|
34
|
+
export function unavailableRetryDelayMs(attempt, random = Math.random) {
|
|
35
|
+
const ceiling = Math.min(UNAVAILABLE_RETRY_BASE_MS * 2 ** attempt, UNAVAILABLE_RETRY_MAX_MS);
|
|
36
|
+
return Math.round(ceiling / 2 + random() * (ceiling / 2));
|
|
37
|
+
}
|
|
23
38
|
const ME_PATH = authRoutePath(AUTH_ROUTE_PREFIX, "me");
|
|
24
39
|
const APPS_PATH = authRoutePath(AUTH_ROUTE_PREFIX, "apps");
|
|
25
40
|
const LOGOUT_PATH = authRoutePath(AUTH_ROUTE_PREFIX, "logout");
|
|
@@ -33,7 +48,9 @@ export function useAuthSession() {
|
|
|
33
48
|
const [status, setStatus] = React.useState("loading");
|
|
34
49
|
const [user, setUser] = React.useState(null);
|
|
35
50
|
const [apps, setApps] = React.useState([]);
|
|
51
|
+
const [appsError, setAppsError] = React.useState(null);
|
|
36
52
|
const [error, setError] = React.useState(null);
|
|
53
|
+
const [unavailable, setUnavailable] = React.useState(null);
|
|
37
54
|
const [generation, setGeneration] = React.useState(0);
|
|
38
55
|
// POST {routePrefix}/refresh ROTATES the refresh cookie on every call (reuse detection
|
|
39
56
|
// — see @flytedesk/app-kit/auth's plugin.ts) — a second call presenting the same,
|
|
@@ -47,72 +64,198 @@ export function useAuthSession() {
|
|
|
47
64
|
// flag — is what makes the second StrictMode invocation a no-op while still letting
|
|
48
65
|
// `refetch()` (which bumps `generation`) start a genuinely new load.
|
|
49
66
|
const startedGenerationRef = React.useRef(null);
|
|
67
|
+
// The load/retry machinery, created once per mounted hook (lazy useState initializer:
|
|
68
|
+
// guaranteed stable, unlike useMemo). It only touches state setters, which React keeps
|
|
69
|
+
// stable, and its own mutable fields.
|
|
70
|
+
const [session] = React.useState(() => {
|
|
71
|
+
const engine = {
|
|
72
|
+
/** Mirrors `status`, readable from outside a render (the outage listener). */
|
|
73
|
+
status: "loading",
|
|
74
|
+
/** Whether the hook is mounted — a retry never fires into an unmounted one. */
|
|
75
|
+
active: false,
|
|
76
|
+
retryTimer: null,
|
|
77
|
+
retryAttempt: 0,
|
|
78
|
+
/**
|
|
79
|
+
* AK-23 (L-a): which load may still change state. `signOut` and every fresh load
|
|
80
|
+
* (the bootstrap, `refetch`) bump it; a load that finds the epoch moved on since it
|
|
81
|
+
* started is stale and drops its answer — so a retry still in flight can neither
|
|
82
|
+
* put a signed-out session back into "unavailable" nor overwrite a newer load.
|
|
83
|
+
*/
|
|
84
|
+
epoch: 0,
|
|
85
|
+
setStatus(next) {
|
|
86
|
+
engine.status = next;
|
|
87
|
+
setStatus(next);
|
|
88
|
+
},
|
|
89
|
+
clearRetry() {
|
|
90
|
+
if (engine.retryTimer !== null)
|
|
91
|
+
clearTimeout(engine.retryTimer);
|
|
92
|
+
engine.retryTimer = null;
|
|
93
|
+
},
|
|
94
|
+
/** Ends every load in flight and any pending retry — their answers are ignored —
|
|
95
|
+
* and returns the new epoch. */
|
|
96
|
+
supersede() {
|
|
97
|
+
engine.clearRetry();
|
|
98
|
+
engine.epoch += 1;
|
|
99
|
+
return engine.epoch;
|
|
100
|
+
},
|
|
101
|
+
/**
|
|
102
|
+
* Refresh -> me -> apps. `fresh` (a new bootstrap) supersedes any load in flight and
|
|
103
|
+
* shows "loading"; a retry keeps whatever the user is looking at until it has an
|
|
104
|
+
* answer.
|
|
105
|
+
*
|
|
106
|
+
* The refresh is httpClient's shared, single-flighted `refreshAccessToken`, so a
|
|
107
|
+
* load started while another one (or any 401-triggered refresh) is waiting on
|
|
108
|
+
* `POST /refresh` JOINS that request instead of presenting the same rotating cookie
|
|
109
|
+
* a second time. `/me` and `/apps` skip the 401 -> refresh -> recovery dance: right
|
|
110
|
+
* after a refresh, a 401 is the answer itself, and a load must never start the
|
|
111
|
+
* app's auth recovery (which could sign a user back in after `signOut`).
|
|
112
|
+
*/
|
|
113
|
+
async load(fresh) {
|
|
114
|
+
const epoch = fresh ? engine.supersede() : engine.epoch;
|
|
115
|
+
const current = () => epoch === engine.epoch;
|
|
116
|
+
if (fresh) {
|
|
117
|
+
engine.retryAttempt = 0;
|
|
118
|
+
setError(null);
|
|
119
|
+
setUnavailable(null);
|
|
120
|
+
engine.setStatus("loading");
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const accessToken = await refreshAccessToken();
|
|
124
|
+
if (!current())
|
|
125
|
+
return;
|
|
126
|
+
if (accessToken === null) {
|
|
127
|
+
engine.settleSignedOut();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const me = await apiRequest(ME_PATH, { skipAuthRetry: true });
|
|
131
|
+
if (!current())
|
|
132
|
+
return;
|
|
133
|
+
const feed = await engine.loadApps();
|
|
134
|
+
if (!current())
|
|
135
|
+
return;
|
|
136
|
+
setUser(me.user);
|
|
137
|
+
setApps(feed.apps);
|
|
138
|
+
setAppsError(feed.error);
|
|
139
|
+
engine.retryAttempt = 0;
|
|
140
|
+
setUnavailable(null);
|
|
141
|
+
engine.setStatus("authenticated");
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (!current())
|
|
145
|
+
return;
|
|
146
|
+
if (err instanceof AuthUnavailableError) {
|
|
147
|
+
engine.scheduleRetry(err);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (err instanceof ApiRequestError && err.status === 401) {
|
|
151
|
+
engine.settleSignedOut();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
engine.retryAttempt = 0;
|
|
155
|
+
setUnavailable(null);
|
|
156
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
157
|
+
engine.setStatus("error");
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
/** The switcher feed, non-blocking (AK-23, N2): a failure is `apps: []` plus the
|
|
161
|
+
* error, never a failed load. */
|
|
162
|
+
async loadApps() {
|
|
163
|
+
try {
|
|
164
|
+
const res = await apiRequest(APPS_PATH, { skipAuthRetry: true });
|
|
165
|
+
return { apps: res.apps, error: null };
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
return { apps: [], error: err instanceof Error ? err : new Error(String(err)) };
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
/** The server said this browser has no session: local state follows. */
|
|
172
|
+
settleSignedOut() {
|
|
173
|
+
engine.clearRetry();
|
|
174
|
+
engine.retryAttempt = 0;
|
|
175
|
+
setAccessToken(null);
|
|
176
|
+
setUser(null);
|
|
177
|
+
setApps([]);
|
|
178
|
+
setAppsError(null);
|
|
179
|
+
setUnavailable(null);
|
|
180
|
+
engine.setStatus("unauthenticated");
|
|
181
|
+
},
|
|
182
|
+
/** Enters (or stays in) "unavailable" and re-verifies after the next backoff delay.
|
|
183
|
+
* One pending retry at a time: a burst of failing requests schedules one. */
|
|
184
|
+
scheduleRetry(err) {
|
|
185
|
+
if (!engine.active || engine.retryTimer !== null)
|
|
186
|
+
return;
|
|
187
|
+
const retryInMs = unavailableRetryDelayMs(engine.retryAttempt);
|
|
188
|
+
engine.retryAttempt += 1;
|
|
189
|
+
setUnavailable({ error: err, retryInMs });
|
|
190
|
+
engine.setStatus("unavailable");
|
|
191
|
+
engine.retryTimer = setTimeout(() => {
|
|
192
|
+
engine.retryTimer = null;
|
|
193
|
+
void engine.load(false);
|
|
194
|
+
}, retryInMs);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
return engine;
|
|
198
|
+
});
|
|
199
|
+
React.useEffect(() => {
|
|
200
|
+
session.active = true;
|
|
201
|
+
// An ordinary request (anywhere in the app) hitting an outage while signed in: keep
|
|
202
|
+
// the user, show "unavailable", and re-verify — never treat it as a sign-out.
|
|
203
|
+
const unsubscribe = onAuthUnavailable((err) => {
|
|
204
|
+
if (session.status === "authenticated")
|
|
205
|
+
session.scheduleRetry(err);
|
|
206
|
+
});
|
|
207
|
+
return () => {
|
|
208
|
+
session.active = false;
|
|
209
|
+
session.clearRetry();
|
|
210
|
+
unsubscribe();
|
|
211
|
+
};
|
|
212
|
+
}, [session]);
|
|
50
213
|
React.useEffect(() => {
|
|
51
214
|
if (startedGenerationRef.current === generation)
|
|
52
215
|
return;
|
|
53
216
|
startedGenerationRef.current = generation;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
setError(null);
|
|
57
|
-
try {
|
|
58
|
-
const refreshed = await apiRequest(REFRESH_PATH, { method: "POST" });
|
|
59
|
-
setAccessToken(refreshed.accessToken);
|
|
60
|
-
const me = await apiRequest(ME_PATH);
|
|
61
|
-
setUser(me.user);
|
|
62
|
-
const appsRes = await apiRequest(APPS_PATH);
|
|
63
|
-
setApps(appsRes.apps);
|
|
64
|
-
setStatus("authenticated");
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
if (err instanceof ApiRequestError && err.status === 401) {
|
|
68
|
-
setStatus("unauthenticated");
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
72
|
-
setStatus("error");
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
void load();
|
|
77
|
-
}, [generation]);
|
|
217
|
+
void session.load(true);
|
|
218
|
+
}, [generation, session]);
|
|
78
219
|
const signOut = React.useCallback(async () => {
|
|
220
|
+
// Whatever load or retry is in flight belongs to the session being ended: its answer
|
|
221
|
+
// must never land after this one (AK-23, L-a).
|
|
222
|
+
session.supersede();
|
|
79
223
|
try {
|
|
80
224
|
await apiRequest(LOGOUT_PATH, { method: "POST" });
|
|
81
225
|
}
|
|
82
226
|
catch (err) {
|
|
83
227
|
if (err instanceof ApiRequestError && err.code === "logout_store_failed") {
|
|
84
|
-
// The plugin
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
228
|
+
// The plugin clears the refresh cookie on EVERY logout outcome (AK-23, in a
|
|
229
|
+
// `finally`) and attempts each AuthStore step independently, so this browser
|
|
230
|
+
// no longer holds a usable session no matter which step failed here (plugin.ts's
|
|
231
|
+
// POST {routePrefix}/logout). Leaving local state at
|
|
88
232
|
// "authenticated" here would be exactly the bug the SMS-5 gate found (the design
|
|
89
233
|
// system's AccountMenu swallowing this rejection and showing nothing): a UI
|
|
90
234
|
// claiming a live session the server has already revoked. Clearing local state
|
|
91
235
|
// mirrors the server's actual state; re-throwing still fails loudly so a caller
|
|
92
236
|
// can surface "sign-out did not fully complete" rather than reporting a clean one.
|
|
93
|
-
|
|
94
|
-
setUser(null);
|
|
95
|
-
setApps([]);
|
|
96
|
-
setStatus("unauthenticated");
|
|
237
|
+
session.settleSignedOut();
|
|
97
238
|
}
|
|
98
239
|
throw err;
|
|
99
240
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
setApps([]);
|
|
103
|
-
setStatus("unauthenticated");
|
|
104
|
-
}, []);
|
|
241
|
+
session.settleSignedOut();
|
|
242
|
+
}, [session]);
|
|
105
243
|
const refetch = React.useCallback(() => setGeneration((g) => g + 1), []);
|
|
106
244
|
const actions = { signOut, refetch };
|
|
107
245
|
if (status === "authenticated") {
|
|
108
246
|
// Invariant: the only place status flips to "authenticated" is the line right after
|
|
109
247
|
// setUser(me.user) above, in the same synchronous pass — so by the time a render
|
|
110
248
|
// observes status === "authenticated", `user` is never null.
|
|
111
|
-
return { ...actions, status, user: user, apps, error: null };
|
|
249
|
+
return { ...actions, status, user: user, apps, appsError, error: null };
|
|
250
|
+
}
|
|
251
|
+
if (status === "unavailable" && unavailable) {
|
|
252
|
+
return { ...actions, status, user, apps, appsError, error: unavailable.error, retryInMs: unavailable.retryInMs };
|
|
112
253
|
}
|
|
113
254
|
if (status === "error") {
|
|
114
|
-
return { ...actions, status, user: null, apps, error: error ?? new Error("unknown error") };
|
|
255
|
+
return { ...actions, status, user: null, apps, appsError: null, error: error ?? new Error("unknown error") };
|
|
115
256
|
}
|
|
116
|
-
|
|
257
|
+
if (status === "loading")
|
|
258
|
+
return { ...actions, status, user: null, apps, appsError: null, error: null };
|
|
259
|
+
return { ...actions, status: "unauthenticated", user: null, apps, appsError: null, error: null };
|
|
117
260
|
}
|
|
118
261
|
//# sourceMappingURL=useAuthSession.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuthSession.js","sourceRoot":"","sources":["../../../src/auth/client/useAuthSession.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,
|
|
1
|
+
{"version":3,"file":"useAuthSession.js","sourceRoot":"","sources":["../../../src/auth/client/useAuthSession.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/errors.d.ts
CHANGED
|
@@ -41,19 +41,42 @@ export declare class AuthStoreError extends Error {
|
|
|
41
41
|
readonly cause?: unknown | undefined;
|
|
42
42
|
constructor(message: string, cause?: unknown | undefined);
|
|
43
43
|
}
|
|
44
|
-
/** Thrown by ./oidc-client.ts for any non-2xx HTTP response from flytedesk-id
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
/** Thrown by ./oidc-client.ts for any non-2xx HTTP response from a flytedesk-id
|
|
45
|
+
* RESOURCE endpoint (userinfo, authorization, `/me/apps`), carrying the real status
|
|
46
|
+
* code so classifyIdpError (below) can tell an outage (5xx, network failure) apart from
|
|
47
|
+
* a revocation (401/403) without parsing response text. */
|
|
47
48
|
export declare class OidcHttpError extends Error {
|
|
48
49
|
readonly status: number;
|
|
49
50
|
readonly cause?: unknown | undefined;
|
|
50
51
|
constructor(message: string, status: number, cause?: unknown | undefined);
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* A non-2xx answer from flytedesk-id's `/token` endpoint (AK-23), carrying the OAuth
|
|
55
|
+
* `error` code from its body (RFC 6749 §5.2) when there is one. Its own type because
|
|
56
|
+
* /token's status codes mean something different from a resource endpoint's: a 401
|
|
57
|
+
* there is `invalid_client` — THIS APP's credentials were refused (a bad or rotated
|
|
58
|
+
* client secret) — which says nothing about any user's session.
|
|
59
|
+
*/
|
|
60
|
+
export declare class OidcTokenError extends OidcHttpError {
|
|
61
|
+
readonly oauthError: string | undefined;
|
|
62
|
+
constructor(message: string, status: number, oauthError: string | undefined);
|
|
63
|
+
}
|
|
52
64
|
/**
|
|
53
65
|
* Normalizes anything a flytedesk-id call in ./oidc-client.ts can throw/reject with
|
|
54
|
-
* into an IdpUnavailableError or IdpAccessRevokedError
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
66
|
+
* into an IdpUnavailableError or IdpAccessRevokedError — the difference between a
|
|
67
|
+
* session being revoked and answering 503 (AK-23), so it must be exact:
|
|
68
|
+
*
|
|
69
|
+
* - `/token` (`OidcTokenError`): ONLY `error=invalid_grant` is a revocation — RFC 6749
|
|
70
|
+
* §5.2's answer for a refresh token that is "invalid, expired, revoked". Every other
|
|
71
|
+
* /token error — `invalid_client` (a 401: a bad or rotated client secret),
|
|
72
|
+
* `unauthorized_client`, `invalid_request`, a 5xx — is this app's configuration or
|
|
73
|
+
* flytedesk-id itself failing, true for every session at once: an outage. Treating
|
|
74
|
+
* `invalid_client` as a revocation would revoke every family that refreshed while
|
|
75
|
+
* the secret was wrong.
|
|
76
|
+
* - A resource endpoint (`OidcHttpError`): 401/403 means flytedesk-id refused this
|
|
77
|
+
* session's access token — a revocation. Anything else is an outage.
|
|
78
|
+
* - Anything else (a network failure, a timeout) is an outage.
|
|
79
|
+
*
|
|
80
|
+
* Never throws itself; always returns an Error to `throw`/log at the call site.
|
|
58
81
|
*/
|
|
59
82
|
export declare function classifyIdpError(err: unknown): IdpUnavailableError | IdpAccessRevokedError;
|
package/dist/auth/errors.js
CHANGED
|
@@ -58,9 +58,10 @@ export class AuthStoreError extends Error {
|
|
|
58
58
|
this.name = "AuthStoreError";
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
/** Thrown by ./oidc-client.ts for any non-2xx HTTP response from flytedesk-id
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
/** Thrown by ./oidc-client.ts for any non-2xx HTTP response from a flytedesk-id
|
|
62
|
+
* RESOURCE endpoint (userinfo, authorization, `/me/apps`), carrying the real status
|
|
63
|
+
* code so classifyIdpError (below) can tell an outage (5xx, network failure) apart from
|
|
64
|
+
* a revocation (401/403) without parsing response text. */
|
|
64
65
|
export class OidcHttpError extends Error {
|
|
65
66
|
status;
|
|
66
67
|
cause;
|
|
@@ -71,17 +72,46 @@ export class OidcHttpError extends Error {
|
|
|
71
72
|
this.name = "OidcHttpError";
|
|
72
73
|
}
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* A non-2xx answer from flytedesk-id's `/token` endpoint (AK-23), carrying the OAuth
|
|
77
|
+
* `error` code from its body (RFC 6749 §5.2) when there is one. Its own type because
|
|
78
|
+
* /token's status codes mean something different from a resource endpoint's: a 401
|
|
79
|
+
* there is `invalid_client` — THIS APP's credentials were refused (a bad or rotated
|
|
80
|
+
* client secret) — which says nothing about any user's session.
|
|
81
|
+
*/
|
|
82
|
+
export class OidcTokenError extends OidcHttpError {
|
|
83
|
+
oauthError;
|
|
84
|
+
constructor(message, status, oauthError) {
|
|
85
|
+
super(message, status);
|
|
86
|
+
this.oauthError = oauthError;
|
|
87
|
+
this.name = "OidcTokenError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
74
90
|
/**
|
|
75
91
|
* Normalizes anything a flytedesk-id call in ./oidc-client.ts can throw/reject with
|
|
76
|
-
* into an IdpUnavailableError or IdpAccessRevokedError
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
92
|
+
* into an IdpUnavailableError or IdpAccessRevokedError — the difference between a
|
|
93
|
+
* session being revoked and answering 503 (AK-23), so it must be exact:
|
|
94
|
+
*
|
|
95
|
+
* - `/token` (`OidcTokenError`): ONLY `error=invalid_grant` is a revocation — RFC 6749
|
|
96
|
+
* §5.2's answer for a refresh token that is "invalid, expired, revoked". Every other
|
|
97
|
+
* /token error — `invalid_client` (a 401: a bad or rotated client secret),
|
|
98
|
+
* `unauthorized_client`, `invalid_request`, a 5xx — is this app's configuration or
|
|
99
|
+
* flytedesk-id itself failing, true for every session at once: an outage. Treating
|
|
100
|
+
* `invalid_client` as a revocation would revoke every family that refreshed while
|
|
101
|
+
* the secret was wrong.
|
|
102
|
+
* - A resource endpoint (`OidcHttpError`): 401/403 means flytedesk-id refused this
|
|
103
|
+
* session's access token — a revocation. Anything else is an outage.
|
|
104
|
+
* - Anything else (a network failure, a timeout) is an outage.
|
|
105
|
+
*
|
|
106
|
+
* Never throws itself; always returns an Error to `throw`/log at the call site.
|
|
80
107
|
*/
|
|
81
108
|
export function classifyIdpError(err) {
|
|
82
109
|
if (err instanceof IdpAccessRevokedError || err instanceof IdpUnavailableError)
|
|
83
110
|
return err;
|
|
84
|
-
|
|
111
|
+
const revoked = err instanceof OidcTokenError
|
|
112
|
+
? err.oauthError === "invalid_grant"
|
|
113
|
+
: err instanceof OidcHttpError && (err.status === 401 || err.status === 403);
|
|
114
|
+
if (revoked) {
|
|
85
115
|
return new IdpAccessRevokedError(err.message, err);
|
|
86
116
|
}
|
|
87
117
|
const message = err instanceof Error ? err.message : String(err);
|
package/dist/auth/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/auth/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,mFAAmF;AACnF,MAAM,OAAO,QAAS,SAAQ,KAAK;IAGf;IAFlB,YACE,OAAe,EACC,KAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,UAAK,GAAL,KAAK,CAAU;QAG/B,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AAED;;sBAEsB;AACtB,MAAM,OAAO,mBAAoB,SAAQ,QAAQ;IAC/C,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,eAAgB,SAAQ,mBAAmB;IACtD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED;;+DAE+D;AAC/D,MAAM,OAAO,qBAAsB,SAAQ,QAAQ;IACjD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;;uFAEuF;AACvF,MAAM,OAAO,cAAe,SAAQ,KAAK;IAGrB;IAFlB,YACE,OAAe,EACC,KAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,UAAK,GAAL,KAAK,CAAU;QAG/B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/auth/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,mFAAmF;AACnF,MAAM,OAAO,QAAS,SAAQ,KAAK;IAGf;IAFlB,YACE,OAAe,EACC,KAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,UAAK,GAAL,KAAK,CAAU;QAG/B,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AAED;;sBAEsB;AACtB,MAAM,OAAO,mBAAoB,SAAQ,QAAQ;IAC/C,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,eAAgB,SAAQ,mBAAmB;IACtD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED;;+DAE+D;AAC/D,MAAM,OAAO,qBAAsB,SAAQ,QAAQ;IACjD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;;uFAEuF;AACvF,MAAM,OAAO,cAAe,SAAQ,KAAK;IAGrB;IAFlB,YACE,OAAe,EACC,KAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,UAAK,GAAL,KAAK,CAAU;QAG/B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED;;;4DAG4D;AAC5D,MAAM,OAAO,aAAc,SAAQ,KAAK;IAGpB;IACA;IAHlB,YACE,OAAe,EACC,MAAc,EACd,KAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,WAAM,GAAN,MAAM,CAAQ;QACd,UAAK,GAAL,KAAK,CAAU;QAG/B,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,cAAe,SAAQ,aAAa;IAI7B;IAHlB,YACE,OAAe,EACf,MAAc,EACE,UAA8B;QAE9C,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAFP,eAAU,GAAV,UAAU,CAAoB;QAG9C,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAY;IAC3C,IAAI,GAAG,YAAY,qBAAqB,IAAI,GAAG,YAAY,mBAAmB;QAAE,OAAO,GAAG,CAAC;IAE3F,MAAM,OAAO,GACX,GAAG,YAAY,cAAc;QAC3B,CAAC,CAAC,GAAG,CAAC,UAAU,KAAK,eAAe;QACpC,CAAC,CAAC,GAAG,YAAY,aAAa,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC;IACjF,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,IAAI,qBAAqB,CAAE,GAAa,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAChE,CAAC;IACD,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,OAAO,IAAI,mBAAmB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAC/C,CAAC"}
|
package/dist/auth/guards.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
/**
|
|
3
|
+
* Answers the request when it carries no authorized user, and returns whether it did.
|
|
4
|
+
* AK-23: when the plugin could not DECIDE (flytedesk-id or the AuthStore unavailable,
|
|
5
|
+
* `onAuthorizeUser` threw — `request.authUnavailable`), that is a 503 with the cause
|
|
6
|
+
* code, never the 401 that tells a client its session is over.
|
|
7
|
+
*
|
|
8
|
+
* The ONE authentication guard: `requireAuth`/`requirePermission` below and the
|
|
9
|
+
* `@flytedesk/app-kit/flags` and `@flytedesk/app-kit/profile` routes all answer
|
|
10
|
+
* through it. It reads only the request decorations (`authUser`, `authUnavailable`),
|
|
11
|
+
* so a consumer that populates them some other way still works.
|
|
12
|
+
*/
|
|
13
|
+
export declare function rejectUnauthenticated(request: FastifyRequest, reply: FastifyReply): boolean;
|
|
2
14
|
export declare function requireAuth(request: FastifyRequest, reply: FastifyReply, done: () => void): void;
|
|
3
15
|
/** Every route re-checks permissions server-side. 403s name the missing permission(s)
|
|
4
16
|
* in `detail`, so a caller (or its UI) can tell exactly what was missing. */
|
package/dist/auth/guards.js
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
|
+
import { sendAuthUnavailable } from "./unavailable.js";
|
|
2
|
+
/**
|
|
3
|
+
* Answers the request when it carries no authorized user, and returns whether it did.
|
|
4
|
+
* AK-23: when the plugin could not DECIDE (flytedesk-id or the AuthStore unavailable,
|
|
5
|
+
* `onAuthorizeUser` threw — `request.authUnavailable`), that is a 503 with the cause
|
|
6
|
+
* code, never the 401 that tells a client its session is over.
|
|
7
|
+
*
|
|
8
|
+
* The ONE authentication guard: `requireAuth`/`requirePermission` below and the
|
|
9
|
+
* `@flytedesk/app-kit/flags` and `@flytedesk/app-kit/profile` routes all answer
|
|
10
|
+
* through it. It reads only the request decorations (`authUser`, `authUnavailable`),
|
|
11
|
+
* so a consumer that populates them some other way still works.
|
|
12
|
+
*/
|
|
13
|
+
export function rejectUnauthenticated(request, reply) {
|
|
14
|
+
if (request.authUser)
|
|
15
|
+
return false;
|
|
16
|
+
if (request.authUnavailable) {
|
|
17
|
+
sendAuthUnavailable(reply, request.authUnavailable);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
reply.code(401).send({ error: "Authentication required", code: "unauthenticated" });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
1
23
|
export function requireAuth(request, reply, done) {
|
|
2
|
-
if (
|
|
3
|
-
reply.code(401).send({ error: "Authentication required", code: "unauthenticated" });
|
|
24
|
+
if (rejectUnauthenticated(request, reply))
|
|
4
25
|
return;
|
|
5
|
-
}
|
|
6
26
|
done();
|
|
7
27
|
}
|
|
8
28
|
/** Every route re-checks permissions server-side. 403s name the missing permission(s)
|
|
9
29
|
* in `detail`, so a caller (or its UI) can tell exactly what was missing. */
|
|
10
30
|
export function requirePermission(...permission) {
|
|
11
31
|
return function permissionPreHandler(request, reply, done) {
|
|
12
|
-
if (
|
|
13
|
-
reply.code(401).send({ error: "Authentication required", code: "unauthenticated" });
|
|
32
|
+
if (rejectUnauthenticated(request, reply))
|
|
14
33
|
return;
|
|
15
|
-
}
|
|
16
34
|
const missing = permission.filter((p) => !request.authUser.permissions.includes(p));
|
|
17
35
|
if (missing.length) {
|
|
18
36
|
reply.code(403).send({
|
package/dist/auth/guards.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../src/auth/guards.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../src/auth/guards.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAuB,EAAE,KAAmB;IAChF,IAAI,OAAO,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;QAC5B,mBAAmB,CAAC,KAAK,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;IACpF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAuB,EAAE,KAAmB,EAAE,IAAgB;IACxF,IAAI,qBAAqB,CAAC,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO;IAClD,IAAI,EAAE,CAAC;AACT,CAAC;AAED;8EAC8E;AAC9E,MAAM,UAAU,iBAAiB,CAAC,GAAG,UAAoB;IACvD,OAAO,SAAS,oBAAoB,CAClC,OAAuB,EACvB,KAAmB,EACnB,IAAgB;QAEhB,IAAI,qBAAqB,CAAC,OAAO,EAAE,KAAK,CAAC;YAAE,OAAO;QAClD,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,QAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACrF,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,KAAK,EAAE,oBAAoB;gBAC3B,IAAI,EAAE,WAAW;gBACjB,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;aAC3B,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
|