@webtypen/webframez-core 0.3.75 → 0.3.76

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/auth-client.js CHANGED
@@ -28,17 +28,30 @@ exports.createAuthFetch = function createAuthFetch(options = {}, originalFetch =
28
28
  || ![401, 403].includes(response.status)) return response;
29
29
  const failure = await response.clone().json().catch(() => null);
30
30
  if (!["unauthorized", "invalid_csrf"].includes(failure?.code)) return response;
31
- if (!refreshing) {
31
+ const performRefresh = () => {
32
32
  const refreshHeaders = new Headers();
33
33
  const csrf = csrfCookie(options.cookieName);
34
34
  if (csrf) refreshHeaders.set("X-CSRF-Token", csrf);
35
- refreshing = originalFetch(`${base}/refresh`, { method: "POST", credentials: "same-origin", headers: refreshHeaders })
36
- .then(result => result.ok, () => false).finally(() => { refreshing = undefined; });
35
+ return originalFetch(`${base}/refresh`, { method: "POST", credentials: "same-origin", headers: refreshHeaders });
36
+ };
37
+ const retry = () => {
38
+ const nextCsrf = csrfCookie(options.cookieName);
39
+ if (nextCsrf) headers.set("X-CSRF-Token", nextCsrf);
40
+ return originalFetch(retryInput, { ...init, headers });
41
+ };
42
+ if (typeof navigator !== "undefined" && navigator.locks) {
43
+ // Hold the shared tab lock through the retry so another refresh cannot invalidate it.
44
+ return navigator.locks.request(`webframez-auth:${base}`, async () => {
45
+ const renewed = await performRefresh().then(result => result.ok, () => false);
46
+ return renewed ? retry() : response;
47
+ });
48
+ }
49
+ if (!refreshing) {
50
+ refreshing = performRefresh().then(result => result.ok, () => false)
51
+ .finally(() => { refreshing = undefined; });
37
52
  }
38
53
  if (!await refreshing) return response;
39
- const nextCsrf = csrfCookie(options.cookieName);
40
- if (nextCsrf) headers.set("X-CSRF-Token", nextCsrf);
41
- return originalFetch(retryInput, { ...init, headers });
54
+ return retry();
42
55
  };
43
56
  };
44
57
 
@@ -65,6 +65,8 @@ export declare class SessionAuth {
65
65
  verifyCsrf(token: string, csrf: string, kind?: "access" | "refresh"): Promise<boolean>;
66
66
  /** One winner under concurrency. A previously consumed refresh token revokes its device session. */
67
67
  refresh(token: string): Promise<SessionTokenPair | null>;
68
+ /** Trusted server refresh-secret lookup; never expose as a public endpoint. */
69
+ inspectRefresh(token: string): Promise<AuthSession | null>;
68
70
  /** Trusted browser GET resumption: renew access without consuming or replaying refresh rotation. */
69
71
  renewAccess(token: string): Promise<SessionTokenPair | null>;
70
72
  /** Browser login metadata; arbitrary request data and token hashes are never exposed. */
@@ -144,6 +144,15 @@ class SessionAuth {
144
144
  return this.pair(next, access, refresh);
145
145
  });
146
146
  }
147
+ /** Trusted server refresh-secret lookup; never expose as a public endpoint. */
148
+ inspectRefresh(token) {
149
+ return __awaiter(this, void 0, void 0, function* () {
150
+ const id = tokenSessionId(token);
151
+ if (!id)
152
+ return null;
153
+ return this.allowed(yield (yield this.rows()).findOne(Object.assign(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }), this.liveFilter())));
154
+ });
155
+ }
147
156
  /** Trusted browser GET resumption: renew access without consuming or replaying refresh rotation. */
148
157
  renewAccess(token) {
149
158
  return __awaiter(this, void 0, void 0, function* () {
@@ -151,7 +160,7 @@ class SessionAuth {
151
160
  if (!id)
152
161
  return null;
153
162
  const rows = yield this.rows();
154
- const filter = Object.assign(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }), this.liveFilter());
163
+ const filter = Object.assign(Object.assign(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }), this.liveFilter()), { accessExpiresAt: { $lte: Date.now() } });
155
164
  const row = yield rows.findOne(filter);
156
165
  if (!row || !(yield this.allowed(row)))
157
166
  return null;
@@ -96,7 +96,7 @@ class WebAuth {
96
96
  return null;
97
97
  const pair = yield this.sessions.renewAccess(token);
98
98
  if (!pair)
99
- return null;
99
+ return this.sessions.inspectRefresh(token);
100
100
  this.writePair(res, pair);
101
101
  const name = this.cookies.prefix + "access";
102
102
  const cookies = (0, AuthSecurity_1.authHeader)(req, "cookie").split(";").map(value => value.trim()).filter(value => value && !value.startsWith(`${name}=`));
@@ -475,7 +475,9 @@ unsafe requests, revive expired sessions, or extend absolute expiry. Explicit
475
475
  POST refresh rotates both secrets and retains replay protection. Middleware uses
476
476
  this resumption automatically when a response is available. Browser `createAuthFetch`
477
477
  refreshes rejected authentication/CSRF requests once, then repeats the rejected
478
- request once; login failures and permission errors are never retried.
478
+ request once; login failures and permission errors are never retried. Web Locks
479
+ serialize refresh calls across tabs in browsers supporting that API; a per-window
480
+ single-flight promise remains the fallback.
479
481
 
480
482
  Reset tokens have 256-bit random secrets; only a SHA-256 digest bound to the scope
481
483
  and current password is stored. Per-account request cooldown is an atomic conditional
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtypen/webframez-core",
3
- "version": "0.3.75",
3
+ "version": "0.3.76",
4
4
  "description": "The core package of the typescript based webframez (web-framework) by webtypen.de",
5
5
  "homepage": "https://webtypen.de/",
6
6
  "license": "MIT",