Skip to content

WIP user representation#155

Closed
garronej wants to merge 3 commits intomainfrom
user-representation-2
Closed

WIP user representation#155
garronej wants to merge 3 commits intomainfrom
user-representation-2

Conversation

@garronej
Copy link
Copy Markdown
Collaborator

@garronej garronej commented Feb 8, 2026

Summary by CodeRabbit

  • New Features
    • User data management: customize user creation via a callback
    • New getUser method to fetch and manage user information
    • Subscribe to user data changes for reactive updates
    • refreshUser functionality to update cached user data
    • Mock user support for testing
    • SPA utilities and builder updated to support the user abstraction and thread user data through auth flows

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

The PR adds a User generic and createUser flow across core and React layers, implements createGetUser (caching, subscriptions, refresh), and wires getUser/subscribeToUserChange/refreshUser into Oidc LoggedIn and SPA utilities, updating types and builders accordingly.

Changes

Cohort / File(s) Summary
Core Types
src/core/Oidc.ts
Added User generic to Oidc/LoggedIn types and surfaced getUser / user-related members on LoggedIn.
GetUser Controller
src/core/createGetUser.ts
New factory implementing lazy user resolution, hash-based invalidation, subscribeToUserChange, refreshUser, and fetchUserInfo integration.
OIDC Factory
src/core/createOidc.ts
Propagated User generic and optional createUser param into createOidc; wired token-change observable into user flow and exposed getUser on logged-in object (errors if createUser missing).
React SPA utils
src/react-spa/createOidcSpaUtils.ts, src/react-spa/types.ts, src/react-spa/utilsBuilder.ts
Threaded User generic through SPA types, utils builder, and factory; added withUser(createUser, user_mock), updated signatures for useOidc, getOidc, bootstrapOidc, and propagated user_mock option.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Oidc as OIDC Core
    participant GetUser as createGetUser Controller
    participant CreateUser as createUser Callback
    participant UserInfo as UserInfo Endpoint
    participant Tokens as Token Store / Events
    participant Listeners as User Change Listeners

    App->>Oidc: call getUser() / subscribeToUserChange()
    Oidc->>GetUser: delegate getUser / register listener
    GetUser->>Tokens: read current tokens / decodedIdToken
    GetUser->>GetUser: compute hash(accessToken, decodedIdToken)
    alt no cached user or hash changed
        GetUser->>CreateUser: invoke createUser({accessToken, decodedIdToken, fetchUserInfo, issuerUri})
        CreateUser->>UserInfo: fetch userinfo (Bearer accessToken)
        UserInfo-->>CreateUser: user payload
        CreateUser-->>GetUser: resolved User
        GetUser->>Listeners: notify (newUser, previousUser)
    else cached user valid
        GetUser-->>Oidc: return cached User
    end

    Tokens->>GetUser: token change event
    GetUser->>GetUser: recompute hash -> if changed trigger createUser flow and notify listeners
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • Angular next #93: Touches src/core/createOidc.ts params/type shape — closely related to the factory/type changes here.
  • Bundle size optimization #81: Modifies createOidc control flow and imports — overlaps with wiring and token-change handling added in this PR.
  • V7 #75: Changes src/core/Oidc.ts LoggedIn generics/members — overlaps with the type surface edits introduced here.

Poem

🐰 I nibble tokens, sniff the user trace,
Cache a carrot-shaped hash in place.
When tokens hop, I fetch anew,
Subscribers cheer — fresh user, whoo! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'WIP user representation' is vague and generic, using 'WIP' (work-in-progress) without clearly describing the actual implementation changes. Replace with a more descriptive title that clarifies the main change, such as 'Add user representation API with getUser method and subscriptions' to reflect the core additions across the codebase.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch user-representation-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/react-spa/createOidcSpaUtils.ts (2)

258-276: ⚠️ Potential issue | 🟠 Major

Missing User generic on UseOidc.Oidc.LoggedIn<DecodedIdToken>.

