@authgate/browser 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,20 +2,33 @@
2
2
 
3
3
  Minimal browser-side helpers for applications using **AuthGate**.
4
4
 
5
- This package provides small, explicit utilities to integrate browser-based UIs
6
- with an AuthGate-backed authentication flow. It intentionally avoids framework
7
- coupling and hidden behavior.
5
+ This package provides **explicit, framework-agnostic primitives** for integrating
6
+ browser-based UIs (SSR or SPA) with an AuthGate-backed authentication system.
7
+
8
+ It intentionally avoids hidden behavior, background state mutation, or
9
+ framework-specific abstractions.
8
10
 
9
11
  ---
10
12
 
11
- ## Features
13
+ ## Design goals
12
14
 
13
- - Read AuthGate CSRF token from cookies
14
- - Perform a safe logout request with CSRF protection
15
- - Explicit success/failure signaling
16
- - Optional redirect after logout
15
+ - Explicit behavior (no magic, no background auth)
16
+ - Browser-only responsibility (cookies, CSRF, refresh)
17
+ - Framework-agnostic (React, Vue, Svelte, vanilla JS)
18
+ - Composable primitives + optional convenience helpers
17
19
  - Zero dependencies
18
- - Framework-agnostic (works with React, Vue, vanilla JS, etc.)
20
+
21
+ ---
22
+
23
+ ## Features
24
+
25
+ - Read AuthGate CSRF token from browser cookies
26
+ - Perform a CSRF-protected logout request
27
+ - Explicit browser-side session refresh **with audience selection**
28
+ - Optional `fetch` wrapper with **single-retry refresh semantics**
29
+ - Clear success / failure signaling
30
+ - Optional redirect on logout
31
+ - No runtime dependencies
19
32
 
20
33
  ---
21
34
 
@@ -39,11 +52,12 @@ const csrf = getCSRFToken();
39
52
 
40
53
  Returns the value of the `authgate_csrf` cookie, or `null` if not present.
41
54
 
42
- This function only **reads** the CSRF token. It does not generate or validate it.
55
+ This function only **reads** the CSRF token.
56
+ It does not generate or validate it.
43
57
 
44
58
  ---
45
59
 
46
- ### Logout
60
+ ## Logout
47
61
 
48
62
  ```ts
49
63
  import { logout } from "@authgate/browser";
@@ -57,7 +71,7 @@ This will:
57
71
  - Attach the CSRF token via `X-CSRF-Token`
58
72
  - Include credentials (`cookies`)
59
73
 
60
- The function returns a result indicating whether logout succeeded:
74
+ The function returns an explicit result:
61
75
 
62
76
  ```ts
63
77
  type LogoutResult =
@@ -65,8 +79,8 @@ type LogoutResult =
65
79
  | { ok: false; reason: "missing_csrf" | "request_failed" | "unauthorized" };
66
80
  ```
67
81
 
68
- Applications that do not need to react programmatically to logout may safely
69
- ignore the return value.
82
+ Applications that do not need to react programmatically may safely ignore the
83
+ return value.
70
84
 
71
85
  ---
72
86
 
@@ -79,54 +93,149 @@ await logout({ redirectTo: "/" });
79
93
  If the logout request succeeds, the browser is redirected to the given path.
80
94
 
81
95
  Redirecting is an optional side-effect and does **not** define success.
82
- Applications may choose to handle navigation themselves instead.
83
96
 
84
97
  ---
85
98
 
86
- ### Example (React / SPA)
99
+ ## Session refresh (explicit)
100
+
101
+ ### `refreshSession`
87
102
 
88
103
  ```ts
