@cellarnode/auth 0.13.3 → 0.15.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.
Files changed (41) hide show
  1. package/README.md +120 -4
  2. package/dist/auth-api.d.ts.map +1 -1
  3. package/dist/auth-api.js +44 -5
  4. package/dist/auth-api.js.map +1 -1
  5. package/dist/auth-client.d.ts.map +1 -1
  6. package/dist/auth-client.js +61 -17
  7. package/dist/auth-client.js.map +1 -1
  8. package/dist/auth-store.d.ts +2 -2
  9. package/dist/auth-store.d.ts.map +1 -1
  10. package/dist/auth-store.js +564 -173
  11. package/dist/auth-store.js.map +1 -1
  12. package/dist/auth-transport.d.ts +5 -0
  13. package/dist/auth-transport.d.ts.map +1 -0
  14. package/dist/auth-transport.js +45 -0
  15. package/dist/auth-transport.js.map +1 -0
  16. package/dist/auth-user.d.ts +7 -0
  17. package/dist/auth-user.d.ts.map +1 -0
  18. package/dist/auth-user.js +73 -0
  19. package/dist/auth-user.js.map +1 -0
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/react/dev-sign-in.d.ts +54 -0
  25. package/dist/react/dev-sign-in.d.ts.map +1 -0
  26. package/dist/react/dev-sign-in.js +63 -0
  27. package/dist/react/dev-sign-in.js.map +1 -0
  28. package/dist/react/index.d.ts.map +1 -1
  29. package/dist/react/index.js +11 -0
  30. package/dist/react/index.js.map +1 -1
  31. package/dist/react/login-form.d.ts.map +1 -1
  32. package/dist/react/login-form.js +105 -3
  33. package/dist/react/login-form.js.map +1 -1
  34. package/dist/session-continuity.d.ts +13 -0
  35. package/dist/session-continuity.d.ts.map +1 -0
  36. package/dist/session-continuity.js +107 -0
  37. package/dist/session-continuity.js.map +1 -0
  38. package/dist/types.d.ts +130 -2
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/types.js.map +1 -1
  41. package/package.json +2 -1
package/README.md CHANGED
@@ -13,7 +13,14 @@ npm install @cellarnode/auth
13
13
  ### Core (framework-agnostic)
14
14
 
15
15
  ```ts
16
- import { createAuthStore, createAuthClient, createAuthApi } from "@cellarnode/auth";
16
+ import {
17
+ canReplaySession,
18
+ captureSessionContinuity,
19
+ createAuthApi,
20
+ createAuthClient,
21
+ createAuthStore,
22
+ resolveSessionForReplay,
23
+ } from "@cellarnode/auth";
17
24
 
18
25
  const authStore = createAuthStore({
19
26
  baseUrl: "http://localhost:4000",
@@ -28,14 +35,107 @@ const authClient = createAuthClient({
28
35
  const authApi = createAuthApi({ client: authClient, store: authStore });
29
36
  ```
30
37
 
38
+ All package-owned requests require HTTPS. HTTP works automatically only for
39
+ exact loopback hosts `localhost`, `127.0.0.1`, and `[::1]` during local
40
+ development. Invalid URLs, userinfo, other schemes, non-loopback HTTP, and
41
+ request paths that escape the configured origin or base path fail before
42
+ network transport. Redirects are rejected so credentials cannot follow a
43
+ cross-origin or HTTPS-to-HTTP redirect.
44
+
45
+ Resolve token and authoritative identity together before enabling protected
46
+ work. `unavailable` preserves credentials while authority getters fail closed.
47
+ Caller abort returns `superseded` without cancelling shared adoption.
48
+
49
+ ```ts
50
+ const session = await authStore.resolveSession({ refresh: true, signal });
51
+ if (session.status === "ready") {
52
+ // session.token and validated session.user are from one guarded generation
53
+ }
54
+ ```
55
+
56
+ `ensureAccessToken()` remains a legacy credential-only wrapper. It may return a
57
+ retained token while authority is transiently `unavailable`; authority getters
58
+ still fail closed. Never use its token as proof that user or organisation
59
+ authority is ready. Use `resolveSession()` and require `status === "ready"` for
60
+ authorization decisions.
61
+
62
+ `onSessionStateChange` immediately reports current state. A synchronous
63
+ `resolving` notification precedes credential or authority changes, allowing
64
+ consumers to suspend writes and clear captured tenant queries first.
65
+ `getSessionState()` returns the same guarded state as a defensive snapshot;
66
+ continuity capture requires that snapshot to remain `ready`. Internal refresh
67
+ and identity requests time out after 10 seconds by default; configure
68
+ `resolutionTimeoutMs` on `createAuthStore` when needed.
69
+
70
+ Authenticated transports that retry after 401 must capture continuity before
71
+ their first request and replay only for same validated user and organisation:
72
+
73
+ ```ts
74
+ const before = captureSessionContinuity(authStore);
75
+ if (!before) throw new Error("Session authority unavailable");
76
+ const response = await fetch(url, init);
77
+ if (response.status === 401) {
78
+ const after = await resolveSessionForReplay(authStore, before, {
79
+ signal: init.signal,
80
+ });
81
+ if (canReplaySession(before, after, authStore)) {
82
+ // retry once with after.token
83
+ }
84
+ }
85
+ ```
86
+
87
+ `resolveSessionForReplay` centralizes stale-401 handling. It never refreshes a
88
+ replacement session. A ready rotated token can be reused once only when
89
+ validated user and organisation still match captured request authority.
90
+
31
91
  ### React Components
