@plitzi/sdk-auth 0.32.25 → 0.33.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.
@@ -1,90 +1,280 @@
1
+ import { readSessionHint as e } from "./helpers/sessionHint.mjs";
2
+ import t from "./helpers/SessionStore.mjs";
3
+ import { nowInSeconds as n, toSeconds as r, tokenExpiresAt as i } from "./helpers/tokenClaims.mjs";
4
+ import { onAuthFailure as a, sameRegistrableDomain as o } from "@plitzi/sdk-shared/auth";
1
5
  //#region src/AuthProvider.ts
2
- var e = class {
3
- cache;
4
- enableRefresh = !1;
5
- tokenStorage = "localStorage";
6
+ var s = 60, c = 300, l = 2147483647, u = class {
6
7
  state = "init";
8
+ session = { validatedAt: 0 };
9
+ store;
10
+ gate;
11
+ hintCookie;
12
+ revalidateSeconds;
7
13
  listeners = /* @__PURE__ */ new Set();
8
- constructor({ enableRefresh: e = !1, tokenStorage: t = "localStorage" } = {}) {
9
- this.enableRefresh = e, this.tokenStorage = t;
10
- try {
11
- let e = null;
12
- t === "localStorage" ? e = localStorage.getItem("user_state") : t === "sessionStorage" && (e = sessionStorage.getItem("user_state")), e && this.setCache("token", JSON.parse(e));
13
- } catch {}
14
- return new Proxy(this, { get: (e, n, r) => {
15
- if (n in e) return Reflect.get(e, n, r);
16
- if (!(e.cache && n in e.cache && n === "token" && t === "") && e.cache && n in e.cache) return e.cache[n];
17
- } });
14
+ renewal;
15
+ renewalTimer;
16
+ detachListeners;
17
+ offline = !1;
18
+ spaceKey;
19
+ constructor({ spaceKey: e = "", tokenStorage: n = "localStorage", sessionHintCookie: r, sessionGate: i = "optimistic", sessionRevalidateSeconds: a = c, storageKey: o = "plitzi_auth_session" } = {}) {
20
+ this.spaceKey = e, this.store = new t(n, o), this.gate = i, this.hintCookie = r, this.revalidateSeconds = a;
18
21
  }
19
- internalRefresh(e) {
20
- e || (this.setState("guest"), this.setCache(void 0, void 0)), this.setCache("token", e);
22
+ get endpoints() {
23
+ return [];
21
24
  }
22
- internalGetUser(e) {
23
- this.setCache("user", e), e ? this.setState("authenticated") : (this.setState("guest"), this.isExpired() && this.clearCache());
25
+ requestExchange() {
26
+ return Promise.resolve(void 0);
24
27
  }
25
- internalLogin(e, t) {
26
- this.setCache("user", e), this.setCache("token", t), e ? this.setState("authenticated") : this.setState("guest");
28
+ consumeRedirect() {
29
+ return Promise.resolve(void 0);
27
30
  }
28
- internalLogout() {
29
- this.clearCache(), this.setState("guest"), this.emit({ type: "logout" });
31
+ get user() {
32
+ return this.session.user;
30
33
  }
31
- isExpired() {
32
- return !this.cache || !this.cache.token || !this.cache.token.expiresAt || Date.now() >= this.cache.token.expiresAt;
34
+ get token() {
35
+ return this.session.token;
33
36
  }
34
- setCache(e, t) {
35
- if (!e) {
36
- this.cache = t, typeof localStorage < "u" && this.tokenStorage === "localStorage" && this.cache.token ? localStorage.setItem("user_state", JSON.stringify(this.cache.token)) : typeof sessionStorage < "u" && this.tokenStorage === "sessionStorage" && this.cache.token && sessionStorage.setItem("user_state", JSON.stringify(this.cache.token));
37
+ getState() {
38
+ return this.state;
39
+ }
40
+ can(e) {
41
+ let t = this.session.user?.permissions;
42
+ return Array.isArray(t) && t.includes(e);
43
+ }
44
+ peekState() {
45
+ return this.peek().state;
46
+ }
47
+ peek() {
48
+ let t = this.store.read(), a = e(this.hintCookie);
49
+ if (this.hintCookie && !a) return { state: "guest" };
50
+ let o = t?.token, c = r(o?.expiresAt) ?? i(o?.accessToken);
51
+ return o?.accessToken && (c === void 0 || c - s > n()) && t?.user ? {
52
+ state: "authenticated",
53
+ user: t.user,
54
+ token: t.token
55
+ } : o?.accessToken || o?.refreshToken || a ? {
56
+ state: "init",
57
+ user: t?.user,
58
+ token: t?.token
59
+ } : { state: "guest" };
60
+ }
61
+ async init(e = {}) {
62
+ this.attach(), await this.decide(e), this.settle();
63
+ }
64
+ settle() {
65
+ (this.state === "init" || this.state === "initLoading") && this.setState(this.session.user || this.hasLiveToken() ? "authenticated" : "guest", "settled");
66
+ }
67
+ async decide(t) {
68
+ if (t.skipAuth) {
69
+ this.setState("guest", "skip-auth");
37
70
  return;
38
71
  }
39
- this.cache ||= {}, this.cache[e] = t, this.cache.token && (typeof localStorage < "u" && this.tokenStorage === "localStorage" ? localStorage.setItem("user_state", JSON.stringify(this.cache.token)) : typeof sessionStorage < "u" && this.tokenStorage === "sessionStorage" && sessionStorage.setItem("user_state", JSON.stringify(this.cache.token)));
72
+ let n = await this.consumeRedirect();
73
+ if (n) {
74
+ n.ok ? (this.adopt(n), await this.handOffToServer(), this.emit({
75
+ type: "login",
76
+ token: this.session.token
77
+ })) : this.endSession(n.reason);
78
+ return;
79
+ }
80
+ if (t.user) {
81
+ this.adopt({
82
+ ok: !0,
83
+ user: t.user,
84
+ token: t.accessToken ? {
85
+ accessToken: t.accessToken,
86
+ expiresAt: t.expiresAt ?? null,
87
+ refreshToken: null
88
+ } : this.session.token
89
+ }, !0, "bootstrap");
90
+ return;
91
+ }
92
+ let r = this.store.read(), i = e(this.hintCookie);
93
+ if (this.session = {
94
+ user: r?.user,
95
+ token: r?.token,
96
+ validatedAt: r?.validatedAt ?? 0
97
+ }, this.hintCookie && !i) {
98
+ this.endSession(void 0, "hint-missing");
99
+ return;
100
+ }
101
+ if (this.hasLiveToken() && this.session.user) {
102
+ this.setState("authenticated", "restored"), this.scheduleRenewal();
103
+ let e = this.isFresh() && this.gate !== "strict" ? void 0 : this.revalidate(!0);
104
+ this.gate === "strict" && await e;
105
+ return;
106
+ }
107
+ if (!this.hasEvidence()) {
108
+ this.endSession(void 0, "no-evidence");
109
+ return;
110
+ }
111
+ if (this.setState("initLoading", "no-evidence"), this.hasLiveToken() && this.capabilities.identity) {
112
+ await this.loadIdentity();
113
+ return;
114
+ }
115
+ if (this.capabilities.renew) {
116
+ await this.renew();
117
+ return;
118
+ }
119
+ if (this.capabilities.identity) {
120
+ await this.loadIdentity();
121
+ return;
122
+ }
123
+ this.endSession();
124
+ }
125
+ dispose() {
126
+ clearTimeout(this.renewalTimer), this.renewalTimer = void 0, this.detachListeners?.(), this.detachListeners = void 0;
127
+ }
128
+ async login(e) {
129
+ this.setState("authenticating", "authenticating");
130
+ let t = await this.requestLogin(e);
131
+ if (!t.ok) {
132
+ this.endSession(t.reason);
133
+ return;
134
+ }
135
+ if (this.adopt(t), await this.handOffToServer(), this.state !== "guest") return !this.session.user && this.capabilities.identity && await this.loadIdentity(), this.emit({
136
+ type: "login",
137
+ token: this.session.token
138
+ }), this.session.token;
139
+ }
140
+ async handOffToServer() {
141
+ let e = await this.requestExchange();
142
+ if (e) {
143
+ if (e.ok) {
144
+ this.adopt(e);
145
+ return;
146
+ }
147
+ if (e.reason === "network") {
148
+ this.offline = !0;
149
+ return;
150
+ }
151
+ this.endSession(e.reason);
152
+ }
153
+ }
154
+ async refresh() {
155
+ return await this.renew(), this.session.token;
40
156
  }
41
- clearCache(e) {
42
- if (e && this.cache) {
43
- delete this.cache[e], e === "token" && (typeof localStorage < "u" && this.tokenStorage === "localStorage" ? localStorage.removeItem("user_state") : typeof sessionStorage < "u" && this.tokenStorage === "sessionStorage" && sessionStorage.removeItem("user_state"));
157
+ async revalidate(e = !1) {
158
+ return this.hasEvidence() ? !e && this.isFresh() && this.hasLiveToken() ? this.state === "authenticated" : this.hasLiveToken() && this.capabilities.identity ? this.loadIdentity() : this.capabilities.renew ? this.renew() : this.capabilities.identity ? this.loadIdentity() : (this.hasLiveToken() || this.endSession("expired"), this.state === "authenticated") : (this.state !== "guest" && this.endSession(), !1);
159
+ }
160
+ invalidate(e = "expired") {
161
+ if (e === "network") {
162
+ this.offline = !0;
163
+ return;
164
+ }
165
+ if (e === "expired" && this.capabilities.renew) {
166
+ this.renew();
44
167
  return;
45
168
  }
46
- typeof localStorage < "u" && this.tokenStorage === "localStorage" ? localStorage.removeItem("user_state") : typeof sessionStorage < "u" && this.tokenStorage === "sessionStorage" && sessionStorage.removeItem("user_state"), this.cache = void 0;
169
+ this.endSession(e);
170
+ }
171
+ async logout() {
172
+ await this.requestLogout(), this.endSession(), this.emit({ type: "logout" });
47
173
  }
48
174
  on(e) {
49
- return Array.isArray(e) ? (e.forEach((e) => this.listeners.add(e)), () => e.forEach((e) => this.listeners.delete(e))) : (this.listeners.add(e), () => this.listeners.delete(e));
175
+ let t = Array.isArray(e) ? e : [e];
176
+ return t.forEach((e) => this.listeners.add(e)), () => t.forEach((e) => this.listeners.delete(e));
50
177
  }
51
178
  emit(e) {
52
179
  for (let t of this.listeners) t(e);
53
180
  }
54
- setState(e) {
181
+ setState(e, t) {
55
182
  this.state !== e && (this.state = e, this.emit({
56
183
  type: "state",
57
- state: e
184
+ state: e,
185
+ reason: t
58
186
  }));
59
187
  }
60
- getState() {
61
- return this.state;
188
+ async renew() {
189
+ return this.renewal ??= this.performRenewal().finally(() => {
190
+ this.renewal = void 0;
191
+ }), this.renewal;
62
192
  }
63
- async request(e, t) {
64
- let n;
65
- try {
66
- n = await fetch(e, t);
67
- } catch {
68
- return Promise.resolve({
69
- data: void 0,
70
- status: 500
71
- });
72
- }
73
- if (!n.ok) return Promise.resolve({
74
- data: void 0,
75
- status: n.status
193
+ async performRenewal() {
194
+ let e = await this.requestRenewal(this.session.token?.refreshToken ?? void 0);
195
+ return e.ok ? (this.adopt(e), !this.session.user && this.capabilities.identity ? this.loadIdentity() : this.state === "authenticated") : (this.handleFailure(e.reason), !1);
196
+ }
197
+ async loadIdentity() {
198
+ let e = await this.requestIdentity();
199
+ return e.ok ? (this.adopt(e), this.state === "authenticated") : e.reason === "expired" && this.capabilities.renew && !this.renewal ? this.renew() : (this.handleFailure(e.reason), !1);
200
+ }
201
+ handleFailure(e) {
202
+ if (e === "network") {
203
+ this.offline = !0;
204
+ return;
205
+ }
206
+ this.endSession(e);
207
+ }
208
+ adopt(e, t = !0, r = "granted") {
209
+ this.session = {
210
+ user: e.user ?? this.session.user,
211
+ token: e.token ?? this.session.token,
212
+ validatedAt: n()
213
+ }, this.offline = !1, t && this.store.write(this.session), this.scheduleRenewal(), this.setState(this.session.user || this.hasLiveToken() ? "authenticated" : "guest", r);
214
+ }
215
+ endSession(e, t = "signed-out") {
216
+ clearTimeout(this.renewalTimer), this.renewalTimer = void 0, this.session = { validatedAt: n() }, this.store.write(this.session), this.setState("guest", t), e && this.emit({
217
+ type: "expired",
218
+ reason: e
76
219
  });
77
- let r;
78
- try {
79
- r = await n.json();
80
- } catch {
81
- r = void 0;
82
- }
83
- return {
84
- data: r,
85
- status: n.status
220
+ }
221
+ expiresAt() {
222
+ return r(this.session.token?.expiresAt) ?? i(this.session.token?.accessToken);
223
+ }
224
+ hasLiveToken() {
225
+ if (!this.session.token?.accessToken) return !1;
226
+ let e = this.expiresAt();
227
+ return e === void 0 || e - s > n();
228
+ }
229
+ isFresh() {
230
+ return n() - this.session.validatedAt < this.revalidateSeconds;
231
+ }
232
+ hasEvidence() {
233
+ return this.session.token?.accessToken || this.session.token?.refreshToken ? !0 : !!e(this.hintCookie);
234
+ }
235
+ scheduleRenewal() {
236
+ clearTimeout(this.renewalTimer), this.renewalTimer = void 0;
237
+ let e = this.expiresAt();
238
+ if (e === void 0 || typeof window > "u" || this.state === "guest") return;
239
+ let t = Math.min((e - s - n()) * 1e3, l);
240
+ if (t <= 0) {
241
+ this.onRenewalDue();
242
+ return;
243
+ }
244
+ this.renewalTimer = setTimeout(() => this.onRenewalDue(), t);
245
+ }
246
+ onRenewalDue() {
247
+ if (this.capabilities.renew) {
248
+ this.renew();
249
+ return;
250
+ }
251
+ this.endSession("expired");
252
+ }
253
+ attach() {
254
+ if (typeof window > "u" || this.detachListeners) return;
255
+ let e = () => {
256
+ document.visibilityState === "visible" && this.revalidate();
257
+ }, t = () => {
258
+ this.offline && this.revalidate(!0);
259
+ }, n = this.store.subscribe((e) => this.adoptFromStorage(e)), r = a(({ reason: e, url: t }) => {
260
+ let n = this.endpoints.filter(Boolean);
261
+ (n.length === 0 || n.some((e) => o(e, t))) && this.invalidate(e);
262
+ });
263
+ document.addEventListener("visibilitychange", e), window.addEventListener("online", t), this.detachListeners = () => {
264
+ document.removeEventListener("visibilitychange", e), window.removeEventListener("online", t), n(), r();
86
265
  };
87
266
  }
267
+ adoptFromStorage(e) {
268
+ if (!e?.token && !e?.user) {
269
+ clearTimeout(this.renewalTimer), this.renewalTimer = void 0, this.session = { validatedAt: n() }, this.setState("guest", "another-tab"), this.emit({ type: "logout" });
270
+ return;
271
+ }
272
+ this.adopt({
273
+ ok: !0,
274
+ user: e.user,
275
+ token: e.token
276
+ }, !1);
277
+ }
88
278
  };
89
279
  //#endregion
90
- export { e as default };
280
+ export { u as default };
@@ -0,0 +1,34 @@
1
+ import { Schema, TokenResult } from '@plitzi/sdk-shared';
2
+ export type StorageKind = Exclude<Schema['settings']['tokenStorage'], undefined>;
3
+ export type StoredSession<U> = {
4
+ version: number;
5
+ token?: TokenResult;
6
+ user?: U;
7
+ /**
8
+ * Unix seconds of the last time the backend answered about this session — including the answer that nobody is
9
+ * signed in. Storing the negative answer is what stops a signed-out visitor from paying for a request on every
10
+ * single page load just to be told the same thing again.
11
+ */
12
+ validatedAt: number;
13
+ };
14
+ /**
15
+ * Where a session survives a reload. It holds the user next to the token on purpose: a token alone says a session is
16
+ * live but not whose it is, so a page that stored only the token had to ask the backend who it was talking to before
17
+ * it could render anything — which is the reason boot used to cost a round trip.
18
+ */
19
+ declare class SessionStore<U> {
20
+ private readonly kind;
21
+ private readonly key;
22
+ constructor(kind: StorageKind, key: string);
23
+ private get area();
24
+ read(): StoredSession<U> | undefined;
25
+ write(session: Omit<StoredSession<U>, 'version'>): void;
26
+ clear(): void;
27
+ /**
28
+ * Fires when another tab writes the session. Signing out in one tab has to end it in all of them, and a renewal in
29
+ * one has to be adopted by the rest — two tabs that each renew on their own would rotate the refresh token out from
30
+ * under each other, and whichever lost the race would sign its user out for no reason.
31
+ */
32
+ subscribe(listener: (session?: StoredSession<U>) => void): () => void;
33
+ }
34
+ export default SessionStore;
@@ -0,0 +1,45 @@
1
+ //#region src/helpers/SessionStore.ts
2
+ var e = (e) => typeof e == "object" && !!e && e.version === 1, t = class {
3
+ kind;
4
+ key;
5
+ constructor(e, t) {
6
+ this.kind = e, this.key = t;
7
+ }
8
+ get area() {
9
+ try {
10
+ if (this.kind === "localStorage" && typeof localStorage < "u") return localStorage;
11
+ if (this.kind === "sessionStorage" && typeof sessionStorage < "u") return sessionStorage;
12
+ } catch {}
13
+ }
14
+ read() {
15
+ let t = this.area?.getItem(this.key);
16
+ if (t) try {
17
+ let n = JSON.parse(t);
18
+ return e(n) ? n : void 0;
19
+ } catch {
20
+ return;
21
+ }
22
+ }
23
+ write(e) {
24
+ try {
25
+ this.area?.setItem(this.key, JSON.stringify({
26
+ version: 1,
27
+ ...e
28
+ }));
29
+ } catch {}
30
+ }
31
+ clear() {
32
+ try {
33
+ this.area?.removeItem(this.key);
34
+ } catch {}
35
+ }
36
+ subscribe(e) {
37
+ if (this.kind !== "localStorage" || typeof window > "u") return () => void 0;
38
+ let t = (t) => {
39
+ (t.key === null || t.key === this.key) && e(this.read());
40
+ };
41
+ return window.addEventListener("storage", t), () => window.removeEventListener("storage", t);
42
+ }
43
+ };
44
+ //#endregion
45
+ export { t as default };
@@ -0,0 +1,15 @@
1
+ export type SessionHint = {
2
+ /** Unix seconds the session's access credential dies at. */
3
+ expiresAt: number;
4
+ /** Unix seconds renewal stops being possible at, when the backend states it. */
5
+ refreshExpiresAt?: number;
6
+ };
7
+ /**
8
+ * Reads the session hint your backend publishes beside its httpOnly session cookie (see `sessionHintCookie` in the
9
+ * space settings). It answers, with no request, the one question local storage cannot: whether this browser holds a
10
+ * session that some other page — a sibling app on the same domain, a social sign-in redirect — established.
11
+ *
12
+ * Its absence is an answer too, and the more valuable one: a configured hint that is not there means nobody is signed
13
+ * in, which is how a signed-out visitor gets a page rendered without a round trip to be told so.
14
+ */
15
+ export declare const readSessionHint: (cookieName?: string) => SessionHint | undefined;
@@ -0,0 +1,17 @@
1
+ //#region src/helpers/sessionHint.ts
2
+ var e = (e) => {
3
+ let [t, n] = e.split("."), r = Number(t);
4
+ if (!Number.isFinite(r) || r <= 0) return;
5
+ let i = Number(n);
6
+ return {
7
+ expiresAt: r,
8
+ refreshExpiresAt: Number.isFinite(i) && i > 0 ? i : void 0
9
+ };
10
+ }, t = (t) => {
11
+ if (!(!t || typeof document > "u")) for (let n of document.cookie.split(";")) {
12
+ let r = n.indexOf("=");
13
+ if (r !== -1 && n.slice(0, r).trim() === t) return e(decodeURIComponent(n.slice(r + 1).trim()));
14
+ }
15
+ };
16
+ //#endregion
17
+ export { t as readSessionHint };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The `exp` claim of a JWT, in unix seconds, or undefined for a token that is opaque or unreadable.
3
+ *
4
+ * Decoded, never verified: a browser holds no signing secret, and a signature it cannot check tells it nothing. This
5
+ * is only ever used to skip asking a question the token already answers — "has this lapsed?" — and never to grant
6
+ * anything. A forged `exp` buys an attacker one refused request from the server, which is the only opinion that counts.
7
+ */
8
+ export declare const tokenExpiresAt: (token?: string | null) => number | undefined;
9
+ /** Unix seconds, the unit every expiry in this package is stated in — `Date.now()` is the odd one out. */
10
+ export declare const nowInSeconds: () => number;
11
+ export declare const toSeconds: (expiry?: number | null) => number | undefined;
@@ -0,0 +1,19 @@
1
+ //#region src/helpers/tokenClaims.ts
2
+ var e = (e) => {
3
+ try {
4
+ let t = e.replaceAll("-", "+").replaceAll("_", "/"), n = t.padEnd(t.length + (4 - t.length % 4) % 4, "="), r = JSON.parse(atob(n));
5
+ return typeof r == "object" && r ? r : void 0;
6
+ } catch {
7
+ return;
8
+ }
9
+ }, t = (t) => {
10
+ if (!t) return;
11
+ let n = t.split(".");
12
+ if (n.length !== 3) return;
13
+ let r = e(n[1])?.exp;
14
+ return typeof r == "number" && Number.isFinite(r) ? r : void 0;
15
+ }, n = () => Math.floor(Date.now() / 1e3), r = 1e11, i = (e) => {
16
+ if (!(typeof e != "number" || !Number.isFinite(e) || e <= 0)) return Math.floor(e > r ? e / 1e3 : e);
17
+ };
18
+ //#endregion
19
+ export { n as nowInSeconds, i as toSeconds, t as tokenExpiresAt };
@@ -1,19 +1,20 @@
1
1
  import { AuthManager } from '../../AuthManager';
2
- import { Schema, Server } from '@plitzi/sdk-shared';
2
+ import { AuthProviderSettings } from '../../types';
3
+ import { AuthState, Server } from '@plitzi/sdk-shared';
3
4
  export type UseAuthProps = {
4
5
  server?: Server;
5
6
  isHydrating?: boolean;
6
- tokenStorage?: Schema['settings']['tokenStorage'];
7
- provider?: Schema['settings']['userProvider'];
8
- loginUrl?: Schema['settings']['loginUrl'];
9
- userUrl?: Schema['settings']['userUrl'];
10
- refreshUrl?: Schema['settings']['refreshUrl'];
11
- logoutUrl?: Schema['settings']['logoutUrl'];
12
- detailsPath?: Schema['settings']['detailsPath'];
13
- tokenPath?: Schema['settings']['tokenPath'];
14
- expirationTimePath?: Schema['settings']['expirationTimePath'];
7
+ provider?: string;
8
+ /** Held stable by the caller: a new object here builds a new provider and re-runs the whole boot decision. */
9
+ settings: AuthProviderSettings;
15
10
  };
16
- declare const useAuth: ({ server, isHydrating, tokenStorage, provider, loginUrl, userUrl, refreshUrl, logoutUrl, detailsPath, tokenPath, expirationTimePath }: UseAuthProps) => {
11
+ /**
12
+ * `loading` is what a space's pages are held back by, so it is deliberately narrow: it means "this page cannot be
13
+ * rendered yet", not "auth is doing something". A session restored from storage renders immediately and confirms
14
+ * itself behind the render, which is the difference between a page that appears at once and one that waits on a
15
+ * round trip to be told what it already knew.
16
+ */
17
+ declare const useAuth: ({ server, isHydrating, provider, settings }: UseAuthProps) => {
17
18
  manager: AuthManager<{
18
19
  id: number;
19
20
  username: string;
@@ -21,8 +22,35 @@ declare const useAuth: ({ server, isHydrating, tokenStorage, provider, loginUrl,
21
22
  verified: boolean;
22
23
  permissions: string[];
23
24
  roles: string[];
24
- }, "basic">;
25
+ }>;
26
+ state: AuthState;
27
+ /** Who the server rendered this page for, so a consumer can stand on it until the browser has decided. */
28
+ bootstrapUser: import('@plitzi/sdk-shared').User | undefined;
29
+ bootstrapToken: string | undefined;
30
+ /**
31
+ * Who this browser can be seen to hold a session for, read synchronously before `init()` runs.
32
+ *
33
+ * Published for the same reason `bootstrapUser` is: the provider fills in from an effect, so on a
34
+ * client-rendered page the auth data source was EMPTY for the first commit — `{{user.*}}` rendered blank and
35
+ * filled in a moment later, on every reload of every signed-in visitor.
36
+ */
37
+ peekedUser: {
38
+ id: number;
39
+ username: string;
40
+ email: string;
41
+ verified: boolean;
42
+ permissions: string[];
43
+ roles: string[];
44
+ } | undefined;
45
+ peekedToken: string | undefined;
25
46
  loading: boolean;
47
+ /**
48
+ * Before the browser has decided, the server's answer stands. `state` only becomes `authenticated` from
49
+ * `init()`, which runs in an effect — and effects do not run during a server render, so this read was false on
50
+ * the server for a visitor the server had just resolved. Page-level access rules are evaluated against it, so
51
+ * an `accessLevel: 'authenticated'` page never won on the server, and the browser then picked a different page
52
+ * than the HTML it was hydrating.
53
+ */
26
54
  authenticated: boolean;
27
55
  };
28
56
  export default useAuth;
@@ -1,58 +1,45 @@
1
1
  import { AuthManager as e } from "../../AuthManager.mjs";
2
2
  import { useCallback as t, useEffect as n, useMemo as r, useState as i } from "react";
3
3
  //#region src/hooks/useAuth/useAuth.ts
4
- var a = ({ server: a, isHydrating: o = !1, tokenStorage: s = "localStorage", provider: c = "", loginUrl: l = "", userUrl: u = "", refreshUrl: d = "", logoutUrl: f = "", detailsPath: p = "details", tokenPath: m = "access_token", expirationTimePath: h = "expire_at" }) => {
5
- let g = typeof window > "u" || o || !!a?.user && a.authenticated, [_, v] = i(!!u && !g), [y, b] = i(!1), x = t((e) => {
6
- e.type === "state" && (v(e.state === "initLoading"), b(e.state === "authenticated"));
7
- }, []), S = r(() => {
8
- let t = {};
9
- c === "basic" && (t = {
10
- tokenStorage: s,
11
- loginUrl: l,
12
- userUrl: u,
13
- refreshUrl: d,
14
- logoutUrl: f,
15
- detailsPath: p,
16
- tokenPath: m,
17
- expirationTimePath: h,
18
- isSSR: g
19
- });
20
- let n = new e(c, x, t);
21
- return n.init(a?.authenticated ? a.user?.details : void 0, a?.skipAuth), n;
22
- }, [
23
- c,
24
- x,
25
- a?.authenticated,
26
- a?.user?.details,
27
- a?.skipAuth,
4
+ var a = ({ server: a, isHydrating: o = !1, provider: s = "", settings: c }) => {
5
+ let l = a?.authenticated ? a.user?.details : void 0, u = typeof a?.user?.accessToken == "string" ? a.user.accessToken : void 0, [d, f] = i("init"), p = t((e) => {
6
+ e.type === "state" && f(e.state);
7
+ }, []), m = r(() => new e(s, p, c), [
28
8
  s,
29
- l,
30
- u,
31
- d,
32
- f,
33
9
  p,
10
+ c
11
+ ]), h = r(() => o || l ? void 0 : m.peek(), [
34
12
  m,
35
- h,
36
- g
37
- ]);
38
- return n(() => {
39
- let e = S.getProvider();
40
- if (!e || !e.token?.expiresAt) return;
41
- let t = Math.floor(Date.now() / 1e3);
42
- if (e.token.expiresAt) {
43
- let n = setTimeout(() => {
44
- S.logout();
45
- }, Math.abs(t - e.token.expiresAt) * 1e3);
46
- return () => clearTimeout(n);
47
- }
48
- }, [S, y]), r(() => ({
49
- manager: S,
50
- loading: _,
51
- authenticated: y
13
+ o,
14
+ l
15
+ ]), g = d !== "init" && d !== "initLoading" || !!l || o || h?.state === "authenticated" || h?.state === "guest";
16
+ return n(() => (m.init({
17
+ user: l,
18
+ accessToken: u,
19
+ expiresAt: a?.user?.expiresAt,
20
+ skipAuth: a?.skipAuth
21
+ }), () => m.dispose()), [
22
+ m,
23
+ l,
24
+ u,
25
+ a?.user?.expiresAt,
26
+ a?.skipAuth
27
+ ]), r(() => ({
28
+ manager: m,
29
+ state: d,
30
+ bootstrapUser: l,
31
+ bootstrapToken: u,
32
+ peekedUser: h?.user,
33
+ peekedToken: h?.token?.accessToken,
34
+ loading: d === "initLoading" || !g,
35
+ authenticated: d === "authenticated" || d === "init" && (!!l || h?.state === "authenticated")
52
36
  }), [
53
- S,
54
- _,
55
- y
37
+ m,
38
+ d,
39
+ l,
40
+ u,
41
+ g,
42
+ h
56
43
  ]);
57
44
  };
58
45
  //#endregion
package/dist/index.d.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import { default as AuthContext } from './AuthContext';
2
2
  import { default as AuthContextProvider } from './AuthContextProvider';
3
+ import { default as AuthProvider } from './AuthProvider';
3
4
  import { default as useAuth } from './hooks/useAuth';
4
5
  export * from './hooks/useAuth';
5
6
  export * from './AuthContext';
6
7
  export * from './AuthProvider';
7
8
  export * from './AuthManager';
8
- export { useAuth, AuthContext, AuthContextProvider };
9
+ export * from './types';
10
+ export { readSessionHint } from './helpers/sessionHint';
11
+ export { nowInSeconds, tokenExpiresAt } from './helpers/tokenClaims';
12
+ export { useAuth, AuthContext, AuthContextProvider, AuthProvider };