@flytedesk/app-kit 0.5.0 → 0.7.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/dist/auth/client/httpClient.d.ts +85 -0
- package/dist/auth/client/httpClient.js +218 -0
- package/dist/auth/client/httpClient.js.map +1 -0
- package/dist/auth/client/index.d.ts +9 -0
- package/dist/auth/client/index.js +9 -0
- package/dist/auth/client/index.js.map +1 -0
- package/dist/auth/client/useAuthSession.d.ts +25 -0
- package/dist/auth/client/useAuthSession.js +97 -0
- package/dist/auth/client/useAuthSession.js.map +1 -0
- package/dist/auth/errors.d.ts +47 -0
- package/dist/auth/errors.js +75 -0
- package/dist/auth/errors.js.map +1 -0
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/oidc-client.d.ts +3 -14
- package/dist/auth/oidc-client.js +5 -4
- package/dist/auth/oidc-client.js.map +1 -1
- package/dist/auth/plugin.js +149 -28
- package/dist/auth/plugin.js.map +1 -1
- package/dist/auth/shared-types.d.ts +49 -0
- package/dist/auth/shared-types.js +11 -0
- package/dist/auth/shared-types.js.map +1 -0
- package/dist/auth/testing/fake-idp.d.ts +98 -0
- package/dist/auth/testing/fake-idp.js +336 -0
- package/dist/auth/testing/fake-idp.js.map +1 -0
- package/dist/auth/testing/index.d.ts +5 -0
- package/dist/auth/testing/index.js +6 -0
- package/dist/auth/testing/index.js.map +1 -0
- package/dist/auth/types.d.ts +6 -18
- package/package.json +20 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe BFF session client for `@flytedesk/app-kit/auth`'s cookie/bearer
|
|
3
|
+
* pattern (POST {api}/auth/refresh -> Bearer on every call, transparent 401 -> refresh
|
|
4
|
+
* -> retry-once). Extracted (AK-16) from media-planner's
|
|
5
|
+
* packages/design-system/src/apiClient.ts, which every @flytedesk/app-kit/auth
|
|
6
|
+
* consumer was otherwise on track to re-implement by hand (see sms-app's
|
|
7
|
+
* apps/web/src/api/client.ts before this card: no 401-retry, no deadline, a different
|
|
8
|
+
* single-flight mechanism). This module has ZERO Node imports — no `node:*`, no
|
|
9
|
+
* Fastify, nothing server-only — so it is safe to import from a browser bundle. If a
|
|
10
|
+
* future change here ever needs one, it belongs in the server (`../plugin.js`) or in a
|
|
11
|
+
* new Node-only module instead.
|
|
12
|
+
*/
|
|
13
|
+
import type { ApiErrorBody } from "../shared-types.js";
|
|
14
|
+
/** `expires_in`-free deadline for every call this client makes, unless a caller passes
|
|
15
|
+
* its own `deadlineMs` — matches media-planner's AUTH_REQUEST_DEADLINE_MS. */
|
|
16
|
+
export declare const DEFAULT_REQUEST_DEADLINE_MS = 10000;
|
|
17
|
+
export declare class ApiRequestError extends Error {
|
|
18
|
+
code: string;
|
|
19
|
+
field?: string;
|
|
20
|
+
status: number;
|
|
21
|
+
/** The full decoded error body, beyond the standard `{error, code, field?}` shape —
|
|
22
|
+
* every route's error body still satisfies ApiErrorBody; this is that same object,
|
|
23
|
+
* untyped, for routes that add fields to it. */
|
|
24
|
+
body: ApiErrorBody & Record<string, unknown>;
|
|
25
|
+
constructor(status: number, body: ApiErrorBody & Record<string, unknown>);
|
|
26
|
+
}
|
|
27
|
+
/** A request that missed its deadline (see withDeadline). */
|
|
28
|
+
export declare class RequestDeadlineError extends Error {
|
|
29
|
+
constructor(what: string, ms: number);
|
|
30
|
+
}
|
|
31
|
+
export declare function configureApiClient(opts: {
|
|
32
|
+
baseUrl: string;
|
|
33
|
+
}): void;
|
|
34
|
+
export declare function setAccessToken(token: string | null): void;
|
|
35
|
+
export declare function getAccessToken(): string | null;
|
|
36
|
+
/** As configured via configureApiClient — same origin/path every request in this
|
|
37
|
+
* module targets. */
|
|
38
|
+
export declare function getBaseUrl(): string;
|
|
39
|
+
export interface AuthRecovery {
|
|
40
|
+
/** Runs (or joins) the shared recovery; resolves when it is over, however it ended. */
|
|
41
|
+
recover(): Promise<void>;
|
|
42
|
+
/** Whose session this is right now — a stable user id — or null when signed out. */
|
|
43
|
+
principal(): string | null;
|
|
44
|
+
}
|
|
45
|
+
/** Registers what runs when a request 401s and the automatic refresh-then-retry dance
|
|
46
|
+
* (below) also fails to recover it — i.e. the session is genuinely gone, not just a
|
|
47
|
+
* normal access-token expiry a refresh already fixed silently. A consuming app's own
|
|
48
|
+
* session layer (e.g. one doing a silent OIDC resume, as media-planner's session.ts
|
|
49
|
+
* does) is the one owner: it decides what "recovered" means and, failing that,
|
|
50
|
+
* invalidates its session state. Left entirely unset is fine — decodeWithAuthRetry
|
|
51
|
+
* then just reports the second 401 as a failure, with no recovery attempt. */
|
|
52
|
+
export declare function setAuthRecovery(recovery: AuthRecovery): () => void;
|
|
53
|
+
export interface RequestOptions {
|
|
54
|
+
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
55
|
+
body?: unknown;
|
|
56
|
+
query?: Record<string, string | number | boolean | string[] | undefined>;
|
|
57
|
+
/** Skip the automatic 401 -> refresh -> retry dance (used by auth routes themselves). */
|
|
58
|
+
skipAuthRetry?: boolean;
|
|
59
|
+
/** Bounds the whole call — first attempt, any refresh it waits on, the retry and the
|
|
60
|
+
* body decode. Past it the call rejects with a RequestDeadlineError and does nothing
|
|
61
|
+
* further (no retry, no auth-failure event). */
|
|
62
|
+
deadlineMs?: number;
|
|
63
|
+
/** Set only by a session layer's own bootstrap/probe calls, which ARE the auth
|
|
64
|
+
* recovery (or start it): a 401 that survives the refresh still starts the shared
|
|
65
|
+
* recovery, but this call does not wait for it — waiting would mean the recovery
|
|
66
|
+
* waiting on itself. */
|
|
67
|
+
ownsAuthRecovery?: boolean;
|
|
68
|
+
/** Passed straight to `fetch`'s own `keepalive` — lets a request started right
|
|
69
|
+
* before the page unloads actually complete instead of being cancelled the instant
|
|
70
|
+
* the page goes away. Only meaningfully used with a small body: the browser caps
|
|
71
|
+
* the total size of in-flight keepalive requests (~64KB combined). */
|
|
72
|
+
keepalive?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** The one place a browser app talks to its own BFF api. Handles bearer auth,
|
|
75
|
+
* transparent access-token refresh on 401, and the { error, code, field? } error
|
|
76
|
+
* contract every @flytedesk/app-kit/auth-backed api returns. */
|
|
77
|
+
export declare function apiRequest<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
78
|
+
/** Sibling to apiRequest for multipart/form-data uploads. Same bearer auth, 401 ->
|
|
79
|
+
* refresh -> retry-once dance, and error contract — but always POSTs `formData` as-is
|
|
80
|
+
* (no JSON.stringify) and deliberately never sets a Content-Type header: the browser
|
|
81
|
+
* must set its own multipart boundary, and setting Content-Type manually on a
|
|
82
|
+
* FormData body breaks that boundary. */
|
|
83
|
+
export declare function apiUpload<T>(path: string, formData: FormData, options?: {
|
|
84
|
+
skipAuthRetry?: boolean;
|
|
85
|
+
}): Promise<T>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/** `expires_in`-free deadline for every call this client makes, unless a caller passes
|
|
2
|
+
* its own `deadlineMs` — matches media-planner's AUTH_REQUEST_DEADLINE_MS. */
|
|
3
|
+
export const DEFAULT_REQUEST_DEADLINE_MS = 10_000;
|
|
4
|
+
export class ApiRequestError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
field;
|
|
7
|
+
status;
|
|
8
|
+
/** The full decoded error body, beyond the standard `{error, code, field?}` shape —
|
|
9
|
+
* every route's error body still satisfies ApiErrorBody; this is that same object,
|
|
10
|
+
* untyped, for routes that add fields to it. */
|
|
11
|
+
body;
|
|
12
|
+
constructor(status, body) {
|
|
13
|
+
super(body.error);
|
|
14
|
+
this.name = "ApiRequestError";
|
|
15
|
+
this.code = body.code;
|
|
16
|
+
this.field = body.field;
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.body = body;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** A request that missed its deadline (see withDeadline). */
|
|
22
|
+
export class RequestDeadlineError extends Error {
|
|
23
|
+
constructor(what, ms) {
|
|
24
|
+
super(`${what} did not complete within ${ms}ms`);
|
|
25
|
+
this.name = "RequestDeadlineError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
let accessToken = null;
|
|
29
|
+
let refreshPromise = null;
|
|
30
|
+
let baseUrl = "/api/v1";
|
|
31
|
+
export function configureApiClient(opts) {
|
|
32
|
+
baseUrl = opts.baseUrl;
|
|
33
|
+
}
|
|
34
|
+
export function setAccessToken(token) {
|
|
35
|
+
accessToken = token;
|
|
36
|
+
}
|
|
37
|
+
export function getAccessToken() {
|
|
38
|
+
return accessToken;
|
|
39
|
+
}
|
|
40
|
+
/** As configured via configureApiClient — same origin/path every request in this
|
|
41
|
+
* module targets. */
|
|
42
|
+
export function getBaseUrl() {
|
|
43
|
+
return baseUrl;
|
|
44
|
+
}
|
|
45
|
+
let authRecovery = null;
|
|
46
|
+
/** Registers what runs when a request 401s and the automatic refresh-then-retry dance
|
|
47
|
+
* (below) also fails to recover it — i.e. the session is genuinely gone, not just a
|
|
48
|
+
* normal access-token expiry a refresh already fixed silently. A consuming app's own
|
|
49
|
+
* session layer (e.g. one doing a silent OIDC resume, as media-planner's session.ts
|
|
50
|
+
* does) is the one owner: it decides what "recovered" means and, failing that,
|
|
51
|
+
* invalidates its session state. Left entirely unset is fine — decodeWithAuthRetry
|
|
52
|
+
* then just reports the second 401 as a failure, with no recovery attempt. */
|
|
53
|
+
export function setAuthRecovery(recovery) {
|
|
54
|
+
authRecovery = recovery;
|
|
55
|
+
return () => {
|
|
56
|
+
if (authRecovery === recovery)
|
|
57
|
+
authRecovery = null;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Runs `run` under a deadline: past `ms`, its AbortSignal aborts (so the underlying
|
|
61
|
+
* fetch, and any body read on it, is cancelled) and the returned promise rejects with
|
|
62
|
+
* a RequestDeadlineError — the rejection does not depend on whatever `run` awaits
|
|
63
|
+
* honouring the signal, so this settles no matter what. */
|
|
64
|
+
async function withDeadline(what, ms, run) {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
let timer;
|
|
67
|
+
const deadline = new Promise((_, reject) => {
|
|
68
|
+
timer = setTimeout(() => {
|
|
69
|
+
const reason = new RequestDeadlineError(what, ms);
|
|
70
|
+
controller.abort(reason);
|
|
71
|
+
reject(reason);
|
|
72
|
+
}, ms);
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
return await Promise.race([run(controller.signal), deadline]);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Single-flighted, and always deadline-bound: every caller that 401s shares this one
|
|
82
|
+
* refresh, so a refresh that never answered would stall all of them. Any failure — a
|
|
83
|
+
* non-2xx, a network error, the deadline — resolves null ("no new token"), which
|
|
84
|
+
* sendAndDecode treats exactly like a refused refresh. */
|
|
85
|
+
async function refreshAccessToken() {
|
|
86
|
+
if (!refreshPromise) {
|
|
87
|
+
refreshPromise = withDeadline("POST /auth/refresh", DEFAULT_REQUEST_DEADLINE_MS, async (signal) => {
|
|
88
|
+
const res = await fetch(`${baseUrl}/auth/refresh`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
credentials: "include",
|
|
91
|
+
signal,
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok)
|
|
94
|
+
return null;
|
|
95
|
+
const body = (await res.json());
|
|
96
|
+
setAccessToken(body.accessToken);
|
|
97
|
+
return body.accessToken;
|
|
98
|
+
})
|
|
99
|
+
.catch(() => null)
|
|
100
|
+
.finally(() => {
|
|
101
|
+
refreshPromise = null;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return refreshPromise;
|
|
105
|
+
}
|
|
106
|
+
/** True when baseUrl is itself a full origin (e.g. "https://api.example.com/api/v1"),
|
|
107
|
+
* as opposed to a same-origin path like "/api/v1" that a dev proxy handles. */
|
|
108
|
+
function isAbsoluteUrl(value) {
|
|
109
|
+
return /^[a-z][a-z\d+.-]*:\/\//i.test(value);
|
|
110
|
+
}
|
|
111
|
+
function buildUrl(path, query) {
|
|
112
|
+
const url = new URL(baseUrl + path, typeof window !== "undefined" ? window.location.origin : "http://localhost");
|
|
113
|
+
if (query) {
|
|
114
|
+
for (const [key, value] of Object.entries(query)) {
|
|
115
|
+
if (value == null || value === "")
|
|
116
|
+
continue;
|
|
117
|
+
if (Array.isArray(value))
|
|
118
|
+
value.forEach((v) => url.searchParams.append(key, String(v)));
|
|
119
|
+
else
|
|
120
|
+
url.searchParams.set(key, String(value));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// baseUrl is either a real absolute origin (production/deployed builds) or a
|
|
124
|
+
// same-origin path proxied by a dev server. Only strip the origin in the dev case —
|
|
125
|
+
// a deployed build must keep the real API origin or every request silently resolves
|
|
126
|
+
// against the web app's own origin instead.
|
|
127
|
+
return isAbsoluteUrl(baseUrl) ? url.toString() : url.pathname + url.search;
|
|
128
|
+
}
|
|
129
|
+
/** Shared tail of both apiRequest and apiUpload: fires `doFetch`, transparently
|
|
130
|
+
* retries once after a refresh on 401 (unless skipped) — and, if that still 401s, once
|
|
131
|
+
* more after the shared auth recovery (see setAuthRecovery) — and applies the same
|
|
132
|
+
* { error, code, field? } error contract / 204-handling / JSON decode to the result —
|
|
133
|
+
* all under `deadlineMs` when one is given (see withDeadline). Kept as one place so
|
|
134
|
+
* the auth-retry dance can't drift between the JSON and multipart transports. */
|
|
135
|
+
async function sendAndDecode(label, doFetch, options) {
|
|
136
|
+
const { deadlineMs } = options;
|
|
137
|
+
if (deadlineMs === undefined)
|
|
138
|
+
return decodeWithAuthRetry(doFetch, options);
|
|
139
|
+
return withDeadline(label, deadlineMs, (signal) => decodeWithAuthRetry(doFetch, options, signal));
|
|
140
|
+
}
|
|
141
|
+
async function decodeWithAuthRetry(doFetch, { skipAuthRetry, ownsAuthRecovery }, signal) {
|
|
142
|
+
// Who this request is being sent as — the only identity a recovery may replay it for.
|
|
143
|
+
const recovery = authRecovery;
|
|
144
|
+
const sentAs = recovery?.principal() ?? null;
|
|
145
|
+
let res = await doFetch(signal);
|
|
146
|
+
if (res.status === 401 && !skipAuthRetry) {
|
|
147
|
+
const refreshed = await refreshAccessToken();
|
|
148
|
+
// The shared refresh is not cancelled by this call's deadline (other callers may
|
|
149
|
+
// still want it) — but a call that has already missed its deadline must not act on
|
|
150
|
+
// its outcome: no retry, and no auth-failure event for a caller that gave up.
|
|
151
|
+
signal?.throwIfAborted();
|
|
152
|
+
if (refreshed)
|
|
153
|
+
res = await doFetch(signal);
|
|
154
|
+
signal?.throwIfAborted();
|
|
155
|
+
if (res.status === 401) {
|
|
156
|
+
// Both the original request and the refresh-then-retry (or the refresh call
|
|
157
|
+
// itself, if it never returned a token) came back 401 — the session is gone,
|
|
158
|
+
// not just this in-memory access token. See setAuthRecovery.
|
|
159
|
+
if (recovery && ownsAuthRecovery) {
|
|
160
|
+
void recovery.recover();
|
|
161
|
+
}
|
|
162
|
+
else if (recovery) {
|
|
163
|
+
await recovery.recover();
|
|
164
|
+
signal?.throwIfAborted();
|
|
165
|
+
// Replay only for the same user. A recovery that ended signed out, or signed
|
|
166
|
+
// in as someone else, leaves this request failed.
|
|
167
|
+
if (sentAs !== null && recovery.principal() === sentAs)
|
|
168
|
+
res = await doFetch(signal);
|
|
169
|
+
}
|
|
170
|
+
signal?.throwIfAborted();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
const errorBody = (await res
|
|
175
|
+
.json()
|
|
176
|
+
.catch(() => ({ error: res.statusText, code: "unknown" })));
|
|
177
|
+
throw new ApiRequestError(res.status, errorBody);
|
|
178
|
+
}
|
|
179
|
+
if (res.status === 204)
|
|
180
|
+
return undefined;
|
|
181
|
+
return (await res.json());
|
|
182
|
+
}
|
|
183
|
+
/** The one place a browser app talks to its own BFF api. Handles bearer auth,
|
|
184
|
+
* transparent access-token refresh on 401, and the { error, code, field? } error
|
|
185
|
+
* contract every @flytedesk/app-kit/auth-backed api returns. */
|
|
186
|
+
export async function apiRequest(path, options = {}) {
|
|
187
|
+
const { method = "GET", body, query, skipAuthRetry, deadlineMs, ownsAuthRecovery, keepalive } = options;
|
|
188
|
+
const doFetch = async (signal) => fetch(buildUrl(path, query), {
|
|
189
|
+
method,
|
|
190
|
+
credentials: "include",
|
|
191
|
+
headers: {
|
|
192
|
+
...(body != null ? { "Content-Type": "application/json" } : {}),
|
|
193
|
+
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
194
|
+
},
|
|
195
|
+
body: body != null ? JSON.stringify(body) : undefined,
|
|
196
|
+
signal,
|
|
197
|
+
...(keepalive ? { keepalive: true } : {}),
|
|
198
|
+
});
|
|
199
|
+
return sendAndDecode(`${method} ${path}`, doFetch, { skipAuthRetry, deadlineMs, ownsAuthRecovery });
|
|
200
|
+
}
|
|
201
|
+
/** Sibling to apiRequest for multipart/form-data uploads. Same bearer auth, 401 ->
|
|
202
|
+
* refresh -> retry-once dance, and error contract — but always POSTs `formData` as-is
|
|
203
|
+
* (no JSON.stringify) and deliberately never sets a Content-Type header: the browser
|
|
204
|
+
* must set its own multipart boundary, and setting Content-Type manually on a
|
|
205
|
+
* FormData body breaks that boundary. */
|
|
206
|
+
export async function apiUpload(path, formData, options = {}) {
|
|
207
|
+
const { skipAuthRetry } = options;
|
|
208
|
+
const doFetch = async () => fetch(buildUrl(path), {
|
|
209
|
+
method: "POST",
|
|
210
|
+
credentials: "include",
|
|
211
|
+
headers: {
|
|
212
|
+
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
213
|
+
},
|
|
214
|
+
body: formData,
|
|
215
|
+
});
|
|
216
|
+
return sendAndDecode(`POST ${path}`, doFetch, { skipAuthRetry });
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=httpClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpClient.js","sourceRoot":"","sources":["../../../src/auth/client/httpClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe client for `@flytedesk/app-kit/auth` — a separate subpath export (see
|
|
3
|
+
* package.json's "exports") so this never drags a single Node/Fastify import into a
|
|
4
|
+
* browser bundle. See httpClient.ts and useAuthSession.ts's own doc comments for what
|
|
5
|
+
* was extracted from where and why (AK-16).
|
|
6
|
+
*/
|
|
7
|
+
export { ApiRequestError, RequestDeadlineError, DEFAULT_REQUEST_DEADLINE_MS, configureApiClient, setAccessToken, getAccessToken, getBaseUrl, setAuthRecovery, apiRequest, apiUpload, type AuthRecovery, type RequestOptions, } from "./httpClient.js";
|
|
8
|
+
export { useAuthSession, type AuthStatus, type AuthSessionState } from "./useAuthSession.js";
|
|
9
|
+
export type { AppSwitcherEntry, AuthedUserDto, IdpRole, ApiErrorBody } from "../shared-types.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe client for `@flytedesk/app-kit/auth` — a separate subpath export (see
|
|
3
|
+
* package.json's "exports") so this never drags a single Node/Fastify import into a
|
|
4
|
+
* browser bundle. See httpClient.ts and useAuthSession.ts's own doc comments for what
|
|
5
|
+
* was extracted from where and why (AK-16).
|
|
6
|
+
*/
|
|
7
|
+
export { ApiRequestError, RequestDeadlineError, DEFAULT_REQUEST_DEADLINE_MS, configureApiClient, setAccessToken, getAccessToken, getBaseUrl, setAuthRecovery, apiRequest, apiUpload, } from "./httpClient.js";
|
|
8
|
+
export { useAuthSession } from "./useAuthSession.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/auth/client/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,2BAA2B,EAC3B,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,UAAU,EACV,eAAe,EACf,UAAU,EACV,SAAS,GAGV,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAA0C,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AppSwitcherEntry } from "../shared-types.js";
|
|
2
|
+
export type AuthStatus = "loading" | "authenticated" | "unauthenticated" | "error";
|
|
3
|
+
export interface AuthSessionState<TUser> {
|
|
4
|
+
status: AuthStatus;
|
|
5
|
+
user: TUser | null;
|
|
6
|
+
apps: AppSwitcherEntry[];
|
|
7
|
+
/** Set only when loading the session failed for a reason OTHER than being signed
|
|
8
|
+
* out — the api errored, or a request it depends on missed its deadline. */
|
|
9
|
+
error: Error | null;
|
|
10
|
+
/** Surfaces a failed sign-out as a rejection (fail loudly, AK-16) — local state is
|
|
11
|
+
* cleared ONLY once the api confirms it, never optimistically. A caller showing a
|
|
12
|
+
* pending state on this promise (as a design system's AccountMenu typically does)
|
|
13
|
+
* gets an honest failure instead of a UI that claims "signed out" while the refresh
|
|
14
|
+
* cookie and its token family are still valid server-side. */
|
|
15
|
+
signOut: () => Promise<void>;
|
|
16
|
+
/** Starts a new bootstrap load from scratch — e.g. a Retry action on the error state. */
|
|
17
|
+
refetch: () => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `TUser` defaults to `unknown` deliberately: a consuming app almost always has its
|
|
21
|
+
* own richer local type (or the exact shape @flytedesk/app-kit/auth's AuthedUser
|
|
22
|
+
* describes) it wants `GET {api}/me`'s `user` decoded as, and should pass it
|
|
23
|
+
* explicitly (`useAuthSession<MyUser>(...)`) rather than this module guessing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useAuthSession<TUser = unknown>(): AuthSessionState<TUser>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic browser session hook for `@flytedesk/app-kit/auth`'s BFF pattern: bootstraps
|
|
3
|
+
* via POST {api}/auth/refresh (the httpOnly refresh cookie) -> GET {api}/me -> GET
|
|
4
|
+
* {api}/apps (the "All flytedesk apps" switcher feed, now served by the plugin itself —
|
|
5
|
+
* see ../plugin.js's GET {routePrefix}/apps). Every consuming app was on track to
|
|
6
|
+
* re-implement this by hand (sms-app's apps/web/src/session/useAuthSession.ts, before
|
|
7
|
+
* this card) — extracting it here (AK-16) means every one gets the StrictMode-safe
|
|
8
|
+
* single-flight bootstrap and the fail-loudly sign-out for free, instead of each app
|
|
9
|
+
* getting a slightly different (and possibly wrong) version of the same ~80 lines.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately narrower than media-planner's SessionKit (packages/design-system/src/
|
|
12
|
+
* session.ts): no silent OIDC resume, no cross-tab identity-switch reload, no RBAC
|
|
13
|
+
* flag helpers. Those stay app-specific policy layered on top of this shared
|
|
14
|
+
* mechanism — media-planner's own switch-over (the PLN-1 card this AK-16 card files)
|
|
15
|
+
* keeps SessionKit, just pointed at this module's apiRequest instead of its local copy.
|
|
16
|
+
*/
|
|
17
|
+
import * as React from "react";
|
|
18
|
+
import { apiRequest, ApiRequestError, setAccessToken } from "./httpClient.js";
|
|
19
|
+
/**
|
|
20
|
+
* `TUser` defaults to `unknown` deliberately: a consuming app almost always has its
|
|
21
|
+
* own richer local type (or the exact shape @flytedesk/app-kit/auth's AuthedUser
|
|
22
|
+
* describes) it wants `GET {api}/me`'s `user` decoded as, and should pass it
|
|
23
|
+
* explicitly (`useAuthSession<MyUser>(...)`) rather than this module guessing.
|
|
24
|
+
*/
|
|
25
|
+
export function useAuthSession() {
|
|
26
|
+
const [status, setStatus] = React.useState("loading");
|
|
27
|
+
const [user, setUser] = React.useState(null);
|
|
28
|
+
const [apps, setApps] = React.useState([]);
|
|
29
|
+
const [error, setError] = React.useState(null);
|
|
30
|
+
const [generation, setGeneration] = React.useState(0);
|
|
31
|
+
// POST /auth/refresh ROTATES the refresh cookie on every call (reuse detection — see
|
|
32
|
+
// @flytedesk/app-kit/auth's plugin.ts) — a second call presenting the same,
|
|
33
|
+
// now-already-consumed cookie is treated as token replay and revokes the whole
|
|
34
|
+
// session. React 18+'s <StrictMode> intentionally double-invokes this effect in dev
|
|
35
|
+
// (mount -> cleanup -> mount, on the SAME fiber, so this ref survives it) with the
|
|
36
|
+
// SAME `generation`, which without a guard fires two concurrent /auth/refresh calls
|
|
37
|
+
// against that one cookie: the loser gets "token_reuse_detected" and this hook
|
|
38
|
+
// bounces to "unauthenticated" for no signed-out reason at all. Tracking which
|
|
39
|
+
// generation has already started a load — rather than an unconditional `cancelled`
|
|
40
|
+
// flag — is what makes the second StrictMode invocation a no-op while still letting
|
|
41
|
+
// `refetch()` (which bumps `generation`) start a genuinely new load.
|
|
42
|
+
const startedGenerationRef = React.useRef(null);
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
if (startedGenerationRef.current === generation)
|
|
45
|
+
return;
|
|
46
|
+
startedGenerationRef.current = generation;
|
|
47
|
+
async function load() {
|
|
48
|
+
setStatus("loading");
|
|
49
|
+
setError(null);
|
|
50
|
+
try {
|
|
51
|
+
const refreshed = await apiRequest("/auth/refresh", { method: "POST" });
|
|
52
|
+
setAccessToken(refreshed.accessToken);
|
|
53
|
+
const me = await apiRequest("/me");
|
|
54
|
+
setUser(me.user);
|
|
55
|
+
try {
|
|
56
|
+
const appsRes = await apiRequest("/apps");
|
|
57
|
+
setApps(appsRes.apps);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// 403 (no permission to see the switcher) is an expected, non-error outcome
|
|
61
|
+
// — every other failure is a real error and must propagate.
|
|
62
|
+
if (err instanceof ApiRequestError && err.status === 403) {
|
|
63
|
+
setApps([]);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
setStatus("authenticated");
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err instanceof ApiRequestError && err.status === 401) {
|
|
73
|
+
setStatus("unauthenticated");
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
77
|
+
setStatus("error");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
void load();
|
|
82
|
+
}, [generation]);
|
|
83
|
+
const signOut = React.useCallback(async () => {
|
|
84
|
+
// Fail loudly: only clear local state once the api actually confirms sign-out —
|
|
85
|
+
// swallowing this and clearing state regardless would leave the refresh cookie and
|
|
86
|
+
// its token family valid server-side, silently signing the user back in on the
|
|
87
|
+
// next load (exactly the SMS-5 code-quality gate finding this hook fixes for good).
|
|
88
|
+
await apiRequest("/auth/logout", { method: "POST" });
|
|
89
|
+
setAccessToken(null);
|
|
90
|
+
setUser(null);
|
|
91
|
+
setApps([]);
|
|
92
|
+
setStatus("unauthenticated");
|
|
93
|
+
}, []);
|
|
94
|
+
const refetch = React.useCallback(() => setGeneration((g) => g + 1), []);
|
|
95
|
+
return { status, user, apps, error, signOut, refetch };
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=useAuthSession.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAuthSession.js","sourceRoot":"","sources":["../../../src/auth/client/useAuthSession.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error surface for @flytedesk/app-kit/auth's calls out to flytedesk-id —
|
|
3
|
+
* mirrors ../bigquery/errors.ts's classify-into-a-small-known-set pattern so a caller
|
|
4
|
+
* (the plugin's own routes, or a consuming app) can branch on WHY an upstream call
|
|
5
|
+
* failed instead of string-matching flytedesk-id's raw response. Fail loudly (AK-16):
|
|
6
|
+
* every one of these is meant to be logged with context, never swallowed silently.
|
|
7
|
+
*/
|
|
8
|
+
/** Base class for every typed error this module raises talking to flytedesk-id. */
|
|
9
|
+
export declare class IdpError extends Error {
|
|
10
|
+
readonly cause?: unknown | undefined;
|
|
11
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
12
|
+
}
|
|
13
|
+
/** flytedesk-id itself is unreachable, erroring, or timed out — a transient outage
|
|
14
|
+
* unrelated to this particular user's access. Safe to retry later; not a reason to
|
|
15
|
+
* force re-login. */
|
|
16
|
+
export declare class IdpUnavailableError extends IdpError {
|
|
17
|
+
constructor(message: string, cause?: unknown);
|
|
18
|
+
}
|
|
19
|
+
/** flytedesk-id answered and said no: the access/refresh token it was given is no
|
|
20
|
+
* longer valid — revoked, expired past recovery, or the account's access to this app
|
|
21
|
+
* was pulled. The caller should force re-login, not retry. */
|
|
22
|
+
export declare class IdpAccessRevokedError extends IdpError {
|
|
23
|
+
constructor(message: string, cause?: unknown);
|
|
24
|
+
}
|
|
25
|
+
/** The injected AuthStore (findIdpSession/updateIdpSession/deleteIdpSession/...) threw —
|
|
26
|
+
* a storage-layer failure (DB down, constraint violation, ...), never to be confused
|
|
27
|
+
* with "no session found" (which is a legitimate, expected outcome, not an error). */
|
|
28
|
+
export declare class AuthStoreError extends Error {
|
|
29
|
+
readonly cause?: unknown | undefined;
|
|
30
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
31
|
+
}
|
|
32
|
+
/** Thrown by ./oidc-client.ts for any non-2xx HTTP response from flytedesk-id, carrying
|
|
33
|
+
* the real status code so classifyIdpError (below) can tell an outage (5xx, network
|
|
34
|
+
* failure) apart from a revocation (401/403) without parsing response text. */
|
|
35
|
+
export declare class OidcHttpError extends Error {
|
|
36
|
+
readonly status: number;
|
|
37
|
+
readonly cause?: unknown | undefined;
|
|
38
|
+
constructor(message: string, status: number, cause?: unknown | undefined);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Normalizes anything a flytedesk-id call in ./oidc-client.ts can throw/reject with
|
|
42
|
+
* into an IdpUnavailableError or IdpAccessRevokedError. 401/403 means flytedesk-id
|
|
43
|
+
* actively rejected the token (revoked/expired/forbidden); anything else — a 5xx, a
|
|
44
|
+
* network failure, a timeout — is treated as an outage. Never throws itself; always
|
|
45
|
+
* returns an Error to `throw`/log at the call site.
|
|
46
|
+
*/
|
|
47
|
+
export declare function classifyIdpError(err: unknown): IdpUnavailableError | IdpAccessRevokedError;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error surface for @flytedesk/app-kit/auth's calls out to flytedesk-id —
|
|
3
|
+
* mirrors ../bigquery/errors.ts's classify-into-a-small-known-set pattern so a caller
|
|
4
|
+
* (the plugin's own routes, or a consuming app) can branch on WHY an upstream call
|
|
5
|
+
* failed instead of string-matching flytedesk-id's raw response. Fail loudly (AK-16):
|
|
6
|
+
* every one of these is meant to be logged with context, never swallowed silently.
|
|
7
|
+
*/
|
|
8
|
+
/** Base class for every typed error this module raises talking to flytedesk-id. */
|
|
9
|
+
export class IdpError extends Error {
|
|
10
|
+
cause;
|
|
11
|
+
constructor(message, cause) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.cause = cause;
|
|
14
|
+
this.name = "IdpError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** flytedesk-id itself is unreachable, erroring, or timed out — a transient outage
|
|
18
|
+
* unrelated to this particular user's access. Safe to retry later; not a reason to
|
|
19
|
+
* force re-login. */
|
|
20
|
+
export class IdpUnavailableError extends IdpError {
|
|
21
|
+
constructor(message, cause) {
|
|
22
|
+
super(message, cause);
|
|
23
|
+
this.name = "IdpUnavailableError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** flytedesk-id answered and said no: the access/refresh token it was given is no
|
|
27
|
+
* longer valid — revoked, expired past recovery, or the account's access to this app
|
|
28
|
+
* was pulled. The caller should force re-login, not retry. */
|
|
29
|
+
export class IdpAccessRevokedError extends IdpError {
|
|
30
|
+
constructor(message, cause) {
|
|
31
|
+
super(message, cause);
|
|
32
|
+
this.name = "IdpAccessRevokedError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** The injected AuthStore (findIdpSession/updateIdpSession/deleteIdpSession/...) threw —
|
|
36
|
+
* a storage-layer failure (DB down, constraint violation, ...), never to be confused
|
|
37
|
+
* with "no session found" (which is a legitimate, expected outcome, not an error). */
|
|
38
|
+
export class AuthStoreError extends Error {
|
|
39
|
+
cause;
|
|
40
|
+
constructor(message, cause) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.cause = cause;
|
|
43
|
+
this.name = "AuthStoreError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Thrown by ./oidc-client.ts for any non-2xx HTTP response from flytedesk-id, carrying
|
|
47
|
+
* the real status code so classifyIdpError (below) can tell an outage (5xx, network
|
|
48
|
+
* failure) apart from a revocation (401/403) without parsing response text. */
|
|
49
|
+
export class OidcHttpError extends Error {
|
|
50
|
+
status;
|
|
51
|
+
cause;
|
|
52
|
+
constructor(message, status, cause) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.status = status;
|
|
55
|
+
this.cause = cause;
|
|
56
|
+
this.name = "OidcHttpError";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Normalizes anything a flytedesk-id call in ./oidc-client.ts can throw/reject with
|
|
61
|
+
* into an IdpUnavailableError or IdpAccessRevokedError. 401/403 means flytedesk-id
|
|
62
|
+
* actively rejected the token (revoked/expired/forbidden); anything else — a 5xx, a
|
|
63
|
+
* network failure, a timeout — is treated as an outage. Never throws itself; always
|
|
64
|
+
* returns an Error to `throw`/log at the call site.
|
|
65
|
+
*/
|
|
66
|
+
export function classifyIdpError(err) {
|
|
67
|
+
if (err instanceof IdpAccessRevokedError || err instanceof IdpUnavailableError)
|
|
68
|
+
return err;
|
|
69
|
+
if (err instanceof OidcHttpError && (err.status === 401 || err.status === 403)) {
|
|
70
|
+
return new IdpAccessRevokedError(err.message, err);
|
|
71
|
+
}
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
return new IdpUnavailableError(message, err);
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +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;;+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;;gFAEgF;AAChF,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,UAAU,gBAAgB,CAAC,GAAY;IAC3C,IAAI,GAAG,YAAY,qBAAqB,IAAI,GAAG,YAAY,mBAAmB;QAAE,OAAO,GAAG,CAAC;IAE3F,IAAI,GAAG,YAAY,aAAa,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,EAAE,CAAC;QAC/E,OAAO,IAAI,qBAAqB,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACrD,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/index.d.ts
CHANGED
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
export { flytedeskAuth } from "./plugin.js";
|
|
30
30
|
export { requireAuth, requirePermission } from "./guards.js";
|
|
31
31
|
export type { AuthedUser, AuthStore, CreateIdpSessionInput, CreateRefreshTokenInput, FlytedeskAuthOptions, IdpRole, IdpSessionRow, RefreshTokenRow, UpdateIdpSessionInput, UserinfoClaims, } from "./types.js";
|
|
32
|
-
export type {
|
|
32
|
+
export type { AppSwitcherEntry, IdpAuthorization, IdpTokenResponse, IdTokenClaims, OidcClient, } from "./oidc-client.js";
|
|
33
33
|
export { createOidcClient } from "./oidc-client.js";
|
|
34
|
+
export type { ApiErrorBody } from "./shared-types.js";
|
|
35
|
+
export { IdpError, IdpUnavailableError, IdpAccessRevokedError, AuthStoreError, OidcHttpError, classifyIdpError, } from "./errors.js";
|
|
34
36
|
export { generatePkce, randomState, type PkcePair } from "./pkce.js";
|
|
35
37
|
export { generateRefreshToken, hashRefreshToken, signAccessToken, verifyAccessToken, type AccessTokenClaims, } from "./tokens.js";
|
package/dist/auth/index.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
export { flytedeskAuth } from "./plugin.js";
|
|
30
30
|
export { requireAuth, requirePermission } from "./guards.js";
|
|
31
31
|
export { createOidcClient } from "./oidc-client.js";
|
|
32
|
+
export { IdpError, IdpUnavailableError, IdpAccessRevokedError, AuthStoreError, OidcHttpError, classifyIdpError, } from "./errors.js";
|
|
32
33
|
export { generatePkce, randomState } from "./pkce.js";
|
|
33
34
|
export { generateRefreshToken, hashRefreshToken, signAccessToken, verifyAccessToken, } from "./tokens.js";
|
|
34
35
|
//# sourceMappingURL=index.js.map
|
package/dist/auth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAsB7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAsB7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAIpD,OAAO,EACL,QAAQ,EACR,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,aAAa,EACb,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,YAAY,EAAE,WAAW,EAAiB,MAAM,WAAW,CAAC;AAErE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,iBAAiB,GAElB,MAAM,aAAa,CAAC"}
|