@flytedesk/app-kit 0.6.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.
@@ -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"}
@@ -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 { IdpAppSwitcherEntry, IdpAuthorization, IdpTokenResponse, IdTokenClaims, OidcClient, } from "./oidc-client.js";
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";
@@ -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
@@ -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;AAEpD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAiB,MAAM,WAAW,CAAC;AAErE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,iBAAiB,GAElB,MAAM,aAAa,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"}