Skip to content

Conversation

ancheetah
Copy link
Collaborator

@ancheetah ancheetah commented Sep 22, 2025

JIRA Ticket

https://pingidentity.atlassian.net/browse/SDKS-4361

Description

  • standardizes set and remove storage client return type to GenericError | null
  • updates storage unit tests to check for errors in custom storage
  • improves OIDC client where storage client methods are used

Added changeset minor for OIDC client and storage

Summary by CodeRabbit

  • Bug Fixes

    • Revoke/logout flows: clearer success/error signaling and more consistent surfaced responses.
    • Storage operations now return null on success and standardized structured error objects on failure.
  • Refactor

    • Unified storage behavior and standardized error shape (GenericError) across backends.
    • Result/response types for revoke/logout/storage clarified and consolidated.
  • Tests

    • Expanded storage and logout tests for success, error, and complex-value scenarios.
  • Chores

    • Dependency and changelog updates.

Copy link

changeset-bot bot commented Sep 22, 2025

🦋 Changeset detected

Latest commit: 93595d2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@forgerock/storage Minor
@forgerock/oidc-client Minor
@forgerock/davinci-client Minor
@forgerock/protect Minor
@forgerock/sdk-types Minor
@forgerock/sdk-utilities Minor
@forgerock/iframe-manager Minor
@forgerock/sdk-logger Minor
@forgerock/sdk-oidc Minor
@forgerock/sdk-request-middleware Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

coderabbitai bot commented Sep 22, 2025

Walkthrough

Standardizes storage API returns to GenericError | null, adds a unified storage error factory, updates types for custom storage, modifies OIDC revoke/logout types and flows to carry inner responses consistently, and updates tests and changelog/changeset metadata.

Changes

Cohort / File(s) Summary
Release metadata
.changeset/slow-teeth-melt.md
Adds changeset marking @forgerock/storage and @forgerock/oidc-client as minor and updates changelog entries about storage return-type standardization and OIDC client changes.
Storage implementation
packages/sdk-effects/storage/src/lib/storage.effects.ts
Adds createStorageError helper; unifies get/set/remove with try/catch; standardizes error shape to GenericError; changes set/remove to return `Promise<GenericError
Storage types
packages/sdk-types/src/lib/tokens.types.ts
Expands CustomStorageObject method signatures to allow GenericError: `get: Promise<string
Storage tests
packages/sdk-effects/storage/src/lib/storage.effects.test.ts
Updates mocks to return GenericError on failures, expects null on success, adds parsing/stringify assertions, validates error shapes, and resets custom store state between tests.
OIDC types
packages/oidc-client/src/lib/client.types.ts
Changes success result shapes to use null for inner responses and makes error result types explicit with error plus inner responses typed `GenericError
OIDC revoke flow
packages/oidc-client/src/lib/client.store.ts
Simplifies inner delete handling to use deleteResponse directly; inner-error detection relies on response presence; success/error results adjusted to the updated types.
OIDC logout flow
packages/oidc-client/src/lib/logout.request.ts
Switches several imports to type-only, logoutµ now accepts StorageClient<OauthTokens>, removes temporary delete fallback, and propagates actual sessionResponse, revokeResponse, and deleteResponse values.
OIDC tests (storage stubbing)
packages/oidc-client/src/lib/client.store.test.ts, packages/oidc-client/src/lib/logout.request.test.ts
Test setup stubs global.sessionStorage/global.localStorage as null to force use of provided custom storage in tests.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App
  participant OIDC as "OIDC Client"
  participant Storage as "StorageClient"
  participant AS as "Auth Server"

  rect rgb(245,250,255)
    note right of OIDC: logout/revoke flow now preserves inner responses (session/revoke/delete)
    App->>OIDC: logoutµ(config, storageClient)
    OIDC->>Storage: get()  -- retrieve tokens
    alt tokens present
      OIDC->>AS: revoke(token)
      AS-->>OIDC: revokeResponse (ok / GenericError)
      OIDC->>Storage: remove()
      Storage-->>OIDC: deleteResponse (null / GenericError)
      OIDC-->>App: Success { sessionResponse, revokeResponse, deleteResponse }
    else no tokens
      OIDC-->>App: Success { sessionResponse: null, revokeResponse: null, deleteResponse: null }
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • cerebrl
  • ryanbas21

Poem

I thump my paw — errors tidy and neat,
Storage gives null when tasks complete.
Tokens hop off, revoke keeps each trace,
Inner responses hold their place.
I twitch my nose — the tests all pass, hooray! 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title concisely references the relevant JIRA ticket and summarizes the principal change of improving the storage client type, which aligns with the main focus of the pull request. It is clear and specific enough for a teammate to understand the primary update when scanning the commit history. The phrasing avoids unnecessary details and directly conveys the key scope of the change. Therefore, the title accurately represents the core modifications introduced.
Description Check ✅ Passed The pull request description follows the repository template by including a JIRA Ticket section with a valid link and a Description section that enumerates the key changes. It outlines the standardization of storage client return types, the corresponding test updates, OIDC client improvements, and confirms the addition of a changeset. All required template sections are present and sufficiently detailed to convey the scope and intent of the work. Consequently, the description is complete and meets the expected format.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SDKS-4361-storage-client-type

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
🧪 Early access (Sonnet 4.5): enabled

We are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

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

Copy link

nx-cloud bot commented Sep 22, 2025

View your CI Pipeline Execution ↗ for commit 93595d2

Command Status Duration Result
nx run-many -t build ✅ Succeeded <1s View ↗
nx affected -t build typecheck lint test e2e-ci ✅ Succeeded 1m 2s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2025-09-29 23:06:01 UTC

@ancheetah ancheetah force-pushed the SDKS-4361-storage-client-type branch from 286fc00 to a3f1cf9 Compare September 22, 2025 22:20
@codecov-commenter
Copy link

codecov-commenter commented Sep 22, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 56.19%. Comparing base (4fbb22c) to head (93595d2).
⚠️ Report is 12 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #417      +/-   ##
==========================================
+ Coverage   55.63%   56.19%   +0.56%     
==========================================
  Files          32       32              
  Lines        2051     2091      +40     
  Branches      344      353       +9     
==========================================
+ Hits         1141     1175      +34     
- Misses        910      916       +6     

see 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

github-actions bot commented Sep 22, 2025

Deployed 77507d6 to https://ForgeRock.github.io/ping-javascript-sdk/pr-417/77507d6d9e98357e14e47f96a546dd5aa914aa02 branch gh-pages in ForgeRock/ping-javascript-sdk

Copy link
Contributor

github-actions bot commented Sep 22, 2025

📦 Bundle Size Analysis

📦 Bundle Size Analysis

🆕 New Packages

🆕 @forgerock/device-client - 9.2 KB (new)
🆕 @forgerock/oidc-client - 23.0 KB (new)
🆕 @forgerock/protect - 150.1 KB (new)
🆕 @forgerock/sdk-utilities - 4.0 KB (new)
🆕 @forgerock/sdk-types - 5.9 KB (new)
🆕 @forgerock/storage - 1.4 KB (new)
🆕 @forgerock/sdk-logger - 1.6 KB (new)
🆕 @forgerock/iframe-manager - 2.4 KB (new)
🆕 @forgerock/sdk-request-middleware - 4.4 KB (new)
🆕 @forgerock/sdk-oidc - 2.5 KB (new)
🆕 @forgerock/davinci-client - 34.5 KB (new)


11 packages analyzed • Baseline from latest main build

Legend

🆕 New package
🔺 Size increased
🔻 Size decreased
➖ No change

ℹ️ How bundle sizes are calculated
  • Current Size: Total gzipped size of all files in the package's dist directory
  • Baseline: Comparison against the latest build from the main branch
  • Files included: All build outputs except source maps and TypeScript build cache
  • Exclusions: .map, .tsbuildinfo, and .d.ts.map files

🔄 Updated automatically on each push to this PR

if (value === null) {
return value;
}
try {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you elaborate as to why you've nested an additional try-catch here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

As a follow up, it seems you're wrapping the storage access in a try-catch. This includes the native methods as well. If we're going to do this in the other methods, let's do it consistently here too. Also, can we unnest these try-catches and do them more sequentially?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I had options and I went back and forth and gave up and settled on nested 😅 . I can un-nest them.

Yeah I can wrap the storage access in a try catch in the get method. Seems we were already doing this for set and remove but forgot about get

@ancheetah ancheetah force-pushed the SDKS-4361-storage-client-type branch 2 times, most recently from 857304d to 24fdd8d Compare September 23, 2025 17:32
@ancheetah ancheetah marked this pull request as ready for review September 23, 2025 17:32
Copy link

@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

Caution

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

⚠️ Outside diff range comments (6)
.changeset/slow-teeth-melt.md (1)

8-8: Stray character at EOF.

There’s an orphaned “8” on the last line. Remove it.

-8
+
packages/oidc-client/src/lib/client.store.ts (5)

375-391: Fix type mismatch: success result fields must be null per types

RevokeSuccessResult in client.types.ts requires revokeResponse/deleteResponse to be null on success, but this block passes union-typed vars. Assign explicit nulls.

Apply this diff:

-              const result: RevokeSuccessResult = {
-                revokeResponse,
-                deleteResponse,
-              };
+              const result: RevokeSuccessResult = {
+                revokeResponse: null,
+                deleteResponse: null,
+              };

201-204: Propagate storage.set errors (now returns GenericError | null)

set() result is ignored. If storage write fails, callers will think exchange succeeded. Handle and fail the effect when set() returns an error.

Minimal refactor:

-        }).pipe(
-          Micro.tap(async (tokens) => {
-            await storageClient.set(tokens);
-          }),
-        );
+        }).pipe(
+          Micro.flatMap((tokens) =>
+            Micro.promise(() => storageClient.set(tokens)).pipe(
+              Micro.flatMap((res) =>
+                res && 'error' in res
+                  ? Micro.fail({
+                      error: 'Token storage failure',
+                      message: res.message,
+                      type: 'state_error',
+                      status: res.status ?? 'unknown',
+                    } as const)
+                  : Micro.succeed(tokens),
+              ),
+            ),
+          ),
+        );

290-300: Propagate storage.remove/set errors during background renewal

Both remove() and set() return GenericError | null but errors are ignored. This can leave stale or missing tokens after “successful” renewals.

Example approach:

-          Micro.tap(async (tokens) => {
-            await store.dispatch(
+          Micro.flatMap(async (tokens) => {
+            await store.dispatch(
               oidcApi.endpoints.revoke.initiate({
                 accessToken: tokens.accessToken,
                 clientId: config.clientId,
                 endpoint: wellknown.revocation_endpoint,
               }),
             );
-            await storageClient.remove();
-            await storageClient.set(tokens);
+            const del = await storageClient.remove();
+            if (del && 'error' in del) {
+              return Micro.fail({
+                error: 'Token storage removal failure',
+                message: del.message,
+                type: 'state_error',
+                status: del.status ?? 'unknown',
+              } as const);
+            }
+            const set = await storageClient.set(tokens);
+            if (set && 'error' in set) {
+              return Micro.fail({
+                error: 'Token storage write failure',
+                message: set.message,
+                type: 'state_error',
+                status: set.status ?? 'unknown',
+              } as const);
+            }
+            return Micro.succeed(tokens);
           }),

334-341: Handle GenericError from storage.get before field checks

get() may return GenericError. The current check treats that as “No access token found”, masking the real error.

Apply this diff:

-        const tokens = await storageClient.get();
-
-        if (!tokens || !('accessToken' in tokens)) {
+        const tokens = await storageClient.get();
+        if (tokens && 'error' in tokens) {
+          return {
+            error: 'Error occurred while retrieving tokens',
+            message: tokens.message,
+            status: tokens.status ?? 'unknown',
+            type: 'state_error',
+          };
+        }
+        if (!tokens || !('accessToken' in tokens)) {
           return {
             error: 'No access token found',
             type: 'state_error',
           };
         }

436-443: Handle GenericError from storage.get in user.info

Same masking issue as revoke(). Return the actual storage error instead.

Apply this diff:

-        const tokens = await storageClient.get();
-
-        if (!tokens || !('accessToken' in tokens)) {
+        const tokens = await storageClient.get();
+        if (tokens && 'error' in tokens) {
+          return {
+            error: 'Error occurred while retrieving tokens',
+            message: tokens.message,
+            status: tokens.status ?? 'unknown',
+            type: 'state_error',
+          };
+        }
+        if (!tokens || !('accessToken' in tokens)) {
           return {
             error: 'No access token found',
             type: 'auth_error',
           };
         }
🧹 Nitpick comments (7)
.changeset/slow-teeth-melt.md (1)

6-7: Tighten up release notes for clarity.

Spell out the exact public change and add punctuation. Suggested tweak:

-- Standardizes return types on storage client and updates tests
-- Improves OIDC client where storage client methods are used
+- Standardizes StorageClient.set/remove return types to `GenericError | null`; updates tests.
+- Improves OIDC client usage where storage client methods are called.
packages/oidc-client/src/lib/client.store.ts (1)

279-289: Guard token_endpoint presence before exchange in background renew

You validated authorization_endpoint, but buildTokenExchangeµ requires token_endpoint. Add a check for clearer errors.

Example:

         const attemptAuthorizeGetTokensµ = authorizeµ(
           wellknown,
           config,
           log,
           store,
           authorizeOptions,
         ).pipe(
+          Micro.tap(() => {
+            if (!wellknown.token_endpoint) {
+              return Micro.fail({
+                error: 'Wellknown missing token endpoint',
+                type: 'wellknown_error',
+              } as const);
+            }
+            return Micro.unit;
+          }),
packages/sdk-effects/storage/src/lib/storage.effects.ts (3)

41-47: Avoid implicit any: annotate value as string | null

Prevents implicit-any and clarifies intent.

Apply:

-        let value;
+        let value: string | null;
@@
-        let value;
+        let value: string | null;
@@
-      let value;
+      let value: string | null;

Also applies to: 67-73, 93-100


124-124: Return null directly in async functions (avoid Promise.resolve)

Minor cleanup; async will wrap automatically.

-          return Promise.resolve(null);
+          return null;
@@
-          return Promise.resolve(null);
+          return null;
@@
-        return Promise.resolve(null);
+        return null;
@@
-          return Promise.resolve(null);
+          return null;
@@
-          return Promise.resolve(null);
+          return null;
@@
-        return Promise.resolve(null);
+        return null;

Also applies to: 136-136, 147-147, 160-160, 172-172, 183-183


30-31: Make return type explicit and drop the assertion

Prefer a typed signature over a trailing cast.

-export function createStorage<Value>(config: StorageConfig) {
+export function createStorage<Value>(config: StorageConfig): StorageClient<Value> {
@@
-  } as StorageClient<Value>;
+  };

Also applies to: 192-193

packages/sdk-effects/storage/src/lib/storage.effects.test.ts (2)

178-178: Await the set before get to avoid potential flakiness

Even though the mock updates synchronously, awaiting is safer and clearer.

-      storageInstance.set(testObject);
+      await storageInstance.set(testObject);

11-41: Optional: align storage mocks closer to Web Storage API

Real Storage methods are synchronous and accept/return strings. Simplifying mocks (sync getItem/setItem and no extra JSON.stringify/parse) would reduce test indirection. Not required for this PR.

Also applies to: 45-74

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4fbb22c and 24fdd8d.

📒 Files selected for processing (6)
  • .changeset/slow-teeth-melt.md (1 hunks)
  • packages/oidc-client/src/lib/client.store.ts (2 hunks)
  • packages/oidc-client/src/lib/client.types.ts (1 hunks)
  • packages/oidc-client/src/lib/logout.request.ts (4 hunks)
  • packages/sdk-effects/storage/src/lib/storage.effects.test.ts (12 hunks)
  • packages/sdk-effects/storage/src/lib/storage.effects.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
packages/sdk-effects/storage/src/lib/storage.effects.test.ts (1)
packages/sdk-types/src/lib/tokens.types.ts (1)
  • CustomStorageObject (18-22)
packages/sdk-effects/storage/src/lib/storage.effects.ts (1)
packages/sdk-types/src/lib/error.types.ts (1)
  • GenericError (7-23)
packages/oidc-client/src/lib/client.types.ts (2)
packages/oidc-client/src/types.ts (1)
  • GenericError (6-6)
packages/sdk-types/src/lib/error.types.ts (1)
  • GenericError (7-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (10)
.changeset/slow-teeth-melt.md (1)

1-4: Double-check version-bump level — bump to MAJOR if StorageClient return types changed.

Both packages exist in the repo: packages/sdk-effects/storage/package.json (@forgerock/storage) and packages/oidc-client/package.json (@forgerock/oidc-client). If StorageClient.set/remove now return GenericError | null (a public return-type change), this is breaking — change the entries in .changeset/slow-teeth-melt.md from minor → major.

packages/oidc-client/src/lib/logout.request.ts (3)

10-13: Good: type-only imports reduce runtime footprint

Using type-only imports for OauthTokens/OidcConfig/WellKnownResponse/StorageClient/Logout* types is correct and tree-shake friendly.


26-27: Good: storageClient typed as StorageClient

Signature now reflects the concrete client API and aligns with storage package changes.


53-58: Inner-error detection is correct and aligned with StorageClient return type

Checking for 'error' on session/revoke/delete responses matches the new GenericError | null semantics.

packages/oidc-client/src/lib/client.types.ts (4)

11-14: Success result fields set to null is clear and unambiguous

This contract is fine, but ensure implementations assign null on success (see revoke/logout code paths).


16-20: RevokeErrorResult shape is sensible

Carrying both inner responses (nullable) on error is useful for diagnostics.


22-24: LogoutSuccessResult extends RevokeSuccessResult with null sessionResponse

Consistent with “null on success” convention.


26-31: LogoutErrorResult mirrors inner responses for troubleshooting

Looks good. Confirm all callers/tests expect this standalone error shape (not intersections).

packages/sdk-effects/storage/src/lib/storage.effects.ts (1)

41-64: Thanks for un-nesting the try/catches around retrieval and parse

Addresses the earlier feedback and improves readability without changing behavior.

packages/sdk-effects/storage/src/lib/storage.effects.test.ts (1)

146-154: Tests correctly assert null on success and structured errors on failure

Covers new return semantics and error shapes across backends.

Also applies to: 166-174, 224-231, 235-244, 246-253, 297-303, 327-335

Comment on lines 69 to 72
sessionResponse,
revokeResponse,
deleteResponse,
};
Copy link

@coderabbitai coderabbitai bot Sep 23, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix type mismatch: success result fields must be null per types

LogoutSuccessResult requires sessionResponse/revokeResponse/deleteResponse to be null on success. Assign explicit nulls.

Apply this diff:

-            const result: LogoutSuccessResult = {
-              sessionResponse,
-              revokeResponse,
-              deleteResponse,
-            };
+            const result: LogoutSuccessResult = {
+              sessionResponse: null,
+              revokeResponse: null,
+              deleteResponse: null,
+            };
📝 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
sessionResponse,
revokeResponse,
deleteResponse,
};
const result: LogoutSuccessResult = {
sessionResponse: null,
revokeResponse: null,
deleteResponse: null,
};
🤖 Prompt for AI Agents
In packages/oidc-client/src/lib/logout.request.ts around lines 69 to 72, the
success result currently populates sessionResponse, revokeResponse, and
deleteResponse with actual responses, but the LogoutSuccessResult type requires
these three fields to be null on success; update the success branch to set
sessionResponse = null, revokeResponse = null, and deleteResponse = null
(instead of returning the response objects) so the returned object matches the
LogoutSuccessResult shape.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

They should all be null at this point because of the type narrowing above but I can change it back to explicit nulls if anyone has an issue with this.

Copy link

Choose a reason for hiding this comment

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

@ancheetah I understand your reasoning about the type narrowing, but I think there might be a subtle distinction here.

The type narrowing above eliminates responses that have an error property, but it doesn't necessarily make the values null. The sessionResponse, revokeResponse, and deleteResponse could still be successful response objects (without error properties) rather than null values.

Looking at the LogoutSuccessResult type definition, it explicitly requires these fields to be null on success:

export type LogoutSuccessResult = { 
  sessionResponse: null; 
  revokeResponse: null; 
  deleteResponse: null; 
};

So while the type narrowing ensures these aren't error responses, they might still be successful response objects. For strict type compliance and clarity, I'd recommend the explicit null assignments to match the type definition exactly.

That said, if you're confident about the runtime behavior and the current approach works, I defer to your judgment on the codebase!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not a blocker for me, but it may be better to have the hard coded null values. Since type checking is not existent in the run time, but we do expect to only have null values at this point.

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I changed it back to null :)

Comment on lines +11 to +12
set: (value: Value) => Promise<GenericError | null>;
remove: () => Promise<GenericError | null>;
Copy link

@coderabbitai coderabbitai bot Sep 23, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

Standardized return types for set/remove — good; audit call sites for handling GenericError | null

Change aligns API. Ensure all callers check the returned value and handle errors instead of assuming void.

Run to list likely call sites to review:


🏁 Script executed:

#!/bin/bash
# Find set/remove usages that should now handle GenericError|null
rg -nP --type=ts -C2 '\bawait\s+[\w\.\[\]$]+\.\s*(set|remove)\s*\('

Length of output: 10817


Handle GenericError | null returned from storage.set/remove at callers

Several call sites await set/remove but ignore the returned GenericError | null — update them to check the result and handle errors.

  • packages/davinci-client/src/lib/client.store.ts — await serverInfo.set(serverSlice) (line 126); await serverInfo.remove() (line 200)
  • packages/oidc-client/src/lib/client.store.ts — await storageClient.set(tokens) (line 202); await storageClient.remove() and await storageClient.set(tokens) (lines 298–299)
🤖 Prompt for AI Agents
In packages/sdk-effects/storage/src/lib/storage.effects.ts around lines 11-12
the storage API methods return Promise<GenericError | null> but several callers
in other files ignore this; update the listed call sites to await and check the
result, e.g. assign the await to a variable, if the result is non-null treat it
as an error and handle it consistently (log via existing logger and either throw
or return an appropriate failure value), specifically: in
packages/davinci-client/src/lib/client.store.ts check the result of await
serverInfo.set(serverSlice) at line 126 and of await serverInfo.remove() at line
200; in packages/oidc-client/src/lib/client.store.ts check the result of await
storageClient.set(tokens) at line 202 and both await storageClient.remove() and
await storageClient.set(tokens) at lines 298–299; ensure you do not swallow
errors and follow the module's existing error-handling pattern (log + throw or
return error) so callers receive/observe failures.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There are a few places in the OIDC client and DaVinci client where we call storage client methods and do not handle the error. The storage client does not throw, but instead returns a GenericError that must be checked for. I have not touched these as it may be out of the scope of the ticket. Here's where we can improve. Let me know if these should be addressed at this time.

  1. In the davinci client, the serverInfo store uses local storage which may throw if the browser's storage quota is exceeded or the browser is in incognito mode with blocked cookies or disabled local storage.
    https://github.com/ForgeRock/ping-javascript-sdk/blob/main/packages/davinci-client/src/lib/client.store.ts#L69-L72
  2. In the oidc client, there are several places where we wrap a storage client method in a Micro which will never fail due to storage client never throwing. Similarly, in the snippet below there is a set of calls which will never throw and the error is not handled.
    https://github.com/ForgeRock/ping-javascript-sdk/blob/main/packages/oidc-client/src/lib/client.store.ts#L290-L300

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

Let me examine the specific locations you've identified to better understand the scope of the changes needed.

@ancheetah, you've identified a valid architectural concern. The API change from void to GenericError | null creates a breaking change that requires all call sites to handle potential errors.

Based on the scope considerations:

Recommendation: Create a follow-up issue

This should be addressed in a separate PR because:

  1. Scope separation: The current ticket (SDKS-4361) focuses on standardizing storage client return types. Adding error handling across multiple clients expands the scope significantly.

  2. Impact assessment: Proper error handling will require:

    • Reviewing error handling patterns in each client
    • Deciding on consistent error propagation strategies
    • Potentially updating client APIs if errors need to bubble up
    • Comprehensive testing of error scenarios
  3. Risk management: Bundling these changes could make the current PR harder to review and increase the risk of introducing bugs.

Would you like me to create a GitHub issue to track this follow-up work? I can include the specific locations you've identified and provide context about the error handling improvements needed.

  (\   /)
 ( ._. )
o_("_")("_")

thinking deeply about architectural decisions


🏁 Script executed:

#!/bin/bash
# Check the DaVinci client store usage
echo "=== DaVinci client store usage ==="
cat packages/davinci-client/src/lib/client.store.ts | sed -n '65,75p'

echo -e "\n=== OIDC client store usage ==="  
cat packages/oidc-client/src/lib/client.store.ts | sed -n '285,305p'

# Find all storage client method calls that might need error handling
echo -e "\n=== All storage client set/remove calls ==="
rg -nP --type=ts -A2 -B2 'await\s+.*\.(set|remove)\s*\(' packages/davinci-client packages/oidc-client

Length of output: 2922


Perfect! Now I can see the exact locations and contexts. This reinforces my recommendation for a separate issue.

Analysis of the call sites:

  1. OIDC Client (lines 202, 298-299): These are inside Micro.tap blocks, which are designed for side effects. Storage failures here shouldn't necessarily crash the authentication flow, but they should be logged and potentially expose the errors to callers for observability.

  2. DaVinci Client (lines 126, 200):

    • Line 126: Storing continuation state - failure here could impact the user's ability to resume flows
    • Line 200: Cleanup after resume - failure here is less critical but should be logged

The error handling strategy needs to be thoughtful:

  • Should storage errors fail the entire operation or just log warnings?
  • How should errors be surfaced to application developers?
  • What's the fallback behavior when storage is unavailable?

This definitely warrants a dedicated issue and PR to:

  1. Define consistent error handling patterns for each client
  2. Implement proper error checking at all call sites
  3. Add comprehensive tests for storage failure scenarios
  4. Update documentation about storage error handling

Would you like me to create a GitHub issue to track this work? I can include:

  • The specific locations identified
  • Context about the API change that created this need
  • Recommendations for error handling strategies
  • Links back to this PR and discussion

This approach keeps your current PR focused and ensures the error handling work gets the attention it deserves.


✏ Learnings added
Learnt from: ancheetah
PR: ForgeRock/ping-javascript-sdk#417
File: packages/sdk-effects/storage/src/lib/storage.effects.ts:11-12
Timestamp: 2025-09-23T20:50:26.496Z
Learning: When the storage client API was changed from returning void to GenericError | null in SDKS-4361, call sites in DaVinci client and OIDC client were not updated to handle the returned errors, creating a follow-up task for proper error handling implementation.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Collaborator

@cerebrl cerebrl left a comment

Choose a reason for hiding this comment

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

I sent a separate message about the repetition of the internal storage implementation with its if then try then catch then error return. If we can address that, I think this is good to go.

@ancheetah ancheetah force-pushed the SDKS-4361-storage-client-type branch from 24fdd8d to a9eff34 Compare September 25, 2025 22:06
Copy link

@nx-cloud nx-cloud bot left a comment

Choose a reason for hiding this comment

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

Nx Cloud has identified a possible root cause for your failed CI:

The test failures are occurring because the test environment lacks DOM APIs (localStorage and sessionStorage) that are required by the storage effects module. The error messages consistently show "ReferenceError: localStorage is not defined" and "ReferenceError: sessionStorage is not defined" at line 28 of the storage effects file.

This failure is classified as 'environment_state' because:

  1. The errors are not related to the actual code changes in the pull request, which focused on standardizing storage client return types and improving OIDC client error handling
  2. The failures are caused by missing browser APIs in the Node.js test environment, not by the logic changes made in the PR
  3. This is a test environment configuration issue where the storage effects module expects browser globals that aren't available in the test runner
  4. The same storage code was working before, indicating this is an environmental setup problem rather than a code problem

The solution would require configuring the test environment to provide mocks for localStorage and sessionStorage, which is a testing infrastructure concern rather than a code fix related to the PR changes.

A code change would likely not resolve this issue, so no action was taken.

Nx CloudView in Nx Cloud ↗


⚙️ An Nx Cloud workspace admin can disable these reviews in workspace settings.

@ancheetah ancheetah force-pushed the SDKS-4361-storage-client-type branch from a9eff34 to ea41302 Compare September 25, 2025 22:41
@ancheetah ancheetah requested a review from cerebrl September 25, 2025 22:45
Copy link

@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: 1

🧹 Nitpick comments (8)
packages/sdk-types/src/lib/tokens.types.ts (1)

8-8: Use a type-only import for GenericError

To avoid emitting a runtime import for a type, switch to a type-only import.

-import { GenericError } from './error.types.js';
+import type { GenericError } from './error.types.js';
packages/oidc-client/src/lib/logout.request.ts (1)

53-58: Prefer a type guard for clearer narrowing

Replace repeated 'error' in x checks with a reusable type guard to improve readability and inference.

Example (add once near the top of the file):

function isGenericError(x: unknown): x is { error: unknown } {
  return !!x && typeof x === 'object' && 'error' in x;
}

Then:

-          const isInnerRequestError =
-            (sessionResponse && 'error' in sessionResponse) ||
-            (revokeResponse && 'error' in revokeResponse) ||
-            (deleteResponse && 'error' in deleteResponse);
+          const isInnerRequestError =
+            isGenericError(sessionResponse) ||
+            isGenericError(revokeResponse) ||
+            isGenericError(deleteResponse);
packages/sdk-effects/storage/src/lib/storage.effects.test.ts (2)

188-198: Await the set before get to avoid race potential

Minor stability improvement: ensure the write completes before read.

-      storageInstance.set(testObject);
+      await storageInstance.set(testObject);

80-101: Align mock method signatures with CustomStorageObject

For stricter type conformance and clarity, mirror the interface parameter/return types.

-  get: vi.fn(async (key: string): Promise<string | GenericError> => {
+  get: vi.fn(async (key: string): Promise<string | null | GenericError> => {
@@
-  set: vi.fn(async (key: string, valueToSet: unknown): Promise<void | GenericError> => {
+  set: vi.fn(async (key: string, valueToSet: string): Promise<void | GenericError> => {
packages/sdk-effects/storage/src/lib/storage.effects.ts (4)

70-82: Catch exceptions from custom get()

If a custom store throws, return a standardized retrieving error (consistent with browser storage path).

-      if (storeType === 'custom') {
-        const value = await config.custom.get(key);
+      if (storeType === 'custom') {
+        let value: string | GenericError | null;
+        try {
+          value = await config.custom.get(key);
+        } catch {
+          return createStorageError(storeType, 'Retrieving');
+        }
         if (value === null || (typeof value === 'object' && 'error' in value)) {
           return value;
         }
 
         try {
           const parsed = JSON.parse(value);
           return parsed as Value;
         } catch {
           return createStorageError(storeType, 'Parsing');
         }
       }

101-114: Avoid shadowing and redundant Promise.resolve in async function

Use a distinct variable name and return the value directly.

     set: async function storageSet(value: Value): Promise<GenericError | null> {
       const valueToStore = JSON.stringify(value);
       if (storeType === 'custom') {
-        const value = await config.custom.set(key, valueToStore);
-        return Promise.resolve(value ?? null);
+        try {
+          const err = await config.custom.set(key, valueToStore);
+          return err ?? null;
+        } catch {
+          return createStorageError(storeType, 'Storing');
+        }
       }
 
       try {
         await storageTypes[storeType].setItem(key, valueToStore);
-        return Promise.resolve(null);
+        return null;
       } catch {
         return createStorageError(storeType, 'Storing');
       }
     },

115-127: Symmetric error handling for custom remove(); remove redundant Promise.resolve

Mirror the set() improvements for consistency.

     remove: async function storageRemove(): Promise<GenericError | null> {
       if (storeType === 'custom') {
-        const value = await config.custom.remove(key);
-        return Promise.resolve(value ?? null);
+        try {
+          const err = await config.custom.remove(key);
+          return err ?? null;
+        } catch {
+          return createStorageError(storeType, 'Removing');
+        }
       }
 
       try {
         await storageTypes[storeType].removeItem(key);
-        return Promise.resolve(null);
+        return null;
       } catch {
         return createStorageError(storeType, 'Removing');
       }
     },

11-12: Audit unhandled set/remove calls for error handling

  • packages/davinci-client/src/lib/client.store.ts: line 126 (await serverInfo.set…), line 200 (await serverInfo.remove())
  • packages/oidc-client/src/lib/client.store.ts: line 202 (await storageClient.set…), line 298 (await storageClient.remove())

Capture each call’s GenericError | null return and handle or log errors.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24fdd8d and ea41302.

📒 Files selected for processing (9)
  • .changeset/slow-teeth-melt.md (1 hunks)
  • packages/oidc-client/src/lib/client.store.test.ts (1 hunks)
  • packages/oidc-client/src/lib/client.store.ts (2 hunks)
  • packages/oidc-client/src/lib/client.types.ts (1 hunks)
  • packages/oidc-client/src/lib/logout.request.test.ts (1 hunks)
  • packages/oidc-client/src/lib/logout.request.ts (4 hunks)
  • packages/sdk-effects/storage/src/lib/storage.effects.test.ts (13 hunks)
  • packages/sdk-effects/storage/src/lib/storage.effects.ts (2 hunks)
  • packages/sdk-types/src/lib/tokens.types.ts (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/oidc-client/src/lib/client.store.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/oidc-client/src/lib/client.store.ts
  • .changeset/slow-teeth-melt.md
  • packages/oidc-client/src/lib/client.types.ts
🧰 Additional context used
📓 Path-based instructions (1)
packages/**/?(*.)test.@(ts|tsx)

📄 CodeRabbit inference engine (CLAUDE.md)

Write unit tests with Vitest for package code

Files:

  • packages/oidc-client/src/lib/logout.request.test.ts
  • packages/sdk-effects/storage/src/lib/storage.effects.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: ancheetah
PR: ForgeRock/ping-javascript-sdk#417
File: packages/sdk-effects/storage/src/lib/storage.effects.ts:11-12
Timestamp: 2025-09-23T20:50:26.496Z
Learning: When the storage client API was changed from returning void to GenericError | null in SDKS-4361, call sites in DaVinci client and OIDC client were not updated to handle the returned errors, creating a follow-up task for proper error handling implementation.
📚 Learning: 2025-09-23T20:50:26.496Z
Learnt from: ancheetah
PR: ForgeRock/ping-javascript-sdk#417
File: packages/sdk-effects/storage/src/lib/storage.effects.ts:11-12
Timestamp: 2025-09-23T20:50:26.496Z
Learning: When the storage client API was changed from returning void to GenericError | null in SDKS-4361, call sites in DaVinci client and OIDC client were not updated to handle the returned errors, creating a follow-up task for proper error handling implementation.

Applied to files:

  • packages/sdk-effects/storage/src/lib/storage.effects.ts
🧬 Code graph analysis (3)
packages/sdk-effects/storage/src/lib/storage.effects.ts (1)
packages/sdk-types/src/lib/error.types.ts (1)
  • GenericError (7-23)
packages/sdk-effects/storage/src/lib/storage.effects.test.ts (1)
packages/sdk-types/src/lib/tokens.types.ts (1)
  • CustomStorageObject (20-24)
packages/oidc-client/src/lib/logout.request.ts (2)
packages/sdk-effects/storage/src/lib/storage.effects.ts (1)
  • StorageClient (9-13)
packages/oidc-client/src/lib/config.types.ts (1)
  • OauthTokens (29-35)
🔇 Additional comments (4)
packages/sdk-types/src/lib/tokens.types.ts (1)

21-23: API shape looks good; consistent with StorageClient

The widened return types align with the new StorageClient contract and tests.

Please confirm any custom storage implementations in the repo/workspaces have been updated to match the new CustomStorageObject signature (especially set/remove returning Promise<void | GenericError>). If needed, I can generate a script to find them.

packages/oidc-client/src/lib/logout.request.ts (3)

10-13: Good: convert to type-only imports

Reduces emitted code and keeps typings clean.


26-26: Good: parameterized StorageClient type

Switching to StorageClient<OauthTokens> matches the public storage API.


68-72: Fix type mismatch: success result fields must be null per types
The success object should set sessionResponse, revokeResponse, and deleteResponse to null explicitly to satisfy LogoutSuccessResult.

-            const result: LogoutSuccessResult = {
-              sessionResponse,
-              revokeResponse,
-              deleteResponse,
-            };
+            const result: LogoutSuccessResult = {
+              sessionResponse: null,
+              revokeResponse: null,
+              deleteResponse: null,
+            };

Comment on lines +69 to +70
Object.defineProperty(global, 'sessionStorage', { value: null });
Object.defineProperty(global, 'localStorage', { value: null });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid redefining localStorage/sessionStorage with non-configurable stubs

storage.effects.test.ts already installs both globals via Object.defineProperty without configurable/writable, so this redefinition throws TypeError: Cannot redefine property: localStorage as soon as the suite loads. Please gate the override and only redefine when the descriptor is reconfigurable (or relax the original stub first) so the tests can execute.

-Object.defineProperty(global, 'sessionStorage', { value: null });
-Object.defineProperty(global, 'localStorage', { value: null });
+const disableBrowserStorage = (key: 'localStorage' | 'sessionStorage') => {
+  const descriptor = Object.getOwnPropertyDescriptor(globalThis, key);
+
+  if (descriptor && descriptor.configurable === false && descriptor.writable === false) {
+    return;
+  }
+
+  Object.defineProperty(globalThis, key, {
+    value: null,
+    configurable: true,
+    writable: true,
+  });
+};
+
+disableBrowserStorage('sessionStorage');
+disableBrowserStorage('localStorage');
📝 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
Object.defineProperty(global, 'sessionStorage', { value: null });
Object.defineProperty(global, 'localStorage', { value: null });
const disableBrowserStorage = (key: 'localStorage' | 'sessionStorage') => {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, key);
// If the property is already non-configurable and non-writable, skip redefinition
if (descriptor && descriptor.configurable === false && descriptor.writable === false) {
return;
}
Object.defineProperty(globalThis, key, {
value: null,
configurable: true,
writable: true,
});
};
disableBrowserStorage('sessionStorage');
disableBrowserStorage('localStorage');

@ancheetah ancheetah force-pushed the SDKS-4361-storage-client-type branch from ea41302 to bed870c Compare September 29, 2025 20:15
@ancheetah
Copy link
Collaborator Author

Updates:

  • use type-only import for GenericError in sdk-types
  • await storage methods in tests to prevent race conditions

Copy link

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
packages/oidc-client/src/lib/client.store.test.ts (1)

76-84: Update customStorage to match the new CustomStorageObject signature.

The customStorage object's set and remove methods return Promise<void>, but the updated CustomStorageObject interface (per packages/sdk-types/src/lib/tokens.types.ts) now requires Promise<void | GenericError>. Additionally, no tests verify error handling when custom storage operations fail.

Update the custom storage implementation and add error-path tests:

 const customStorage = {
   get: async (key: string): Promise<string | null> => storeObj[key] || null,
-  set: async (key: string, valueToSet: string) => {
+  set: async (key: string, valueToSet: string): Promise<void | GenericError> => {
     storeObj[key] = valueToSet;
   },
-  remove: async (key: string) => {
+  remove: async (key: string): Promise<void | GenericError> => {
     delete storeObj[key];
   },
 };

Add a test suite that exercises error returns from set/remove, for example:

it('Handles storage set error', async () => {
  const failingStorage = {
    get: async () => null,
    set: async () => ({ error: 'storage unavailable' }),
    remove: async () => null,
  };
  const oidcClient = await oidc({
    config,
    storage: { type: 'custom', name: 'oidcTokens', custom: failingStorage },
  });
  // Assert that set errors are handled appropriately
});

Based on learnings (SDKS-4361: call sites need proper error handling).

🧹 Nitpick comments (2)
packages/sdk-effects/storage/src/lib/storage.effects.ts (2)

30-54: Consider handling the default case explicitly.

The error factory standardizes error shapes nicely across storage backends. However, the default case (line 45) doesn't set storageName, which will result in undefined in the error message. Since TypeScript's type system should prevent unknown storage types from reaching this function, consider either:

  1. Removing the default case (let TypeScript enforce exhaustiveness), or
  2. Explicitly setting storageName = 'unknown' and throwing an error

Apply this diff to make the switch exhaustive:

-  let storageName;
+  let storageName: string;
   switch (storeType) {
     case 'localStorage':
       storageName = 'local';
       break;
     case 'sessionStorage':
       storageName = 'session';
       break;
     case 'custom':
       storageName = 'custom';
       break;
-    default:
-      break;
   }

84-99: Consider unnesting the try/catch blocks as per previous review feedback.

The nested try/catch blocks (lines 85-92 for retrieval, lines 94-99 for parsing) are functionally correct but could be more readable if unnested and sequential. This aligns with the previous review comment requesting consistent and unnested error handling.

Apply this diff to unnest the blocks:

-      let value: string | null;
-      try {
-        value = await storageTypes[storeType].getItem(key);
-        if (value === null) {
-          return value;
-        }
-      } catch {
-        return createStorageError(storeType, 'Retrieving');
-      }
-
-      try {
-        const parsed = JSON.parse(value);
-        return parsed as Value;
-      } catch {
-        return createStorageError(storeType, 'Parsing');
-      }
+      let value: string | null;
+      try {
+        value = await storageTypes[storeType].getItem(key);
+      } catch {
+        return createStorageError(storeType, 'Retrieving');
+      }
+
+      if (value === null) {
+        return value;
+      }
+
+      try {
+        const parsed = JSON.parse(value);
+        return parsed as Value;
+      } catch {
+        return createStorageError(storeType, 'Parsing');
+      }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea41302 and bed870c.

📒 Files selected for processing (9)
  • .changeset/slow-teeth-melt.md (1 hunks)
  • packages/oidc-client/src/lib/client.store.test.ts (1 hunks)
  • packages/oidc-client/src/lib/client.store.ts (2 hunks)
  • packages/oidc-client/src/lib/client.types.ts (1 hunks)
  • packages/oidc-client/src/lib/logout.request.test.ts (1 hunks)
  • packages/oidc-client/src/lib/logout.request.ts (4 hunks)
  • packages/sdk-effects/storage/src/lib/storage.effects.test.ts (15 hunks)
  • packages/sdk-effects/storage/src/lib/storage.effects.ts (2 hunks)
  • packages/sdk-types/src/lib/tokens.types.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/oidc-client/src/lib/logout.request.test.ts
  • .changeset/slow-teeth-melt.md
  • packages/oidc-client/src/lib/client.store.ts
🧰 Additional context used
📓 Path-based instructions (1)
packages/**/?(*.)test.@(ts|tsx)

📄 CodeRabbit inference engine (CLAUDE.md)

Write unit tests with Vitest for package code

Files:

  • packages/oidc-client/src/lib/client.store.test.ts
  • packages/sdk-effects/storage/src/lib/storage.effects.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: ancheetah
PR: ForgeRock/ping-javascript-sdk#417
File: packages/sdk-effects/storage/src/lib/storage.effects.ts:11-12
Timestamp: 2025-09-23T20:50:26.496Z
Learning: When the storage client API was changed from returning void to GenericError | null in SDKS-4361, call sites in DaVinci client and OIDC client were not updated to handle the returned errors, creating a follow-up task for proper error handling implementation.
📚 Learning: 2025-09-23T20:50:26.496Z
Learnt from: ancheetah
PR: ForgeRock/ping-javascript-sdk#417
File: packages/sdk-effects/storage/src/lib/storage.effects.ts:11-12
Timestamp: 2025-09-23T20:50:26.496Z
Learning: When the storage client API was changed from returning void to GenericError | null in SDKS-4361, call sites in DaVinci client and OIDC client were not updated to handle the returned errors, creating a follow-up task for proper error handling implementation.

Applied to files:

  • packages/sdk-effects/storage/src/lib/storage.effects.ts
🧬 Code graph analysis (4)
packages/oidc-client/src/lib/client.types.ts (1)
packages/sdk-types/src/lib/error.types.ts (1)
  • GenericError (7-23)
packages/oidc-client/src/lib/logout.request.ts (2)
packages/sdk-effects/storage/src/lib/storage.effects.ts (1)
  • StorageClient (9-13)
packages/oidc-client/src/lib/config.types.ts (1)
  • OauthTokens (29-35)
packages/sdk-effects/storage/src/lib/storage.effects.ts (1)
packages/sdk-types/src/lib/error.types.ts (1)
  • GenericError (7-23)
packages/sdk-effects/storage/src/lib/storage.effects.test.ts (1)
packages/sdk-types/src/lib/tokens.types.ts (1)
  • CustomStorageObject (20-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (19)
packages/oidc-client/src/lib/client.store.test.ts (1)

16-17: LGTM! Valid test environment setup.

Setting global.localStorage to null correctly simulates the absence of the localStorage API, ensuring that the stubbed sessionStorage is used during tests.

packages/sdk-types/src/lib/tokens.types.ts (2)

8-9: LGTM! Import supports new error return types.

The GenericError import is required for the updated CustomStorageObject signature.


21-23: LGTM! Type signatures align with standardized error handling.

The updated CustomStorageObject methods now correctly return GenericError unions, aligning with the PR objective to standardize storage client return types. This is an additive change—existing implementations returning void remain compatible.

packages/oidc-client/src/lib/logout.request.ts (4)

10-13: LGTM! Type-only imports improve module boundaries.

Using type imports for OauthTokens, OidcConfig, WellKnownResponse, StorageClient, LogoutErrorResult, and LogoutSuccessResult is correct and enables better tree-shaking.


26-26: LGTM! Clearer type annotation.

Using StorageClient<OauthTokens> directly is clearer and more explicit than the ReturnType utility.


53-57: LGTM! Simplified deleteResponse handling.

The removal of the undefined fallback and direct use of deleteResponse aligns with the updated storageClient.remove() signature, which now returns Promise<GenericError | null>. The error check on line 57 correctly identifies GenericError by the presence of the error property.


69-72: Type mismatch: Response fields must be null per LogoutSuccessResult definition.

The LogoutSuccessResult type requires all three fields to be null:

  • RevokeSuccessResult defines revokeResponse: null and deleteResponse: null
  • LogoutSuccessResult extends it with sessionResponse: null

The current code assigns actual response values, violating the type contract.

Apply this fix:

           } else {
             const result: LogoutSuccessResult = {
-              sessionResponse,
-              revokeResponse,
-              deleteResponse,
+              sessionResponse: null,
+              revokeResponse: null,
+              deleteResponse: null,
             };
             return Micro.succeed(result);
           }
packages/sdk-effects/storage/src/lib/storage.effects.test.ts (5)

124-130: LGTM!

The test setup correctly clears mock state and storage between tests. The switch from resetting mock methods to clearing the customStore object (line 129) aligns well with the new object-based mock pattern.


132-199: LGTM!

The localStorage tests correctly verify the new return types (null on success) and comprehensively test object/array serialization. The assertions properly ensure localStorage methods are called exclusively without cross-contamination from other storage backends.


201-266: LGTM!

The sessionStorage tests correctly verify the new return types and comprehensively test object/array handling. The test coverage for parsing and serialization is thorough.


268-359: LGTM!

The custom storage tests comprehensively cover both success and error paths. The error handling tests (lines 297-306, 328-337, 349-358) properly verify the GenericError shape, and the object/array handling tests ensure proper serialization/deserialization. The test coverage is thorough and aligns well with the new API.


78-112: Mock implementation is correct and properly validates the interface contract.

After reviewing the implementation in storage.effects.ts, line 100 shows const valueToStore = JSON.stringify(value); which is then passed to config.custom.set(key, valueToStore) on line 102. The CustomStorageObject interface (defined in packages/sdk-types/src/lib/tokens.types.ts) specifies that set must receive a string: set: (key: string, valueToSet: string) => Promise<void | GenericError>.

The mock's type check on line 92 (typeof valueToSet !== 'string') serves as defensive validation of the interface contract. While this condition won't trigger during normal operation (since the implementation always stringifies), it correctly tests that the mock adheres to its interface specification. The actual error scenario is tested via the valueToSet === '"bad-value"' condition, which works as intended.

Likely an incorrect or invalid review comment.

packages/oidc-client/src/lib/client.types.ts (4)

11-14: LGTM!

The change to make revokeResponse and deleteResponse always null in the success result is correct. Success paths should not carry error payloads, making this type more precise and aligned with the storage API changes.


16-20: LGTM!

The change from an intersection type to a standalone error type improves clarity. The fields revokeResponse and deleteResponse as GenericError | null allow callers to determine which specific operations failed, providing better error observability.


22-24: LGTM!

The change to make sessionResponse always null in the success result is consistent with the RevokeSuccessResult changes and correctly represents that success paths should not carry error payloads.


26-31: LGTM!

The change to a standalone error type with three GenericError | null fields (sessionResponse, revokeResponse, deleteResponse) provides comprehensive error observability for the logout flow, allowing callers to determine which specific operations succeeded or failed.

packages/sdk-effects/storage/src/lib/storage.effects.ts (3)

101-114: LGTM!

The set() implementation correctly stringifies values, handles both custom and browser storage paths, and returns the standardized GenericError | null response. The nullish coalescing operator (line 105) properly converts void to null for custom storage. The error handling is clean and not nested.


115-127: LGTM!

The remove() implementation correctly handles both custom and browser storage paths with consistent error handling. The nullish coalescing operator (line 118) properly converts void to null, and the try/catch block is clean and not nested.


128-128: LGTM!

The type assertion to StorageClient<Value> is appropriate and ensures the returned object matches the public interface. This is a standard pattern for factory functions that return interface-conforming objects.

Copy link
Collaborator

@cerebrl cerebrl left a comment

Choose a reason for hiding this comment

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

This looks good other than my one comment.

if (storeType === 'custom') {
const value = await config.custom.get(key);
if (value === null) {
if (value === null || (typeof value === 'object' && 'error' in value)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm a bit confused by this. If custom.get can only return a string or null, why are we checking for objects and error properties?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It can return a GenericError as well

@ancheetah ancheetah force-pushed the SDKS-4361-storage-client-type branch from bed870c to 93595d2 Compare September 29, 2025 23:03
Copy link

@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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bed870c and 93595d2.

📒 Files selected for processing (9)
  • .changeset/slow-teeth-melt.md (1 hunks)
  • packages/oidc-client/src/lib/client.store.test.ts (1 hunks)
  • packages/oidc-client/src/lib/client.store.ts (1 hunks)
  • packages/oidc-client/src/lib/client.types.ts (1 hunks)
  • packages/oidc-client/src/lib/logout.request.test.ts (1 hunks)
  • packages/oidc-client/src/lib/logout.request.ts (3 hunks)
  • packages/sdk-effects/storage/src/lib/storage.effects.test.ts (15 hunks)
  • packages/sdk-effects/storage/src/lib/storage.effects.ts (2 hunks)
  • packages/sdk-types/src/lib/tokens.types.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/oidc-client/src/lib/logout.request.test.ts
  • .changeset/slow-teeth-melt.md
🧰 Additional context used
📓 Path-based instructions (1)
packages/**/?(*.)test.@(ts|tsx)

📄 CodeRabbit inference engine (CLAUDE.md)

Write unit tests with Vitest for package code

Files:

  • packages/oidc-client/src/lib/client.store.test.ts
  • packages/sdk-effects/storage/src/lib/storage.effects.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: ancheetah
PR: ForgeRock/ping-javascript-sdk#417
File: packages/sdk-effects/storage/src/lib/storage.effects.ts:11-12
Timestamp: 2025-09-23T20:50:26.496Z
Learning: When the storage client API was changed from returning void to GenericError | null in SDKS-4361, call sites in DaVinci client and OIDC client were not updated to handle the returned errors, creating a follow-up task for proper error handling implementation.
📚 Learning: 2025-09-23T20:50:26.496Z
Learnt from: ancheetah
PR: ForgeRock/ping-javascript-sdk#417
File: packages/sdk-effects/storage/src/lib/storage.effects.ts:11-12
Timestamp: 2025-09-23T20:50:26.496Z
Learning: When the storage client API was changed from returning void to GenericError | null in SDKS-4361, call sites in DaVinci client and OIDC client were not updated to handle the returned errors, creating a follow-up task for proper error handling implementation.

Applied to files:

  • packages/sdk-effects/storage/src/lib/storage.effects.ts
🧬 Code graph analysis (4)
packages/sdk-effects/storage/src/lib/storage.effects.ts (1)
packages/sdk-types/src/lib/error.types.ts (1)
  • GenericError (7-23)
packages/oidc-client/src/lib/logout.request.ts (2)
packages/sdk-effects/storage/src/lib/storage.effects.ts (1)
  • StorageClient (9-13)
packages/oidc-client/src/lib/config.types.ts (1)
  • OauthTokens (29-35)
packages/sdk-effects/storage/src/lib/storage.effects.test.ts (1)
packages/sdk-types/src/lib/tokens.types.ts (1)
  • CustomStorageObject (20-24)
packages/oidc-client/src/lib/client.types.ts (1)
packages/sdk-types/src/lib/error.types.ts (1)
  • GenericError (7-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (22)
packages/oidc-client/src/lib/client.store.test.ts (2)

16-17: LGTM: Simulating localStorage absence.

Setting localStorage to null properly simulates environments where localStorage is unavailable, ensuring tests exercise fallback storage paths.


76-99: Verify storage error handling in tests.

The custom storage mock now returns void (no errors) for all operations. Per the PR objectives, "Updates storage unit tests to check for errors in custom storage implementations." Consider adding test cases that exercise error returns from set and remove to verify that OIDC client handles these errors appropriately.

Based on the retrieved learning, call sites may not be handling storage errors yet. Do you want to add tests that verify error propagation when custom storage operations fail?

packages/oidc-client/src/lib/client.store.ts (1)

373-396: LGTM: Revoke flow correctly handles new storage API.

The revoke flow correctly adapts to the new storage client API where remove() returns GenericError | null. The type narrowing logic properly detects errors from both the revoke request and storage deletion, and the success path correctly returns nulls per the RevokeSuccessResult type.

packages/sdk-types/src/lib/tokens.types.ts (2)

8-9: LGTM: Type-only import for GenericError.

Using a type-only import is appropriate since GenericError is only referenced in type positions, preventing unnecessary runtime dependencies.


20-24: LGTM: Standardized error handling for storage operations.

The updated CustomStorageObject interface standardizes error returns to GenericError for all operations, providing consistent error handling across the storage API. Note this is a breaking change for existing custom storage implementations.

packages/oidc-client/src/lib/logout.request.ts (3)

10-13: LGTM: Type-only imports reduce runtime dependencies.

Converting to type-only imports prevents unnecessary runtime dependencies and clarifies that these are only used for type checking.


26-26: LGTM: Using StorageClient interface for better abstraction.

Changing from ReturnType<typeof createStorage<OauthTokens>> to StorageClient<OauthTokens> improves type clarity and decouples from the implementation.


53-74: LGTM: Logout flow correctly handles new storage API.

The logout flow properly adapts to the new storage client API where remove() returns GenericError | null. The explicit null assignments in the success path (lines 69-71) provide runtime clarity, as discussed in previous reviews.

packages/sdk-effects/storage/src/lib/storage.effects.test.ts (6)

7-9: LGTM: Updated imports for error handling.

Removing unused Mock type and adding GenericError import aligns with the new error handling patterns in custom storage tests.


78-112: LGTM: Comprehensive error handling in custom storage mock.

The updated custom storage mock properly implements the new CustomStorageObject interface with GenericError returns for error cases:

  • get returns error when key is missing
  • set returns error for invalid values
  • remove returns error when key is missing

This provides good test coverage for error propagation through the storage layer.


158-166: LGTM: Verifying null return on successful set.

Test correctly verifies that set returns null on success, aligning with the new standardized API.


297-306: LGTM: Testing error propagation from custom storage get.

Test properly verifies that errors from custom storage get are propagated as structured GenericError objects.


328-337: LGTM: Testing error propagation from custom storage set.

Test properly verifies that errors from custom storage set are propagated as structured GenericError objects.


349-358: LGTM: Testing error propagation from custom storage remove.

Test properly verifies that errors from custom storage remove are propagated as structured GenericError objects, completing comprehensive error handling coverage.

packages/oidc-client/src/lib/client.types.ts (4)

11-14: LGTM! Success type correctly uses null.

The change from GenericError | null to null for success results is semantically correct—a successful operation returns no errors.


16-20: LGTM! Error type properly decoupled from success type.

Removing the inheritance and making this a standalone type with explicit error fields is clearer and more maintainable. The GenericError | null fields appropriately capture individual operation failures.


22-24: LGTM! Logout success correctly extends revoke success.

The composition is logical—logout includes revoke operations plus session cleanup—and maintains the consistent pattern of null fields for successful operations.


26-31: LGTM! Comprehensive error capture for logout operations.

The standalone error type with GenericError | null for all three operations (session, revoke, delete) enables precise error reporting while maintaining consistency with the broader storage API changes.

packages/sdk-effects/storage/src/lib/storage.effects.ts (4)

11-12: API standardization looks good.

The updated return types (Promise<GenericError | null>) establish a consistent pattern across all storage operations. Based on learnings, call site updates for error handling are tracked as a follow-up task.


69-100: LGTM! Get implementation correctly handles all storage types.

The implementation properly:

  • Handles custom storage returning string | null | GenericError (line 72)
  • Uses try-catch for browser storage retrieval and parsing
  • Returns standardized errors via createStorageError
  • Maintains consistent null-on-missing semantics

101-114: LGTM! Set implementation correctly delegates and handles errors.

The implementation:

  • Properly delegates to custom storage and coalesces undefined to null (line 105)
  • Uses try-catch for browser storage with standardized error creation
  • Returns null on success per the new API contract

115-127: LGTM! Remove implementation follows consistent patterns.

The implementation mirrors the set method:

  • Delegates to custom storage with null coalescing (line 118)
  • Uses try-catch for browser storage
  • Returns standardized errors
  • Sequential try-catch structure avoids nesting complexity

Comment on lines +30 to +54
function createStorageError(
storeType: 'localStorage' | 'sessionStorage' | 'custom',
action: 'Storing' | 'Retrieving' | 'Removing' | 'Parsing',
): GenericError {
let storageName;
switch (storeType) {
case 'localStorage':
storageName = 'local';
break;
case 'sessionStorage':
storageName = 'session';
break;
case 'custom':
storageName = 'custom';
break;
default:
break;
}

return {
error: `${action}_error`,
message: `Error ${action.toLowerCase()} value from ${storageName} storage`,
type: action === 'Parsing' ? 'parse_error' : 'unknown_error',
};
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle the default case in the switch statement.

Lines 45-46 have an empty default case. If storeType doesn't match any case, storageName remains undefined, resulting in error messages like "Error storing value from undefined storage".

Either remove the unreachable default case or initialize storageName to handle unexpected values defensively.

Apply this diff to initialize storageName:

 function createStorageError(
   storeType: 'localStorage' | 'sessionStorage' | 'custom',
   action: 'Storing' | 'Retrieving' | 'Removing' | 'Parsing',
 ): GenericError {
-  let storageName;
+  let storageName: string;
   switch (storeType) {
     case 'localStorage':
       storageName = 'local';
       break;
     case 'sessionStorage':
       storageName = 'session';
       break;
     case 'custom':
       storageName = 'custom';
       break;
     default:
-      break;
+      storageName = 'unknown';
+      break;
   }

   return {
     error: `${action}_error`,
     message: `Error ${action.toLowerCase()} value from ${storageName} storage`,
     type: action === 'Parsing' ? 'parse_error' : 'unknown_error',
   };
 }
📝 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
function createStorageError(
storeType: 'localStorage' | 'sessionStorage' | 'custom',
action: 'Storing' | 'Retrieving' | 'Removing' | 'Parsing',
): GenericError {
let storageName;
switch (storeType) {
case 'localStorage':
storageName = 'local';
break;
case 'sessionStorage':
storageName = 'session';
break;
case 'custom':
storageName = 'custom';
break;
default:
break;
}
return {
error: `${action}_error`,
message: `Error ${action.toLowerCase()} value from ${storageName} storage`,
type: action === 'Parsing' ? 'parse_error' : 'unknown_error',
};
}
function createStorageError(
storeType: 'localStorage' | 'sessionStorage' | 'custom',
action: 'Storing' | 'Retrieving' | 'Removing' | 'Parsing',
): GenericError {
let storageName: string;
switch (storeType) {
case 'localStorage':
storageName = 'local';
break;
case 'sessionStorage':
storageName = 'session';
break;
case 'custom':
storageName = 'custom';
break;
default:
storageName = 'unknown';
break;
}
return {
error: `${action}_error`,
message: `Error ${action.toLowerCase()} value from ${storageName} storage`,
type: action === 'Parsing' ? 'parse_error' : 'unknown_error',
};
}
🤖 Prompt for AI Agents
In packages/sdk-effects/storage/src/lib/storage.effects.ts around lines 30 to
54, the switch leaves storageName undefined for unexpected storeType values
causing messages like "from undefined storage"; initialize storageName
defensively (e.g., let storageName = 'unknown') or set the default case to
assign 'unknown' (or 'custom' as desired) so returned error.message always
contains a valid storage name; keep the rest of the function unchanged.

@ancheetah ancheetah merged commit 226f03f into main Sep 29, 2025
4 checks passed
@ancheetah ancheetah deleted the SDKS-4361-storage-client-type branch September 29, 2025 23:22
@ryanbas21 ryanbas21 mentioned this pull request Sep 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants