@graphoria/react 0.1.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.
@@ -0,0 +1,37 @@
1
+ export interface TokenResponse<TRole extends string = string> {
2
+ access_token: string;
3
+ expires_in: number;
4
+ role: TRole;
5
+ }
6
+ export interface User<TRole extends string = string> {
7
+ role: TRole;
8
+ }
9
+ export interface AuthState<TRole extends string = string> {
10
+ isAuthenticated: boolean;
11
+ isLoading: boolean;
12
+ user: User<TRole> | null;
13
+ error: string | null;
14
+ }
15
+ export interface AuthContextType<TRole extends string = string> extends AuthState<TRole> {
16
+ login: (username: string, password: string) => Promise<User<TRole> | null>;
17
+ logout: () => Promise<void>;
18
+ hasRole: (role: TRole) => boolean;
19
+ hasAnyRole: (roles: TRole[]) => boolean;
20
+ /** Manually trigger a token refresh */
21
+ refreshToken: () => Promise<boolean>;
22
+ }
23
+ export interface RouteConfig<TRole extends string = string> {
24
+ /** Map of path to required roles (null = public) */
25
+ permissions: Record<string, TRole[] | null>;
26
+ /** Default route for each role after login */
27
+ defaultRoutes: Partial<Record<TRole, string>>;
28
+ /** Fallback route if role not in defaultRoutes */
29
+ fallbackRoute: string;
30
+ }
31
+ export interface AuthTransportOptions {
32
+ /** GraphQL HTTP endpoint for auth mutations (default: "/graphql") */
33
+ httpUri?: string;
34
+ /** Include credentials (cookies) in requests (default: true) */
35
+ includeCredentials?: boolean;
36
+ }
37
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IAC1D,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,KAAK,CAAC;CACb;AAED,MAAM,WAAW,IAAI,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IACjD,IAAI,EAAE,KAAK,CAAC;CACb;AAED,MAAM,WAAW,SAAS,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IACtD,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,eAAe,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,SAAS,CAAC,KAAK,CAAC;IACtF,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3E,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC;IAClC,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;IACxC,uCAAuC;IACvC,YAAY,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC;AAMD,MAAM,WAAW,WAAW,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IACxD,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC;IAC5C,8CAA8C;IAC9C,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,kDAAkD;IAClD,aAAa,EAAE,MAAM,CAAC;CACvB;AAMD,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B"}
@@ -0,0 +1 @@
1
+ {"fileNames":["../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es5.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.dom.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.array.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.collection.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.intl.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.collection.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.object.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.promise.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.string.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.collection.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.float16.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.intl.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.iterator.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.promise.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.regexp.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.array.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.collection.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.date.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.error.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.intl.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.temporal.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.typedarrays.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.decorators.d.ts","../../../node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../../node_modules/.bun/@types+react@19.2.17/node_modules/@types/react/global.d.ts","../../../node_modules/.bun/csstype@3.2.3/node_modules/csstype/index.d.ts","../../../node_modules/.bun/@types+react@19.2.17/node_modules/@types/react/index.d.ts","../../../node_modules/.bun/@types+react@19.2.17/node_modules/@types/react/jsx-runtime.d.ts","../src/types.ts","../src/tokenStore.ts","../src/transport.ts","../src/AuthContext.tsx","../src/AppProvider.tsx","../src/gates.tsx","../src/index.ts"],"fileIdsList":[[89,90],[91],[91,92,93,96],[91,92,93,94,95],[91,92,96],[92,93,94,95,96,97,98],[92]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"1e9332c23e9a907175e0ffc6a49e236f97b48838cc8aec9ce7e4cec21e544b65","impliedFormat":1},{"version":"3753fbc1113dc511214802a2342280a8b284ab9094f6420e7aa171e868679f91","impliedFormat":1},{"version":"999ca32883495a866aa5737fe1babc764a469e4cde6ee6b136a4b9ae68853e4b","impliedFormat":1},{"version":"17f13ecb98cbc39243f2eee1f16d45cd8ec4706b03ee314f1915f1a8b42f6984","impliedFormat":1},{"version":"d6b1eba8496bdd0eed6fc8a685768fe01b2da4a0388b5fe7df558290bffcf32f","affectsGlobalScope":true,"impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"0bd714129fca875f7d4c477a1a392200b0bcd13fb2e80928cd334b63830ea047","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2c9037ae6cd2c52d80ceef0b3c5ffdb488627d71529cf4f63776daf11161c9a","affectsGlobalScope":true,"impliedFormat":1},{"version":"135d5cf4d345f59f1a9caadfafcd858d3d9cc68290db616cc85797224448cccc","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc238c3f81c2984751932b6aab223cd5b830e0ac6cad76389e5e9d2ffc03287d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4a07f9b76d361f572620927e5735b77d6d2101c23cdd94383eb5b706e7b36357","affectsGlobalScope":true,"impliedFormat":1},{"version":"7c4e8dc6ab834cc6baa0227e030606d29e3e8449a9f67cdf5605ea5493c4db29","affectsGlobalScope":true,"impliedFormat":1},{"version":"de7ba0fd02e06cd9a5bd4ab441ed0e122735786e67dde1e849cced1cd8b46b78","affectsGlobalScope":true,"impliedFormat":1},{"version":"6148e4e88d720a06855071c3db02069434142a8332cf9c182cda551adedf3156","affectsGlobalScope":true,"impliedFormat":1},{"version":"d63dba625b108316a40c95a4425f8d4294e0deeccfd6c7e59d819efa19e23409","affectsGlobalScope":true,"impliedFormat":1},{"version":"0568d6befee03dd435bed4fc25c4e46865b24bdcb8c563fdc21f580a2c301904","affectsGlobalScope":true,"impliedFormat":1},{"version":"30d62269b05b584741f19a5369852d5d34895aa2ac4fd948956f886d15f9cc0d","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"ffbe6d7b295306b2ba88030f65b74c107d8d99bdcf596ea99c62a02f606108b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"996fb27b15277369c68a4ba46ed138b4e9e839a02fb4ec756f7997629242fd9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"79b712591b270d4778c89706ca2cfc56ddb8c3f895840e477388f1710dc5eda9","affectsGlobalScope":true,"impliedFormat":1},{"version":"20884846cef428b992b9bd032e70a4ef88e349263f63aeddf04dda837a7dba26","affectsGlobalScope":true,"impliedFormat":1},{"version":"5fcab789c73a97cd43828ee3cc94a61264cf24d4c44472ce64ced0e0f148bdb2","affectsGlobalScope":true,"impliedFormat":1},{"version":"db59a81f070c1880ad645b2c0275022baa6a0c4f0acdc58d29d349c6efcf0903","affectsGlobalScope":true,"impliedFormat":1},{"version":"673294292640f5722b700e7d814e17aaf7d93f83a48a2c9b38f33cbc940ad8b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"d786b48f934cbca483b3c6d0a798cb43bbb4ada283e76fb22c28e53ae05b9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"142efd4ce210576f777dc34df121777be89eda476942d6d6663b03dcb53be3ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"379bc41580c2d774f82e828c70308f24a005b490c25ba34d679d84bcf05c3d9d","affectsGlobalScope":true,"impliedFormat":1},{"version":"ed484fb2aa8a1a23d0277056ec3336e0a0b52f9b8d6a961f338a642faf43235d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ffedae1d1c2d53fdbca1c96d3c7dda544281f7d262f99b6880634f8fd8d9820","affectsGlobalScope":true,"impliedFormat":1},{"version":"83a730b125d477dd264df8ba479afab27a3dae7152b005c214ab94dc7ee44fd3","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"dc782ff85b2cb10075ecffc158af7bfb27ff97bf8491c917efea0c3d622d5ac4","impliedFormat":1},{"version":"b838d4c72740eb0afd284bf7575b74c624b105eff2e8c7b4aeead57e7ac320ff","impliedFormat":1},{"version":"f9d0fc094a64675bd2d04c6acc96171dec23930f055ca7c624dc3d8e951c186e","signature":"52684ff5e4d09e6445b874d804119a756e8865f8b08b8eb575e4224ce4464cb3"},{"version":"101dd6b7ea22568fa464b3d9a2eb6998477db0db88e7874c987e0eddef620596","signature":"a395764aadf834f27ee1767b7b991699768ab52fc188779609de1250c93af9a1"},{"version":"891d209f1dc2bc3b3a0cfecf2c612b4bd53c8ba841c29681e69138e171d3b5d0","signature":"cbad5a8a440f24b8d7b1ecb7014ab9460e43968c185aac23924c8a3a8c66a5e0"},{"version":"7acd1540db9702bcc6b2e21b735cd3480b346106addc0d0bc8c5778cbb0644ab","signature":"747fd3377ca9c6c52b539295ea6f039908bd9f4b88466992cc07d7883f5a6019"},{"version":"cb093b335f8e04271d823577499ac37388ec9d7a5d372eb254bbb67966cd3344","signature":"ba278872b5f95ab09279667c66a2b24348a4ee038ad8c2861db2d5d01bdea062"},{"version":"1bda17129f8893ac3c7dea4810ca62a1396d1cba7987182722bd06cc9555ecfb","signature":"4abdb40eaf156e2df744c8fd523cadae6f1062260d333435ff46425af91474b5"},{"version":"49d98b727e332155dee58c6c8650a794e9ca19ff3be68d92f9a9a62defde91e3","signature":"3fa3884c6cd1f5facf1b804eb65cbc17a19598b57ccd57fbab10b5788f33d556"}],"root":[[93,99]],"options":{"allowImportingTsExtensions":true,"allowJs":true,"composite":true,"declaration":true,"declarationMap":true,"emitDeclarationOnly":true,"jsx":4,"module":99,"noFallthroughCasesInSwitch":true,"noPropertyAccessFromIndexSignature":false,"noUnusedLocals":false,"noUnusedParameters":false,"outDir":"./","rootDir":"..","skipLibCheck":true,"strict":true,"target":99,"verbatimModuleSyntax":true},"referencedMap":[[91,1],[92,2],[97,3],[96,4],[98,5],[99,6],[94,7],[95,7],[93,7]],"latestChangedDtsFile":"./src/gates.d.ts","version":"6.0.3"}
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@graphoria/react",
3
+ "version": "0.1.0",
4
+ "description": "React hooks and utilities for Graphoria GraphQL APIs (client-agnostic)",
5
+ "keywords": [
6
+ "authentication",
7
+ "graphoria",
8
+ "graphql",
9
+ "hooks",
10
+ "react",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://github.com/graphoria/graphoria#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/graphoria/graphoria/issues"
16
+ },
17
+ "license": "Apache-2.0",
18
+ "author": "Alex Ferreli <ferreli.ale@gmail.com>",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/graphoria/graphoria.git",
22
+ "directory": "packages/react"
23
+ },
24
+ "files": [
25
+ "src/**/*.ts",
26
+ "src/**/*.tsx",
27
+ "!src/**/*.test.ts",
28
+ "!src/**/*.test.tsx",
29
+ "!src/__test/**",
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE",
33
+ "NOTICE"
34
+ ],
35
+ "type": "module",
36
+ "main": "src/index.ts",
37
+ "module": "src/index.ts",
38
+ "types": "dist/src/index.d.ts",
39
+ "exports": {
40
+ ".": {
41
+ "types": "./dist/src/index.d.ts",
42
+ "bun": "./src/index.ts",
43
+ "import": "./src/index.ts"
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "publishConfig": {
48
+ "registry": "http://localhost:4873/"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc",
52
+ "type-check": "tsc --noEmit",
53
+ "test": "bun test",
54
+ "prepublishOnly": "bun run build"
55
+ },
56
+ "devDependencies": {
57
+ "@happy-dom/global-registrator": "^20.9.0",
58
+ "@sinonjs/fake-timers": "^15.4.0",
59
+ "@testing-library/react": "^16.3.2",
60
+ "@types/sinonjs__fake-timers": "^15.0.1",
61
+ "happy-dom": "^20.9.0",
62
+ "react": "19.2.4",
63
+ "react-dom": "19.2.4"
64
+ },
65
+ "peerDependencies": {
66
+ "react": "^18.0.0 || ^19.0.0",
67
+ "typescript": "^6.0.0"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "typescript": {
71
+ "optional": true
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,138 @@
1
+ import { createContext, useContext, useMemo } from "react";
2
+
3
+ import type { ReactNode } from "react";
4
+ import type { AuthTransportOptions, RouteConfig } from "./types";
5
+
6
+ import { AuthProvider, useAuth } from "./AuthContext";
7
+
8
+ // ============================================================================
9
+ // App Provider - composes AuthProvider + RouteConfigContext.
10
+ // Bring your own GraphQL client (Apollo, urql, relay, raw fetch — anything).
11
+ // ============================================================================
12
+
13
+ export interface RouteConfigContextType<TRole extends string = string> {
14
+ config: RouteConfig<TRole>;
15
+ isProtectedRoute: (path: string) => boolean;
16
+ getRequiredRoles: (path: string) => TRole[] | null;
17
+ canRoleAccess: (path: string, role: TRole | null) => boolean;
18
+ getRedirectPath: (role: TRole, returnTo?: string) => string;
19
+ }
20
+
21
+ const RouteConfigContext = createContext<RouteConfigContextType<string> | null>(null);
22
+
23
+ export function useRouteConfig<TRole extends string = string>(): RouteConfigContextType<TRole> {
24
+ const context = useContext(RouteConfigContext);
25
+ if (!context) {
26
+ throw new Error("useRouteConfig must be used within an AppProvider");
27
+ }
28
+ return context as RouteConfigContextType<TRole>;
29
+ }
30
+
31
+ interface AppProviderProps<TRole extends string = string> extends AuthTransportOptions {
32
+ children: ReactNode;
33
+ /** Route configuration with permissions and default routes */
34
+ routeConfig: RouteConfig<TRole>;
35
+ /** Called when authentication state changes */
36
+ onAuthChange?: (user: { role: TRole } | null) => void;
37
+ /** Called after server logout completes — clear your GraphQL cache here */
38
+ onLogout?: () => void | Promise<void>;
39
+ /** Called after a successful token refresh — restart your WS / re-auth your middleware */
40
+ onTokenRefresh?: (accessToken: string, expiresIn: number) => void;
41
+ /** Rendered while initial session refresh is in flight */
42
+ loadingFallback?: ReactNode;
43
+ }
44
+
45
+ const configValue = <TRole extends string = string>({
46
+ routeConfig,
47
+ }: {
48
+ routeConfig: RouteConfig<TRole>;
49
+ }): RouteConfigContextType<TRole> => {
50
+ const { permissions, defaultRoutes, fallbackRoute } = routeConfig;
51
+
52
+ const canRoleAccess = (path: string, role: string | null): boolean => {
53
+ const requiredRoles = permissions[path];
54
+ if (requiredRoles === null || requiredRoles === undefined) return true;
55
+ if (!role) return false;
56
+ return requiredRoles.includes(role as TRole);
57
+ };
58
+
59
+ return {
60
+ config: routeConfig,
61
+ isProtectedRoute: (path: string) => {
62
+ const roles = permissions[path];
63
+ return roles !== null && roles !== undefined;
64
+ },
65
+ getRequiredRoles: (path: string) => permissions[path] ?? null,
66
+ canRoleAccess,
67
+ getRedirectPath: (role: string, returnTo?: string) => {
68
+ if (returnTo && canRoleAccess(returnTo, role)) return returnTo;
69
+ return defaultRoutes[role as TRole] ?? fallbackRoute;
70
+ },
71
+ };
72
+ };
73
+
74
+ /**
75
+ * Main application provider. Wraps the app with:
76
+ * - AuthProvider (authentication state + proactive token refresh)
77
+ * - RouteConfigContext (route access control)
78
+ *
79
+ * Bring your own GraphQL client. Wire it via `getAccessToken`,
80
+ * `subscribeAccessToken`, and `ensureFreshToken` from the public API.
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * const routeConfig: RouteConfig = {
85
+ * permissions: { "/": null, "/dashboard": ["admin", "judge"] },
86
+ * defaultRoutes: { admin: "/dashboard", judge: "/dashboard" },
87
+ * fallbackRoute: "/dashboard",
88
+ * };
89
+ *
90
+ * <AppProvider
91
+ * routeConfig={routeConfig}
92
+ * onLogout={() => apolloClient.clearStore()}
93
+ * onTokenRefresh={() => wsClient.terminate()}
94
+ * >
95
+ * <App />
96
+ * </AppProvider>
97
+ * ```
98
+ */
99
+ export function AppProvider<TRole extends string = string>({
100
+ children,
101
+ routeConfig,
102
+ onAuthChange,
103
+ onLogout,
104
+ onTokenRefresh,
105
+ loadingFallback,
106
+ httpUri,
107
+ includeCredentials,
108
+ }: AppProviderProps<TRole>) {
109
+ const config = useMemo(() => configValue({ routeConfig }), [routeConfig]);
110
+
111
+ return (
112
+ <AuthProvider
113
+ onAuthChange={onAuthChange}
114
+ onLogout={onLogout}
115
+ onTokenRefresh={onTokenRefresh}
116
+ loadingFallback={loadingFallback}
117
+ httpUri={httpUri}
118
+ includeCredentials={includeCredentials}
119
+ >
120
+ <RouteConfigContext.Provider value={config as RouteConfigContextType<string>}>
121
+ {children}
122
+ </RouteConfigContext.Provider>
123
+ </AuthProvider>
124
+ );
125
+ }
126
+
127
+ /**
128
+ * Hook to check if current user can access a specific route.
129
+ */
130
+ export function useCanAccess(path: string): boolean {
131
+ const { isAuthenticated, hasAnyRole } = useAuth();
132
+ const { getRequiredRoles } = useRouteConfig();
133
+ const requiredRoles = getRequiredRoles(path);
134
+
135
+ if (!requiredRoles) return true;
136
+ if (!isAuthenticated) return false;
137
+ return hasAnyRole(requiredRoles);
138
+ }
@@ -0,0 +1,365 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+
11
+ import type { ReactNode } from "react";
12
+ import type {
13
+ AuthContextType,
14
+ AuthState,
15
+ AuthTransportOptions,
16
+ TokenResponse,
17
+ User,
18
+ } from "./types";
19
+
20
+ import { setAccessToken, setLogoutHandler, setRefreshHandler } from "./tokenStore";
21
+ import { GraphQLFetchError, gqlFetch } from "./transport";
22
+
23
+ // ============================================================================
24
+ // Auth Context - in-memory token storage with proactive refresh.
25
+ // Auth mutations go over raw fetch — no GraphQL client dependency.
26
+ // ============================================================================
27
+
28
+ const LOGIN_MUTATION = `
29
+ mutation Login($username: String!, $password: String!) {
30
+ auth_login(username: $username, password: $password) {
31
+ access_token
32
+ expires_in
33
+ role
34
+ }
35
+ }
36
+ `;
37
+
38
+ const REFRESH_MUTATION = `
39
+ mutation RefreshToken {
40
+ auth_refresh {
41
+ access_token
42
+ expires_in
43
+ role
44
+ }
45
+ }
46
+ `;
47
+
48
+ const LOGOUT_MUTATION = `
49
+ mutation Logout {
50
+ auth_logout
51
+ }
52
+ `;
53
+
54
+ function createAuthContext<TRole extends string>() {
55
+ return createContext<AuthContextType<TRole> | null>(null);
56
+ }
57
+
58
+ const AuthContext = createAuthContext<string>();
59
+
60
+ // Module-level cache for the boot refresh. React 18 StrictMode mounts the
61
+ // provider twice in dev (mount → cleanup → mount). Without this, the same
62
+ // rotating refresh cookie is consumed twice and the second request fails
63
+ // with "Token reuse detected". Cleared on logout, and shortly after settling
64
+ // so HMR / legit remounts still re-validate.
65
+ let bootRefreshPromise: Promise<TokenResponse<string> | null> | null = null;
66
+ let bootRefreshClearTimer: ReturnType<typeof setTimeout> | null = null;
67
+
68
+ function armBootRefreshCacheClear() {
69
+ if (bootRefreshClearTimer) clearTimeout(bootRefreshClearTimer);
70
+ // 1s is far longer than StrictMode's synchronous double-mount window and
71
+ // short enough that a real subsequent mount still re-checks the session.
72
+ bootRefreshClearTimer = setTimeout(() => {
73
+ bootRefreshPromise = null;
74
+ bootRefreshClearTimer = null;
75
+ }, 1000);
76
+ }
77
+
78
+ function resetBootRefreshCache() {
79
+ bootRefreshPromise = null;
80
+ if (bootRefreshClearTimer) {
81
+ clearTimeout(bootRefreshClearTimer);
82
+ bootRefreshClearTimer = null;
83
+ }
84
+ }
85
+
86
+ function createInitialState<TRole extends string>(): AuthState<TRole> {
87
+ return {
88
+ isAuthenticated: false,
89
+ isLoading: true,
90
+ user: null,
91
+ error: null,
92
+ };
93
+ }
94
+
95
+ interface AuthProviderProps<TRole extends string> extends AuthTransportOptions {
96
+ children: ReactNode;
97
+ /** Called when authentication state changes */
98
+ onAuthChange?: (user: User<TRole> | null) => void;
99
+ /** Called after server logout completes — user can clear GraphQL cache here */
100
+ onLogout?: () => void | Promise<void>;
101
+ /** Called after a successful token refresh — user can restart WS / re-auth middleware */
102
+ onTokenRefresh?: (accessToken: string, expiresIn: number) => void;
103
+ /** Rendered while initial session refresh is in flight (default: null) */
104
+ loadingFallback?: ReactNode;
105
+ }
106
+
107
+ export function AuthProvider<TRole extends string = string>({
108
+ children,
109
+ onAuthChange,
110
+ onLogout,
111
+ onTokenRefresh,
112
+ loadingFallback = null,
113
+ httpUri = "/graphql",
114
+ includeCredentials = true,
115
+ }: AuthProviderProps<TRole>) {
116
+ const [state, setState] = useState<AuthState<TRole>>(createInitialState<TRole>());
117
+
118
+ const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
119
+ const onAuthChangeRef = useRef(onAuthChange);
120
+ const onLogoutRef = useRef(onLogout);
121
+ const onTokenRefreshRef = useRef(onTokenRefresh);
122
+
123
+ useEffect(() => {
124
+ onAuthChangeRef.current = onAuthChange;
125
+ onLogoutRef.current = onLogout;
126
+ onTokenRefreshRef.current = onTokenRefresh;
127
+ }, [onAuthChange, onLogout, onTokenRefresh]);
128
+
129
+ const clearRefreshTimer = useCallback(() => {
130
+ if (refreshTimerRef.current) {
131
+ clearTimeout(refreshTimerRef.current);
132
+ refreshTimerRef.current = null;
133
+ }
134
+ }, []);
135
+
136
+ const applyTokens = useCallback((tokens: TokenResponse<TRole>) => {
137
+ setAccessToken(tokens.access_token);
138
+ onTokenRefreshRef.current?.(tokens.access_token, tokens.expires_in);
139
+ }, []);
140
+
141
+ const setAuthFromToken = useCallback(
142
+ (tokens: TokenResponse<TRole>) => {
143
+ applyTokens(tokens);
144
+
145
+ const user: User<TRole> = { role: tokens.role };
146
+ setState({
147
+ isAuthenticated: true,
148
+ isLoading: false,
149
+ user,
150
+ error: null,
151
+ });
152
+
153
+ onAuthChangeRef.current?.(user);
154
+ },
155
+ [applyTokens],
156
+ );
157
+
158
+ const callRefresh = useCallback(async (): Promise<TokenResponse<TRole> | null> => {
159
+ const data = await gqlFetch<{ auth_refresh: TokenResponse<TRole> | null }>(
160
+ httpUri,
161
+ REFRESH_MUTATION,
162
+ undefined,
163
+ { credentials: includeCredentials },
164
+ );
165
+ return data.auth_refresh ?? null;
166
+ }, [httpUri, includeCredentials]);
167
+
168
+ const scheduleRefresh = useCallback(
169
+ (expiresIn: number) => {
170
+ clearRefreshTimer();
171
+ // Refresh 30s before expiry (minimum 10s)
172
+ const refreshInMs = Math.max((expiresIn - 30) * 1000, 10000);
173
+
174
+ refreshTimerRef.current = setTimeout(() => {
175
+ callRefresh()
176
+ .then((tokens) => {
177
+ if (tokens) {
178
+ applyTokens(tokens);
179
+ setState((s) => ({ ...s, user: { role: tokens.role } }));
180
+ scheduleRefresh(tokens.expires_in);
181
+ }
182
+ })
183
+ .catch((err) => {
184
+ console.error("[Auth] Refresh failed:", err);
185
+ });
186
+ }, refreshInMs);
187
+ },
188
+ [clearRefreshTimer, callRefresh, applyTokens],
189
+ );
190
+
191
+ const refreshToken = useCallback(async (): Promise<boolean> => {
192
+ try {
193
+ const tokens = await callRefresh();
194
+ if (tokens) {
195
+ applyTokens(tokens);
196
+ scheduleRefresh(tokens.expires_in);
197
+ setState((s) => ({ ...s, user: { role: tokens.role } }));
198
+ return true;
199
+ }
200
+ return false;
201
+ } catch {
202
+ return false;
203
+ }
204
+ }, [callRefresh, applyTokens, scheduleRefresh]);
205
+
206
+ const logout = useCallback(async (): Promise<void> => {
207
+ clearRefreshTimer();
208
+ resetBootRefreshCache();
209
+ setAccessToken(null);
210
+
211
+ setState({
212
+ isAuthenticated: false,
213
+ isLoading: false,
214
+ user: null,
215
+ error: null,
216
+ });
217
+
218
+ try {
219
+ await gqlFetch<{ auth_logout: boolean }>(httpUri, LOGOUT_MUTATION, undefined, {
220
+ credentials: includeCredentials,
221
+ });
222
+ } catch (error) {
223
+ console.error("Logout request failed:", error);
224
+ }
225
+
226
+ try {
227
+ await onLogoutRef.current?.();
228
+ } catch (error) {
229
+ console.error("onLogout callback failed:", error);
230
+ }
231
+
232
+ onAuthChangeRef.current?.(null);
233
+ }, [clearRefreshTimer, httpUri, includeCredentials]);
234
+
235
+ // Wire the module-level token store so any GraphQL client can drive
236
+ // the refresh/logout loop via ensureFreshToken().
237
+ useEffect(() => {
238
+ setRefreshHandler(refreshToken);
239
+ setLogoutHandler(logout);
240
+ return () => {
241
+ setRefreshHandler(null);
242
+ setLogoutHandler(null);
243
+ };
244
+ }, [refreshToken, logout]);
245
+
246
+ // Boot: try refreshing the session using the HTTP-only cookie.
247
+ useEffect(() => {
248
+ let mounted = true;
249
+
250
+ const tryRefresh = async () => {
251
+ try {
252
+ if (!bootRefreshPromise) {
253
+ bootRefreshPromise = callRefresh() as Promise<TokenResponse<string> | null>;
254
+ bootRefreshPromise.finally(armBootRefreshCacheClear).catch(() => {});
255
+ }
256
+ const tokens = (await bootRefreshPromise) as TokenResponse<TRole> | null;
257
+ if (!mounted) return;
258
+
259
+ if (tokens) {
260
+ setAuthFromToken(tokens);
261
+ scheduleRefresh(tokens.expires_in);
262
+ } else {
263
+ setState((s) => ({ ...s, isLoading: false }));
264
+ }
265
+ } catch {
266
+ if (!mounted) return;
267
+ setState((s) => ({ ...s, isLoading: false }));
268
+ }
269
+ };
270
+ void tryRefresh();
271
+
272
+ return () => {
273
+ mounted = false;
274
+ clearRefreshTimer();
275
+ };
276
+ }, [callRefresh, setAuthFromToken, scheduleRefresh, clearRefreshTimer]);
277
+
278
+ const login = useCallback(
279
+ async (username: string, password: string): Promise<User<TRole> | null> => {
280
+ setState((s) => ({ ...s, isLoading: true, error: null }));
281
+
282
+ try {
283
+ const data = await gqlFetch<{ auth_login: TokenResponse<TRole> | null }>(
284
+ httpUri,
285
+ LOGIN_MUTATION,
286
+ { username, password },
287
+ { credentials: includeCredentials },
288
+ );
289
+
290
+ if (data.auth_login) {
291
+ const tokens = data.auth_login;
292
+ // Invalidate any cached "no session" boot result so a later
293
+ // provider remount re-checks against the now-valid refresh cookie.
294
+ resetBootRefreshCache();
295
+ setAuthFromToken(tokens);
296
+ scheduleRefresh(tokens.expires_in);
297
+ return { role: tokens.role };
298
+ }
299
+
300
+ setState((s) => ({
301
+ ...s,
302
+ isLoading: false,
303
+ error: "Invalid credentials",
304
+ }));
305
+ return null;
306
+ } catch (error: unknown) {
307
+ const message =
308
+ error instanceof GraphQLFetchError
309
+ ? error.message
310
+ : error instanceof Error
311
+ ? error.message
312
+ : "Login failed";
313
+
314
+ setState((s) => ({ ...s, isLoading: false, error: message }));
315
+ return null;
316
+ }
317
+ },
318
+ [httpUri, includeCredentials, setAuthFromToken, scheduleRefresh],
319
+ );
320
+
321
+ const hasRole = useCallback((role: TRole) => state.user?.role === role, [state.user]);
322
+
323
+ const hasAnyRole = useCallback(
324
+ (roles: TRole[]): boolean => {
325
+ return state.user?.role ? roles.includes(state.user.role) : false;
326
+ },
327
+ [state.user],
328
+ );
329
+
330
+ const value = useMemo<AuthContextType<TRole>>(
331
+ () => ({
332
+ ...state,
333
+ login,
334
+ logout,
335
+ hasRole,
336
+ hasAnyRole,
337
+ refreshToken,
338
+ }),
339
+ [state, login, logout, hasRole, hasAnyRole, refreshToken],
340
+ );
341
+
342
+ if (state.isLoading) return <>{loadingFallback}</>;
343
+
344
+ return (
345
+ // AuthContext is type-erased to AuthContextType<string> at the React
346
+ // boundary; useAuth<TRole>() casts back. Both casts need the unknown
347
+ // intermediate because TRole's contravariant positions defeat narrowing.
348
+ <AuthContext.Provider value={value as unknown as AuthContextType<string>}>
349
+ {children}
350
+ </AuthContext.Provider>
351
+ );
352
+ }
353
+
354
+ export function useAuth<TRole extends string = string>(): AuthContextType<TRole> {
355
+ const context = useContext(AuthContext);
356
+ if (!context) {
357
+ throw new Error("useAuth must be used within an AuthProvider");
358
+ }
359
+ return context as unknown as AuthContextType<TRole>;
360
+ }
361
+
362
+ /** @internal Test-only: flushes the StrictMode boot-refresh cache. Not re-exported from index.ts. */
363
+ export function __resetBootRefreshCacheForTest(): void {
364
+ resetBootRefreshCache();
365
+ }