@flytedesk/app-kit 0.8.0 → 2.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 +27 -0
- package/dist/auth/client/httpClient.d.ts +3 -1
- package/dist/auth/client/httpClient.js +5 -3
- package/dist/auth/client/httpClient.js.map +1 -1
- package/dist/auth/client/index.d.ts +2 -1
- package/dist/auth/client/index.js +1 -0
- package/dist/auth/client/index.js.map +1 -1
- package/dist/auth/client/silentAuth.d.ts +17 -0
- package/dist/auth/client/silentAuth.js +106 -0
- package/dist/auth/client/silentAuth.js.map +1 -0
- package/dist/auth/errors.d.ts +12 -0
- package/dist/auth/errors.js +15 -0
- package/dist/auth/errors.js.map +1 -1
- package/dist/auth/index.d.ts +23 -8
- package/dist/auth/index.js +20 -5
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/oidc-client.d.ts +40 -0
- package/dist/auth/oidc-client.js +61 -17
- package/dist/auth/oidc-client.js.map +1 -1
- package/dist/auth/plugin.js +303 -94
- package/dist/auth/plugin.js.map +1 -1
- package/dist/auth/shared-types.d.ts +24 -0
- package/dist/auth/shared-types.js +19 -0
- package/dist/auth/shared-types.js.map +1 -1
- package/dist/auth/testing/fake-idp.d.ts +35 -0
- package/dist/auth/testing/fake-idp.js +101 -6
- package/dist/auth/testing/fake-idp.js.map +1 -1
- package/dist/auth/types.d.ts +153 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -25,6 +25,33 @@ own domain work resumes.
|
|
|
25
25
|
pnpm add @flytedesk/app-kit
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
## Auth (`@flytedesk/app-kit/auth`)
|
|
29
|
+
|
|
30
|
+
`idpRequestTimeoutMs` (`FlytedeskAuthOptions`/`OidcClientOptions`, default 5s) bounds
|
|
31
|
+
**every** call this package makes to flytedesk-id — token, userinfo, the per-app
|
|
32
|
+
authorization check, `/me/apps`, discovery and revocation — not just `/token`. A hung
|
|
33
|
+
flytedesk-id endpoint fails loudly with the typed `IdpTimeoutError` instead of hanging
|
|
34
|
+
the request indefinitely; this matters most for the authorization check, which runs on
|
|
35
|
+
every authenticated request to every consumer of this package. Sign-out still completes
|
|
36
|
+
locally (cookies cleared, refresh-token family revoked) even when the IdP-side
|
|
37
|
+
revocation call itself times out.
|
|
38
|
+
|
|
39
|
+
**Residual risk in the refresh-token grace window (RFC 9700 §4.14.2).** `POST
|
|
40
|
+
{routePrefix}/refresh` rotates the presented refresh token on every call and treats a
|
|
41
|
+
second presentation of an already-rotated token as a legitimate concurrent-tab race
|
|
42
|
+
(401, nothing revoked) rather than reuse, as long as it arrives within
|
|
43
|
+
`refreshReuseGraceMs` (default 20s) of the rotation. This is deliberate — two browser
|
|
44
|
+
tabs sharing one cookie jar can otherwise trip false-positive reuse detection — but it
|
|
45
|
+
means that within that same window, an attacker who has obtained a copy of a
|
|
46
|
+
not-yet-presented refresh token and wins the race against the legitimate browser is
|
|
47
|
+
**indistinguishable from a same-device two-tab race**: both look like "a token was
|
|
48
|
+
rotated, then its predecessor was presented again shortly after." A presentation
|
|
49
|
+
**outside** the window, or of a token that was never rotated at all, is always treated
|
|
50
|
+
as reuse and revokes the whole family. Consumers who need a tighter bound can lower
|
|
51
|
+
`refreshReuseGraceMs`, trading some tolerance for slow concurrent-tab requests for a
|
|
52
|
+
narrower attack window; it cannot be set to zero without reintroducing the false-positive
|
|
53
|
+
two-tab problem this window exists to solve.
|
|
54
|
+
|
|
28
55
|
## Publishing
|
|
29
56
|
|
|
30
57
|
Published to the public npm registry under the `@flytedesk` org, access `restricted`.
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { type ApiErrorBody } from "../shared-types.js";
|
|
14
14
|
/** `expires_in`-free deadline for every call this client makes, unless a caller passes
|
|
15
|
-
* its own `deadlineMs` —
|
|
15
|
+
* its own `deadlineMs` — the same `AUTH_REQUEST_DEADLINE_MS` (`../shared-types.js`) the
|
|
16
|
+
* server plugin's default `refreshReuseGraceMs` is sized around (2x this), so the two
|
|
17
|
+
* can never drift apart — see that option's doc comment in `../types.ts`. */
|
|
16
18
|
export declare const DEFAULT_REQUEST_DEADLINE_MS = 10000;
|
|
17
19
|
export declare class ApiRequestError extends Error {
|
|
18
20
|
code: string;
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* future change here ever needs one, it belongs in the server (`../plugin.js`) or in a
|
|
11
11
|
* new Node-only module instead.
|
|
12
12
|
*/
|
|
13
|
-
import { AUTH_ROUTE_PREFIX, authRoutePath } from "../shared-types.js";
|
|
13
|
+
import { AUTH_REQUEST_DEADLINE_MS, AUTH_ROUTE_PREFIX, authRoutePath } from "../shared-types.js";
|
|
14
14
|
/** `expires_in`-free deadline for every call this client makes, unless a caller passes
|
|
15
|
-
* its own `deadlineMs` —
|
|
16
|
-
|
|
15
|
+
* its own `deadlineMs` — the same `AUTH_REQUEST_DEADLINE_MS` (`../shared-types.js`) the
|
|
16
|
+
* server plugin's default `refreshReuseGraceMs` is sized around (2x this), so the two
|
|
17
|
+
* can never drift apart — see that option's doc comment in `../types.ts`. */
|
|
18
|
+
export const DEFAULT_REQUEST_DEADLINE_MS = AUTH_REQUEST_DEADLINE_MS;
|
|
17
19
|
export class ApiRequestError extends Error {
|
|
18
20
|
code;
|
|
19
21
|
field;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpClient.js","sourceRoot":"","sources":["../../../src/auth/client/httpClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAqB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"httpClient.js","sourceRoot":"","sources":["../../../src/auth/client/httpClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,5 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { ApiRequestError, RequestDeadlineError, DEFAULT_REQUEST_DEADLINE_MS, configureApiClient, setAccessToken, getAccessToken, getBaseUrl, setAuthRecovery, apiRequest, apiUpload, type AuthRecovery, type RequestOptions, } from "./httpClient.js";
|
|
8
8
|
export { useAuthSession, type AuthStatus, type AuthSessionState, type LoadingSessionState, type AuthenticatedSessionState, type UnauthenticatedSessionState, type ErrorSessionState, } from "./useAuthSession.js";
|
|
9
|
-
export
|
|
9
|
+
export { attemptSilentResume, type SilentResumeResult, type AttemptSilentResumeOptions, } from "./silentAuth.js";
|
|
10
|
+
export type { AppSwitcherEntry, AuthedUserDto, IdpRole, ApiErrorBody, SilentAuthMessage } from "../shared-types.js";
|
|
10
11
|
export { AUTH_ROUTE_PREFIX, AUTH_ROUTE_SEGMENTS, authRoutePath, type AuthRouteSegment } from "../shared-types.js";
|
|
@@ -6,5 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { ApiRequestError, RequestDeadlineError, DEFAULT_REQUEST_DEADLINE_MS, configureApiClient, setAccessToken, getAccessToken, getBaseUrl, setAuthRecovery, apiRequest, apiUpload, } from "./httpClient.js";
|
|
8
8
|
export { useAuthSession, } from "./useAuthSession.js";
|
|
9
|
+
export { attemptSilentResume, } from "./silentAuth.js";
|
|
9
10
|
export { AUTH_ROUTE_PREFIX, AUTH_ROUTE_SEGMENTS, authRoutePath } from "../shared-types.js";
|
|
10
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +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,EACL,cAAc,GAOf,MAAM,qBAAqB,CAAC;
|
|
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,EACL,cAAc,GAOf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,mBAAmB,GAGpB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,aAAa,EAAyB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type SilentResumeResult = "success" | "login_required" | "error";
|
|
2
|
+
export interface AttemptSilentResumeOptions {
|
|
3
|
+
/** Well past the typical case (a same-datacenter redirect chain — under a second) but
|
|
4
|
+
* short enough that a genuinely hung network doesn't leave the caller waiting
|
|
5
|
+
* forever. Default 8000ms, matching media-planner's `SILENT_RESUME_TIMEOUT_MS`.
|
|
6
|
+
* Treated as `"error"` (not `"login_required"`) — a timeout means we don't actually
|
|
7
|
+
* know whether the IdP-side session is alive or dead. */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Single-flighted: a second call while one attempt is already running reuses that same
|
|
12
|
+
* attempt (and its eventual result) instead of opening a second hidden iframe — a
|
|
13
|
+
* session layer's reactive path (a request 401ed) and its own cold-start bootstrap can
|
|
14
|
+
* both end up calling this for the same underlying condition, and this keeps them from
|
|
15
|
+
* racing two independent iframes/silent-auth attempts against flytedesk-id.
|
|
16
|
+
*/
|
|
17
|
+
export declare function attemptSilentResume(options?: AttemptSilentResumeOptions): Promise<SilentResumeResult>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe silent (`prompt=none`) re-auth for `@flytedesk/app-kit/auth`'s BFF
|
|
3
|
+
* pattern (AK-14, gap 2) — generalized from media-planner's
|
|
4
|
+
* `packages/design-system/src/silentAuth.ts`. ZERO Node imports, same rule as
|
|
5
|
+
* `./httpClient.ts`: this is a browser bundle, not a server module.
|
|
6
|
+
*
|
|
7
|
+
* `prompt=none` (OIDC Core 3.1.2.1) is a silent authorization request that answers from
|
|
8
|
+
* flytedesk-id's own existing end-user session cookie with no visible UI at all — the
|
|
9
|
+
* architectural equivalent of a "Continue as {name}" Google One Tap for an app whose IdP
|
|
10
|
+
* is flytedesk-id, not Google, so there's nothing for a literal One Tap widget to attach
|
|
11
|
+
* to. See `../plugin.ts`'s `GET {routePrefix}/login` (`?prompt=none`) and its
|
|
12
|
+
* `{routePrefix}/callback`'s silent branch for the server half this drives.
|
|
13
|
+
*
|
|
14
|
+
* Driven from a hidden iframe rather than a top-level navigation so the running app is
|
|
15
|
+
* never unmounted/reloaded for an attempt that, most of the time, resolves in well under
|
|
16
|
+
* a second: the iframe navigates login -> flytedesk-id -> callback entirely on its own,
|
|
17
|
+
* and the callback's silent branch answers with a tiny page whose only job is a single
|
|
18
|
+
* `window.parent.postMessage(...)` this module listens for (the `SilentAuthMessage`
|
|
19
|
+
* contract, `../shared-types.js` — shared with the server so the two sides can never
|
|
20
|
+
* disagree on its shape).
|
|
21
|
+
*/
|
|
22
|
+
import { getBaseUrl } from "./httpClient.js";
|
|
23
|
+
import { AUTH_ROUTE_PREFIX, authRoutePath, SILENT_AUTH_MESSAGE_SOURCE, } from "../shared-types.js";
|
|
24
|
+
const DEFAULT_SILENT_RESUME_TIMEOUT_MS = 8_000;
|
|
25
|
+
function isSilentAuthMessage(data) {
|
|
26
|
+
return (typeof data === "object" &&
|
|
27
|
+
data !== null &&
|
|
28
|
+
data.source === SILENT_AUTH_MESSAGE_SOURCE);
|
|
29
|
+
}
|
|
30
|
+
function resolveLoginUrl() {
|
|
31
|
+
const base = getBaseUrl();
|
|
32
|
+
const absolute = /^[a-z][a-z\d+.-]*:\/\//i.test(base) ? base : `${window.location.origin}${base}`;
|
|
33
|
+
const origin = new URL(absolute).origin;
|
|
34
|
+
return { url: `${absolute}${authRoutePath(AUTH_ROUTE_PREFIX, "login")}?prompt=none`, origin };
|
|
35
|
+
}
|
|
36
|
+
let inFlight = null;
|
|
37
|
+
/**
|
|
38
|
+
* Single-flighted: a second call while one attempt is already running reuses that same
|
|
39
|
+
* attempt (and its eventual result) instead of opening a second hidden iframe — a
|
|
40
|
+
* session layer's reactive path (a request 401ed) and its own cold-start bootstrap can
|
|
41
|
+
* both end up calling this for the same underlying condition, and this keeps them from
|
|
42
|
+
* racing two independent iframes/silent-auth attempts against flytedesk-id.
|
|
43
|
+
*/
|
|
44
|
+
export function attemptSilentResume(options = {}) {
|
|
45
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
46
|
+
return Promise.resolve("error");
|
|
47
|
+
}
|
|
48
|
+
if (!inFlight) {
|
|
49
|
+
inFlight = runSilentResume(options).finally(() => {
|
|
50
|
+
inFlight = null;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return inFlight;
|
|
54
|
+
}
|
|
55
|
+
function runSilentResume(options) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const { url, origin } = resolveLoginUrl();
|
|
58
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_SILENT_RESUME_TIMEOUT_MS;
|
|
59
|
+
const iframe = document.createElement("iframe");
|
|
60
|
+
iframe.style.display = "none";
|
|
61
|
+
iframe.setAttribute("aria-hidden", "true");
|
|
62
|
+
iframe.setAttribute("tabindex", "-1");
|
|
63
|
+
let settled = false;
|
|
64
|
+
const finish = (result) => {
|
|
65
|
+
if (settled)
|
|
66
|
+
return;
|
|
67
|
+
settled = true;
|
|
68
|
+
window.removeEventListener("message", onMessage);
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
iframe.remove();
|
|
71
|
+
resolve(result);
|
|
72
|
+
};
|
|
73
|
+
const onMessage = (event) => {
|
|
74
|
+
// Both checks matter: `source` confirms the message came from THIS iframe (not
|
|
75
|
+
// some other frame/extension), `origin` confirms it came from this app's own API
|
|
76
|
+
// origin specifically — the only origin GET {routePrefix}/login?prompt=none's
|
|
77
|
+
// callback ever posts from.
|
|
78
|
+
if (event.source !== iframe.contentWindow)
|
|
79
|
+
return;
|
|
80
|
+
if (!isSilentAuthMessage(event.data))
|
|
81
|
+
return;
|
|
82
|
+
if (event.origin !== origin) {
|
|
83
|
+
// Our own iframe answered, but from an origin other than the API base URL's:
|
|
84
|
+
// the OIDC redirect_uri is not on that origin. The message is still rejected —
|
|
85
|
+
// never trusted — but as a failure now, loudly, rather than ignored until the
|
|
86
|
+
// timeout: this can never succeed, and waiting on it only holds the app on its
|
|
87
|
+
// spinner.
|
|
88
|
+
console.error(`Silent sign-in callback posted from ${event.origin}, expected ${origin}: the OIDC ` +
|
|
89
|
+
`redirect_uri must be on the API base URL's origin.`);
|
|
90
|
+
finish("error");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (event.data.status === "success") {
|
|
94
|
+
finish("success");
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
finish(event.data.detail === "login_required" ? "login_required" : "error");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
window.addEventListener("message", onMessage);
|
|
101
|
+
const timer = setTimeout(() => finish("error"), timeoutMs);
|
|
102
|
+
iframe.src = url;
|
|
103
|
+
document.body.appendChild(iframe);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=silentAuth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"silentAuth.js","sourceRoot":"","sources":["../../../src/auth/client/silentAuth.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/errors.d.ts
CHANGED
|
@@ -16,6 +16,18 @@ export declare class IdpError extends Error {
|
|
|
16
16
|
export declare class IdpUnavailableError extends IdpError {
|
|
17
17
|
constructor(message: string, cause?: unknown);
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* A call to flytedesk-id's `/token` endpoint missed its `tokenRequestTimeoutMs` deadline
|
|
21
|
+
* (AK-14, gap 5) — distinct from a generic `IdpUnavailableError` so a caller can tell "the
|
|
22
|
+
* IdP hung" apart from "the IdP answered with an error" without inspecting a message
|
|
23
|
+
* string. A subclass of `IdpUnavailableError`, not a sibling: every existing
|
|
24
|
+
* `classifyIdpError`/`instanceof IdpUnavailableError` call site already treats a timeout
|
|
25
|
+
* exactly like any other outage (safe to retry, not a reason to force re-login) — this
|
|
26
|
+
* only adds a more specific type for a caller that wants to distinguish the two.
|
|
27
|
+
*/
|
|
28
|
+
export declare class IdpTimeoutError extends IdpUnavailableError {
|
|
29
|
+
constructor(message: string, cause?: unknown);
|
|
30
|
+
}
|
|
19
31
|
/** flytedesk-id answered and said no: the access/refresh token it was given is no
|
|
20
32
|
* longer valid — revoked, expired past recovery, or the account's access to this app
|
|
21
33
|
* was pulled. The caller should force re-login, not retry. */
|
package/dist/auth/errors.js
CHANGED
|
@@ -23,6 +23,21 @@ export class IdpUnavailableError extends IdpError {
|
|
|
23
23
|
this.name = "IdpUnavailableError";
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* A call to flytedesk-id's `/token` endpoint missed its `tokenRequestTimeoutMs` deadline
|
|
28
|
+
* (AK-14, gap 5) — distinct from a generic `IdpUnavailableError` so a caller can tell "the
|
|
29
|
+
* IdP hung" apart from "the IdP answered with an error" without inspecting a message
|
|
30
|
+
* string. A subclass of `IdpUnavailableError`, not a sibling: every existing
|
|
31
|
+
* `classifyIdpError`/`instanceof IdpUnavailableError` call site already treats a timeout
|
|
32
|
+
* exactly like any other outage (safe to retry, not a reason to force re-login) — this
|
|
33
|
+
* only adds a more specific type for a caller that wants to distinguish the two.
|
|
34
|
+
*/
|
|
35
|
+
export class IdpTimeoutError extends IdpUnavailableError {
|
|
36
|
+
constructor(message, cause) {
|
|
37
|
+
super(message, cause);
|
|
38
|
+
this.name = "IdpTimeoutError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
26
41
|
/** flytedesk-id answered and said no: the access/refresh token it was given is no
|
|
27
42
|
* longer valid — revoked, expired past recovery, or the account's access to this app
|
|
28
43
|
* was pulled. The caller should force re-login, not retry. */
|
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;;+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"}
|
|
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;;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
|
@@ -5,10 +5,26 @@
|
|
|
5
5
|
* src/{routes/auth.ts,lib/flytedeskId.ts,plugins/auth.ts} so client id/secret, issuer,
|
|
6
6
|
* redirect URI, and app id are config instead of hardcoded to one app.
|
|
7
7
|
*
|
|
8
|
+
* This module (`@flytedesk/app-kit/auth`) is the standalone surface: types, the OIDC
|
|
9
|
+
* client, PKCE/token/error helpers, and the `requireAuth`/`requirePermission` guards —
|
|
10
|
+
* everything that does NOT need Fastify's `FastifyRequest` ambient-augmented with
|
|
11
|
+
* `authUser`. The plugin itself (`flytedeskAuth`, and the `declare module "fastify"`
|
|
12
|
+
* augmentation that comes with registering it) lives at the separate subpath
|
|
13
|
+
* `@flytedesk/app-kit/auth/plugin` (AK-14, gap 6): TypeScript applies a `declare module`
|
|
14
|
+
* augmentation program-wide the instant ANY file importing it is part of the
|
|
15
|
+
* compilation, even transitively — so as long as `flytedeskAuth` was re-exported from
|
|
16
|
+
* here too, importing just `generatePkce`/`randomState`/`signAccessToken` from this
|
|
17
|
+
* module forced the augmentation (and its `AuthedUser` shape) onto a consumer's WHOLE
|
|
18
|
+
* program, with no way to opt out — including a consumer with its own, differently
|
|
19
|
+
* shaped `request.authUser` who never even registers this plugin (`TS2717: Subsequent
|
|
20
|
+
* property declarations must have the same type`). Splitting the plugin into its own
|
|
21
|
+
* subpath means the augmentation only applies to a program that actually imports it.
|
|
22
|
+
*
|
|
8
23
|
* Usage:
|
|
9
24
|
*
|
|
10
25
|
* import Fastify from "fastify";
|
|
11
|
-
* import { flytedeskAuth
|
|
26
|
+
* import { flytedeskAuth } from "@flytedesk/app-kit/auth/plugin";
|
|
27
|
+
* import { requireAuth, requirePermission } from "@flytedesk/app-kit/auth";
|
|
12
28
|
*
|
|
13
29
|
* const app = Fastify();
|
|
14
30
|
* await app.register(flytedeskAuth, {
|
|
@@ -26,13 +42,12 @@
|
|
|
26
42
|
*
|
|
27
43
|
* app.get("/me", { preHandler: requireAuth }, async (request) => request.authUser);
|
|
28
44
|
*/
|
|
29
|
-
export { flytedeskAuth } from "./plugin.js";
|
|
30
45
|
export { requireAuth, requirePermission } from "./guards.js";
|
|
31
|
-
export type { AuthedUser, AuthStore, CreateIdpSessionInput, CreateRefreshTokenInput, FlytedeskAuthOptions, IdpRole, IdpSessionRow, RefreshTokenRow, UpdateIdpSessionInput, UserinfoClaims, } from "./types.js";
|
|
32
|
-
export type { AppSwitcherEntry, IdpAuthorization, IdpTokenResponse, IdTokenClaims, OidcClient, } from "./oidc-client.js";
|
|
33
|
-
export { createOidcClient } from "./oidc-client.js";
|
|
34
|
-
export type { ApiErrorBody } from "./shared-types.js";
|
|
35
|
-
export { AUTH_ROUTE_PREFIX, AUTH_ROUTE_SEGMENTS, authRoutePath, type AuthRouteSegment, } from "./shared-types.js";
|
|
36
|
-
export { IdpError, IdpUnavailableError, IdpAccessRevokedError, AuthStoreError, OidcHttpError, classifyIdpError, } from "./errors.js";
|
|
46
|
+
export type { AuthedUser, AuthStore, CreateIdpSessionInput, CreateRefreshTokenInput, FlytedeskAuthOptions, IdpRole, IdpSessionRow, RefreshTokenRow, RevokeFamilyReason, UpdateIdpSessionInput, UserinfoClaims, } from "./types.js";
|
|
47
|
+
export type { AppSwitcherEntry, IdpAuthorization, IdpTokenResponse, IdTokenClaims, OidcClient, OidcClientOptions, } from "./oidc-client.js";
|
|
48
|
+
export { createOidcClient, DEFAULT_OIDC_SCOPES, DEFAULT_IDP_REQUEST_TIMEOUT_MS } from "./oidc-client.js";
|
|
49
|
+
export type { ApiErrorBody, SilentAuthMessage } from "./shared-types.js";
|
|
50
|
+
export { AUTH_REQUEST_DEADLINE_MS, AUTH_ROUTE_PREFIX, AUTH_ROUTE_SEGMENTS, authRoutePath, SILENT_AUTH_MESSAGE_SOURCE, type AuthRouteSegment, } from "./shared-types.js";
|
|
51
|
+
export { IdpError, IdpUnavailableError, IdpTimeoutError, IdpAccessRevokedError, AuthStoreError, OidcHttpError, classifyIdpError, } from "./errors.js";
|
|
37
52
|
export { generatePkce, randomState, type PkcePair } from "./pkce.js";
|
|
38
53
|
export { generateRefreshToken, hashRefreshToken, signAccessToken, verifyAccessToken, type AccessTokenClaims, } from "./tokens.js";
|
package/dist/auth/index.js
CHANGED
|
@@ -5,10 +5,26 @@
|
|
|
5
5
|
* src/{routes/auth.ts,lib/flytedeskId.ts,plugins/auth.ts} so client id/secret, issuer,
|
|
6
6
|
* redirect URI, and app id are config instead of hardcoded to one app.
|
|
7
7
|
*
|
|
8
|
+
* This module (`@flytedesk/app-kit/auth`) is the standalone surface: types, the OIDC
|
|
9
|
+
* client, PKCE/token/error helpers, and the `requireAuth`/`requirePermission` guards —
|
|
10
|
+
* everything that does NOT need Fastify's `FastifyRequest` ambient-augmented with
|
|
11
|
+
* `authUser`. The plugin itself (`flytedeskAuth`, and the `declare module "fastify"`
|
|
12
|
+
* augmentation that comes with registering it) lives at the separate subpath
|
|
13
|
+
* `@flytedesk/app-kit/auth/plugin` (AK-14, gap 6): TypeScript applies a `declare module`
|
|
14
|
+
* augmentation program-wide the instant ANY file importing it is part of the
|
|
15
|
+
* compilation, even transitively — so as long as `flytedeskAuth` was re-exported from
|
|
16
|
+
* here too, importing just `generatePkce`/`randomState`/`signAccessToken` from this
|
|
17
|
+
* module forced the augmentation (and its `AuthedUser` shape) onto a consumer's WHOLE
|
|
18
|
+
* program, with no way to opt out — including a consumer with its own, differently
|
|
19
|
+
* shaped `request.authUser` who never even registers this plugin (`TS2717: Subsequent
|
|
20
|
+
* property declarations must have the same type`). Splitting the plugin into its own
|
|
21
|
+
* subpath means the augmentation only applies to a program that actually imports it.
|
|
22
|
+
*
|
|
8
23
|
* Usage:
|
|
9
24
|
*
|
|
10
25
|
* import Fastify from "fastify";
|
|
11
|
-
* import { flytedeskAuth
|
|
26
|
+
* import { flytedeskAuth } from "@flytedesk/app-kit/auth/plugin";
|
|
27
|
+
* import { requireAuth, requirePermission } from "@flytedesk/app-kit/auth";
|
|
12
28
|
*
|
|
13
29
|
* const app = Fastify();
|
|
14
30
|
* await app.register(flytedeskAuth, {
|
|
@@ -26,11 +42,10 @@
|
|
|
26
42
|
*
|
|
27
43
|
* app.get("/me", { preHandler: requireAuth }, async (request) => request.authUser);
|
|
28
44
|
*/
|
|
29
|
-
export { flytedeskAuth } from "./plugin.js";
|
|
30
45
|
export { requireAuth, requirePermission } from "./guards.js";
|
|
31
|
-
export { createOidcClient } from "./oidc-client.js";
|
|
32
|
-
export { AUTH_ROUTE_PREFIX, AUTH_ROUTE_SEGMENTS, authRoutePath, } from "./shared-types.js";
|
|
33
|
-
export { IdpError, IdpUnavailableError, IdpAccessRevokedError, AuthStoreError, OidcHttpError, classifyIdpError, } from "./errors.js";
|
|
46
|
+
export { createOidcClient, DEFAULT_OIDC_SCOPES, DEFAULT_IDP_REQUEST_TIMEOUT_MS } from "./oidc-client.js";
|
|
47
|
+
export { AUTH_REQUEST_DEADLINE_MS, AUTH_ROUTE_PREFIX, AUTH_ROUTE_SEGMENTS, authRoutePath, SILENT_AUTH_MESSAGE_SOURCE, } from "./shared-types.js";
|
|
48
|
+
export { IdpError, IdpUnavailableError, IdpTimeoutError, IdpAccessRevokedError, AuthStoreError, OidcHttpError, classifyIdpError, } from "./errors.js";
|
|
34
49
|
export { generatePkce, randomState } from "./pkce.js";
|
|
35
50
|
export { generateRefreshToken, hashRefreshToken, signAccessToken, verifyAccessToken, } from "./tokens.js";
|
|
36
51
|
//# 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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAwB7D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,8BAA8B,EAAE,MAAM,kBAAkB,CAAC;AAGzG,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,0BAA0B,GAE3B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,QAAQ,EACR,mBAAmB,EACnB,eAAe,EACf,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"}
|
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
import type { AppSwitcherEntry } from "./shared-types.js";
|
|
2
2
|
import type { UserinfoClaims } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Requested by default (AK-14, gap 1): without `offline_access`, flytedesk-id's
|
|
5
|
+
* oidc-provider never issues a refresh token, and every session dies the moment its
|
|
6
|
+
* ~1h IdP-side access token expires — a previously-shipped production bug (see
|
|
7
|
+
* media-planner's now-superseded `lib/flytedeskId.ts` doc comment on `buildAuthorizeUrl`).
|
|
8
|
+
* `offline_access` is ON by default here because a session that dies after an hour is
|
|
9
|
+
* almost never what a consuming app actually wants; a consumer that genuinely needs a
|
|
10
|
+
* narrower grant can override via `FlytedeskAuthOptions.scopes`/`OidcClientOptions.scopes`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_OIDC_SCOPES: readonly ["openid", "email", "profile", "offline_access"];
|
|
13
|
+
/** Sane default for EVERY flytedesk-id call this client makes (AK-20, gap 1 —
|
|
14
|
+
* generalized from AK-14 gap 5's `/token`-only timeout) — matches media-planner's
|
|
15
|
+
* `IDP_TOKEN_TIMEOUT_MS`, which `services/idpSession.ts` there sizes a DB transaction +
|
|
16
|
+
* advisory lock around specifically so it can never be held open while a flytedesk-id
|
|
17
|
+
* call is still in flight. Configurable via `OidcClientOptions.idpRequestTimeoutMs`. */
|
|
18
|
+
export declare const DEFAULT_IDP_REQUEST_TIMEOUT_MS = 5000;
|
|
3
19
|
export interface OidcClientOptions {
|
|
4
20
|
issuer: string;
|
|
5
21
|
clientId: string;
|
|
6
22
|
clientSecret: string;
|
|
7
23
|
redirectUri: string;
|
|
24
|
+
/** OAuth scopes requested by `buildAuthorizeUrl`. Default `DEFAULT_OIDC_SCOPES`
|
|
25
|
+
* (includes `offline_access` — see that constant's doc comment). */
|
|
26
|
+
scopes?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Bounds EVERY flytedesk-id call this client makes — `/token` (`exchangeCodeForTokens`,
|
|
29
|
+
* `refreshIdpTokens`), `/me` (`fetchUserinfo`), `/apps/{id}/authorization`
|
|
30
|
+
* (`fetchAuthorization`), `/me/apps` (`fetchMyApps`), discovery and revocation
|
|
31
|
+
* (`revokeIdpToken`) — via `AbortSignal.timeout`, through the one bounded-request
|
|
32
|
+
* helper every one of those methods goes through (AK-20, gap 1; generalized from
|
|
33
|
+
* AK-14 gap 5, which only bounded `/token`). Default `DEFAULT_IDP_REQUEST_TIMEOUT_MS`.
|
|
34
|
+
* A hung flytedesk-id response rejects with the typed `IdpTimeoutError` instead of
|
|
35
|
+
* hanging the caller (and, transitively, whatever lock/transaction/request it's
|
|
36
|
+
* inside) indefinitely. `fetchAuthorization` runs on every authenticated request
|
|
37
|
+
* (see `../plugin.ts`'s `loadAuthedUser`), so a hang there previously hung every
|
|
38
|
+
* signed-in request to every consumer of this package — this option is what closes
|
|
39
|
+
* that gap. No alias or deprecated name is kept for the old, narrower
|
|
40
|
+
* `tokenRequestTimeoutMs`.
|
|
41
|
+
*/
|
|
42
|
+
idpRequestTimeoutMs?: number;
|
|
8
43
|
}
|
|
9
44
|
export interface IdpTokenResponse {
|
|
10
45
|
access_token: string;
|
|
@@ -28,9 +63,14 @@ export interface IdpAuthorization {
|
|
|
28
63
|
}
|
|
29
64
|
export type { AppSwitcherEntry };
|
|
30
65
|
export interface OidcClient {
|
|
66
|
+
/** `prompt` is passed straight through to flytedesk-id's `/auth` endpoint —
|
|
67
|
+
* `"none"` (OIDC Core 3.1.2.1) is what drives silent (hidden-iframe) re-auth; see
|
|
68
|
+
* `../plugin.ts`'s `GET {routePrefix}/login` and `./client/silentAuth.ts`. Omitted
|
|
69
|
+
* entirely (not sent as an empty string) when not given. */
|
|
31
70
|
buildAuthorizeUrl(opts: {
|
|
32
71
|
state: string;
|
|
33
72
|
challenge: string;
|
|
73
|
+
prompt?: string;
|
|
34
74
|
}): string;
|
|
35
75
|
exchangeCodeForTokens(code: string, verifier: string): Promise<IdpTokenResponse>;
|
|
36
76
|
refreshIdpTokens(refreshToken: string): Promise<IdpTokenResponse>;
|
package/dist/auth/oidc-client.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
2
|
-
import { OidcHttpError } from "./errors.js";
|
|
2
|
+
import { IdpTimeoutError, IdpUnavailableError, OidcHttpError } from "./errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Requested by default (AK-14, gap 1): without `offline_access`, flytedesk-id's
|
|
5
|
+
* oidc-provider never issues a refresh token, and every session dies the moment its
|
|
6
|
+
* ~1h IdP-side access token expires — a previously-shipped production bug (see
|
|
7
|
+
* media-planner's now-superseded `lib/flytedeskId.ts` doc comment on `buildAuthorizeUrl`).
|
|
8
|
+
* `offline_access` is ON by default here because a session that dies after an hour is
|
|
9
|
+
* almost never what a consuming app actually wants; a consumer that genuinely needs a
|
|
10
|
+
* narrower grant can override via `FlytedeskAuthOptions.scopes`/`OidcClientOptions.scopes`.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_OIDC_SCOPES = ["openid", "email", "profile", "offline_access"];
|
|
13
|
+
/** Sane default for EVERY flytedesk-id call this client makes (AK-20, gap 1 —
|
|
14
|
+
* generalized from AK-14 gap 5's `/token`-only timeout) — matches media-planner's
|
|
15
|
+
* `IDP_TOKEN_TIMEOUT_MS`, which `services/idpSession.ts` there sizes a DB transaction +
|
|
16
|
+
* advisory lock around specifically so it can never be held open while a flytedesk-id
|
|
17
|
+
* call is still in flight. Configurable via `OidcClientOptions.idpRequestTimeoutMs`. */
|
|
18
|
+
export const DEFAULT_IDP_REQUEST_TIMEOUT_MS = 5_000;
|
|
3
19
|
/**
|
|
4
20
|
* One instance per FlytedeskAuthOptions (i.e. per plugin registration), not a module
|
|
5
21
|
* singleton — keeps the JWKS/discovery caches below scoped to one issuer/client pair
|
|
@@ -23,15 +39,42 @@ export function createOidcClient(opts) {
|
|
|
23
39
|
const clientSecret = encodeURIComponent(opts.clientSecret);
|
|
24
40
|
return "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
25
41
|
}
|
|
42
|
+
const idpRequestTimeoutMs = opts.idpRequestTimeoutMs ?? DEFAULT_IDP_REQUEST_TIMEOUT_MS;
|
|
43
|
+
/**
|
|
44
|
+
* The ONE bounded-request helper every flytedesk-id call in this client goes through
|
|
45
|
+
* (AK-20, gap 1) — `tokenRequest` below, `fetchUserinfo`, `fetchAuthorization`,
|
|
46
|
+
* `fetchMyApps`, `getDiscovery` and `revokeIdpToken`'s own POST all call this instead
|
|
47
|
+
* of `fetch` directly. Bounded by `AbortSignal.timeout`: unbounded before this card, a
|
|
48
|
+
* hung flytedesk-id response on ANY of these — most importantly `fetchAuthorization`,
|
|
49
|
+
* which runs on every authenticated request via `../plugin.ts`'s `loadAuthedUser` —
|
|
50
|
+
* could hold open whatever the caller wraps it in (for a consumer following
|
|
51
|
+
* media-planner's model, a Postgres transaction + advisory lock). A timeout throws the
|
|
52
|
+
* more specific `IdpTimeoutError`; any other network failure (DNS, refused connection,
|
|
53
|
+
* ...) is classified as `IdpUnavailableError` rather than propagating an unclassified
|
|
54
|
+
* error — both are typed, neither is swallowed. Deliberately does NOT inspect
|
|
55
|
+
* `res.ok`: a non-2xx response is still a normal, successfully-received `Response`,
|
|
56
|
+
* and each call site classifies it into its own message/error shape.
|
|
57
|
+
*/
|
|
58
|
+
async function idpFetch(url, init, label) {
|
|
59
|
+
try {
|
|
60
|
+
return await fetch(url, { ...init, signal: AbortSignal.timeout(idpRequestTimeoutMs) });
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
64
|
+
throw new IdpTimeoutError(`flytedesk-id ${label} did not respond within ${idpRequestTimeoutMs}ms`, err);
|
|
65
|
+
}
|
|
66
|
+
throw new IdpUnavailableError(`flytedesk-id ${label} request failed: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
26
69
|
async function tokenRequest(body) {
|
|
27
|
-
const res = await
|
|
70
|
+
const res = await idpFetch(issuerUrl("/token"), {
|
|
28
71
|
method: "POST",
|
|
29
72
|
headers: {
|
|
30
73
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
31
74
|
Authorization: basicAuthHeader(),
|
|
32
75
|
},
|
|
33
76
|
body: new URLSearchParams(body).toString(),
|
|
34
|
-
});
|
|
77
|
+
}, "/token");
|
|
35
78
|
if (!res.ok) {
|
|
36
79
|
const text = await res.text().catch(() => "");
|
|
37
80
|
throw new OidcHttpError(`flytedesk-id /token returned ${res.status}: ${text}`, res.status);
|
|
@@ -45,23 +88,26 @@ export function createOidcClient(opts) {
|
|
|
45
88
|
}
|
|
46
89
|
let discoveryCache;
|
|
47
90
|
function getDiscovery() {
|
|
48
|
-
discoveryCache ??=
|
|
91
|
+
discoveryCache ??= idpFetch(issuerUrl("/.well-known/openid-configuration"), {}, "discovery").then((res) => {
|
|
49
92
|
if (!res.ok)
|
|
50
93
|
throw new Error(`flytedesk-id discovery returned ${res.status}`);
|
|
51
94
|
return res.json();
|
|
52
95
|
});
|
|
53
96
|
return discoveryCache;
|
|
54
97
|
}
|
|
98
|
+
const scope = (opts.scopes ?? DEFAULT_OIDC_SCOPES).join(" ");
|
|
55
99
|
return {
|
|
56
|
-
buildAuthorizeUrl({ state, challenge }) {
|
|
100
|
+
buildAuthorizeUrl({ state, challenge, prompt }) {
|
|
57
101
|
const url = new URL(issuerUrl("/auth"));
|
|
58
102
|
url.searchParams.set("client_id", opts.clientId);
|
|
59
103
|
url.searchParams.set("redirect_uri", opts.redirectUri);
|
|
60
104
|
url.searchParams.set("response_type", "code");
|
|
61
|
-
url.searchParams.set("scope",
|
|
105
|
+
url.searchParams.set("scope", scope);
|
|
62
106
|
url.searchParams.set("code_challenge", challenge);
|
|
63
107
|
url.searchParams.set("code_challenge_method", "S256");
|
|
64
108
|
url.searchParams.set("state", state);
|
|
109
|
+
if (prompt)
|
|
110
|
+
url.searchParams.set("prompt", prompt);
|
|
65
111
|
return url.toString();
|
|
66
112
|
},
|
|
67
113
|
/** Authorization Code + PKCE exchange — the one moment this app's client_secret is used. */
|
|
@@ -105,9 +151,7 @@ export function createOidcClient(opts) {
|
|
|
105
151
|
/** `GET {issuer}/me` — the profile claims (email/name/email_verified) the id_token
|
|
106
152
|
* deliberately doesn't carry for the Authorization Code flow. */
|
|
107
153
|
async fetchUserinfo(idpAccessToken) {
|
|
108
|
-
const res = await
|
|
109
|
-
headers: { Authorization: `Bearer ${idpAccessToken}` },
|
|
110
|
-
});
|
|
154
|
+
const res = await idpFetch(issuerUrl("/me"), { headers: { Authorization: `Bearer ${idpAccessToken}` } }, "/me");
|
|
111
155
|
if (!res.ok) {
|
|
112
156
|
throw new OidcHttpError(`flytedesk-id userinfo (/me) returned ${res.status}`, res.status);
|
|
113
157
|
}
|
|
@@ -127,9 +171,7 @@ export function createOidcClient(opts) {
|
|
|
127
171
|
* by calling this live rather than memoizing the result — see FlytedeskAuthOptions
|
|
128
172
|
* authorizationCacheTtlMs's doc comment for the one sanctioned exception. */
|
|
129
173
|
async fetchAuthorization(idpAccessToken, appId) {
|
|
130
|
-
const res = await
|
|
131
|
-
headers: { Authorization: `Bearer ${idpAccessToken}` },
|
|
132
|
-
});
|
|
174
|
+
const res = await idpFetch(issuerUrl(`/apps/${appId}/authorization`), { headers: { Authorization: `Bearer ${idpAccessToken}` } }, "authorization endpoint");
|
|
133
175
|
if (!res.ok) {
|
|
134
176
|
throw new OidcHttpError(`flytedesk-id authorization endpoint returned ${res.status}`, res.status);
|
|
135
177
|
}
|
|
@@ -137,9 +179,7 @@ export function createOidcClient(opts) {
|
|
|
137
179
|
},
|
|
138
180
|
/** `GET {issuer}/me/apps` — see fetchMyApps's doc comment on OidcClient above. */
|
|
139
181
|
async fetchMyApps(idpAccessToken) {
|
|
140
|
-
const res = await
|
|
141
|
-
headers: { Authorization: `Bearer ${idpAccessToken}` },
|
|
142
|
-
});
|
|
182
|
+
const res = await idpFetch(issuerUrl("/me/apps"), { headers: { Authorization: `Bearer ${idpAccessToken}` } }, "/me/apps");
|
|
143
183
|
if (!res.ok) {
|
|
144
184
|
throw new OidcHttpError(`flytedesk-id /me/apps returned ${res.status}`, res.status);
|
|
145
185
|
}
|
|
@@ -155,17 +195,21 @@ export function createOidcClient(opts) {
|
|
|
155
195
|
const discovery = await getDiscovery();
|
|
156
196
|
if (!discovery.revocation_endpoint)
|
|
157
197
|
return false;
|
|
158
|
-
const res = await
|
|
198
|
+
const res = await idpFetch(discovery.revocation_endpoint, {
|
|
159
199
|
method: "POST",
|
|
160
200
|
headers: {
|
|
161
201
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
162
202
|
Authorization: basicAuthHeader(),
|
|
163
203
|
},
|
|
164
204
|
body: new URLSearchParams({ token }).toString(),
|
|
165
|
-
});
|
|
205
|
+
}, "revocation");
|
|
166
206
|
return res.ok;
|
|
167
207
|
}
|
|
168
208
|
catch {
|
|
209
|
+
// Best-effort by design (see this method's doc comment above) — a timed-out or
|
|
210
|
+
// otherwise-failed revocation (AK-20, gap 1) must never block local logout, so
|
|
211
|
+
// an IdpTimeoutError/IdpUnavailableError from idpFetch is swallowed here exactly
|
|
212
|
+
// like any other failure, not treated specially.
|
|
169
213
|
return false;
|
|
170
214
|
}
|
|
171
215
|
},
|