UseOidc.Oidc.LoggedIn now requires two type parameters <DecodedIdToken, User> (see src/react-spa/types.ts line 47), but only DecodedIdToken is supplied here. This will cause a compile error since User has no default. The returned object also doesn't include the user or refreshUser properties that LoggedIn<DecodedIdToken, User> now expects.

-        return id<UseOidc.Oidc.LoggedIn<DecodedIdToken>>({
+        return id<UseOidc.Oidc.LoggedIn<DecodedIdToken, User>>({
             isUserLoggedIn: true,
             ...
+            user: ...,       // needs wiring
+            refreshUser: ..., // needs wiring
         });

315-316: ⚠️ Potential issue | 🟠 Major

Missing User generic on GetOidc.Oidc.LoggedIn<DecodedIdToken>.

Same issue as useOidc: GetOidc.Oidc.LoggedIn requires <DecodedIdToken, User> but only one generic is provided. The getUser field is also absent from the returned object.

🤖 Fix all issues with AI agents
In `@src/core/createGetUser.ts`:
- Around line 126-144: The getUser implementation references undefined
identifiers; change references of prUser to the state-held promise (use
state?.prUser or read const prUser = state?.prUser at top of getUser) and
replace the call to __updatePrUser() with the correct function
__updatePrUserIfHashChanged(); ensure the assert and subsequent await use the
same state-derived prUser variable and that refreshUser and
subscribeToUserChange are returned unchanged.
- Around line 69-114: __updatePrUserIfHashChanged currently references tokens
before they are fetched and relies on computeHash which returns nothing; fix by
first awaiting getTokens() at the top of __updatePrUserIfHashChanged and use
that tokens object when calling computeHash and later when calling createUser
(keep passing tokens.decodedIdToken_original to createUser as before), and
implement computeHash to return a deterministic string (e.g., compute a stable
SHA-256 or other stable digest over the accessToken and decodedIdToken payload)
so hash comparisons work correctly; ensure references to computeHash, getTokens,
__updatePrUserIfHashChanged, and createUser are updated to use the same tokens
source.
- Line 124: Replace the no-op token-change subscription with a call to refresh
the stored user state: change the subscribeToTokensChange callback from an empty
function to invoke __updatePrUserIfHashChanged() (e.g.,
subscribeToTokensChange(tokens => __updatePrUserIfHashChanged());) so
rotating/updated tokens trigger the user hash check and refresh; ensure you
import or reference __updatePrUserIfHashChanged in the same scope where
subscribeToTokensChange is used.
- Around line 96-105: The forEach callback currently returns the call expression
which triggers the Biome lint rule; in the block where you await prUser_current
and iterate on onUserChanges, change the arrow callback passed to
onUserChanges.forEach from an expression body to a block body that calls
onUserChange({...}); (i.e., wrap the call in braces so it becomes a void
statement) so functions like onUserChange({ user: user_new, user_previous:
user_current }) do not implicitly return a value.
- Around line 46-53: The userinfo fetch in createGetUser (the request to
userinfo_endpoint using accessToken) doesn't check HTTP status; update the logic
to verify r.ok before calling r.json(): if !r.ok, attempt to read/parse the
error body safely (fallback to r.statusText) and throw or return a structured
error including status and message, otherwise return the parsed JSON; ensure the
Authorization header and existing callsites of createGetUser remain unchanged
but now receive a thrown error or explicit error object on non-2xx responses.
- Around line 116-122: The refreshUser implementation is fire-and-forget and
returns undefined; update refreshUser (typed as R_GetUser["refreshUser"]) to
await the token renewal and return the refreshed User: inside refreshUser set
state.hash = "" as before, then await renewTokens() (i.e., const refreshed =
await renewTokens() or simply await renewTokens()), and finally return the
refreshed User (either the value returned by renewTokens() or the current user
from state/getter) so the function resolves to a Promise<User> as declared.

In `@src/core/createOidc.ts`:
- Line 68: The file createOidc.ts fails Prettier formatting; run the project's
formatter (yarn format) or apply Prettier to src/core/createOidc.ts to fix style
issues so the CI passes; locate the exported createOidc function and surrounding
imports/exports in that file and ensure the formatter updates whitespace, line
breaks and trailing commas per project Prettier config, then commit the
formatted file.
- Around line 1942-1956: The current getUser stub is unsafe: it returns user:
null as any and refreshUser as a void-returning function, which violates the
Oidc.LoggedIn signature and hides type errors; replace the stub with a proper
implementation using the createGetUser factory (import createGetUser) so the
returned object matches Oidc.LoggedIn, ensure user has the correct typed shape
(not any/null) and that refreshUser returns Promise<User> (or rejects) per the
declared signature, and wire this up to the existing createUser check so
createGetUser is used when createUser is provided.
- Around line 72-86: ParamsOfCreateOidc uses the generic type MaybeAsync in the
createUser signature but MaybeAsync is not imported; add a type-only import for
MaybeAsync at the top of this module (so the compiler can resolve MaybeAsync
used in ParamsOfCreateOidc and the createUser param typings) importing it from
the module where MaybeAsync is defined and ensure it is imported as a type
(e.g., `import type { MaybeAsync } from '...'`) to avoid runtime imports.

In `@src/react-spa/createOidcSpaUtils.ts`:
- Around line 33-38: The createUser and user_mock parameters are destructured
but never wired into the OIDC flow; update the factory to pass createUser and
user_mock into createOidc (or into the mock branch that builds the logged-in
state) and ensure the returned useOidc/getOidc implementations populate
user-related fields: set UseOidc.Oidc.LoggedIn.{user,refreshUser} and
GetOidc.Oidc.LoggedIn.getUser to use createUser (async if needed) and to fall
back to user_mock (or decodedIdToken_mock) when present; locate usages around
createOidc, useOidc and getOidc in this module and thread the
createUser/user_mock values through the mock login/initialization paths so the
user is populated.
- Line 40: ParamsOfBootstrap is instantiated with only two generics in the
declaration of dParamsOfBootstrap; update the instantiation to include the third
User generic to match the updated type signature. Locate the declaration const
dParamsOfBootstrap = new Deferred<ParamsOfBootstrap<AutoLogin,
DecodedIdToken>>(); and change the generic to ParamsOfBootstrap<AutoLogin,
DecodedIdToken, User> (import or reference the appropriate User type if needed)
so the Deferred uses the correct three-parameter type.

In `@src/react-spa/types.ts`:
- Around line 141-147: The getUser return type currently declares refreshUser as
() => void but this operation is asynchronous; update the signature in
src/react-spa/types.ts so refreshUser returns a Promise (e.g., change
refreshUser: () => void to refreshUser: () => Promise<void>) to match other
Oidc.LoggedIn variants and allow callers to await the token renewal/user refetch
in the getUser result.
🧹 Nitpick comments (3)
src/core/createGetUser.ts (1)

1-6: Unused import: decodeJwt.

decodeJwt is imported at line 2 but never used in this file.

src/core/createOidc.ts (1)

358-363: createOidc function signature doesn't expose User generic.

The public createOidc function doesn't include User as a generic parameter, and the return type still uses Oidc.LoggedIn<DecodedIdToken> (with User defaulting to never). While ParamsOfCreateOidc accepts createUser, the function can't propagate the user type to callers. This will need to be addressed when completing the feature.

src/react-spa/utilsBuilder.ts (1)

36-52: withUser's createUser signature includes fetchUserInfo and issuerUri, but the internal builder type at line 69 does not.

The public withUser method accepts a createUser callback with { decodedIdToken, accessToken, fetchUserInfo, issuerUri }, but the internal createOidcSpaUtilsBuilder param type (line 68-70) only has { decodedIdToken: DecodedIdToken; accessToken: string }. This mismatch is masked by the @ts-expect-error at line 96.

When createOidcSpaUtils eventually wires this through, it will need the full signature. Consider aligning the internal type to avoid the suppress:

     createUser:
-        | ((params: { decodedIdToken: DecodedIdToken; accessToken: string }) => MaybeAsync<User>)
+        | ((params: {
+              decodedIdToken: DecodedIdToken;
+              accessToken: string;
+              fetchUserInfo: () => Promise<{ [key: string]: unknown; sub: string }>;
+              issuerUri: string;
+          }) => MaybeAsync<User>)
         | undefined;

Comment thread src/core/createGetUser.ts
Comment on lines +46 to +53
const r = await fetch(userinfo_endpoint, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});

return r.json();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

No HTTP error handling for the userinfo endpoint response.

fetch will not throw on 4xx/5xx responses. If the authorization server returns a 401 (expired/invalid token) or 500, r.json() will parse the error body (or fail if the body isn't JSON), producing misleading results. Check r.ok before parsing.

🛡️ Proposed fix
     const r = await fetch(userinfo_endpoint, {
         headers: {
             Authorization: `Bearer ${accessToken}`
         }
     });

+    if (!r.ok) {
+        throw new Error(
+            `oidc-spa: userinfo endpoint responded with ${r.status} ${r.statusText}`
+        );
+    }
+
     return r.json();
🤖 Prompt for AI Agents
In `@src/core/createGetUser.ts` around lines 46 - 53, The userinfo fetch in
createGetUser (the request to userinfo_endpoint using accessToken) doesn't check
HTTP status; update the logic to verify r.ok before calling r.json(): if !r.ok,
attempt to read/parse the error body safely (fallback to r.statusText) and throw
or return a structured error including status and message, otherwise return the
parsed JSON; ensure the Authorization header and existing callsites of
createGetUser remain unchanged but now receive a thrown error or explicit error
object on non-2xx responses.

Comment thread src/core/createGetUser.ts
Comment on lines +69 to +114
function __updatePrUserIfHashChanged() {
assert(createUser !== undefined, "94302");

const hash_current = state?.hash;

const hash_new = computeHash({
accessToken: tokens.accessToken,
decodedIdToken: tokens.decodedIdToken
});

const prUser_new = (async () => {
const tokens = await getTokens();

const prUser_current = state?.prUser;

if (hash_current === hash_new) {
assert(prUser_current !== undefined);
return prUser_current;
}

const user_new = await createUser({
accessToken: tokens.accessToken,
decodedIdToken: tokens.decodedIdToken_original,
issuerUri,
fetchUserInfo: () => fetchUserInfo({ accessToken: tokens.accessToken })
});

{
const user_current = await prUser_current;

onUserChanges.forEach(onUserChange =>
onUserChange({
user: user_new,
user_previous: user_current
})
);
}

return user_new;
})();

state = {
hash: hash_new,
prUser: prUser_new
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Multiple critical bugs in __updatePrUserIfHashChanged.

  1. tokens is not in scope at lines 74-76. The variable tokens is only declared inside the async IIFE at line 80 (const tokens = await getTokens()), but computeHash at line 74 references tokens.accessToken and tokens.decodedIdToken in the outer function scope where no such variable exists. This is a ReferenceError at runtime.

  2. computeHash (line 149-152) has an empty body — it declares a return type of string but returns nothing (undefined). Every hash comparison will compare undefined === undefined, so the "hash changed" logic will never work correctly.

These two issues together mean user creation/refresh will never work as intended.

🧰 Tools
🪛 Biome (2.3.13)

[error] 99-99: This callback passed to forEach() iterable method should not return a value.

Either remove this return or remove the returned value.

(lint/suspicious/useIterableCallbackReturn)

🤖 Prompt for AI Agents
In `@src/core/createGetUser.ts` around lines 69 - 114, __updatePrUserIfHashChanged
currently references tokens before they are fetched and relies on computeHash
which returns nothing; fix by first awaiting getTokens() at the top of
__updatePrUserIfHashChanged and use that tokens object when calling computeHash
and later when calling createUser (keep passing tokens.decodedIdToken_original
to createUser as before), and implement computeHash to return a deterministic
string (e.g., compute a stable SHA-256 or other stable digest over the
accessToken and decodedIdToken payload) so hash comparisons work correctly;
ensure references to computeHash, getTokens, __updatePrUserIfHashChanged, and
createUser are updated to use the same tokens source.

Comment thread src/core/createGetUser.ts
Comment on lines +96 to +105
{
const user_current = await prUser_current;

onUserChanges.forEach(onUserChange =>
onUserChange({
user: user_new,
user_previous: user_current
})
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

forEach callback implicitly returns a value (Biome lint warning).

The arrow function passed to forEach returns the result of onUserChange(...). While harmless at runtime, it triggers the Biome useIterableCallbackReturn rule. Wrap the call in braces to make it a void statement.

Proposed fix
-                onUserChanges.forEach(onUserChange =>
-                    onUserChange({
+                onUserChanges.forEach(onUserChange => {
+                    onUserChange({
                         user: user_new,
                         user_previous: user_current
-                    })
-                );
+                    });
+                });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
const user_current = await prUser_current;
onUserChanges.forEach(onUserChange =>
onUserChange({
user: user_new,
user_previous: user_current
})
);
}
{
const user_current = await prUser_current;
onUserChanges.forEach(onUserChange => {
onUserChange({
user: user_new,
user_previous: user_current
});
});
}
🧰 Tools
🪛 Biome (2.3.13)

[error] 99-99: This callback passed to forEach() iterable method should not return a value.

Either remove this return or remove the returned value.

(lint/suspicious/useIterableCallbackReturn)

🤖 Prompt for AI Agents
In `@src/core/createGetUser.ts` around lines 96 - 105, The forEach callback
currently returns the call expression which triggers the Biome lint rule; in the
block where you await prUser_current and iterate on onUserChanges, change the
arrow callback passed to onUserChanges.forEach from an expression body to a
block body that calls onUserChange({...}); (i.e., wrap the call in braces so it
becomes a void statement) so functions like onUserChange({ user: user_new,
user_previous: user_current }) do not implicitly return a value.

Comment thread src/core/createGetUser.ts
Comment thread src/core/createGetUser.ts Outdated
Comment thread src/core/createOidc.ts
Comment thread src/core/createOidc.ts Outdated
Comment on lines +33 to +38
createUser:
| ((params: { decodedIdToken: DecodedIdToken; accessToken: string }) => MaybeAsync<User>)
| undefined;
user_mock: User | undefined;
}): OidcSpaUtils<AutoLogin, DecodedIdToken, User> {
const { autoLogin, decodedIdTokenSchema, decodedIdToken_mock, createUser, user_mock } = params;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

createUser and user_mock are accepted but never wired into the OIDC flow.

These params are destructured but never passed to createOidc, the mock flow, or used to construct the user-related properties on the returned useOidc/getOidc objects. This means the user/refreshUser fields on UseOidc.Oidc.LoggedIn and getUser on GetOidc.Oidc.LoggedIn will not be populated. Presumably this is a WIP gap — flagging for tracking.

🤖 Prompt for AI Agents
In `@src/react-spa/createOidcSpaUtils.ts` around lines 33 - 38, The createUser and
user_mock parameters are destructured but never wired into the OIDC flow; update
the factory to pass createUser and user_mock into createOidc (or into the mock
branch that builds the logged-in state) and ensure the returned useOidc/getOidc
implementations populate user-related fields: set
UseOidc.Oidc.LoggedIn.{user,refreshUser} and GetOidc.Oidc.LoggedIn.getUser to
use createUser (async if needed) and to fall back to user_mock (or
decodedIdToken_mock) when present; locate usages around createOidc, useOidc and
getOidc in this module and thread the createUser/user_mock values through the
mock login/initialization paths so the user is populated.

}): OidcSpaUtils<AutoLogin, DecodedIdToken, User> {
const { autoLogin, decodedIdTokenSchema, decodedIdToken_mock, createUser, user_mock } = params;

const dParamsOfBootstrap = new Deferred<ParamsOfBootstrap<AutoLogin, DecodedIdToken>>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ParamsOfBootstrap missing third User generic.

ParamsOfBootstrap in types.ts is now ParamsOfBootstrap<AutoLogin, DecodedIdToken, User>, but here it's instantiated with only two parameters. This will be a type error once the WIP types stabilize.

-    const dParamsOfBootstrap = new Deferred<ParamsOfBootstrap<AutoLogin, DecodedIdToken>>();
+    const dParamsOfBootstrap = new Deferred<ParamsOfBootstrap<AutoLogin, DecodedIdToken, User>>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const dParamsOfBootstrap = new Deferred<ParamsOfBootstrap<AutoLogin, DecodedIdToken>>();
const dParamsOfBootstrap = new Deferred<ParamsOfBootstrap<AutoLogin, DecodedIdToken, User>>();
🤖 Prompt for AI Agents
In `@src/react-spa/createOidcSpaUtils.ts` at line 40, ParamsOfBootstrap is
instantiated with only two generics in the declaration of dParamsOfBootstrap;
update the instantiation to include the third User generic to match the updated
type signature. Locate the declaration const dParamsOfBootstrap = new
Deferred<ParamsOfBootstrap<AutoLogin, DecodedIdToken>>(); and change the generic
to ParamsOfBootstrap<AutoLogin, DecodedIdToken, User> (import or reference the
appropriate User type if needed) so the Deferred uses the correct
three-parameter type.

Comment thread src/react-spa/types.ts
Comment on lines +141 to +147
getUser: () => Promise<{
user: User;
subscribeToUserChange: (onUserChange: (user: User) => void) => {
unsubscribeFromUserChange: () => void;
};
refreshUser: () => void;
}>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

refreshUser return type is void — should be Promise<void> (or Promise<User>).

refreshUser will trigger a token renewal + user re-fetch, which are async operations. Declaring it as () => void prevents callers from await-ing the result, which could lead to race conditions. Compare with:

  • Oidc.LoggedIn (core, line 98): refreshUser: () => Promise<User>
  • UseOidc.Oidc.LoggedIn (line 67): refreshUser: () => Promise<void>
  • GetOidc.Oidc.LoggedIn (here, line 146): refreshUser: () => void ← inconsistent
-            refreshUser: () => void;
+            refreshUser: () => Promise<void>;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getUser: () => Promise<{
user: User;
subscribeToUserChange: (onUserChange: (user: User) => void) => {
unsubscribeFromUserChange: () => void;
};
refreshUser: () => void;
}>;
getUser: () => Promise<{
user: User;
subscribeToUserChange: (onUserChange: (user: User) => void) => {
unsubscribeFromUserChange: () => void;
};
refreshUser: () => Promise<void>;
}>;
🤖 Prompt for AI Agents
In `@src/react-spa/types.ts` around lines 141 - 147, The getUser return type
currently declares refreshUser as () => void but this operation is asynchronous;
update the signature in src/react-spa/types.ts so refreshUser returns a Promise
(e.g., change refreshUser: () => void to refreshUser: () => Promise<void>) to
match other Oidc.LoggedIn variants and allow callers to await the token
renewal/user refetch in the getUser result.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/core/createOidc.ts`:
- Around line 73-86: The ParamsOfCreateOidc User generic isn't threaded through
the factory functions: update both createOidc and createOidc_nonMemoized
signatures to accept the same User generic (e.g., <DecodedIdToken, AutoLogin,
User>) and propagate it into any params and return types (for example change
return types to Oidc.LoggedIn<DecodedIdToken, User> or equivalent) and ensure
functions that call ParamsOfCreateOidc use the User type when typing createUser
and getUser so the resolved user type is preserved instead of defaulting to
never.
- Line 1643: In createOidc.ts the Array.from(onTokenChanges).forEach call uses
an expression arrow which implicitly returns a value and triggers the Biome lint
rule; change the callback to a block-bodied function so it returns void
explicitly (e.g., replace the expression-arrow with a block that just calls
onTokenChange(currentTokens)); reference the onTokenChanges iterable and
currentTokens used in the forEach to locate and update the callback.
🧹 Nitpick comments (2)
src/core/createGetUser.ts (2)

154-188: computeHash uses JSON.stringify with sorted top-level keys only — nested objects are not sorted.

If any non-volatile claim in the ID token or access token contains a nested object whose key order varies across token issuances (some providers do not guarantee key order in JSON), the hash could differ even though the semantic content is identical, causing unnecessary createUser invocations. This is unlikely to be a real problem in practice but worth noting.


115-125: refreshUser relies on synchronous dispatch of token-change events to set state.

After await renewTokens() (line 120), line 122 asserts state !== undefined. This only holds if evtTokensChange listeners are invoked synchronously within the renewTokens call path (so that __updatePrUserIfHashChanged runs and sets state before renewTokens resolves).

Confirmed: Evt.post() in src/tools/Evt.ts (lines 42-49) dispatches synchronously—it iterates through listeners in a for loop without deferral. However, this is a fragile coupling: if the Evt implementation were ever changed to use setImmediate or microtasks, the assert would fail. Consider making this dependency explicit or using a different synchronization pattern (e.g., awaiting state updates) to decouple from the implementation detail.

Comment thread src/core/createOidc.ts
Comment on lines 73 to +86
export type ParamsOfCreateOidc<
DecodedIdToken extends Record<string, unknown> = Oidc.Tokens.DecodedIdToken_OidcCoreSpec,
AutoLogin extends boolean = false
AutoLogin extends boolean = false,
User = never
> = {
createUser?: (params: {
decodedIdToken: Oidc.Tokens.DecodedIdToken_OidcCoreSpec;
accessToken: string;
fetchUserInfo: () => Promise<{
[key: string]: unknown;
sub: string;
}>;
issuerUri: string;
}) => MaybeAsync<User>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

User generic not threaded through createOidc or createOidc_nonMemoized.

ParamsOfCreateOidc now accepts a User type parameter (line 76), but createOidc (line 359) and createOidc_nonMemoized (line 439) don't include User in their generic signatures. This means User always defaults to never, making getUser return Promise<{ user: never; ... }> — effectively unusable by callers.

Since this is a WIP PR, flagging for when the feature is completed: both createOidc and createOidc_nonMemoized need User threaded through their generics and return types (e.g., Oidc.LoggedIn<DecodedIdToken, User>).

🤖 Prompt for AI Agents
In `@src/core/createOidc.ts` around lines 73 - 86, The ParamsOfCreateOidc User
generic isn't threaded through the factory functions: update both createOidc and
createOidc_nonMemoized signatures to accept the same User generic (e.g.,
<DecodedIdToken, AutoLogin, User>) and propagate it into any params and return
types (for example change return types to Oidc.LoggedIn<DecodedIdToken, User> or
equivalent) and ensure functions that call ParamsOfCreateOidc use the User type
when typing createUser and getUser so the resolved user type is preserved
instead of defaulting to never.

Comment thread src/core/createOidc.ts
});
}

Array.from(onTokenChanges).forEach(onTokenChange => onTokenChange(currentTokens));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

forEach callback implicitly returns a value (Biome lint).

Same pattern as in createGetUser.ts. While onTokenChange is typed as returning void, the expression-arrow form triggers useIterableCallbackReturn.

Proposed fix
-            Array.from(onTokenChanges).forEach(onTokenChange => onTokenChange(currentTokens));
+            Array.from(onTokenChanges).forEach(onTokenChange => {
+                onTokenChange(currentTokens);
+            });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Array.from(onTokenChanges).forEach(onTokenChange => onTokenChange(currentTokens));
Array.from(onTokenChanges).forEach(onTokenChange => {
onTokenChange(currentTokens);
});
🧰 Tools
🪛 Biome (2.3.13)

[error] 1643-1643: This callback passed to forEach() iterable method should not return a value.

Either remove this return or remove the returned value.

(lint/suspicious/useIterableCallbackReturn)

🤖 Prompt for AI Agents
In `@src/core/createOidc.ts` at line 1643, In createOidc.ts the
Array.from(onTokenChanges).forEach call uses an expression arrow which
implicitly returns a value and triggers the Biome lint rule; change the callback
to a block-bodied function so it returns void explicitly (e.g., replace the
expression-arrow with a block that just calls onTokenChange(currentTokens));
reference the onTokenChanges iterable and currentTokens used in the forEach to
locate and update the callback.

@garronej
Copy link
Copy Markdown
Collaborator Author

Superceeded by #173

@garronej garronej closed this Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant