@scaleway/auth-scw 1.0.4 → 1.1.1

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
@@ -12,6 +12,46 @@ pnpm add @scaleway/auth-scw
12
12
 
13
13
  ...
14
14
 
15
+ ### Choosing a storage backend
16
+
17
+ By default, the JWT and audienceId are persisted in **cookies**. You can opt-in
18
+ to **localStorage** with the `storageType` option:
19
+
20
+ ```tsx
21
+ <AuthScwProvider
22
+ // ...other props
23
+ storageType="localStorage"
24
+ >
25
+ {children}
26
+ </AuthScwProvider>
27
+ ```
28
+
29
+ | `storageType` | Pros | Cons |
30
+ | ---------------- | ----------------------------------------------------------- | ----------------------------------------------------------------- |
31
+ | `'cookie'` | Cross-tab synchronisation, sent to the server automatically | Size limits (~4KB), sent on every request |
32
+ | `'localStorage'` | Larger storage limit (~5MB), not sent on every HTTP request | Origin-scoped, no cross-tab sync (use `storage` events if needed) |
33
+
34
+ ### Live migration from cookie to localStorage
35
+
36
+ When switching from `'cookie'` to `'localStorage'` on an already-deployed app,
37
+ set `migrateFromCookie` to migrate any existing cookie-based session to
38
+ localStorage on provider initialization (then clean up the cookies):
39
+
40
+ ```tsx
41
+ <AuthScwProvider
42
+ // ...other props
43
+ storageType="localStorage"
44
+ migrateFromCookie
45
+ >
46
+ {children}
47
+ </AuthScwProvider>
48
+ ```
49
+
50
+ The migration runs **once**, before the provider reads the active storage. It is
51
+ idempotent and safe to keep enabled even after all users have migrated — it is a
52
+ no-op when nothing is found in cookies. You can also invoke it manually via the
53
+ exported `migrateCookieToLocalStorage()` helper.
54
+
15
55
  ## How to dev
16
56
 
17
57
  ### Prerequisites
package/dist/constants.js CHANGED
@@ -8,7 +8,8 @@ const EmptySession = {
8
8
  issuerId: "",
9
9
  jti: "",
10
10
  updatedAt: void 0,
11
- userAgent: ""
11
+ userAgent: "",
12
+ srn: ""
12
13
  },
13
14
  renewToken: "",
14
15
  token: ""
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { AudienceIdType, AuthScwContextType, EncodedJWT } from "./types/index.js";
1
+ import { COOKIE_AGE, DEFAULT_AUTH_SUFFIX_KEY, DOMAIN, getUniqueHostnameString } from "./useAuthScw/authStoreManager/constants.js";
2
+ import { AuthStoreManager, createAuthStoreManager } from "./useAuthScw/authStoreManager/authStoreManager.js";
3
+ import "./useAuthScw/authStoreManager/index.js";
4
+ import { AudienceIdType, AuthScwContextType, EncodedJWT, StorageType } from "./types/index.js";
2
5
  import { EmptySession } from "./constants.js";
3
6
  import { AuthScwProvider, useAuthScw } from "./useAuthScw/AuthScwProvider.js";
4
7
  import "./useAuthScw/index.js";
5
- import { AuthStoreManager } from "./useAuthScw/authStoreManager.js";
6
- export { type AudienceIdType, type AuthScwContextType, AuthScwProvider, AuthStoreManager, EmptySession, type EncodedJWT, useAuthScw };
8
+ export { type AudienceIdType, type AuthScwContextType, AuthScwProvider, AuthStoreManager, COOKIE_AGE, DEFAULT_AUTH_SUFFIX_KEY, DOMAIN, EmptySession, type EncodedJWT, type StorageType, createAuthStoreManager, getUniqueHostnameString, useAuthScw };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { EmptySession } from "./constants.js";
2
- import { AuthStoreManager } from "./useAuthScw/authStoreManager.js";
2
+ import { COOKIE_AGE, DEFAULT_AUTH_SUFFIX_KEY, DOMAIN, getUniqueHostnameString } from "./useAuthScw/authStoreManager/constants.js";
3
+ import { AuthStoreManager, createAuthStoreManager } from "./useAuthScw/authStoreManager/authStoreManager.js";
3
4
  import { AuthScwProvider, useAuthScw } from "./useAuthScw/AuthScwProvider.js";
4
- export { AuthScwProvider, AuthStoreManager, EmptySession, useAuthScw };
5
+ export { AuthScwProvider, AuthStoreManager, COOKIE_AGE, DEFAULT_AUTH_SUFFIX_KEY, DOMAIN, EmptySession, createAuthStoreManager, getUniqueHostnameString, useAuthScw };
@@ -0,0 +1,13 @@
1
+ //#region ../types/dist/index.d.ts
2
+ /**
3
+ * A utility used for XOR: marks every key that is only in T (and not in U)
4
+ * as optional-never, effectively requiring those keys to be absent.
5
+ */
6
+ export type Without<T, U> = Partial<Record<Exclude<keyof T, keyof U>, never>>;
7
+ /**
8
+ * Exclusive-or of two object types: produces a type that is either
9
+ * (T without any U-specific keys) OR (U without any T-specific keys),
10
+ * so only one of them may be present. For non-object unions it falls back to T | U.
11
+ */
12
+ export type SingleXOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
13
+ //#endregion
@@ -1,3 +1,5 @@
1
+ import { AuthStoreManagerInstance } from "../useAuthScw/authStoreManager/authStoreManager.js";
2
+ import "../useAuthScw/authStoreManager/index.js";
1
3
  import { Iamv1alpha1 } from "./iam/index.js";
2
4
  import { audienceIdSchema, jwtSchema } from "../zodSchemas.js";
3
5
  import { z } from "zod";
@@ -11,7 +13,7 @@ export type OnError = (onError: {
11
13
  extra?: Record<string, unknown>;
12
14
  }) => void;
13
15
  type GetJWT = (audienceId?: string) => Promise<(EncodedJWT & {
14
- source: 'refreshSession' | 'cookie';
16
+ source: 'refreshSession' | 'storage';
15
17
  }) | undefined>;
16
18
  type GetJwtTokenResponse = NonNullable<Awaited<ReturnType<GetJWT>>>['token'] | undefined;
17
19
  export type AuthScwContextType = {
@@ -25,6 +27,7 @@ export type AuthScwContextType = {
25
27
  getJwtToken: (audienceId?: string) => Promise<GetJwtTokenResponse>;
26
28
  logout: () => void;
27
29
  authenticated: boolean;
30
+ storeManager: AuthStoreManagerInstance;
28
31
  };
29
32
  export declare class IamV1alpha1API extends API {
30
33
  deleteJWT: Iamv1alpha1.API['deleteJWT'];
@@ -45,4 +48,19 @@ export type CookieConfigType = {
45
48
  sameSite: 'lax' | 'none' | 'strict' | boolean | undefined;
46
49
  secure: boolean;
47
50
  };
51
+ export type CookieOptions = {
52
+ /** Suffix appended to storage keys to namespace sessions. */
53
+ suffix: string;
54
+ /** Cookie attributes (httpOnly, path, sameSite, secure). Defaults to `COOKIE_CONFIG`. */
55
+ config?: CookieConfigType;
56
+ /** Cookie max-age in seconds. Defaults to `COOKIE_AGE` (31 days). */
57
+ age?: number;
58
+ };
59
+ /**
60
+ * Where the JWT and audienceId are persisted.
61
+ *
62
+ * - `'cookie'` (default): backward-compatible, allows synchronisation across tabs.
63
+ * - `'localStorage'`: scoped to a single origin, survives reloads, avoids cookie size limits.
64
+ */
65
+ export type StorageType = 'cookie' | 'localStorage';
48
66
  //#endregion
@@ -1,13 +1,28 @@
1
- import { AuthScwContextType, ConfigAuthProvider, CookieConfigType, OnError } from "../types/index.js";
1
+ import { AuthScwContextType, ConfigAuthProvider, CookieOptions, OnError } from "../types/index.js";
2
+ import { SingleXOR } from "../types/dist/index.js";
2
3
  import { ReactNode } from "react";
3
4
  //#region src/useAuthScw/AuthScwProvider.d.ts
5
+ type CookieStorageOptions = {
6
+ storageType?: 'cookie';
7
+ cookie: CookieOptions;
8
+ };
9
+ type LocalStorageOptions = {
10
+ storageType: 'localStorage';
11
+ cookie?: CookieOptions;
12
+ };
4
13
  export type AuthProviderParamType = ConfigAuthProvider & {
5
14
  children: ReactNode;
6
- cookieSuffix: string;
7
15
  urlParamTokenName: string;
8
16
  onError?: OnError;
9
- cookieConfig?: CookieConfigType;
10
- };
17
+ /**
18
+ * When `true` and `storageType` is `'localStorage'`, any JWT/audienceId
19
+ * previously stored in cookies will be migrated to localStorage on
20
+ * initialization, then removed from cookies. Useful for live migrations.
21
+ *
22
+ * Defaults to `false`.
23
+ */
24
+ migrateFromCookie?: boolean;
25
+ } & SingleXOR<CookieStorageOptions, LocalStorageOptions>;
11
26
  export declare const useAuthScw: () => AuthScwContextType;
12
- export declare const AuthScwProvider: ({ clientSettings, IamV1Alpha1, IamUnauthenticatedV1Alpha1, children, cookieSuffix, cookieConfig, urlParamTokenName, onError }: AuthProviderParamType) => import("react").JSX.Element;
27
+ export declare const AuthScwProvider: ({ clientSettings, IamV1Alpha1, IamUnauthenticatedV1Alpha1, children, cookie, urlParamTokenName, onError, storageType, migrateFromCookie }: AuthProviderParamType) => import("react").JSX.Element;
13
28
  //#endregion
@@ -1,56 +1,66 @@
1
1
  import { isExpired } from "../isExpired.js";
2
- import { AuthStoreManager, setCookieConfig } from "./authStoreManager.js";
2
+ import { createAuthStoreManager } from "./authStoreManager/authStoreManager.js";
3
3
  import { clientSingleton } from "./createClient.js";
4
- import { decodeToken, encodeToken, getCookieJWT, refreshSession } from "./helpers.js";
4
+ import { decodeToken, encodeToken, getStoredJWT, refreshSession } from "./helpers.js";
5
5
  import { proxyJwt } from "./proxyJwt.js";
6
6
  import { createContext, useCallback, useContext, useMemo, useState } from "react";
7
7
  import { jsx } from "react/jsx-runtime";
8
8
  //#region src/useAuthScw/AuthScwProvider.tsx
9
9
  const AuthScwContext = createContext({});
10
10
  const useAuthScw = () => useContext(AuthScwContext);
11
- const AuthScwProvider = ({ clientSettings, IamV1Alpha1, IamUnauthenticatedV1Alpha1, children, cookieSuffix, cookieConfig, urlParamTokenName, onError }) => {
12
- if (cookieConfig) setCookieConfig(cookieConfig);
11
+ const AuthScwProvider = ({ clientSettings, IamV1Alpha1, IamUnauthenticatedV1Alpha1, children, cookie, urlParamTokenName, onError, storageType = "cookie", migrateFromCookie = false }) => {
12
+ const storeManager = useMemo(() => createAuthStoreManager({
13
+ storageType,
14
+ cookieConfig: cookie?.config,
15
+ cookieAge: cookie?.age,
16
+ suffixKey: cookie?.suffix
17
+ }), [
18
+ storageType,
19
+ cookie?.config,
20
+ cookie?.age,
21
+ cookie?.suffix
22
+ ]);
13
23
  clientSingleton.setAPIsAndSettings({
14
24
  clientSettings,
15
25
  IamV1Alpha1,
16
26
  IamUnauthenticatedV1Alpha1
17
27
  });
18
- const initAudienceId = useCallback(() => {
19
- AuthStoreManager.setSuffixKey(cookieSuffix);
28
+ const initAudienceId = () => {
29
+ if (storageType === "localStorage" && migrateFromCookie) storeManager.migrateCookieToLocalStorage();
20
30
  const currentUrl = new URL(globalThis.location.href);
21
31
  const tokenRaw = currentUrl.searchParams.get(urlParamTokenName);
22
32
  if (tokenRaw) {
23
33
  currentUrl.searchParams.delete(urlParamTokenName);
24
- const token = decodeToken(tokenRaw);
34
+ const token = decodeToken(tokenRaw, storeManager);
25
35
  if (token) {
26
36
  const jwtProxy = proxyJwt(token);
27
- if (jwtProxy.jwt) AuthStoreManager.setJwt({ jwtInfo: jwtProxy });
37
+ if (jwtProxy.jwt) storeManager.setJwt({ jwtInfo: jwtProxy });
28
38
  globalThis.history.replaceState({}, "", currentUrl);
29
39
  return jwtProxy.jwt?.audienceId;
30
40
  }
31
41
  }
32
- const audienceId = AuthStoreManager.getAudienceId();
42
+ const audienceId = storeManager.getAudienceId();
33
43
  if (audienceId) {
34
- if (AuthStoreManager.getJwt(audienceId)) return audienceId;
44
+ if (storeManager.getJwt(audienceId)) return audienceId;
35
45
  }
36
- }, [cookieSuffix, urlParamTokenName]);
46
+ };
37
47
  const [currentAudienceId, setCurrentAudienceId] = useState(() => initAudienceId());
38
48
  const setJWT = useCallback((jwtInfoParam) => {
39
49
  const currentJWT = proxyJwt(jwtInfoParam);
40
50
  if (currentJWT.jwt?.audienceId) {
41
- AuthStoreManager.setJwt({ jwtInfo: currentJWT });
51
+ storeManager.setJwt({ jwtInfo: currentJWT });
42
52
  setCurrentAudienceId(currentJWT.jwt.audienceId);
43
53
  }
44
- }, [setCurrentAudienceId]);
54
+ }, [setCurrentAudienceId, storeManager]);
45
55
  const getJWT = useCallback(async (audienceId = currentAudienceId) => {
46
56
  if (audienceId) {
47
- const cookieJWT = getCookieJWT(audienceId);
48
- if (cookieJWT?.jwt) {
49
- const { renewToken, token, jwt: currentJWT } = cookieJWT;
57
+ const storedJWT = getStoredJWT(audienceId, storeManager);
58
+ if (storedJWT?.jwt) {
59
+ const { renewToken, token, jwt: currentJWT } = storedJWT;
50
60
  if (currentJWT.expiresAt && !isExpired(new Date(currentJWT.expiresAt))) return {
51
61
  jwt: currentJWT,
52
62
  renewToken,
53
- source: "cookie",
63
+ source: "storage",
54
64
  token
55
65
  };
56
66
  const jwtInfoData = await refreshSession({
@@ -72,7 +82,8 @@ const AuthScwProvider = ({ clientSettings, IamV1Alpha1, IamUnauthenticatedV1Alph
72
82
  }, [
73
83
  currentAudienceId,
74
84
  setJWT,
75
- onError
85
+ onError,
86
+ storeManager
76
87
  ]);
77
88
  const getJwtToken = useCallback(async (audienceId) => {
78
89
  const currentJWT = await getJWT(audienceId);
@@ -80,37 +91,42 @@ const AuthScwProvider = ({ clientSettings, IamV1Alpha1, IamUnauthenticatedV1Alph
80
91
  }, [getJWT]);
81
92
  const logout = useCallback(() => {
82
93
  if (currentAudienceId) {
83
- const cookieJWT = getCookieJWT(currentAudienceId);
84
- AuthStoreManager.deleteJwt(currentAudienceId);
85
- if (cookieJWT?.jwt) {
94
+ const storedJWT = getStoredJWT(currentAudienceId, storeManager);
95
+ storeManager.deleteJwt(currentAudienceId);
96
+ if (storedJWT?.jwt) {
86
97
  const { deleteJWT } = clientSingleton.createClient({ getAsyncToken: getJwtToken });
87
- deleteJWT?.({ jti: cookieJWT.jwt.jti }).catch(() => null);
98
+ deleteJWT?.({ jti: storedJWT.jwt.jti }).catch(() => null);
88
99
  }
89
100
  }
90
- AuthStoreManager.deleteAudienceId();
101
+ storeManager.deleteAudienceId();
91
102
  setCurrentAudienceId(void 0);
92
- }, [getJwtToken, currentAudienceId]);
103
+ }, [
104
+ getJwtToken,
105
+ currentAudienceId,
106
+ storeManager
107
+ ]);
93
108
  const jti = useMemo(() => {
94
109
  if (currentAudienceId) {
95
- const cookieJWT = getCookieJWT(currentAudienceId);
96
- if (cookieJWT?.jwt) return cookieJWT.jwt.jti;
110
+ const storedJWT = getStoredJWT(currentAudienceId, storeManager);
111
+ if (storedJWT?.jwt) return storedJWT.jwt.jti;
97
112
  }
98
- }, [currentAudienceId]);
99
- const isAuthenticated = useMemo(() => Boolean(currentAudienceId && getCookieJWT(currentAudienceId)), [currentAudienceId]);
113
+ }, [currentAudienceId, storeManager]);
114
+ const isAuthenticated = useMemo(() => Boolean(currentAudienceId && getStoredJWT(currentAudienceId, storeManager)), [currentAudienceId, storeManager]);
100
115
  const setAudienceId = useCallback((audienceId) => {
101
116
  setCurrentAudienceId(audienceId);
102
117
  }, []);
103
118
  const value = useMemo(() => ({
104
119
  audienceId: currentAudienceId ?? void 0,
105
120
  authenticated: isAuthenticated,
106
- decodeToken,
121
+ decodeToken: (encodedToken) => decodeToken(encodedToken, storeManager),
107
122
  encodeToken,
108
123
  getJWT,
109
124
  getJwtToken,
110
125
  jti,
111
126
  logout,
112
127
  setAudienceId,
113
- setJWT
128
+ setJWT,
129
+ storeManager
114
130
  }), [
115
131
  getJWT,
116
132
  getJwtToken,
@@ -119,7 +135,8 @@ const AuthScwProvider = ({ clientSettings, IamV1Alpha1, IamUnauthenticatedV1Alph
119
135
  setAudienceId,
120
136
  currentAudienceId,
121
137
  isAuthenticated,
122
- jti
138
+ jti,
139
+ storeManager
123
140
  ]);
124
141
  return /* @__PURE__ */ jsx(AuthScwContext, {
125
142
  value,
@@ -0,0 +1,59 @@
1
+ import { CreateAuthStoreManagerOptions } from "./types.js";
2
+ import { COOKIE_AGE, DEFAULT_AUTH_SUFFIX_KEY, DOMAIN, getUniqueHostnameString } from "./constants.js";
3
+ import { AudienceIdType, EncodedJWT, StorageType } from "../../types/index.js";
4
+ //#region src/useAuthScw/authStoreManager/authStoreManager.d.ts
5
+ export type AuthStoreManagerInstance = ReturnType<typeof createAuthStoreManager>;
6
+ /**
7
+ * Factory that creates an isolated AuthStoreManager instance with its own
8
+ * immutable `storageType`, `suffixKey`, and cookie config (`cookieConfig`/`cookieAge`).
9
+ *
10
+ * Each instance owns its own backends, so multiple providers can coexist
11
+ * without clobbering each other's config. The `storageType` is set at creation
12
+ * time and cannot be changed afterwards — use {@link createAuthStoreManager}
13
+ * to create a new instance with a different storage type.
14
+ *
15
+ * Accepts either an options object `{ storageType, cookieConfig, cookieAge }`
16
+ * or, for backward compatibility, a bare `StorageType` string.
17
+ *
18
+ * For backward compatibility, a default singleton `AuthStoreManager` is also
19
+ * exported below.
20
+ */
21
+ export declare const createAuthStoreManager: (param?: StorageType | CreateAuthStoreManagerOptions) => {
22
+ COOKIE_AGE: number;
23
+ DOMAIN: string;
24
+ deleteAllJwts(): void;
25
+ deleteAudienceId(): void;
26
+ deleteJwt(audienceId: string): void;
27
+ getAudienceId(): string | null;
28
+ getJwt(audienceId: string): EncodedJWT | null;
29
+ getKeyAudienceId(): string;
30
+ getKeySession(audienceId: string): string;
31
+ getStorageType(): StorageType;
32
+ getUniqueHostnameString: typeof getUniqueHostnameString;
33
+ /**
34
+ * Migrates any existing JWT and audienceId stored in cookies to localStorage,
35
+ * then removes the cookie entries. Safe to call repeatedly — it is a no-op when
36
+ * nothing is stored in cookies.
37
+ *
38
+ * Accesses the cookie and localStorage backends directly without changing
39
+ * the instance's active `storageType`.
40
+ */
41
+ migrateCookieToLocalStorage(): {
42
+ migrated: boolean;
43
+ audienceId?: string;
44
+ };
45
+ setAudienceId(audienceId: string): void;
46
+ setJwt({ jwtInfo, setAudienceId }: {
47
+ jwtInfo: EncodedJWT;
48
+ setAudienceId?: boolean;
49
+ }): void;
50
+ readonly SUFFIX_KEY: string;
51
+ typeGuardAudienceId(untypedObject: unknown): untypedObject is AudienceIdType;
52
+ typeGuardJWT(untypedObject: unknown): untypedObject is EncodedJWT;
53
+ };
54
+ /**
55
+ * @deprecated Use `useAuthScw().storeManager` or `createAuthStoreManager()` instead.
56
+ * The singleton bypasses the provider's configuration and shares global state across all providers.
57
+ */
58
+ export declare const AuthStoreManager: AuthStoreManagerInstance;
59
+ //#endregion
@@ -0,0 +1,143 @@
1
+ import { COOKIE_AGE, DOMAIN, getUniqueHostnameString } from "./constants.js";
2
+ import { audienceIdSchema, jwtSchema } from "../../zodSchemas.js";
3
+ import { createCookieBackend } from "./cookieBackend.js";
4
+ import { createLocalStorageBackend } from "./localStorageBackend.js";
5
+ //#region src/useAuthScw/authStoreManager/authStoreManager.ts
6
+ const KEY_SESSION = "_scw_session";
7
+ const KEY_AUDIENCE_ID = "_scw_audience_id";
8
+ const DEFAULT_SUFFIX_KEY = "";
9
+ const buildBackends = (options) => ({
10
+ cookie: createCookieBackend({
11
+ cookieConfig: options.cookieConfig,
12
+ cookieAge: options.cookieAge
13
+ }),
14
+ localStorage: createLocalStorageBackend()
15
+ });
16
+ const normalizeOptions = (param) => typeof param === "string" ? { storageType: param } : param ?? {};
17
+ /**
18
+ * Factory that creates an isolated AuthStoreManager instance with its own
19
+ * immutable `storageType`, `suffixKey`, and cookie config (`cookieConfig`/`cookieAge`).
20
+ *
21
+ * Each instance owns its own backends, so multiple providers can coexist
22
+ * without clobbering each other's config. The `storageType` is set at creation
23
+ * time and cannot be changed afterwards — use {@link createAuthStoreManager}
24
+ * to create a new instance with a different storage type.
25
+ *
26
+ * Accepts either an options object `{ storageType, cookieConfig, cookieAge }`
27
+ * or, for backward compatibility, a bare `StorageType` string.
28
+ *
29
+ * For backward compatibility, a default singleton `AuthStoreManager` is also
30
+ * exported below.
31
+ */
32
+ const createAuthStoreManager = (param) => {
33
+ const options = normalizeOptions(param);
34
+ const storageType = options.storageType ?? "cookie";
35
+ const suffixKey = options.suffixKey ? `_${options.suffixKey}` : DEFAULT_SUFFIX_KEY;
36
+ const backends = buildBackends(options);
37
+ const getBackend = () => backends[storageType];
38
+ const instance = {
39
+ COOKIE_AGE,
40
+ DOMAIN,
41
+ deleteAllJwts() {
42
+ const prefix = instance.getKeySession("");
43
+ getBackend().deleteAllWithPrefix(prefix);
44
+ },
45
+ deleteAudienceId() {
46
+ getBackend().delete(instance.getKeyAudienceId());
47
+ },
48
+ deleteJwt(audienceId) {
49
+ getBackend().delete(instance.getKeySession(audienceId));
50
+ },
51
+ getAudienceId() {
52
+ const parsed = getBackend().get(instance.getKeyAudienceId());
53
+ const resultParsed = audienceIdSchema.safeParse(parsed);
54
+ if (resultParsed.success) return resultParsed.data.audienceId;
55
+ return null;
56
+ },
57
+ getJwt(audienceId) {
58
+ const parsed = getBackend().get(instance.getKeySession(audienceId));
59
+ const resultParsed = jwtSchema.safeParse(parsed);
60
+ if (resultParsed.success) return resultParsed.data;
61
+ if (resultParsed.error) {}
62
+ return null;
63
+ },
64
+ getKeyAudienceId() {
65
+ return `${KEY_AUDIENCE_ID}${suffixKey}`;
66
+ },
67
+ getKeySession(audienceId) {
68
+ return `${KEY_SESSION}${suffixKey}_${audienceId}`;
69
+ },
70
+ getStorageType() {
71
+ return storageType;
72
+ },
73
+ getUniqueHostnameString,
74
+ /**
75
+ * Migrates any existing JWT and audienceId stored in cookies to localStorage,
76
+ * then removes the cookie entries. Safe to call repeatedly — it is a no-op when
77
+ * nothing is stored in cookies.
78
+ *
79
+ * Accesses the cookie and localStorage backends directly without changing
80
+ * the instance's active `storageType`.
81
+ */
82
+ migrateCookieToLocalStorage() {
83
+ const cookieBackend = backends.cookie;
84
+ const localStorageBackend = backends.localStorage;
85
+ const audienceIdRaw = cookieBackend.get(instance.getKeyAudienceId());
86
+ const audienceIdParsed = audienceIdSchema.safeParse(audienceIdRaw);
87
+ if (!audienceIdParsed.success) return { migrated: false };
88
+ const { audienceId } = audienceIdParsed.data;
89
+ const jwtRaw = cookieBackend.get(instance.getKeySession(audienceId));
90
+ const jwtParsed = jwtSchema.safeParse(jwtRaw);
91
+ if (!jwtParsed.success) {
92
+ localStorageBackend.set(instance.getKeyAudienceId(), { audienceId });
93
+ cookieBackend.delete(instance.getKeyAudienceId());
94
+ return {
95
+ migrated: true,
96
+ audienceId
97
+ };
98
+ }
99
+ localStorageBackend.set(instance.getKeySession(audienceId), jwtParsed.data);
100
+ localStorageBackend.set(instance.getKeyAudienceId(), { audienceId });
101
+ cookieBackend.delete(instance.getKeySession(audienceId));
102
+ cookieBackend.delete(instance.getKeyAudienceId());
103
+ return {
104
+ migrated: true,
105
+ audienceId
106
+ };
107
+ },
108
+ setAudienceId(audienceId) {
109
+ getBackend().set(instance.getKeyAudienceId(), { audienceId });
110
+ },
111
+ setJwt({ jwtInfo, setAudienceId = true }) {
112
+ const audienceId = jwtInfo.jwt?.audienceId;
113
+ getBackend().set(instance.getKeySession(audienceId), jwtInfo);
114
+ if (setAudienceId) instance.setAudienceId(audienceId);
115
+ },
116
+ get SUFFIX_KEY() {
117
+ return suffixKey;
118
+ },
119
+ typeGuardAudienceId(untypedObject) {
120
+ return audienceIdSchema.safeParse(untypedObject).success;
121
+ },
122
+ typeGuardJWT(untypedObject) {
123
+ return jwtSchema.safeParse(untypedObject).success;
124
+ }
125
+ };
126
+ return instance;
127
+ };
128
+ let authStoreManagerSingletonWarned = false;
129
+ const warnSingletonUsage = () => {
130
+ if (authStoreManagerSingletonWarned) return;
131
+ authStoreManagerSingletonWarned = true;
132
+ console.warn("`AuthStoreManager` singleton is deprecated. It uses shared global state that bypasses the provider. Use `useAuthScw().storeManager` to access the instance managed by `<AuthScwProvider>` instead. If you need a standalone instance, create one via `createAuthStoreManager()`.");
133
+ };
134
+ /**
135
+ * @deprecated Use `useAuthScw().storeManager` or `createAuthStoreManager()` instead.
136
+ * The singleton bypasses the provider's configuration and shares global state across all providers.
137
+ */
138
+ const AuthStoreManager = new Proxy(createAuthStoreManager(), { get(target, prop, receiver) {
139
+ warnSingletonUsage();
140
+ return Reflect.get(target, prop, receiver);
141
+ } });
142
+ //#endregion
143
+ export { AuthStoreManager, createAuthStoreManager };
@@ -0,0 +1,6 @@
1
+ //#region src/useAuthScw/authStoreManager/constants.d.ts
2
+ export declare const COOKIE_AGE: number;
3
+ export declare const DOMAIN: string;
4
+ export declare const getUniqueHostnameString: (inputString: string) => string;
5
+ export declare const DEFAULT_AUTH_SUFFIX_KEY: string;
6
+ //#endregion
@@ -0,0 +1,7 @@
1
+ //#region src/useAuthScw/authStoreManager/constants.ts
2
+ const COOKIE_AGE = 2678400;
3
+ const DOMAIN = typeof globalThis !== "undefined" && "location" in globalThis ? globalThis.location.hostname : "";
4
+ const getUniqueHostnameString = (inputString) => inputString.replaceAll(".", "_").replaceAll("-", "_");
5
+ const DEFAULT_AUTH_SUFFIX_KEY = getUniqueHostnameString(DOMAIN);
6
+ //#endregion
7
+ export { COOKIE_AGE, DEFAULT_AUTH_SUFFIX_KEY, DOMAIN, getUniqueHostnameString };
@@ -0,0 +1,64 @@
1
+ import { COOKIE_CONFIG } from "../../constants.js";
2
+ import { DOMAIN } from "./constants.js";
3
+ import { parseCookie, stringifySetCookie } from "cookie";
4
+ //#region src/useAuthScw/authStoreManager/cookieBackend.ts
5
+ const getCookie = (key) => {
6
+ const cookies = parseCookie(document.cookie);
7
+ if (key in cookies && cookies[key]) {
8
+ const cookieSessionString = cookies[key];
9
+ if (cookieSessionString) return JSON.parse(cookieSessionString);
10
+ return null;
11
+ }
12
+ return null;
13
+ };
14
+ const createCookieBackend = (options = {}) => {
15
+ const config = options.cookieConfig ?? COOKIE_CONFIG;
16
+ const maxAgeDefault = options.cookieAge ?? 2678400;
17
+ const setCookie = (value, key, maxAge = maxAgeDefault) => {
18
+ document.cookie = stringifySetCookie({
19
+ name: key,
20
+ value: JSON.stringify(value),
21
+ domain: DOMAIN,
22
+ httpOnly: config.httpOnly,
23
+ maxAge,
24
+ path: config.path,
25
+ sameSite: config.sameSite,
26
+ secure: config.secure
27
+ });
28
+ };
29
+ const deleteCookie = (key) => {
30
+ document.cookie = stringifySetCookie({
31
+ name: key,
32
+ value: "",
33
+ domain: DOMAIN,
34
+ httpOnly: config.httpOnly,
35
+ maxAge: -1,
36
+ path: config.path,
37
+ sameSite: config.sameSite,
38
+ secure: config.secure
39
+ });
40
+ };
41
+ return {
42
+ delete(key) {
43
+ deleteCookie(key);
44
+ },
45
+ deleteAllWithPrefix(prefix) {
46
+ document.cookie.split(";").forEach((cookieParam) => {
47
+ let [audienceId] = cookieParam.split("=");
48
+ if (audienceId) {
49
+ audienceId = audienceId.trim();
50
+ if (audienceId.startsWith(prefix)) deleteCookie(audienceId);
51
+ }
52
+ });
53
+ },
54
+ get(key) {
55
+ return getCookie(key);
56
+ },
57
+ set(key, value, maxAge = maxAgeDefault) {
58
+ setCookie(value, key, maxAge);
59
+ }
60
+ };
61
+ };
62
+ createCookieBackend();
63
+ //#endregion
64
+ export { createCookieBackend };
@@ -0,0 +1,4 @@
1
+ import { CreateAuthStoreManagerOptions } from "./types.js";
2
+ import { COOKIE_AGE, DEFAULT_AUTH_SUFFIX_KEY, DOMAIN, getUniqueHostnameString } from "./constants.js";
3
+ import { AuthStoreManager, AuthStoreManagerInstance, createAuthStoreManager } from "./authStoreManager.js";
4
+ export { AuthStoreManager, type AuthStoreManagerInstance, COOKIE_AGE, DEFAULT_AUTH_SUFFIX_KEY, DOMAIN, createAuthStoreManager, getUniqueHostnameString };
@@ -0,0 +1,41 @@
1
+ //#region src/useAuthScw/authStoreManager/localStorageBackend.ts
2
+ /**
3
+ * localStorage-based storage backend.
4
+ *
5
+ * Reads/writes are wrapped in try/catch because `localStorage` can throw in
6
+ * restricted environments (e.g. sandboxed iframes, private mode in some
7
+ * browsers, or when the quota is exceeded).
8
+ */
9
+ const createLocalStorageBackend = () => ({
10
+ delete(key) {
11
+ try {
12
+ globalThis.localStorage.removeItem(key);
13
+ } catch {}
14
+ },
15
+ deleteAllWithPrefix(prefix) {
16
+ try {
17
+ const keysToRemove = [];
18
+ for (let i = 0; i < globalThis.localStorage.length; i++) {
19
+ const key = globalThis.localStorage.key(i);
20
+ if (key?.startsWith(prefix)) keysToRemove.push(key);
21
+ }
22
+ keysToRemove.forEach((key) => globalThis.localStorage.removeItem(key));
23
+ } catch {}
24
+ },
25
+ get(key) {
26
+ try {
27
+ const raw = globalThis.localStorage.getItem(key);
28
+ if (!raw) return null;
29
+ return JSON.parse(raw);
30
+ } catch {
31
+ return null;
32
+ }
33
+ },
34
+ set(key, value) {
35
+ try {
36
+ globalThis.localStorage.setItem(key, JSON.stringify(value));
37
+ } catch {}
38
+ }
39
+ });
40
+ //#endregion
41
+ export { createLocalStorageBackend };
@@ -0,0 +1,9 @@
1
+ import { CookieConfigType, StorageType } from "../../types/index.js";
2
+ //#region src/useAuthScw/authStoreManager/types.d.ts
3
+ export type CreateAuthStoreManagerOptions = {
4
+ storageType?: StorageType;
5
+ cookieConfig?: CookieConfigType;
6
+ cookieAge?: number;
7
+ suffixKey?: string;
8
+ };
9
+ //#endregion
@@ -1,4 +1,3 @@
1
- import { AuthStoreManager } from "./authStoreManager.js";
2
1
  import { clientSingleton } from "./createClient.js";
3
2
  import { Errors } from "@scaleway/sdk-client";
4
3
  //#region src/useAuthScw/helpers.ts
@@ -25,16 +24,16 @@ const refreshSession = ({ paramsRenewRequest, setJWT, onError }) => {
25
24
  });
26
25
  return refreshSessionPromise;
27
26
  };
28
- const decodeToken = (encodedToken) => {
27
+ const decodeToken = (encodedToken, storeManager) => {
29
28
  try {
30
29
  const decodedTokenRaw = JSON.parse(atob(encodedToken));
31
- if (AuthStoreManager.typeGuardJWT(decodedTokenRaw)) return decodedTokenRaw;
30
+ if (storeManager.typeGuardJWT(decodedTokenRaw)) return decodedTokenRaw;
32
31
  return null;
33
32
  } catch {
34
33
  return null;
35
34
  }
36
35
  };
37
36
  const encodeToken = (jwt) => btoa(JSON.stringify(jwt));
38
- const getCookieJWT = (audienceId) => AuthStoreManager.getJwt(audienceId);
37
+ const getStoredJWT = (audienceId, storeManager) => storeManager.getJwt(audienceId);
39
38
  //#endregion
40
- export { decodeToken, encodeToken, getCookieJWT, refreshSession };
39
+ export { decodeToken, encodeToken, getStoredJWT, refreshSession };
@@ -1,2 +1,4 @@
1
+ import { DEFAULT_AUTH_SUFFIX_KEY, getUniqueHostnameString } from "./authStoreManager/constants.js";
2
+ import "./authStoreManager/index.js";
1
3
  import { AuthScwProvider, useAuthScw } from "./AuthScwProvider.js";
2
4
  export { AuthScwProvider, useAuthScw };
@@ -1,10 +1,11 @@
1
1
  import { Iamv1alpha1 } from "./types/iam/index.js";
2
2
  import { z } from "zod";
3
3
  //#region src/zodSchemas.d.ts
4
- type TransformJWT = Omit<Iamv1alpha1.JWT, 'createdAt' | 'updatedAt' | 'expiresAt'> & {
4
+ type TransformJWT = Omit<Iamv1alpha1.JWT, 'createdAt' | 'updatedAt' | 'expiresAt' | 'srn'> & {
5
5
  createdAt?: string | Date;
6
6
  updatedAt?: string | Date;
7
7
  expiresAt?: string | Date;
8
+ srn?: string;
8
9
  };
9
10
  type ValidateJWTSchema = {
10
11
  jwt?: TransformJWT;
@@ -9,7 +9,8 @@ const rawJwtSchema = z.object({
9
9
  issuerId: z.string().min(1),
10
10
  jti: z.string().min(1),
11
11
  updatedAt: z.union([z.string().min(1), z.date()]).optional(),
12
- userAgent: z.string().min(1)
12
+ userAgent: z.string().min(1),
13
+ srn: z.string().optional()
13
14
  }),
14
15
  renewToken: z.string().min(1),
15
16
  token: z.string().min(1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scaleway/auth-scw",
3
- "version": "1.0.4",
3
+ "version": "1.1.1",
4
4
  "description": "Auth Scaleway",
5
5
  "homepage": "https://github.com/scaleway/scaleway-lib/tree/main/packages/auth-scw",
6
6
  "bugs": {
@@ -28,17 +28,18 @@
28
28
  "dependencies": {
29
29
  "cookie": "2.0.1",
30
30
  "date-fns": "4.4.0",
31
- "zod": "4.6.2"
31
+ "zod": "4.6.5"
32
32
  },
33
33
  "devDependencies": {
34
- "@scaleway/sdk-client": "2.8.0",
35
- "@scaleway/sdk-iam": "2.15.0",
34
+ "@scaleway/sdk-client": "2.10.0",
35
+ "@scaleway/sdk-iam": "2.16.0",
36
+ "@scaleway/types": "1.2.0",
36
37
  "@types/react": "19.3.0",
37
38
  "react": "19.3.0"
38
39
  },
39
40
  "peerDependencies": {
40
- "@scaleway/sdk-client": "2.8.0",
41
- "@scaleway/sdk-iam": "2.15.0",
41
+ "@scaleway/sdk-client": "2.10.0",
42
+ "@scaleway/sdk-iam": "2.16.0",
42
43
  "react": ">=18.0"
43
44
  },
44
45
  "scripts": {
@@ -1,25 +0,0 @@
1
- import { AudienceIdType, EncodedJWT } from "../types/index.js";
2
- //#region src/useAuthScw/authStoreManager.d.ts
3
- declare const getUniqueHostnameString: (inputString: string) => string;
4
- export declare const AuthStoreManager: {
5
- COOKIE_AGE: number;
6
- DOMAIN: string;
7
- deleteAllJwts(): void;
8
- deleteAudienceId(): void;
9
- deleteJwt(audienceId: string): void;
10
- getAudienceId(): string | null;
11
- getJwt(audienceId: string): EncodedJWT | null;
12
- getKeyAudienceId(): string;
13
- getKeySession(audienceId: string): string;
14
- getUniqueHostnameString: typeof getUniqueHostnameString;
15
- SUFFIX_KEY: string;
16
- setAudienceId(audienceId: string): void;
17
- setJwt({ jwtInfo, setAudienceId }: {
18
- jwtInfo: EncodedJWT;
19
- setAudienceId?: boolean;
20
- }): void;
21
- setSuffixKey(key: string): void;
22
- typeGuardAudienceId(untypedObject: unknown): untypedObject is AudienceIdType;
23
- typeGuardJWT(untypedObject: unknown): untypedObject is EncodedJWT;
24
- };
25
- //#endregion
@@ -1,105 +0,0 @@
1
- import { COOKIE_CONFIG } from "../constants.js";
2
- import { audienceIdSchema, jwtSchema } from "../zodSchemas.js";
3
- import { parseCookie, stringifySetCookie } from "cookie";
4
- //#region src/useAuthScw/authStoreManager.ts
5
- const KEY_SESSION = "_scw_session";
6
- const KEY_AUDIENCE_ID = "_scw_audience_id";
7
- const DEFAULT_SUFFIX_KEY = "";
8
- const COOKIE_AGE = 2678400;
9
- let cookieConfig = COOKIE_CONFIG;
10
- const DOMAIN = typeof window !== "undefined" ? globalThis.location.hostname : "";
11
- const getUniqueHostnameString = (inputString) => inputString.replaceAll(".", "_").replaceAll("-", "_");
12
- const setCookieConfig = (COOKIE_CONFIG_PARAM) => cookieConfig = COOKIE_CONFIG_PARAM;
13
- const setCookie = (value, key, maxAge = COOKIE_AGE) => {
14
- document.cookie = stringifySetCookie({
15
- name: key,
16
- value: JSON.stringify(value),
17
- domain: DOMAIN,
18
- httpOnly: cookieConfig.httpOnly,
19
- maxAge,
20
- path: cookieConfig.path,
21
- sameSite: cookieConfig.sameSite,
22
- secure: cookieConfig.secure
23
- });
24
- };
25
- const getCookie = (key) => {
26
- const cookies = parseCookie(document.cookie);
27
- if (key in cookies && cookies[key]) {
28
- const cookieSessionString = cookies[key];
29
- if (cookieSessionString) return JSON.parse(cookieSessionString);
30
- return null;
31
- }
32
- return null;
33
- };
34
- const deleteCookie = (key) => {
35
- document.cookie = stringifySetCookie({
36
- name: key,
37
- value: "",
38
- domain: DOMAIN,
39
- httpOnly: cookieConfig.httpOnly,
40
- maxAge: -1,
41
- path: cookieConfig.path,
42
- sameSite: cookieConfig.sameSite,
43
- secure: cookieConfig.secure
44
- });
45
- };
46
- const AuthStoreManager = {
47
- COOKIE_AGE,
48
- DOMAIN,
49
- deleteAllJwts() {
50
- const prefix = AuthStoreManager.getKeySession("");
51
- document.cookie.split(";").forEach((cookieParam) => {
52
- let [audienceId] = cookieParam.split("=");
53
- if (audienceId) {
54
- audienceId = audienceId.trim();
55
- if (audienceId.startsWith(prefix)) this.deleteJwt(audienceId);
56
- }
57
- });
58
- },
59
- deleteAudienceId() {
60
- deleteCookie(AuthStoreManager.getKeyAudienceId());
61
- },
62
- deleteJwt(audienceId) {
63
- deleteCookie(AuthStoreManager.getKeySession(audienceId));
64
- },
65
- getAudienceId() {
66
- const cookieParsed = getCookie(AuthStoreManager.getKeyAudienceId());
67
- const resultCookieParsed = audienceIdSchema.safeParse(cookieParsed);
68
- if (resultCookieParsed.success) return resultCookieParsed.data.audienceId;
69
- return null;
70
- },
71
- getJwt(audienceId) {
72
- const cookieParsed = getCookie(AuthStoreManager.getKeySession(audienceId));
73
- const resultCookieParsed = jwtSchema.safeParse(cookieParsed);
74
- if (resultCookieParsed.success) return resultCookieParsed.data;
75
- if (resultCookieParsed.error) {}
76
- return null;
77
- },
78
- getKeyAudienceId() {
79
- return `${KEY_AUDIENCE_ID}${AuthStoreManager.SUFFIX_KEY}`;
80
- },
81
- getKeySession(audienceId) {
82
- return `${KEY_SESSION}${AuthStoreManager.SUFFIX_KEY}_${audienceId}`;
83
- },
84
- getUniqueHostnameString,
85
- SUFFIX_KEY: DEFAULT_SUFFIX_KEY,
86
- setAudienceId(audienceId) {
87
- setCookie({ audienceId }, AuthStoreManager.getKeyAudienceId());
88
- },
89
- setJwt({ jwtInfo, setAudienceId = true }) {
90
- const audienceId = jwtInfo.jwt?.audienceId;
91
- setCookie(jwtInfo, AuthStoreManager.getKeySession(audienceId));
92
- if (setAudienceId) this.setAudienceId(audienceId);
93
- },
94
- setSuffixKey(key) {
95
- AuthStoreManager.SUFFIX_KEY = key ? `_${key}` : DEFAULT_SUFFIX_KEY;
96
- },
97
- typeGuardAudienceId(untypedObject) {
98
- return audienceIdSchema.safeParse(untypedObject).success;
99
- },
100
- typeGuardJWT(untypedObject) {
101
- return jwtSchema.safeParse(untypedObject).success;
102
- }
103
- };
104
- //#endregion
105
- export { AuthStoreManager, setCookieConfig };