32
92
 
33
93
  ```tsx
34
94
  import { LoginForm, RegisterForm, UnauthorizedPage } from "@cellarnode/auth/react";
35
95
  ```
36
96
 
37
- ### Tailwind CSS Content Scan
97
+ ### Dev sign-in bypass (local development only)
98
+
99
+ `LoginForm` renders an extra "Dev sign-in (skip the code)" control **beside** the
100
+ email form when `import.meta.env.DEV` is true. It calls
101
+ `authStore.devLogin(email)`, which POSTs the backend's `/test/login` and adopts
102
+ the returned JWE through the same path `verifyOtp` uses. The OTP flow is
103
+ unchanged and remains the only path in production builds. There is no env var to
104
+ set on the frontend.
105
+
106
+ What is and is not dropped from a production bundle — the distinction matters,
107
+ so do not compress it:
108
+
109
+ - **Dropped.** The `DevSignInBypass` component and its markup. Vite folds
110
+ `import.meta.env.DEV` to `false`, Rollup removes the branch, and the module
111
+ goes with it. Pinned by `__tests__/dev-bypass-treeshake.test.ts`, which
112
+ bundles the form both ways and greps the output.
113
+ - **Kept.** `authStore.devLogin` and its failure copy, plus the
114
+ `readDevLoginEmail` / `rememberDevLoginEmail` helpers. All are reached from
115
+ live function bodies behind runtime `if` guards, so no bundler can prove them
116
+ unreachable. They are inert — the helpers only run inside the DEV branch, and
117
+ `devLogin` calls a route that is not mounted in production.
118
+
119
+ The security boundary is the **server**, not the bundle. `/test/login` is only
120
+ mounted when `NODE_ENV`/`MODE` is non-production **and**
121
+ `ENABLE_TEST_ENDPOINTS=true`, and every handler re-checks the same predicate.
122
+ When it is off, `/test/login` returns a uniform 404 and `devLogin()` resolves to
123
+ `{ ok: false, reason: "test-endpoints-disabled" }`. That 404 is deliberately
124
+ identical to the "no local account for this address" case, so neither the helper
125
+ nor the UI may present it as a statement about the account.
126
+
127
+ `devLogin()` never rejects; every outcome is a `DevLoginResult`. Callers should
128
+ still wrap it, because `devLogin` is optional on the `AuthStore` interface and a
129
+ custom store may reject.
130
+
131
+ ```ts
132
+ const result = await authStore.devLogin?.("producer@example.com");
133
+ if (result?.ok) {
134
+ // session adopted: token set, refresh scheduled, listeners fired
135
+ }
136
+ ```
38
137
 
138
+ ### Tailwind CSS Content Scan
39
139
  Add this to your CSS file so Tailwind picks up utility classes from the package:
40
140
 
41
141
  ```css
@@ -44,8 +144,24 @@ Add this to your CSS file so Tailwind picks up utility classes from the package:
44
144
 
45
145
  ## Exports
46
146
 
47
- - `@cellarnode/auth` — Core: `createAuthStore`, `createAuthClient`, `createAuthApi`, `validateUserType`, types
48
- - `@cellarnode/auth/react` — React: `LoginForm`, `RegisterForm`, `UnauthorizedPage`, `SquircleShift`
147
+ - `@cellarnode/auth` — Core: `createAuthStore`, `createAuthClient`, `createAuthApi`, `captureSessionContinuity`, `resolveSessionForReplay`, `canReplaySession`, `validateUserType`, `hasEntitlement`, `extractAccessToken`, `AuthError`, session-resolution types, and `DevLoginResult`
148
+ - `@cellarnode/auth/react` — React: `LoginForm`, `RegisterForm`, `UnauthorizedPage`, `SquircleShift`, `InputOTP` (+ `Group` / `Slot` / `Separator`)
149
+
150
+ `DevSignInBypass`, `DEV_LOGIN_EMAIL_STORAGE_KEY`, `readDevLoginEmail` and
151
+ `rememberDevLoginEmail` are **deliberately not exported**. They are internals of
152
+ `LoginForm`'s `import.meta.env.DEV` branch; the gate lives at that one call site,
153
+ and an exported symbol carries no gate — a consumer importing it could render the
154
+ bypass UI, or write a sign-in address to `localStorage`, from a production build.
155
+
156
+ ## Publishing
157
+
158
+ Published to npm (public access) via **npm Trusted Publishing (OIDC)** —
159
+ `.github/workflows/publish.yml` publishes automatically on a merge to `main` that changes
160
+ `package.json`'s version. There is no long-lived npm token in the publish job, and nobody runs
161
+ `npm publish` by hand. Provenance is generated automatically (public repo + public package).
162
+
163
+ Bump `version` in `package.json` and land it through a normal PR — merging to `main` is what
164
+ triggers the publish job, gated on the version having actually changed.
49
165
 
50
166
  ## License
51
167
 
@@ -1 +1 @@
1
- {"version":3,"file":"auth-api.d.ts","sourceRoot":"","sources":["../src/auth-api.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,OAAO,EACP,UAAU,EACV,SAAS,EAKV,MAAM,YAAY,CAAC;AAGpB,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,SAAS,CAAC;CAClB,GAAG,OAAO,CAiEV"}
1
+ {"version":3,"file":"auth-api.d.ts","sourceRoot":"","sources":["../src/auth-api.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,OAAO,EACP,UAAU,EACV,SAAS,EAIV,MAAM,YAAY,CAAC;AAIpB,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,SAAS,CAAC;CAClB,GAAG,OAAO,CAoHV"}
package/dist/auth-api.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { AuthError } from "./types.js";
2
2
  import { extractAccessToken } from "./extract-token.js";
3
+ import { parseAuthUser, parseVerifyOtpUser } from "./auth-user.js";
3
4
  export function createAuthApi(config) {
4
5
  const { client, store } = config;
5
6
  return {
@@ -27,26 +28,64 @@ export function createAuthApi(config) {
27
28
  if (!token) {
28
29
  throw new AuthError(500, "TOKEN_EXTRACTION_FAILED", "No access token found in verify-otp response");
29
30
  }
31
+ const user = parseVerifyOtpUser(raw.user);
32
+ if (!user) {
33
+ throw new AuthError(500, "OTP_USER_INVALID", "Invalid user in verify-otp response");
34
+ }
30
35
  const expiresIn = typeof raw.expiresIn === "number" ? raw.expiresIn : 900;
31
36
  store.setAccessToken(token, expiresIn);
32
37
  return {
33
38
  accessToken: token,
34
39
  expiresIn,
35
- user: raw.user,
40
+ user,
36
41
  };
37
42
  },
38
43
  async logout() {
39
- await client.fetch("/auth/logout", { method: "POST" });
44
+ const token = store.getAccessToken();
45
+ await client.fetch("/auth/logout", {
46
+ method: "POST",
47
+ skipAuth: true,
48
+ ...(token
49
+ ? { headers: { Authorization: `Bearer ${token}` } }
50
+ : {}),
51
+ });
40
52
  },
41
53
  async getMe(token) {
42
- if (token) {
43
- return client.fetch("/auth/me", {
54
+ const currentToken = store.getAccessToken();
55
+ const explicitCurrentToken = token !== undefined && token === currentToken;
56
+ if (store.resolveSession && token === undefined) {
57
+ const resolution = await store.resolveSession();
58
+ if (resolution.status === "ready")
59
+ return resolution.user;
60
+ if (resolution.status === "unauthorized") {
61
+ throw new AuthError(401, "UNAUTHORIZED", "Session is unauthorized");
62
+ }
63
+ if (resolution.status === "superseded") {
64
+ throw new AuthError(409, "SESSION_SUPERSEDED", "Session was superseded");
65
+ }
66
+ throw new AuthError(503, "AUTHORITY_UNAVAILABLE", "Session identity is unavailable");
67
+ }
68
+ const raw = token !== undefined
69
+ ? await client.fetch("/auth/me", {
44
70
  method: "GET",
45
71
  skipAuth: true,
46
72
  headers: { Authorization: `Bearer ${token}` },
73
+ })
74
+ : await client.fetch("/auth/me", {
75
+ method: "GET",
76
+ skipAuth: true,
77
+ ...(currentToken
78
+ ? { headers: { Authorization: `Bearer ${currentToken}` } }
79
+ : {}),
47
80
  });
81
+ if (explicitCurrentToken && store.getAccessToken() !== token) {
82
+ throw new AuthError(409, "SESSION_SUPERSEDED", "Session was superseded");
83
+ }
84
+ const user = parseAuthUser(raw);
85
+ if (!user) {
86
+ throw new AuthError(503, "AUTHORITY_UNAVAILABLE", "Session identity is unavailable");
48
87
  }
49
- return client.fetch("/auth/me", { method: "GET" });
88
+ return user;
50
89
  },
51
90
  };
52
91
  }
@@ -1 +1 @@
1
- {"version":3,"file":"auth-api.js","sourceRoot":"","sources":["../src/auth-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAUvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,UAAU,aAAa,CAAC,MAG7B;IACC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;IAEjC,OAAO;QACL,KAAK,CAAC,QAAQ,CAAC,KAAoB;YACjC,OAAO,MAAM,CAAC,KAAK,CACjB,gBAAgB,EAChB;gBACE,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,IAAI;gBACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;aAC5B,CACF,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,KAAa;YAC5B,OAAO,MAAM,CAAC,KAAK,CAAqB,mBAAmB,EAAE;gBAC3D,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,IAAI;gBACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC;aAChC,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,SAAS,CAAC,KAAa,EAAE,IAAY;YACzC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,kBAAkB,EAClB;gBACE,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,IAAI;gBACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;aACtC,CACF,CAAC;YAEF,MAAM,KAAK,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;YACtC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,yBAAyB,EAAE,8CAA8C,CAAC,CAAC;YACtG,CAAC;YAED,MAAM,SAAS,GACb,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC;YAE1D,KAAK,CAAC,cAAc,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YAEvC,OAAO;gBACL,WAAW,EAAE,KAAK;gBAClB,SAAS;gBACT,IAAI,EAAE,GAAG,CAAC,IAAgB;aAC3B,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,MAAM;YACV,MAAM,MAAM,CAAC,KAAK,CAAO,cAAc,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QAC/D,CAAC;QAED,KAAK,CAAC,KAAK,CAAC,KAAc;YACxB,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,MAAM,CAAC,KAAK,CAAW,UAAU,EAAE;oBACxC,MAAM,EAAE,KAAK;oBACb,QAAQ,EAAE,IAAI;oBACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;iBAC9C,CAAC,CAAC;YACL,CAAC;YACD,OAAO,MAAM,CAAC,KAAK,CAAW,UAAU,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/D,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"auth-api.js","sourceRoot":"","sources":["../src/auth-api.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"auth-client.d.ts","sourceRoot":"","sources":["../src/auth-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAqB,MAAM,YAAY,CAAC;AAElF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CAmFrE"}
1
+ {"version":3,"file":"auth-client.d.ts","sourceRoot":"","sources":["../src/auth-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAqB,MAAM,YAAY,CAAC;AAQlF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CA8JrE"}
@@ -1,4 +1,6 @@
1
1
  import { AuthError } from "./types.js";
2
+ import { canReplaySession, captureSessionContinuity, resolveSessionForReplay, } from "./session-continuity.js";
3
+ import { fetchAuthRequest } from "./auth-transport.js";
2
4
  export function createAuthClient(config) {
3
5
  const { baseUrl, store, onAuthFailure } = config;
4
6
  async function parseErrorResponse(res) {
@@ -20,17 +22,32 @@ export function createAuthClient(config) {
20
22
  async fetch(path, options) {
21
23
  const { skipAuth, ...init } = options ?? {};
22
24
  const headers = new Headers(init.headers);
25
+ const continuity = skipAuth ? null : captureSessionContinuity(store);
23
26
  if (!skipAuth) {
24
27
  const token = store.getAccessToken();
25
- if (token) {
26
- headers.set("Authorization", `Bearer ${token}`);
28
+ if (!continuity) {
29
+ throw token
30
+ ? new AuthError(503, "SESSION_UNAVAILABLE", "Session identity is unavailable")
31
+ : new AuthError(401, "UNAUTHORIZED", "Session is unauthorized");
27
32
  }
33
+ headers.set("Authorization", `Bearer ${continuity.token}`);
28
34
  }
29
35
  if (!headers.has("Content-Type") && init.body) {
30
36
  headers.set("Content-Type", "application/json");
31
37
  }
32
- const url = `${baseUrl}${path}`;
33
- const res = await fetch(url, {
38
+ const retryWithToken = async (token) => {
39
+ headers.set("Authorization", `Bearer ${token}`);
40
+ const retryRes = await fetchAuthRequest(baseUrl, path, {
41
+ ...init,
42
+ headers,
43
+ credentials: "include",
44
+ });
45
+ if (retryRes.ok)
46
+ return (await retryRes.json());
47
+ const retryError = await parseErrorResponse(retryRes);
48
+ throw new AuthError(retryRes.status, retryError.code, retryError.error, retryError.remainingAttempts);
49
+ };
50
+ const res = await fetchAuthRequest(baseUrl, path, {
34
51
  ...init,
35
52
  headers,
36
53
  credentials: "include",
@@ -38,23 +55,50 @@ export function createAuthClient(config) {
38
55
  if (res.ok) {
39
56
  return (await res.json());
40
57
  }
41
- // 401: attempt one refresh + retry (only for authenticated requests)
58
+ // 401: refresh once. Replay only when validated user + tenant continuity
59
+ // matches the authority captured before original transport.
42
60
  if (res.status === 401 && !skipAuth) {
43
- const newToken = await store.ensureAccessToken(true);
44
- if (newToken) {
45
- headers.set("Authorization", `Bearer ${newToken}`);
46
- const retryRes = await fetch(url, {
47
- ...init,
48
- headers,
49
- credentials: "include",
61
+ if (!store.resolveSession) {
62
+ const current = captureSessionContinuity(store);
63
+ if (!continuity ||
64
+ !current ||
65
+ current.token !== continuity.token ||
66
+ current.userId !== continuity.userId ||
67
+ current.orgId !== continuity.orgId) {
68
+ throw new AuthError(409, "SESSION_SUPERSEDED", "Request session was superseded");
69
+ }
70
+ store.clearAccessToken();
71
+ if (store.getAccessToken() === null)
72
+ onAuthFailure?.();
73
+ }
74
+ else {
75
+ const resolution = await resolveSessionForReplay(store, continuity, {
76
+ signal: init.signal ?? undefined,
50
77
  });
51
- if (retryRes.ok) {
52
- return (await retryRes.json());
78
+ if (resolution.status === "ready" &&
79
+ canReplaySession(continuity, resolution, store)) {
80
+ return retryWithToken(resolution.token);
81
+ }
82
+ if (resolution.status === "unavailable") {
83
+ throw new AuthError(503, "SESSION_UNAVAILABLE", "Session identity is unavailable");
84
+ }
85
+ if (resolution.status === "superseded") {
86
+ if (init.signal?.aborted) {
87
+ throw new DOMException("Request aborted", "AbortError");
88
+ }
89
+ throw new AuthError(409, "SESSION_SUPERSEDED", "Request session was superseded");
90
+ }
91
+ if (resolution.status === "ready") {
92
+ const currentState = store.getSessionState?.();
93
+ if ((currentState?.status === "resolving" ||
94
+ currentState?.status === "unavailable") &&
95
+ currentState.token === resolution.token) {
96
+ throw new AuthError(503, "SESSION_UNAVAILABLE", "Session identity is unavailable");
97
+ }
98
+ throw new AuthError(409, "SESSION_CONTINUITY_CHANGED", "Request session authority changed");
53
99
  }
100
+ onAuthFailure?.();
54
101
  }
55
- // Refresh failed or retry failed
56
- store.clearAccessToken();
57
- onAuthFailure?.();
58
102
  }
59
103
  const errBody = await parseErrorResponse(res);
60
104
  throw new AuthError(res.status, errBody.code, errBody.error, errBody.remainingAttempts);
@@ -1 +1 @@
1
- {"version":3,"file":"auth-client.js","sourceRoot":"","sources":["../src/auth-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth-client.js","sourceRoot":"","sources":["../src/auth-client.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import type { AuthStore, AuthStoreConfig } from "./types.js";
2
- export declare function createAuthStore(config: AuthStoreConfig): AuthStore;
1
+ import type { AuthStoreConfig, ConcreteAuthStore } from "./types.js";
2
+ export declare function createAuthStore(config: AuthStoreConfig): ConcreteAuthStore;
3
3
  //# sourceMappingURL=auth-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../src/auth-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,SAAS,EACT,eAAe,EAKhB,MAAM,YAAY,CAAC;AAuCpB,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAsRlE"}
1
+ {"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../src/auth-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,eAAe,EAEf,iBAAiB,EASlB,MAAM,YAAY,CAAC;AAsDpB,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,iBAAiB,CAsvB1E"}