@kanzo-tech/auth 0.18.0 → 0.20.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-context.d.ts +3 -1
- package/dist/auth-context.d.ts.map +1 -1
- package/dist/auth-context.js.map +1 -1
- package/dist/auth-provider.js +19 -18
- package/dist/auth-provider.js.map +1 -1
- package/dist/bff-auth.d.ts +2 -1
- package/dist/bff-auth.d.ts.map +1 -1
- package/dist/bff-auth.js +77 -46
- package/dist/bff-auth.js.map +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +65 -42
- package/dist/browser.js.map +1 -1
- package/dist/claims.js +1 -1
- package/dist/claims.js.map +1 -1
- package/dist/cookie-session.d.ts.map +1 -1
- package/dist/cookie-session.js +1 -1
- package/dist/cookie-session.js.map +1 -1
- package/dist/deadline.d.ts +14 -0
- package/dist/deadline.d.ts.map +1 -0
- package/dist/deadline.js +17 -0
- package/dist/deadline.js.map +1 -0
- package/dist/gate.js +9 -9
- package/dist/gate.js.map +1 -1
- package/dist/issuer.d.ts +0 -2
- package/dist/issuer.d.ts.map +1 -1
- package/dist/issuer.js +17 -17
- package/dist/issuer.js.map +1 -1
- package/dist/next-middleware.d.ts +6 -0
- package/dist/next-middleware.d.ts.map +1 -1
- package/dist/next-middleware.js +15 -13
- package/dist/next-middleware.js.map +1 -1
- package/dist/next-proxy.d.ts.map +1 -1
- package/dist/next-proxy.js +27 -26
- package/dist/next-proxy.js.map +1 -1
- package/dist/next-routes.d.ts +9 -0
- package/dist/next-routes.d.ts.map +1 -1
- package/dist/next-routes.js +66 -46
- package/dist/next-routes.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +151 -109
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +40 -21
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -4
- package/dist/types.js.map +1 -1
- package/dist/use-session.d.ts +2 -1
- package/dist/use-session.d.ts.map +1 -1
- package/dist/use-session.js.map +1 -1
- package/package.json +6 -6
package/dist/auth-context.d.ts
CHANGED
|
@@ -3,11 +3,13 @@ import { Auth, Session } from './types';
|
|
|
3
3
|
* The context, in its own module because two files need it and neither should import the other.
|
|
4
4
|
* `createContext` is what makes this file client-only.
|
|
5
5
|
*/
|
|
6
|
-
export type AuthStatus = "loading" | "authenticated" | "anonymous";
|
|
6
|
+
export type AuthStatus = "loading" | "authenticated" | "anonymous" | "failed";
|
|
7
7
|
export interface AuthContextValue {
|
|
8
8
|
readonly auth: Auth;
|
|
9
9
|
readonly session: Session | null;
|
|
10
10
|
readonly status: AuthStatus;
|
|
11
|
+
/** What `getSession` rejected with, exactly as thrown, while `status` is `"failed"`. */
|
|
12
|
+
readonly error: unknown;
|
|
11
13
|
}
|
|
12
14
|
export declare const AuthContext: import('react').Context<AuthContextValue | null>;
|
|
13
15
|
//# sourceMappingURL=auth-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-context.d.ts","sourceRoot":"","sources":["../src/auth-context.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAE7C;;;GAGG;AAEH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,eAAe,GAAG,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"auth-context.d.ts","sourceRoot":"","sources":["../src/auth-context.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAE7C;;;GAGG;AAEH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,eAAe,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE9E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,wFAAwF;IACxF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,WAAW,kDAA+C,CAAC"}
|
package/dist/auth-context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-context.js","sources":["../src/auth-context.ts"],"sourcesContent":["\"use client\";\n\nimport { createContext } from \"react\";\nimport type { Auth, Session } from \"./types\";\n\n/**\n * The context, in its own module because two files need it and neither should import the other.\n * `createContext` is what makes this file client-only.\n */\n\nexport type AuthStatus = \"loading\" | \"authenticated\" | \"anonymous\";\n\nexport interface AuthContextValue {\n readonly auth: Auth;\n readonly session: Session | null;\n readonly status: AuthStatus;\n}\n\nexport const AuthContext = createContext<AuthContextValue | null>(null);\n"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"auth-context.js","sources":["../src/auth-context.ts"],"sourcesContent":["\"use client\";\n\nimport { createContext } from \"react\";\nimport type { Auth, Session } from \"./types\";\n\n/**\n * The context, in its own module because two files need it and neither should import the other.\n * `createContext` is what makes this file client-only.\n */\n\nexport type AuthStatus = \"loading\" | \"authenticated\" | \"anonymous\" | \"failed\";\n\nexport interface AuthContextValue {\n readonly auth: Auth;\n readonly session: Session | null;\n readonly status: AuthStatus;\n /** What `getSession` rejected with, exactly as thrown, while `status` is `\"failed\"`. */\n readonly error: unknown;\n}\n\nexport const AuthContext = createContext<AuthContextValue | null>(null);\n"],"names":[],"mappings":";;AAoBO;;;;"}
|
package/dist/auth-provider.js
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as c } from "react/jsx-runtime";
|
|
3
|
-
import { useState as f, useEffect as l, useMemo as
|
|
4
|
-
import { AuthContext as
|
|
3
|
+
import { useState as f, useEffect as l, useMemo as d } from "react";
|
|
4
|
+
import { AuthContext as m } from "./auth-context.js";
|
|
5
5
|
function x({
|
|
6
|
-
auth:
|
|
7
|
-
children:
|
|
6
|
+
auth: t,
|
|
7
|
+
children: i
|
|
8
8
|
}) {
|
|
9
|
-
const [
|
|
9
|
+
const [s, o] = f({
|
|
10
10
|
session: null,
|
|
11
|
-
status: "loading"
|
|
11
|
+
status: "loading",
|
|
12
|
+
error: void 0
|
|
12
13
|
});
|
|
13
14
|
l(() => {
|
|
14
|
-
let
|
|
15
|
+
let r = !0;
|
|
15
16
|
const n = () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}).catch(() => {
|
|
19
|
-
|
|
17
|
+
t.getSession().then((e) => {
|
|
18
|
+
r && o({ session: e, status: e ? "authenticated" : "anonymous", error: void 0 });
|
|
19
|
+
}).catch((e) => {
|
|
20
|
+
r && o({ session: null, status: "failed", error: e });
|
|
20
21
|
});
|
|
21
|
-
}, a =
|
|
22
|
+
}, a = t.subscribe(n);
|
|
22
23
|
return n(), () => {
|
|
23
|
-
|
|
24
|
+
r = !1, a();
|
|
24
25
|
};
|
|
25
|
-
}, [
|
|
26
|
-
const
|
|
27
|
-
() => ({ auth:
|
|
28
|
-
[
|
|
26
|
+
}, [t]);
|
|
27
|
+
const u = d(
|
|
28
|
+
() => ({ auth: t, session: s.session, status: s.status, error: s.error }),
|
|
29
|
+
[t, s.session, s.status, s.error]
|
|
29
30
|
);
|
|
30
|
-
return /* @__PURE__ */ c(
|
|
31
|
+
return /* @__PURE__ */ c(m.Provider, { value: u, children: i });
|
|
31
32
|
}
|
|
32
33
|
export {
|
|
33
34
|
x as AuthProvider
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-provider.js","sources":["../src/auth-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { AuthContext, type AuthStatus } from \"./auth-context\";\nimport type { Auth, Session } from \"./types\";\n\n/**\n * Holds the session and keeps it current. One per application, at the root.\n *\n * It does not know which pattern is underneath it — `bffAuth` or `browserAuth` both satisfy `Auth`\n * — which is the whole reason the hooks below it can be written once.\n */\nexport function AuthProvider({\n auth,\n children,\n}: {\n readonly auth: Auth;\n readonly children?: React.ReactNode;\n}) {\n const [state, setState] = useState<{ session: Session | null; status: AuthStatus }>({\n session: null,\n status: \"loading\",\n });\n\n useEffect(() => {\n // `live` rather than an AbortController: `getSession` may be a cache read with no request to\n // abort, and what must not happen is a setState after unmount — a resolved promise from the\n // provider that was just torn down, overwriting the one that replaced it.\n let live = true;\n\n const read = () => {\n void auth\n .getSession()\n .then((session) => {\n if (!live) return;\n setState({ session, status: session ? \"authenticated\" : \"anonymous\" });\n })\n .catch(() => {\n //
|
|
1
|
+
{"version":3,"file":"auth-provider.js","sources":["../src/auth-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { AuthContext, type AuthStatus } from \"./auth-context\";\nimport type { Auth, Session } from \"./types\";\n\n/**\n * Holds the session and keeps it current. One per application, at the root.\n *\n * It does not know which pattern is underneath it — `bffAuth` or `browserAuth` both satisfy `Auth`\n * — which is the whole reason the hooks below it can be written once.\n */\nexport function AuthProvider({\n auth,\n children,\n}: {\n readonly auth: Auth;\n readonly children?: React.ReactNode;\n}) {\n const [state, setState] = useState<{ session: Session | null; status: AuthStatus; error: unknown }>({\n session: null,\n status: \"loading\",\n error: undefined,\n });\n\n useEffect(() => {\n // `live` rather than an AbortController: `getSession` may be a cache read with no request to\n // abort, and what must not happen is a setState after unmount — a resolved promise from the\n // provider that was just torn down, overwriting the one that replaced it.\n let live = true;\n\n const read = () => {\n void auth\n .getSession()\n .then((session) => {\n if (!live) return;\n setState({ session, status: session ? \"authenticated\" : \"anonymous\", error: undefined });\n })\n .catch((error: unknown) => {\n // Not \"anonymous\": that sends the person to sign in over an outage that will still be\n // there when they get back. Not \"loading\" either: a spinner nobody can escape.\n if (live) setState({ session: null, status: \"failed\", error });\n });\n };\n\n const unsubscribe = auth.subscribe(read);\n read();\n\n return () => {\n live = false;\n unsubscribe();\n };\n }, [auth]);\n\n const value = useMemo(\n () => ({ auth, session: state.session, status: state.status, error: state.error }),\n [auth, state.session, state.status, state.error],\n );\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n"],"names":[],"mappings":";;;;AAYO;AAAsB;AAC3B;AAEF;AAIE;AAAoG;AACzF;AACD;AACD;AAGT;AAIE;AAEA;AACE;AAGI;AACuF;AAKvF;AAA6D;AAC9D;AAIL;AAGE;AACA;AACF;AAGF;AAAc;AAC8D;AAC3B;AAGjD;AACF;;;;"}
|
package/dist/bff-auth.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ export interface BffAuthConfig {
|
|
|
25
25
|
* It is our own server on the other end, and it is still **data off the wire**: a cast here would
|
|
26
26
|
* make a deploy skew or a proxy's error page arrive as a `Session` whose `user` is undefined, and
|
|
27
27
|
* the failure would surface three components away as a property read on nothing. `null` for
|
|
28
|
-
* anything unrecognisable
|
|
28
|
+
* anything unrecognisable; `bffAuth` reads a 200 it cannot read as `session/unavailable`, because
|
|
29
|
+
* the endpoint answers "nobody is signed in" with a 401 and never with a body.
|
|
29
30
|
*/
|
|
30
31
|
export declare function readSession(value: unknown): Session | null;
|
|
31
32
|
export declare function bffAuth(config?: BffAuthConfig): Auth;
|
package/dist/bff-auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bff-auth.d.ts","sourceRoot":"","sources":["../src/bff-auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bff-auth.d.ts","sourceRoot":"","sources":["../src/bff-auth.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,IAAI,EAAqB,KAAK,OAAO,EAAsB,MAAM,SAAS,CAAC;AAEpG;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IACzC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3C;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAyB1D;AAED,wBAAgB,OAAO,CAAC,MAAM,GAAE,aAAkB,GAAG,IAAI,CA8IxD"}
|
package/dist/bff-auth.js
CHANGED
|
@@ -1,62 +1,93 @@
|
|
|
1
|
-
import { isReplayable as
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { isReplayable as $ } from "./auth-fetch.js";
|
|
2
|
+
import { deadline as p } from "./deadline.js";
|
|
3
|
+
import { singleFlight as y } from "./single-flight.js";
|
|
4
|
+
import { AuthError as A } from "./types.js";
|
|
5
|
+
function v(i) {
|
|
6
|
+
if (typeof i != "object" || i === null) return null;
|
|
7
|
+
const t = i, o = t.alias;
|
|
8
|
+
return typeof o != "string" || o.length === 0 ? null : {
|
|
9
|
+
alias: o,
|
|
10
|
+
id: typeof t.id == "string" ? t.id : void 0,
|
|
11
|
+
roles: Array.isArray(t.roles) ? t.roles.filter((a) => typeof a == "string") : []
|
|
10
12
|
};
|
|
11
13
|
}
|
|
12
|
-
function
|
|
13
|
-
if (typeof
|
|
14
|
-
const
|
|
15
|
-
if (typeof
|
|
16
|
-
const a =
|
|
14
|
+
function T(i) {
|
|
15
|
+
if (typeof i != "object" || i === null) return null;
|
|
16
|
+
const t = i, o = t.user;
|
|
17
|
+
if (typeof o != "object" || o === null) return null;
|
|
18
|
+
const a = o.id;
|
|
17
19
|
if (typeof a != "string" || a.length === 0) return null;
|
|
18
|
-
const l =
|
|
20
|
+
const l = o, c = (s) => typeof l[s] == "string" && l[s] !== "" ? l[s] : void 0;
|
|
19
21
|
return {
|
|
20
|
-
user: { id: a, email:
|
|
21
|
-
roles: Array.isArray(
|
|
22
|
-
organizations: Array.isArray(
|
|
23
|
-
expiresAt: typeof
|
|
22
|
+
user: { id: a, email: c("email"), name: c("name"), username: c("username") },
|
|
23
|
+
roles: Array.isArray(t.roles) ? t.roles.filter((s) => typeof s == "string") : [],
|
|
24
|
+
organizations: Array.isArray(t.organizations) ? t.organizations.map(v).filter((s) => s !== null) : [],
|
|
25
|
+
expiresAt: typeof t.expiresAt == "number" ? t.expiresAt : 0
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
+
function x(i = {}) {
|
|
29
|
+
const t = (i.basePath ?? "/api/auth").replace(/\/$/, ""), o = i.fetch ?? ((...r) => globalThis.fetch(...r)), a = i.navigate ?? ((r) => void (globalThis.location.href = r)), l = /* @__PURE__ */ new Set(), c = () => {
|
|
28
30
|
for (const r of l) r();
|
|
29
31
|
};
|
|
30
|
-
let
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
let s = null, f = !1;
|
|
33
|
+
const u = (r, e, n) => new A(
|
|
34
|
+
"session/unavailable",
|
|
35
|
+
e === void 0 ? `${r} could not be reached` : `${r} answered ${e}`,
|
|
36
|
+
e === void 0 ? {} : { status: e },
|
|
37
|
+
n === void 0 ? void 0 : { cause: n }
|
|
38
|
+
), h = async (r, e) => {
|
|
39
|
+
var n;
|
|
40
|
+
try {
|
|
41
|
+
return await o(r, e);
|
|
42
|
+
} catch (d) {
|
|
43
|
+
throw (n = e.signal) != null && n.aborted ? e.signal.reason : u(r, void 0, d);
|
|
44
|
+
}
|
|
45
|
+
}, m = y(
|
|
46
|
+
() => p("session/silent", async (r) => {
|
|
47
|
+
const e = await h(`${t}/session`, { headers: { Accept: "application/json" }, signal: r });
|
|
48
|
+
if (e.status === 401) return null;
|
|
49
|
+
if (!e.ok) throw u(`${t}/session`, e.status);
|
|
50
|
+
let n;
|
|
51
|
+
try {
|
|
52
|
+
n = await e.json();
|
|
53
|
+
} catch (b) {
|
|
54
|
+
throw u(`${t}/session`, e.status, b);
|
|
55
|
+
}
|
|
56
|
+
const d = T(n);
|
|
57
|
+
if (d === null) throw u(`${t}/session`, e.status);
|
|
58
|
+
return d;
|
|
59
|
+
})
|
|
60
|
+
), w = y(
|
|
61
|
+
() => p("session/silent", async (r) => {
|
|
62
|
+
const e = await h(`${t}/refresh`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { Accept: "application/json" },
|
|
65
|
+
signal: r
|
|
66
|
+
});
|
|
67
|
+
if (e.status >= 500) throw u(`${t}/refresh`, e.status);
|
|
68
|
+
return e.ok;
|
|
69
|
+
})
|
|
70
|
+
), g = async () => {
|
|
71
|
+
const r = s;
|
|
72
|
+
return s = await m(), f = !0, (r === null != (s === null) || (r == null ? void 0 : r.user.id) !== (s == null ? void 0 : s.user.id)) && c(), s;
|
|
42
73
|
};
|
|
43
74
|
return {
|
|
44
75
|
async getSession() {
|
|
45
|
-
return
|
|
76
|
+
return f ? s : g();
|
|
46
77
|
},
|
|
47
78
|
subscribe(r) {
|
|
48
79
|
return l.add(r), () => l.delete(r);
|
|
49
80
|
},
|
|
50
81
|
async signIn(r = {}) {
|
|
51
|
-
var
|
|
52
|
-
const
|
|
53
|
-
|
|
82
|
+
var n;
|
|
83
|
+
const e = new URLSearchParams();
|
|
84
|
+
e.set("returnTo", r.returnTo ?? ((n = globalThis.location) == null ? void 0 : n.href) ?? "/"), r.organization !== void 0 && e.set("organization", r.organization), a(`${t}/signin?${e.toString()}`);
|
|
54
85
|
},
|
|
55
86
|
async signOut(r = {}) {
|
|
56
|
-
const
|
|
57
|
-
r.returnTo !== void 0 &&
|
|
58
|
-
const
|
|
59
|
-
a(
|
|
87
|
+
const e = new URLSearchParams();
|
|
88
|
+
r.returnTo !== void 0 && e.set("returnTo", r.returnTo);
|
|
89
|
+
const n = e.toString();
|
|
90
|
+
a(n ? `${t}/signout?${n}` : `${t}/signout`);
|
|
60
91
|
},
|
|
61
92
|
/**
|
|
62
93
|
* No `Authorization` header — the cookie rides along on a same-origin request by itself — and
|
|
@@ -72,14 +103,14 @@ function A(s = {}) {
|
|
|
72
103
|
* loop against the authorization server. A request whose body cannot be replayed is not
|
|
73
104
|
* retried at all, and `isReplayable` is the same predicate the bearer-token path uses.
|
|
74
105
|
*/
|
|
75
|
-
fetch: async (r,
|
|
76
|
-
const
|
|
77
|
-
return
|
|
106
|
+
fetch: async (r, e) => {
|
|
107
|
+
const n = await o(r, e);
|
|
108
|
+
return n.status !== 401 || !f ? n : await w() ? $(r, e) ? o(r, e) : n : (f = !1, await g().catch(c), n);
|
|
78
109
|
}
|
|
79
110
|
};
|
|
80
111
|
}
|
|
81
112
|
export {
|
|
82
|
-
|
|
83
|
-
|
|
113
|
+
x as bffAuth,
|
|
114
|
+
T as readSession
|
|
84
115
|
};
|
|
85
116
|
//# sourceMappingURL=bff-auth.js.map
|
package/dist/bff-auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bff-auth.js","sources":["../src/bff-auth.ts"],"sourcesContent":["import { isReplayable } from \"./auth-fetch\";\nimport { singleFlight } from \"./single-flight\";\nimport type { Auth, Organization, Session, SignInOptions } from \"./types\";\n\n/**\n * The Backend-For-Frontend pattern: the token never reaches the browser.\n *\n * RFC 10017 calls this one *\"strongly recommended for business applications, sensitive\n * applications, and applications that handle personal data\"*. The server holds the confidential\n * client and the tokens; the browser holds a cookie it cannot read, and asks the server who it is.\n *\n * So there is no engine on this path — no PKCE, no storage, no renewal — which is why it lives on\n * the root barrel beside the hooks rather than behind a subpath. What it needs from the server is\n * three routes, which `@kanzo-tech/auth/next` provides: a session endpoint, a sign-in and a\n * sign-out.\n */\n\nexport interface BffAuthConfig {\n /** Where the BFF's auth routes are mounted. Default `/api/auth`. */\n readonly basePath?: string;\n /** Injectable for tests. Defaults to the global. */\n readonly fetch?: typeof globalThis.fetch;\n /** Injectable for tests. Defaults to assigning `window.location`. */\n readonly navigate?: (url: string) => void;\n}\n\nfunction readOrganization(value: unknown): Organization | null {\n if (typeof value !== \"object\" || value === null) return null;\n const org = value as Record<string, unknown>;\n const alias = org[\"alias\"];\n if (typeof alias !== \"string\" || alias.length === 0) return null;\n return {\n alias,\n id: typeof org[\"id\"] === \"string\" ? org[\"id\"] : undefined,\n roles: Array.isArray(org[\"roles\"])\n ? org[\"roles\"].filter((r): r is string => typeof r === \"string\")\n : [],\n };\n}\n\n/**\n * The session endpoint's answer, checked rather than cast.\n *\n * It is our own server on the other end, and it is still **data off the wire**: a cast here would\n * make a deploy skew or a proxy's error page arrive as a `Session` whose `user` is undefined, and\n * the failure would surface three components away as a property read on nothing. `null` for\n * anything unrecognisable is the same answer as \"not signed in\", which is the safe reading.\n */\nexport function readSession(value: unknown): Session | null {\n if (typeof value !== \"object\" || value === null) return null;\n const body = value as Record<string, unknown>;\n\n const user = body[\"user\"];\n if (typeof user !== \"object\" || user === null) return null;\n const id = (user as Record<string, unknown>)[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) return null;\n\n const person = user as Record<string, unknown>;\n const str = (key: string) =>\n typeof person[key] === \"string\" && person[key] !== \"\" ? (person[key] as string) : undefined;\n\n return {\n user: { id, email: str(\"email\"), name: str(\"name\"), username: str(\"username\") },\n roles: Array.isArray(body[\"roles\"])\n ? body[\"roles\"].filter((r): r is string => typeof r === \"string\")\n : [],\n organizations: Array.isArray(body[\"organizations\"])\n ? body[\"organizations\"]\n .map(readOrganization)\n .filter((o): o is Organization => o !== null)\n : [],\n expiresAt: typeof body[\"expiresAt\"] === \"number\" ? body[\"expiresAt\"] : 0,\n };\n}\n\nexport function bffAuth(config: BffAuthConfig = {}): Auth {\n const base = (config.basePath ?? \"/api/auth\").replace(/\\/$/, \"\");\n const doFetch = config.fetch ?? ((...args) => globalThis.fetch(...args));\n const go = config.navigate ?? ((url: string) => void (globalThis.location.href = url));\n\n const listeners = new Set<() => void>();\n const announce = () => {\n for (const listener of listeners) listener();\n };\n\n let cached: Session | null = null;\n let known = false;\n\n // Single-flight for the same reason the token path needs it: a page that mounts six components\n // asks six times in one tick, and one answer serves them all. Here it costs a request rather\n // than a revoked token chain, which is a smaller bill for the same mistake.\n const read = singleFlight(async (): Promise<Session | null> => {\n const response = await doFetch(`${base}/session`, {\n headers: { Accept: \"application/json\" },\n });\n // 401 is the documented answer for \"nobody is signed in\", not a failure to report.\n if (response.status === 401) return null;\n if (!response.ok) return null;\n return readSession(await response.json().catch(() => null));\n });\n\n /**\n * Ask the BFF to spend the refresh token, once for however many requests noticed at the moment.\n *\n * This is the browser end of the renewal, and without it the session cookie's lifetime and the\n * access token's are two different clocks with nothing between them: a cookie good for eight\n * hours in front of a token good for one produces seven hours in which `/session` answers 200,\n * the whole application draws, and every request for data is a 401 that nothing acts on.\n *\n * `POST`, because the route only answers `POST` — it spends something, and a `GET` that spends\n * something is one prefetch away from spending it unasked.\n */\n const renew = singleFlight(async (): Promise<boolean> => {\n const response = await doFetch(`${base}/refresh`, {\n method: \"POST\",\n headers: { Accept: \"application/json\" },\n });\n return response.ok;\n });\n\n const refresh = async (): Promise<Session | null> => {\n const next = cached;\n cached = await read();\n known = true;\n // Announce only a change, so a poll does not re-render the tree every time.\n if ((next === null) !== (cached === null) || next?.user.id !== cached?.user.id) announce();\n return cached;\n };\n\n return {\n async getSession() {\n if (known) return cached;\n return refresh();\n },\n\n subscribe(onChange) {\n listeners.add(onChange);\n return () => listeners.delete(onChange);\n },\n\n async signIn(options: SignInOptions = {}) {\n const params = new URLSearchParams();\n params.set(\"returnTo\", options.returnTo ?? globalThis.location?.href ?? \"/\");\n if (options.organization !== undefined) params.set(\"organization\", options.organization);\n go(`${base}/signin?${params.toString()}`);\n },\n\n async signOut(options = {}) {\n const params = new URLSearchParams();\n if (options.returnTo !== undefined) params.set(\"returnTo\", options.returnTo);\n const query = params.toString();\n go(query ? `${base}/signout?${query}` : `${base}/signout`);\n },\n\n /**\n * No `Authorization` header — the cookie rides along on a same-origin request by itself — and\n * **one** retry, behind one renewal.\n *\n * A 401 here is ambiguous in a way it is not under `browserAuth`: the cookie was sent and was\n * accepted, so what expired is the access token *behind* the cookie, which this half of the\n * pattern cannot see. So the 401 is taken as \"renew and try again\" first and as \"the session\n * is gone\" only when the renewal is refused — at which point re-reading tells the tree, which\n * is what it did before and all it did before.\n *\n * The retry is once, for the reason `authFetch` gives: twice turns an ended session into a\n * loop against the authorization server. A request whose body cannot be replayed is not\n * retried at all, and `isReplayable` is the same predicate the bearer-token path uses.\n */\n fetch: async (input, init) => {\n const response = await doFetch(input, init);\n if (response.status !== 401 || !known) return response;\n\n if (await renew()) {\n if (!isReplayable(input, init)) return response;\n return doFetch(input, init);\n }\n\n known = false;\n await refresh();\n return response;\n },\n };\n}\n"],"names":["readOrganization","value","org","alias","r","readSession","body","user","id","person","str","key","o","bffAuth","config","base","doFetch","args","go","url","listeners","announce","listener","cached","known","read","singleFlight","response","renew","refresh","next","onChange","options","params","_a","query","input","init","isReplayable"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bff-auth.js","sources":["../src/bff-auth.ts"],"sourcesContent":["import { isReplayable } from \"./auth-fetch\";\nimport { deadline } from \"./deadline\";\nimport { singleFlight } from \"./single-flight\";\nimport { AuthError, type Auth, type Organization, type Session, type SignInOptions } from \"./types\";\n\n/**\n * The Backend-For-Frontend pattern: the token never reaches the browser.\n *\n * RFC 10017 calls this one *\"strongly recommended for business applications, sensitive\n * applications, and applications that handle personal data\"*. The server holds the confidential\n * client and the tokens; the browser holds a cookie it cannot read, and asks the server who it is.\n *\n * So there is no engine on this path — no PKCE, no storage, no renewal — which is why it lives on\n * the root barrel beside the hooks rather than behind a subpath. What it needs from the server is\n * three routes, which `@kanzo-tech/auth/next` provides: a session endpoint, a sign-in and a\n * sign-out.\n */\n\nexport interface BffAuthConfig {\n /** Where the BFF's auth routes are mounted. Default `/api/auth`. */\n readonly basePath?: string;\n /** Injectable for tests. Defaults to the global. */\n readonly fetch?: typeof globalThis.fetch;\n /** Injectable for tests. Defaults to assigning `window.location`. */\n readonly navigate?: (url: string) => void;\n}\n\nfunction readOrganization(value: unknown): Organization | null {\n if (typeof value !== \"object\" || value === null) return null;\n const org = value as Record<string, unknown>;\n const alias = org[\"alias\"];\n if (typeof alias !== \"string\" || alias.length === 0) return null;\n return {\n alias,\n id: typeof org[\"id\"] === \"string\" ? org[\"id\"] : undefined,\n roles: Array.isArray(org[\"roles\"])\n ? org[\"roles\"].filter((r): r is string => typeof r === \"string\")\n : [],\n };\n}\n\n/**\n * The session endpoint's answer, checked rather than cast.\n *\n * It is our own server on the other end, and it is still **data off the wire**: a cast here would\n * make a deploy skew or a proxy's error page arrive as a `Session` whose `user` is undefined, and\n * the failure would surface three components away as a property read on nothing. `null` for\n * anything unrecognisable; `bffAuth` reads a 200 it cannot read as `session/unavailable`, because\n * the endpoint answers \"nobody is signed in\" with a 401 and never with a body.\n */\nexport function readSession(value: unknown): Session | null {\n if (typeof value !== \"object\" || value === null) return null;\n const body = value as Record<string, unknown>;\n\n const user = body[\"user\"];\n if (typeof user !== \"object\" || user === null) return null;\n const id = (user as Record<string, unknown>)[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) return null;\n\n const person = user as Record<string, unknown>;\n const str = (key: string) =>\n typeof person[key] === \"string\" && person[key] !== \"\" ? (person[key] as string) : undefined;\n\n return {\n user: { id, email: str(\"email\"), name: str(\"name\"), username: str(\"username\") },\n roles: Array.isArray(body[\"roles\"])\n ? body[\"roles\"].filter((r): r is string => typeof r === \"string\")\n : [],\n organizations: Array.isArray(body[\"organizations\"])\n ? body[\"organizations\"]\n .map(readOrganization)\n .filter((o): o is Organization => o !== null)\n : [],\n expiresAt: typeof body[\"expiresAt\"] === \"number\" ? body[\"expiresAt\"] : 0,\n };\n}\n\nexport function bffAuth(config: BffAuthConfig = {}): Auth {\n const base = (config.basePath ?? \"/api/auth\").replace(/\\/$/, \"\");\n const doFetch = config.fetch ?? ((...args) => globalThis.fetch(...args));\n const go = config.navigate ?? ((url: string) => void (globalThis.location.href = url));\n\n const listeners = new Set<() => void>();\n const announce = () => {\n for (const listener of listeners) listener();\n };\n\n let cached: Session | null = null;\n let known = false;\n\n const unavailable = (url: string, status: number | undefined, cause?: unknown) =>\n new AuthError(\n \"session/unavailable\",\n status === undefined ? `${url} could not be reached` : `${url} answered ${status}`,\n status === undefined ? {} : { status },\n cause === undefined ? undefined : { cause },\n );\n\n const send = async (url: string, init: RequestInit): Promise<Response> => {\n try {\n return await doFetch(url, init);\n } catch (error) {\n if (init.signal?.aborted) throw init.signal.reason;\n throw unavailable(url, undefined, error);\n }\n };\n\n // Single-flight for the same reason the token path needs it: a page that mounts six components\n // asks six times in one tick, and one answer serves them all. Here it costs a request rather\n // than a revoked token chain, which is a smaller bill for the same mistake.\n //\n // A 401 is the documented answer for \"nobody is signed in\". Anything else that is not a session\n // — a 5xx, a proxy's page, no answer at all — is a failure, and reading it as \"signed out\" would\n // send the person to sign in over an outage that will still be there when they get back.\n const read = singleFlight(() =>\n deadline(\"session/silent\", async (signal): Promise<Session | null> => {\n const response = await send(`${base}/session`, { headers: { Accept: \"application/json\" }, signal });\n if (response.status === 401) return null;\n if (!response.ok) throw unavailable(`${base}/session`, response.status);\n let body: unknown;\n try {\n body = await response.json();\n } catch (error) {\n throw unavailable(`${base}/session`, response.status, error);\n }\n const session = readSession(body);\n if (session === null) throw unavailable(`${base}/session`, response.status);\n return session;\n }),\n );\n\n /**\n * Ask the BFF to spend the refresh token, once for however many requests noticed at the moment.\n *\n * This is the browser end of the renewal, and without it the session cookie's lifetime and the\n * access token's are two different clocks with nothing between them: a cookie good for eight\n * hours in front of a token good for one produces seven hours in which `/session` answers 200,\n * the whole application draws, and every request for data is a 401 that nothing acts on.\n *\n * `POST`, because the route only answers `POST` — it spends something, and a `GET` that spends\n * something is one prefetch away from spending it unasked.\n */\n const renew = singleFlight(() =>\n deadline(\"session/silent\", async (signal): Promise<boolean> => {\n const response = await send(`${base}/refresh`, {\n method: \"POST\",\n headers: { Accept: \"application/json\" },\n signal,\n });\n // A refusal (401, 400) is the end of the session; a 5xx is an outage, not a refusal.\n if (response.status >= 500) throw unavailable(`${base}/refresh`, response.status);\n return response.ok;\n }),\n );\n\n const refresh = async (): Promise<Session | null> => {\n const next = cached;\n cached = await read();\n known = true;\n // Announce only a change, so a poll does not re-render the tree every time.\n if ((next === null) !== (cached === null) || next?.user.id !== cached?.user.id) announce();\n return cached;\n };\n\n return {\n async getSession() {\n if (known) return cached;\n return refresh();\n },\n\n subscribe(onChange) {\n listeners.add(onChange);\n return () => listeners.delete(onChange);\n },\n\n async signIn(options: SignInOptions = {}) {\n const params = new URLSearchParams();\n params.set(\"returnTo\", options.returnTo ?? globalThis.location?.href ?? \"/\");\n if (options.organization !== undefined) params.set(\"organization\", options.organization);\n go(`${base}/signin?${params.toString()}`);\n },\n\n async signOut(options = {}) {\n const params = new URLSearchParams();\n if (options.returnTo !== undefined) params.set(\"returnTo\", options.returnTo);\n const query = params.toString();\n go(query ? `${base}/signout?${query}` : `${base}/signout`);\n },\n\n /**\n * No `Authorization` header — the cookie rides along on a same-origin request by itself — and\n * **one** retry, behind one renewal.\n *\n * A 401 here is ambiguous in a way it is not under `browserAuth`: the cookie was sent and was\n * accepted, so what expired is the access token *behind* the cookie, which this half of the\n * pattern cannot see. So the 401 is taken as \"renew and try again\" first and as \"the session\n * is gone\" only when the renewal is refused — at which point re-reading tells the tree, which\n * is what it did before and all it did before.\n *\n * The retry is once, for the reason `authFetch` gives: twice turns an ended session into a\n * loop against the authorization server. A request whose body cannot be replayed is not\n * retried at all, and `isReplayable` is the same predicate the bearer-token path uses.\n */\n fetch: async (input, init) => {\n const response = await doFetch(input, init);\n if (response.status !== 401 || !known) return response;\n\n if (await renew()) {\n if (!isReplayable(input, init)) return response;\n return doFetch(input, init);\n }\n\n known = false;\n // The caller is owed its own response; a session that cannot be re-read is the provider's to\n // show, and announcing makes it read again and see the failure itself.\n await refresh().catch(announce);\n return response;\n },\n };\n}\n"],"names":["readOrganization","value","org","alias","r","readSession","body","user","id","person","str","key","o","bffAuth","config","base","doFetch","args","go","url","listeners","announce","listener","cached","known","unavailable","status","cause","AuthError","send","init","error","_a","read","singleFlight","deadline","signal","response","session","renew","refresh","next","onChange","options","params","query","input","isReplayable"],"mappings":"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"}
|
package/dist/browser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,mBAAmB,EACzB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EAAa,KAAK,IAAI,EAAoC,MAAM,SAAS,CAAC;AAiCjF,mGAAmG;AACnG,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACpC,cAAc,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,cAAc,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;IAC5D,YAAY,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACzC,eAAe,CAAC,IAAI,CAAC,EAAE;QAAE,wBAAwB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,QAAQ,CAAC,MAAM,EAAE;QACf,aAAa,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;QAC1C,eAAe,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;QAC5C,qBAAqB,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;QAClD,qBAAqB,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;KACnD,CAAC;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,yDAAyD;IACzD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,mBAAmB,KAAK,eAAe,CAAC;CAC7E;AA8CD,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CA6K3D"}
|
package/dist/browser.js
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
|
-
import { WebStorageStateStore as g, InMemoryWebStorage as
|
|
2
|
-
import { authFetch as
|
|
3
|
-
import { claims as
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { WebStorageStateStore as g, InMemoryWebStorage as A, UserManager as I, ErrorResponse as _, ErrorTimeout as R } from "oidc-client-ts";
|
|
2
|
+
import { authFetch as v } from "./auth-fetch.js";
|
|
3
|
+
import { claims as x } from "./claims.js";
|
|
4
|
+
import { DEADLINE as S } from "./deadline.js";
|
|
5
|
+
import { singleFlight as m } from "./single-flight.js";
|
|
6
|
+
import { AuthError as u } from "./types.js";
|
|
7
|
+
const L = "openid profile email organization:*", P = ["code", "state", "session_state", "iss", "error", "error_description"], $ = /* @__PURE__ */ new Set([
|
|
8
|
+
"login_required",
|
|
9
|
+
"interaction_required",
|
|
10
|
+
"consent_required",
|
|
11
|
+
"account_selection_required",
|
|
12
|
+
"invalid_grant"
|
|
13
|
+
]), k = "No matching state found in storage";
|
|
14
|
+
function w(t, r) {
|
|
15
|
+
return t instanceof R ? new u("idp/silent", `${r}: the IdP did not answer`, { after: S }, { cause: t }) : t instanceof _ ? new u("token/exchange-failed", `${r}: the IdP refused (${t.error})`, {}, { cause: t }) : new u("idp/unreachable", `${r}: the IdP could not be reached`, {}, { cause: t });
|
|
9
16
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
17
|
+
function q(t) {
|
|
18
|
+
const r = new URLSearchParams(t);
|
|
19
|
+
return r.has("state") && (r.has("code") || r.has("error"));
|
|
12
20
|
}
|
|
13
|
-
function
|
|
21
|
+
function N(t, r) {
|
|
22
|
+
return r === void 0 ? t : [...t.split(" ").filter((s) => s.length > 0 && !s.startsWith("organization:")), `organization:${r}`].join(" ");
|
|
23
|
+
}
|
|
24
|
+
function B(t) {
|
|
14
25
|
var p;
|
|
15
|
-
const
|
|
26
|
+
const r = ((p = globalThis.location) == null ? void 0 : p.origin) ?? "", a = t.scope ?? L, s = {
|
|
16
27
|
authority: t.issuer,
|
|
17
28
|
client_id: t.clientId,
|
|
18
|
-
redirect_uri: t.redirectUri ??
|
|
19
|
-
post_logout_redirect_uri: t.postLogoutRedirectUri ??
|
|
29
|
+
redirect_uri: t.redirectUri ?? r,
|
|
30
|
+
post_logout_redirect_uri: t.postLogoutRedirectUri ?? r,
|
|
20
31
|
scope: a,
|
|
21
32
|
response_type: "code",
|
|
22
33
|
monitorSession: t.monitorSession ?? !1,
|
|
34
|
+
// Unset, the library waits on discovery, the token endpoint and the JWKS forever; this also
|
|
35
|
+
// becomes the silent renewal's iframe timeout.
|
|
36
|
+
requestTimeoutInSeconds: S / 1e3,
|
|
23
37
|
// **Tokens in memory only.** The library's default is `sessionStorage`, and RFC 10017 is
|
|
24
38
|
// explicit that under a public client anything script can read is something an XSS can steal;
|
|
25
39
|
// a token on disk outlives the page that was compromised to get it.
|
|
@@ -28,59 +42,68 @@ function F(t) {
|
|
|
28
42
|
// session is re-established by a silent renewal or a redirect rather than read off disk. That
|
|
29
43
|
// is one round trip on every load, and a signed-out first paint for any application that draws
|
|
30
44
|
// before `getSession()` settles.
|
|
31
|
-
userStore: new g({ store: new
|
|
45
|
+
userStore: new g({ store: new A() }),
|
|
32
46
|
// The PKCE verifier, `state` and `nonce` are **not** tokens and cannot be in memory: the page
|
|
33
47
|
// is unloaded by the navigation to the IdP, and memory goes with it. `sessionStorage` rather
|
|
34
48
|
// than the library's `localStorage` default — per tab, gone when the tab closes, and never
|
|
35
49
|
// visible to a second tab racing the same sign-in.
|
|
36
50
|
stateStore: new g({ store: globalThis.sessionStorage })
|
|
37
|
-
}, o = (t.createManager ?? ((e) => new
|
|
38
|
-
const
|
|
39
|
-
for (const
|
|
40
|
-
const i = e === void 0 ?
|
|
51
|
+
}, o = (t.createManager ?? ((e) => new I(e)))(s), d = (e) => {
|
|
52
|
+
const n = new URL(globalThis.location.href);
|
|
53
|
+
for (const U of P) n.searchParams.delete(U);
|
|
54
|
+
const i = e === void 0 ? n : new URL(e, n.origin), l = i.origin === n.origin ? i : n;
|
|
41
55
|
globalThis.history.replaceState(null, "", `${l.pathname}${l.search}${l.hash}`);
|
|
42
|
-
},
|
|
43
|
-
if (!
|
|
56
|
+
}, b = async () => {
|
|
57
|
+
if (!q(globalThis.location.search)) return null;
|
|
44
58
|
try {
|
|
45
59
|
const e = await o.signinCallback(globalThis.location.href) ?? null;
|
|
46
|
-
return
|
|
47
|
-
} catch {
|
|
48
|
-
|
|
60
|
+
return d(e == null ? void 0 : e.url_state), e;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if (d(void 0), e instanceof Error && e.message === k) return null;
|
|
63
|
+
throw w(e, "the sign-in could not be completed");
|
|
49
64
|
}
|
|
50
65
|
};
|
|
51
|
-
let
|
|
52
|
-
const
|
|
66
|
+
let h = !1;
|
|
67
|
+
const y = m(async () => {
|
|
68
|
+
if (!h) {
|
|
69
|
+
const e = await b();
|
|
70
|
+
if (h = !0, e !== null) return e;
|
|
71
|
+
}
|
|
72
|
+
return o.getUser();
|
|
73
|
+
}), c = m(async () => {
|
|
53
74
|
try {
|
|
54
75
|
return await o.signinSilent();
|
|
55
|
-
} catch {
|
|
56
|
-
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (e instanceof _ && e.error !== null && $.has(e.error))
|
|
78
|
+
return null;
|
|
79
|
+
throw w(e, "the session could not be renewed");
|
|
57
80
|
}
|
|
58
|
-
}),
|
|
59
|
-
const e = await
|
|
60
|
-
return e === null ||
|
|
61
|
-
},
|
|
62
|
-
const
|
|
63
|
-
return e.expires_at === void 0 ?
|
|
81
|
+
}), E = (e) => e.expires_at !== void 0 && e.expires_at * 1e3 <= Date.now(), f = async () => {
|
|
82
|
+
const e = await y();
|
|
83
|
+
return e === null || E(e) ? c() : e;
|
|
84
|
+
}, T = (e) => {
|
|
85
|
+
const n = x(e.profile, { clientId: t.clientId });
|
|
86
|
+
return e.expires_at === void 0 ? n : { ...n, expiresAt: e.expires_at * 1e3 };
|
|
64
87
|
};
|
|
65
88
|
return {
|
|
66
89
|
async getSession() {
|
|
67
|
-
const e = await
|
|
68
|
-
return e === null ? null :
|
|
90
|
+
const e = await f();
|
|
91
|
+
return e === null ? null : T(e);
|
|
69
92
|
},
|
|
70
93
|
subscribe(e) {
|
|
71
|
-
const
|
|
94
|
+
const n = [
|
|
72
95
|
o.events.addUserLoaded(e),
|
|
73
96
|
o.events.addUserUnloaded(e),
|
|
74
97
|
o.events.addAccessTokenExpired(e),
|
|
75
98
|
o.events.addUserSessionChanged(e)
|
|
76
99
|
];
|
|
77
100
|
return () => {
|
|
78
|
-
for (const i of
|
|
101
|
+
for (const i of n) i();
|
|
79
102
|
};
|
|
80
103
|
},
|
|
81
104
|
async signIn(e = {}) {
|
|
82
105
|
await o.signinRedirect({
|
|
83
|
-
scope:
|
|
106
|
+
scope: N(a, e.organization),
|
|
84
107
|
url_state: e.returnTo ?? globalThis.location.href
|
|
85
108
|
});
|
|
86
109
|
},
|
|
@@ -89,10 +112,10 @@ function F(t) {
|
|
|
89
112
|
post_logout_redirect_uri: e.returnTo ?? t.postLogoutRedirectUri
|
|
90
113
|
});
|
|
91
114
|
},
|
|
92
|
-
fetch:
|
|
115
|
+
fetch: v({
|
|
93
116
|
async current() {
|
|
94
117
|
var e;
|
|
95
|
-
return ((e = await
|
|
118
|
+
return ((e = await f()) == null ? void 0 : e.access_token) ?? null;
|
|
96
119
|
},
|
|
97
120
|
async renew() {
|
|
98
121
|
var e;
|
|
@@ -102,6 +125,6 @@ function F(t) {
|
|
|
102
125
|
};
|
|
103
126
|
}
|
|
104
127
|
export {
|
|
105
|
-
|
|
128
|
+
B as browserAuth
|
|
106
129
|
};
|
|
107
130
|
//# sourceMappingURL=browser.js.map
|
package/dist/browser.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.js","sources":["../src/browser.ts"],"sourcesContent":["import {\n InMemoryWebStorage,\n UserManager,\n WebStorageStateStore,\n type UserManagerSettings,\n} from \"oidc-client-ts\";\nimport { authFetch, type TokenSource } from \"./auth-fetch\";\nimport { claims } from \"./claims\";\nimport { singleFlight } from \"./single-flight\";\nimport type { Auth, Session, SignInOptions } from \"./types\";\n\n/**\n * `./browser` — RFC 10017's *browser-based OAuth 2.0 client*: a public client with PKCE, for the\n * SPA that has no server to hold a confidential one. `agents/viewer` and `hub/browser` are both\n * that shape, and for them it is the only architecture available.\n *\n * **This file is an adapter, not an implementation.** PKCE, silent renewal, storage, the callback\n * and RP-initiated logout are `oidc-client-ts`'s; a `UserManager` goes in and our `Auth` comes out.\n * The reference evaluation picked that library precisely so none of the protocol would be written\n * here, because a hand-written flow is where a mistake stops being a bug and becomes a\n * vulnerability. What is genuinely ours is the four things no library can have: Keycloak's claim\n * vocabulary read into one `Session`, the organization scope, the single-flight renewal, and one\n * `Session` across both deployment patterns.\n *\n * There is no `\"use client\"` here, and that is the rule rather than an oversight: nothing in this\n * module is stateful and no browser global is read at module scope. The listeners are registered\n * inside `subscribe`, and `location` inside the factory.\n */\n\n/**\n * `organization:*`, not `organization`.\n *\n * Plain `organization` returns the single organization when there is one and **prompts for a\n * choice** when there are several — which is the documented behaviour behind the reports of the\n * claim \"disappearing\" for multi-organization users. It never disappeared; the selection was\n * missing. A single-tenant product pays nothing for the star: the claim is simply absent.\n */\nconst DEFAULT_SCOPE = \"openid profile email organization:*\";\n\n/** The query parameters an authorization response puts in the URL, and that must not stay there. */\nconst RESPONSE_PARAMS = [\"code\", \"state\", \"session_state\", \"iss\", \"error\", \"error_description\"];\n\n/** What this adapter reads off the library's `User`. The access token is carried, never opened. */\nexport interface OidcUser {\n readonly profile: unknown;\n readonly access_token: string;\n readonly expires_at?: number;\n readonly url_state?: string;\n}\n\n/**\n * The slice of `UserManager` this file uses.\n *\n * It exists so the manager can be injected, which is what lets everything below be tested with no\n * network, no IdP and no iframe. It is deliberately structural and deliberately small — and the\n * default factory returns a real `UserManager`, so the compiler is what keeps the slice honest.\n */\nexport interface OidcUserManager {\n getUser(): Promise<OidcUser | null>;\n signinRedirect(args?: { scope?: string; url_state?: string }): Promise<void>;\n signinCallback(url?: string): Promise<OidcUser | undefined>;\n signinSilent(): Promise<OidcUser | null>;\n signoutRedirect(args?: { post_logout_redirect_uri?: string }): Promise<void>;\n readonly events: {\n addUserLoaded(cb: () => void): () => void;\n addUserUnloaded(cb: () => void): () => void;\n addAccessTokenExpired(cb: () => void): () => void;\n addUserSessionChanged(cb: () => void): () => void;\n };\n}\n\nexport interface BrowserAuthConfig {\n /** The realm's issuer, e.g. `https://id.kanzo.tech/realms/kanzo`. */\n readonly issuer: string;\n /**\n * This application's Keycloak client id. It selects the credential and, through {@link claims},\n * which of the token's roles are this application's.\n */\n readonly clientId: string;\n /** Must be registered with the IdP. Defaults to the current origin. */\n readonly redirectUri?: string;\n /** Must be registered with the IdP. Defaults to the current origin. */\n readonly postLogoutRedirectUri?: string;\n /** Defaults to `openid profile email organization:*`. */\n readonly scope?: string;\n /**\n * Poll the OP's session state, so a sign-out performed elsewhere reaches this tab.\n *\n * Off by default because it cannot be relied on: it runs a hidden cross-site iframe, and a\n * browser that partitions third-party cookies — which is now most of them — makes it report\n * nothing rather than fail loudly. It is a knob and not a default for exactly that reason.\n */\n readonly monitorSession?: boolean;\n /** The seam the tests inject through. Defaults to the real `UserManager`. */\n readonly createManager?: (settings: UserManagerSettings) => OidcUserManager;\n}\n\n/** `code` or `error`, together with `state`. Nothing else in a URL is an authorization response. */\nfunction isAuthorizationResponse(search: string): boolean {\n const params = new URLSearchParams(search);\n return params.has(\"state\") && (params.has(\"code\") || params.has(\"error\"));\n}\n\nfunction scopeFor(scope: string, organization: string | undefined): string {\n if (organization === undefined) return scope;\n // Replace rather than append: `organization:* organization:acme` asks for both, and the point of\n // naming one is to get one.\n const rest = scope.split(\" \").filter((s) => s.length > 0 && !s.startsWith(\"organization:\"));\n return [...rest, `organization:${organization}`].join(\" \");\n}\n\nexport function browserAuth(config: BrowserAuthConfig): Auth {\n const origin = globalThis.location?.origin ?? \"\";\n const scope = config.scope ?? DEFAULT_SCOPE;\n\n const settings: UserManagerSettings = {\n authority: config.issuer,\n client_id: config.clientId,\n redirect_uri: config.redirectUri ?? origin,\n post_logout_redirect_uri: config.postLogoutRedirectUri ?? origin,\n scope,\n response_type: \"code\",\n monitorSession: config.monitorSession ?? false,\n\n // **Tokens in memory only.** The library's default is `sessionStorage`, and RFC 10017 is\n // explicit that under a public client anything script can read is something an XSS can steal;\n // a token on disk outlives the page that was compromised to get it.\n //\n // The cost is real and is the whole of the trade: a full page load starts with nothing, so the\n // session is re-established by a silent renewal or a redirect rather than read off disk. That\n // is one round trip on every load, and a signed-out first paint for any application that draws\n // before `getSession()` settles.\n userStore: new WebStorageStateStore({ store: new InMemoryWebStorage() }),\n\n // The PKCE verifier, `state` and `nonce` are **not** tokens and cannot be in memory: the page\n // is unloaded by the navigation to the IdP, and memory goes with it. `sessionStorage` rather\n // than the library's `localStorage` default — per tab, gone when the tab closes, and never\n // visible to a second tab racing the same sign-in.\n stateStore: new WebStorageStateStore({ store: globalThis.sessionStorage }),\n };\n\n const manager = (config.createManager ?? ((s) => new UserManager(s)))(settings);\n\n /** Take the spent response out of the URL, and honour `returnTo` if it is ours to honour. */\n const restore = (returnTo: string | undefined): void => {\n const here = new URL(globalThis.location.href);\n for (const key of RESPONSE_PARAMS) here.searchParams.delete(key);\n const wanted = returnTo === undefined ? here : new URL(returnTo, here.origin);\n // `url_state` round-trips through the IdP, so following it anywhere would make our own callback\n // the bounce in an open redirect. Same origin, or we stay where we are.\n const url = wanted.origin === here.origin ? wanted : here;\n globalThis.history.replaceState(null, \"\", `${url.pathname}${url.search}${url.hash}`);\n };\n\n /**\n * Finish a redirect the IdP has just sent back.\n *\n * The URL is cleaned either way. An authorization code is single-use, and one left in the address\n * bar reaches history, bookmarks and the `Referer` — and replays as an error on the next reload.\n */\n const complete = async (): Promise<OidcUser | null> => {\n if (!isAuthorizationResponse(globalThis.location.search)) return null;\n try {\n const user = (await manager.signinCallback(globalThis.location.href)) ?? null;\n restore(user?.url_state);\n return user;\n } catch {\n // A stale state entry or a reloaded callback URL is \"not signed in\", not an exception for\n // every caller of `getSession` to handle.\n restore(undefined);\n return null;\n }\n };\n\n let completing: Promise<OidcUser | null> | undefined;\n\n const load = singleFlight(async (): Promise<OidcUser | null> => {\n completing ??= complete();\n return (await completing) ?? manager.getUser();\n });\n\n /**\n * One renewal at a time, shared by every caller — the most load-bearing line in this file, and\n * the failure `singleFlight` exists for: under the rotation RFC 10017 requires, a second renewal\n * replays a token the first already spent.\n */\n const renew = singleFlight(async (): Promise<OidcUser | null> => {\n try {\n return await manager.signinSilent();\n } catch {\n // A silent renewal fails when the refresh token is gone, rotated out, or the IdP session has\n // ended. All three mean \"no session\", and the answer is the same one `getUser` gives for it.\n return null;\n }\n });\n\n const isExpired = (user: OidcUser): boolean =>\n user.expires_at !== undefined && user.expires_at * 1000 <= Date.now();\n\n /**\n * The user — renewed when the token it carries has expired, and **asked for when there is no\n * user at all**.\n *\n * That second case is the whole of what makes memory-only storage liveable, and it was missing:\n * this returned `null` the moment `load()` did, so every reload signed the person out while the\n * comment on `userStore` promised a silent renewal would pick them back up. Memory-only is a\n * defensible trade against XSS *because* the IdP still holds the session and a `prompt=none`\n * round trip recovers it; without that round trip it is just a session that dies on F5, which no\n * product would accept and which no amount of XSS resistance would buy back.\n *\n * The cost is the one the `userStore` comment already names: an anonymous cold load spends a\n * failed `prompt=none` too. `renew()` answers `null` for it rather than throwing, so the caller\n * sees the same \"not signed in\" it saw before — one round trip later.\n */\n const fresh = async (): Promise<OidcUser | null> => {\n const user = await load();\n if (user === null) return renew();\n return isExpired(user) ? renew() : user;\n };\n\n const toSession = (user: OidcUser): Session => {\n const session = claims(user.profile, { clientId: config.clientId });\n if (user.expires_at === undefined) return session;\n // The access token's expiry comes from the token *response*, not from opening the token —\n // which stays opaque. It is the credential we attach, so it is the one worth counting down.\n return { ...session, expiresAt: user.expires_at * 1000 };\n };\n\n const source: TokenSource = {\n async current() {\n return (await fresh())?.access_token ?? null;\n },\n async renew() {\n return (await renew())?.access_token ?? null;\n },\n };\n\n return {\n async getSession() {\n const user = await fresh();\n return user === null ? null : toSession(user);\n },\n\n subscribe(onChange) {\n const detach = [\n manager.events.addUserLoaded(onChange),\n manager.events.addUserUnloaded(onChange),\n manager.events.addAccessTokenExpired(onChange),\n manager.events.addUserSessionChanged(onChange),\n ];\n return () => {\n for (const off of detach) off();\n };\n },\n\n async signIn(options: SignInOptions = {}) {\n await manager.signinRedirect({\n scope: scopeFor(scope, options.organization),\n url_state: options.returnTo ?? globalThis.location.href,\n });\n },\n\n async signOut(options = {}) {\n // RP-initiated logout through the library, which discovers `end_session_endpoint` and sends\n // the `id_token_hint`. A hand-built URL gets both wrong and neither failure is visible.\n await manager.signoutRedirect({\n post_logout_redirect_uri: options.returnTo ?? config.postLogoutRedirectUri,\n });\n },\n\n fetch: authFetch(source),\n };\n}\n"],"names":["DEFAULT_SCOPE","RESPONSE_PARAMS","isAuthorizationResponse","search","params","scopeFor","scope","organization","s","browserAuth","config","origin","_a","settings","WebStorageStateStore","InMemoryWebStorage","manager","UserManager","restore","returnTo","here","key","wanted","url","complete","user","completing","load","singleFlight","renew","isExpired","fresh","toSession","session","claims","onChange","detach","off","options","authFetch"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"browser.js","sources":["../src/browser.ts"],"sourcesContent":["import {\n ErrorResponse,\n ErrorTimeout,\n InMemoryWebStorage,\n UserManager,\n WebStorageStateStore,\n type UserManagerSettings,\n} from \"oidc-client-ts\";\nimport { authFetch, type TokenSource } from \"./auth-fetch\";\nimport { claims } from \"./claims\";\nimport { DEADLINE } from \"./deadline\";\nimport { singleFlight } from \"./single-flight\";\nimport { AuthError, type Auth, type Session, type SignInOptions } from \"./types\";\n\n/**\n * `./browser` — RFC 10017's *browser-based OAuth 2.0 client*: a public client with PKCE, for the\n * SPA that has no server to hold a confidential one. `agents/viewer` and `hub/browser` are both\n * that shape, and for them it is the only architecture available.\n *\n * **This file is an adapter, not an implementation.** PKCE, silent renewal, storage, the callback\n * and RP-initiated logout are `oidc-client-ts`'s; a `UserManager` goes in and our `Auth` comes out.\n * The reference evaluation picked that library precisely so none of the protocol would be written\n * here, because a hand-written flow is where a mistake stops being a bug and becomes a\n * vulnerability. What is genuinely ours is the four things no library can have: Keycloak's claim\n * vocabulary read into one `Session`, the organization scope, the single-flight renewal, and one\n * `Session` across both deployment patterns.\n *\n * There is no `\"use client\"` here, and that is the rule rather than an oversight: nothing in this\n * module is stateful and no browser global is read at module scope. The listeners are registered\n * inside `subscribe`, and `location` inside the factory.\n */\n\n/**\n * `organization:*`, not `organization`.\n *\n * Plain `organization` returns the single organization when there is one and **prompts for a\n * choice** when there are several — which is the documented behaviour behind the reports of the\n * claim \"disappearing\" for multi-organization users. It never disappeared; the selection was\n * missing. A single-tenant product pays nothing for the star: the claim is simply absent.\n */\nconst DEFAULT_SCOPE = \"openid profile email organization:*\";\n\n/** The query parameters an authorization response puts in the URL, and that must not stay there. */\nconst RESPONSE_PARAMS = [\"code\", \"state\", \"session_state\", \"iss\", \"error\", \"error_description\"];\n\n/** What this adapter reads off the library's `User`. The access token is carried, never opened. */\nexport interface OidcUser {\n readonly profile: unknown;\n readonly access_token: string;\n readonly expires_at?: number;\n readonly url_state?: string;\n}\n\n/**\n * The slice of `UserManager` this file uses.\n *\n * It exists so the manager can be injected, which is what lets everything below be tested with no\n * network, no IdP and no iframe. It is deliberately structural and deliberately small — and the\n * default factory returns a real `UserManager`, so the compiler is what keeps the slice honest.\n */\nexport interface OidcUserManager {\n getUser(): Promise<OidcUser | null>;\n signinRedirect(args?: { scope?: string; url_state?: string }): Promise<void>;\n signinCallback(url?: string): Promise<OidcUser | undefined>;\n signinSilent(): Promise<OidcUser | null>;\n signoutRedirect(args?: { post_logout_redirect_uri?: string }): Promise<void>;\n readonly events: {\n addUserLoaded(cb: () => void): () => void;\n addUserUnloaded(cb: () => void): () => void;\n addAccessTokenExpired(cb: () => void): () => void;\n addUserSessionChanged(cb: () => void): () => void;\n };\n}\n\nexport interface BrowserAuthConfig {\n /** The realm's issuer, e.g. `https://id.kanzo.tech/realms/kanzo`. */\n readonly issuer: string;\n /**\n * This application's Keycloak client id. It selects the credential and, through {@link claims},\n * which of the token's roles are this application's.\n */\n readonly clientId: string;\n /** Must be registered with the IdP. Defaults to the current origin. */\n readonly redirectUri?: string;\n /** Must be registered with the IdP. Defaults to the current origin. */\n readonly postLogoutRedirectUri?: string;\n /** Defaults to `openid profile email organization:*`. */\n readonly scope?: string;\n /**\n * Poll the OP's session state, so a sign-out performed elsewhere reaches this tab.\n *\n * Off by default because it cannot be relied on: it runs a hidden cross-site iframe, and a\n * browser that partitions third-party cookies — which is now most of them — makes it report\n * nothing rather than fail loudly. It is a knob and not a default for exactly that reason.\n */\n readonly monitorSession?: boolean;\n /** The seam the tests inject through. Defaults to the real `UserManager`. */\n readonly createManager?: (settings: UserManagerSettings) => OidcUserManager;\n}\n\n/**\n * The answers to `prompt=none` and to a refresh-token grant that mean \"there is no session here\":\n * the IdP holds none, or the refresh token is gone or rotated out. Every other failure is the IdP\n * refusing the client, not answering, or not being reached, and is thrown.\n */\nconst NO_SESSION = new Set([\n \"login_required\",\n \"interaction_required\",\n \"consent_required\",\n \"account_selection_required\",\n \"invalid_grant\",\n]);\n\n/**\n * `oidc-client-ts`'s own words for a callback whose transaction this tab does not hold — a reloaded\n * callback URL, or a `state` that expired. Pinned against the real library in `browser.test.ts`.\n */\nconst STALE_STATE = \"No matching state found in storage\";\n\n/** A failure from the IdP, coded by what happened: no answer, a refusal, or no reaching it. */\nfunction failure(error: unknown, doing: string): AuthError {\n if (error instanceof ErrorTimeout) {\n return new AuthError(\"idp/silent\", `${doing}: the IdP did not answer`, { after: DEADLINE }, { cause: error });\n }\n if (error instanceof ErrorResponse) {\n return new AuthError(\"token/exchange-failed\", `${doing}: the IdP refused (${error.error})`, {}, { cause: error });\n }\n return new AuthError(\"idp/unreachable\", `${doing}: the IdP could not be reached`, {}, { cause: error });\n}\n\n/** `code` or `error`, together with `state`. Nothing else in a URL is an authorization response. */\nfunction isAuthorizationResponse(search: string): boolean {\n const params = new URLSearchParams(search);\n return params.has(\"state\") && (params.has(\"code\") || params.has(\"error\"));\n}\n\nfunction scopeFor(scope: string, organization: string | undefined): string {\n if (organization === undefined) return scope;\n // Replace rather than append: `organization:* organization:acme` asks for both, and the point of\n // naming one is to get one.\n const rest = scope.split(\" \").filter((s) => s.length > 0 && !s.startsWith(\"organization:\"));\n return [...rest, `organization:${organization}`].join(\" \");\n}\n\nexport function browserAuth(config: BrowserAuthConfig): Auth {\n const origin = globalThis.location?.origin ?? \"\";\n const scope = config.scope ?? DEFAULT_SCOPE;\n\n const settings: UserManagerSettings = {\n authority: config.issuer,\n client_id: config.clientId,\n redirect_uri: config.redirectUri ?? origin,\n post_logout_redirect_uri: config.postLogoutRedirectUri ?? origin,\n scope,\n response_type: \"code\",\n monitorSession: config.monitorSession ?? false,\n // Unset, the library waits on discovery, the token endpoint and the JWKS forever; this also\n // becomes the silent renewal's iframe timeout.\n requestTimeoutInSeconds: DEADLINE / 1000,\n\n // **Tokens in memory only.** The library's default is `sessionStorage`, and RFC 10017 is\n // explicit that under a public client anything script can read is something an XSS can steal;\n // a token on disk outlives the page that was compromised to get it.\n //\n // The cost is real and is the whole of the trade: a full page load starts with nothing, so the\n // session is re-established by a silent renewal or a redirect rather than read off disk. That\n // is one round trip on every load, and a signed-out first paint for any application that draws\n // before `getSession()` settles.\n userStore: new WebStorageStateStore({ store: new InMemoryWebStorage() }),\n\n // The PKCE verifier, `state` and `nonce` are **not** tokens and cannot be in memory: the page\n // is unloaded by the navigation to the IdP, and memory goes with it. `sessionStorage` rather\n // than the library's `localStorage` default — per tab, gone when the tab closes, and never\n // visible to a second tab racing the same sign-in.\n stateStore: new WebStorageStateStore({ store: globalThis.sessionStorage }),\n };\n\n const manager = (config.createManager ?? ((s) => new UserManager(s)))(settings);\n\n /** Take the spent response out of the URL, and honour `returnTo` if it is ours to honour. */\n const restore = (returnTo: string | undefined): void => {\n const here = new URL(globalThis.location.href);\n for (const key of RESPONSE_PARAMS) here.searchParams.delete(key);\n const wanted = returnTo === undefined ? here : new URL(returnTo, here.origin);\n // `url_state` round-trips through the IdP, so following it anywhere would make our own callback\n // the bounce in an open redirect. Same origin, or we stay where we are.\n const url = wanted.origin === here.origin ? wanted : here;\n globalThis.history.replaceState(null, \"\", `${url.pathname}${url.search}${url.hash}`);\n };\n\n /**\n * Finish a redirect the IdP has just sent back.\n *\n * The URL is cleaned either way. An authorization code is single-use, and one left in the address\n * bar reaches history, bookmarks and the `Referer` — and replays as an error on the next reload.\n * Cleaning it is also what makes the next call after a failure find no response to finish.\n */\n const complete = async (): Promise<OidcUser | null> => {\n if (!isAuthorizationResponse(globalThis.location.search)) return null;\n try {\n const user = (await manager.signinCallback(globalThis.location.href)) ?? null;\n restore(user?.url_state);\n return user;\n } catch (error) {\n restore(undefined);\n // A stale state entry or a reloaded callback URL is \"not signed in\", not an exception for\n // every caller of `getSession` to handle. Anything else is the IdP's failure and is thrown.\n if (error instanceof Error && error.message === STALE_STATE) return null;\n throw failure(error, \"the sign-in could not be completed\");\n }\n };\n\n let completed = false;\n\n const load = singleFlight(async (): Promise<OidcUser | null> => {\n if (!completed) {\n const user = await complete();\n completed = true;\n if (user !== null) return user;\n }\n return manager.getUser();\n });\n\n /**\n * One renewal at a time, shared by every caller — the most load-bearing line in this file, and\n * the failure `singleFlight` exists for: under the rotation RFC 10017 requires, a second renewal\n * replays a token the first already spent.\n */\n const renew = singleFlight(async (): Promise<OidcUser | null> => {\n try {\n return await manager.signinSilent();\n } catch (error) {\n // The refresh token gone or rotated out, or the IdP session ended, all mean \"no session\", and\n // the answer is the same one `getUser` gives for it.\n if (error instanceof ErrorResponse && error.error !== null && NO_SESSION.has(error.error)) {\n return null;\n }\n throw failure(error, \"the session could not be renewed\");\n }\n });\n\n const isExpired = (user: OidcUser): boolean =>\n user.expires_at !== undefined && user.expires_at * 1000 <= Date.now();\n\n /**\n * The user — renewed when the token it carries has expired, and **asked for when there is no\n * user at all**.\n *\n * That second case is the whole of what makes memory-only storage liveable, and it was missing:\n * this returned `null` the moment `load()` did, so every reload signed the person out while the\n * comment on `userStore` promised a silent renewal would pick them back up. Memory-only is a\n * defensible trade against XSS *because* the IdP still holds the session and a `prompt=none`\n * round trip recovers it; without that round trip it is just a session that dies on F5, which no\n * product would accept and which no amount of XSS resistance would buy back.\n *\n * The cost is the one the `userStore` comment already names: an anonymous cold load spends a\n * failed `prompt=none` too. `renew()` answers `null` for the IdP's `login_required` rather than\n * throwing, so the caller sees the same \"not signed in\" it saw before — one round trip later.\n */\n const fresh = async (): Promise<OidcUser | null> => {\n const user = await load();\n if (user === null) return renew();\n return isExpired(user) ? renew() : user;\n };\n\n const toSession = (user: OidcUser): Session => {\n const session = claims(user.profile, { clientId: config.clientId });\n if (user.expires_at === undefined) return session;\n // The access token's expiry comes from the token *response*, not from opening the token —\n // which stays opaque. It is the credential we attach, so it is the one worth counting down.\n return { ...session, expiresAt: user.expires_at * 1000 };\n };\n\n const source: TokenSource = {\n async current() {\n return (await fresh())?.access_token ?? null;\n },\n async renew() {\n return (await renew())?.access_token ?? null;\n },\n };\n\n return {\n async getSession() {\n const user = await fresh();\n return user === null ? null : toSession(user);\n },\n\n subscribe(onChange) {\n const detach = [\n manager.events.addUserLoaded(onChange),\n manager.events.addUserUnloaded(onChange),\n manager.events.addAccessTokenExpired(onChange),\n manager.events.addUserSessionChanged(onChange),\n ];\n return () => {\n for (const off of detach) off();\n };\n },\n\n async signIn(options: SignInOptions = {}) {\n await manager.signinRedirect({\n scope: scopeFor(scope, options.organization),\n url_state: options.returnTo ?? globalThis.location.href,\n });\n },\n\n async signOut(options = {}) {\n // RP-initiated logout through the library, which discovers `end_session_endpoint` and sends\n // the `id_token_hint`. A hand-built URL gets both wrong and neither failure is visible.\n await manager.signoutRedirect({\n post_logout_redirect_uri: options.returnTo ?? config.postLogoutRedirectUri,\n });\n },\n\n fetch: authFetch(source),\n };\n}\n"],"names":["DEFAULT_SCOPE","RESPONSE_PARAMS","NO_SESSION","STALE_STATE","failure","error","doing","ErrorTimeout","AuthError","DEADLINE","ErrorResponse","isAuthorizationResponse","search","params","scopeFor","scope","organization","browserAuth","config","origin","_a","settings","WebStorageStateStore","InMemoryWebStorage","manager","s","UserManager","restore","returnTo","here","key","wanted","url","complete","user","completed","load","singleFlight","renew","isExpired","fresh","toSession","session","claims","onChange","detach","off","options","authFetch"],"mappings":"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"}
|