89
- const result = await logout();
104
+ import { refreshSession } from "@authgate/browser";
105
+
106
+ const refreshed = await refreshSession("app");
107
+ ```
108
+
109
+ Attempts to refresh the current AuthGate session by calling:
110
+
111
+ ```text
112
+ POST /auth/refresh
113
+ ```
114
+
115
+ with an explicit **audience declaration**.
116
+
117
+ ### Audience
118
+
119
+ The audience determines **which access token is minted** (e.g. `"app"`, `"admin"`).
120
+
121
+ - The client explicitly requests an audience
122
+ - AuthGate validates the requested audience against the user’s roles
123
+ - Requests for unauthorized audiences fail with `401`
124
+
125
+ If no audience is provided, `"app"` is used by default.
126
+
127
+ ### Behavior
128
+
129
+ - Returns `true` if refresh succeeded
130
+ - Returns `false` if refresh failed for any reason
131
+
132
+ This function:
133
+
134
+ - does **not** retry
135
+ - does **not** redirect
136
+ - does **not** throw
137
+ - does **not** modify application state
138
+
139
+ It is intended for applications that want **manual control** over refresh logic.
140
+
141
+ ---
142
+
143
+ ## Fetch wrapper (optional convenience)
90
144
 
91
- if (result.ok) {
145
+ ### `authFetch`
146
+
147
+ ```ts
148
+ import { authFetch } from "@authgate/browser";
149
+
150
+ const res = await authFetch("/api/data");
151
+ ```
152
+
153
+ `authFetch` is an **optional convenience wrapper** around `fetch` with
154
+ AuthGate-aware refresh behavior.
155
+
156
+ ### Behavior
157
+
158
+ 1. Performs the request with credentials
159
+ 2. If the response is **not `401`**, returns it directly
160
+ 3. If the response **is `401`**:
161
+ - Attempts `refreshSession()` with the same audience
162
+ - If refresh succeeds, retries the original request **once**
163
+ - Otherwise, returns the original `401` response
164
+
165
+ ### Audience-aware requests
166
+
167
+ ```ts
168
+ await authFetch("/admin/api/users", {}, { audience: "admin" });
169
+ ```
170
+
171
+ - The same audience is used for the refresh attempt
172
+ - Unauthorized audiences fail cleanly without retry loops
173
+
174
+ ### Important properties
175
+
176
+ - At most **one retry**
177
+ - No redirects
178
+ - No background refresh
179
+ - No swallowed failures
180
+
181
+ Applications remain fully in control of UX decisions.
182
+
183
+ ---
184
+
185
+ ## Example (React / SPA)
186
+
187
+ ```ts
188
+ const res = await authFetch("/api/me");
189
+
190
+ if (res.status === 401) {
92
191
  setUser(null);
93
- } else {
94
- console.error("Logout failed:", result.reason);
95
192
  }
96
193
  ```
97
194
 
195
+ Admin request:
196
+
197
+ ```ts
198
+ const res = await authFetch(
199
+ "/admin/api/users",
200
+ {},
201
+ { audience: "admin" },
202
+ );
203
+ ```
204
+
98
205
  ---
99
206
 
100
- ## Security Model
207
+ ## Security model
101
208
 
102
209
  - CSRF tokens are **not generated** by this package
103
210
  - CSRF validation is **enforced by AuthGate**
104
- - This package only forwards existing CSRF state
211
+ - Refresh tokens are **never exposed to JavaScript**
212
+ - All authentication state is owned by AuthGate
213
+ - Audiences are **explicitly requested and server-validated**
105
214
 
106
- No cookies are set, modified, or cleared by this library.
215
+ This package only forwards existing browser state explicitly.
107
216
 
108
217
  ---
109
218
 
110
- ## What This Package Does NOT Do
219
+ ## What this package does NOT do
111
220
 
112
221
  - No authentication logic
113
- - No token refresh
222
+ - No credential storage
223
+ - No background token refresh
114
224
  - No session management
225
+ - No authorization or role handling
115
226
  - No implicit redirects
116
227
  - No framework-specific helpers
117
228
 
118
- This package exists solely to reduce boilerplate and prevent integration mistakes.
229
+ This package exists solely to reduce boilerplate and prevent integration mistakes
230
+ while preserving full application control.
119
231
 
120
232
  ---
121
233
 
122
234
  ## Compatibility
123
235
 
124
236
  - Works with any backend protected by AuthGate
125
- - Compatible with SSR and SPA architectures
237
+ - Supports SSR, SPA, and hybrid architectures
126
238
 
127
239
  ---
128
240
 
129
241
  ## License
130
-
131
- MIT
132
-
package/dist/index.d.ts CHANGED
@@ -37,4 +37,47 @@ type LogoutResult = {
37
37
  export declare function logout(opts?: {
38
38
  redirectTo?: string;
39
39
  }): Promise<LogoutResult>;
40
+ /**
41
+ * authFetch performs a fetch request with AuthGate-aware, refresh-once behavior
42
+ * for a specific audience.
43
+ *
44
+ * Behavior:
45
+ * - Always includes credentials
46
+ * - Performs the initial request as-is
47
+ * - If the response is NOT 401, returns it directly
48
+ * - If the response IS 401:
49
+ * - Attempts POST /auth/refresh with CSRF and the same requested audience
50
+ * - If refresh succeeds, retries the original request ONCE
51
+ * - If refresh fails, returns the original 401 response
52
+ *
53
+ * authFetch never redirects or mutates application state. Callers are expected
54
+ * to handle authentication failures explicitly.
55
+ *
56
+ * @param input The resource to fetch (same as `fetch`).
57
+ * @param init Optional fetch options. Credentials are always included.
58
+ * @param opts Optional AuthGate options.
59
+ * @param opts.audience The audience for which the request is made (e.g. "app", "admin").
60
+ * Defaults to "app".
61
+ * @returns The final `Response` from the original request or the retried request.
62
+ */
63
+ export declare function authFetch(input: RequestInfo | URL, init?: RequestInit, opts?: {
64
+ audience?: string;
65
+ }): Promise<Response>;
66
+ /**
67
+ * refreshSession attempts to refresh the current AuthGate session for a
68
+ * specific audience.
69
+ *
70
+ * It performs:
71
+ * POST /auth/refresh
72
+ * with CSRF protection, credentials, and an explicit audience declaration.
73
+ *
74
+ * The server validates the requested audience against the user's roles and
75
+ * rejects the request if the audience is not permitted.
76
+ *
77
+ * @param audience The audience for which a new access token should be minted.
78
+ * Defaults to "app".
79
+ * @returns `true` if the refresh succeeded, or `false` if refresh failed for
80
+ * any reason (unauthorized, expired session, or error).
81
+ */
82
+ export declare function refreshSession(audience?: string): Promise<boolean>;
40
83
  export {};
package/dist/index.js CHANGED
@@ -67,3 +67,80 @@ export async function logout(opts) {
67
67
  }
68
68
  return { ok: true };
69
69
  }
70
+ /**
71
+ * authFetch performs a fetch request with AuthGate-aware, refresh-once behavior
72
+ * for a specific audience.
73
+ *
74
+ * Behavior:
75
+ * - Always includes credentials
76
+ * - Performs the initial request as-is
77
+ * - If the response is NOT 401, returns it directly
78
+ * - If the response IS 401:
79
+ * - Attempts POST /auth/refresh with CSRF and the same requested audience
80
+ * - If refresh succeeds, retries the original request ONCE
81
+ * - If refresh fails, returns the original 401 response
82
+ *
83
+ * authFetch never redirects or mutates application state. Callers are expected
84
+ * to handle authentication failures explicitly.
85
+ *
86
+ * @param input The resource to fetch (same as `fetch`).
87
+ * @param init Optional fetch options. Credentials are always included.
88
+ * @param opts Optional AuthGate options.
89
+ * @param opts.audience The audience for which the request is made (e.g. "app", "admin").
90
+ * Defaults to "app".
91
+ * @returns The final `Response` from the original request or the retried request.
92
+ */
93
+ export async function authFetch(input, init = {}, opts) {
94
+ const audience = opts?.audience ?? "app";
95
+ const res = await fetch(input, withCredentials(init));
96
+ if (res.status !== 401) {
97
+ return res;
98
+ }
99
+ const refreshed = await refreshSession(audience);
100
+ if (!refreshed) {
101
+ return res;
102
+ }
103
+ return fetch(input, withCredentials(init));
104
+ }
105
+ function withCredentials(init) {
106
+ return {
107
+ ...init,
108
+ credentials: "include",
109
+ };
110
+ }
111
+ /**
112
+ * refreshSession attempts to refresh the current AuthGate session for a
113
+ * specific audience.
114
+ *
115
+ * It performs:
116
+ * POST /auth/refresh
117
+ * with CSRF protection, credentials, and an explicit audience declaration.
118
+ *
119
+ * The server validates the requested audience against the user's roles and
120
+ * rejects the request if the audience is not permitted.
121
+ *
122
+ * @param audience The audience for which a new access token should be minted.
123
+ * Defaults to "app".
124
+ * @returns `true` if the refresh succeeded, or `false` if refresh failed for
125
+ * any reason (unauthorized, expired session, or error).
126
+ */
127
+ export async function refreshSession(audience = "app") {
128
+ const csrf = getCSRFToken();
129
+ if (!csrf) {
130
+ return false;
131
+ }
132
+ let res;
133
+ try {
134
+ res = await fetch(`/auth/refresh?audience=${encodeURIComponent(audience)}`, {
135
+ method: "POST",
136
+ headers: {
137
+ "X-CSRF-Token": csrf,
138
+ },
139
+ credentials: "include",
140
+ });
141
+ }
142
+ catch {
143
+ return false;
144
+ }
145
+ return res.ok;
146
+ }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "type": "git",
5
5
  "url": "https://github.com/alexlup06-authgate/authgate-browser.git"
6
6
  },
7
- "version": "0.2.0",
7
+ "version": "0.4.0",
8
8
  "description": "Browser-side helpers for AuthGate (logout, CSRF forwarding)",
9
9
  "license": "MIT",
10
10
  "type": "module",